I am working towards making a basic Julia wrapper for the Discourse API.
I’m taking inspiration from pydiscourse (a python API wrapper) and am stuck with my code. I was trying to implement get_user()
API endpoint from here.
I generated a package with PkgTemplates and am this far…
src/
Discourse.jl
module Discourse
using HTTP, Dates, JSON, CodecZlib, Base64
# HTTP verbs to be used as non string literals
DELETE = "DELETE"
GET = "GET"
POST = "POST"
PUT = "PUT"
mutable struct DiscourseClient
host::String
api_username::String
api_key::String
timeout::Second
function DiscourseClient(args...)
new(host, api_username, api_key, timeout)
end
end
include("utils/utils.jl")
export
convert_HTTP_Response_To_JSON
_get
_request
include("Categories/Categories.jl")
include("Users/User.jl")
export
get_user
end # module
src/Users/User.jl
using Discourse
function get_user(username::String)
"""
Get User information for a specific user.
ARGS:
username::String -> username to return
RETURNS:
user_info::Dictionary
"""
return _get("/users/$(username).json")["user"]
end
utils/utils.jl
using Discourse
function convert_HTTP_Response_To_JSON(response)
compressed = HTTP.payload(response);
decompressed = transcode(GzipDecompressor, compressed);
json = JSON.parse(IOBuffer(decompressed))
return json
end
function _get(path, kwargs...)
"""
HTTP Helper function
ARGS:
path:
kwargs:
RETURNS:
"""
return _request(GET, path, params=kwargs)
end
function _request(verb, path, params::Dict, files::Dict, data::Dict, json::Dict)
"""
Executes HTTP request to API and handles response
ARGS:
verb: HTTP verb as string; defined in Discourse.jl
path: path on Discourse API
params: dict of parameters to include in API
RETURNS:
response_body_data::Dictionary / None::None
"""
url = DiscourseClient.host + path
headers = Dict(
"Accept" => "application/json; charset=utf-8",
"Api-key" => DiscourseClient.api_key,
"Api-username" => DiscourseClient.api_username
)
# times we should retry request if rate limited.
retry_count = 4
# Extra time (on top of that required by API) to wait on a retry
retry_backoff = 1
while retry_count>=0
r = HTTP.request(
verb,
url,
allow_redirects=false,
params=params,
files=files,
data=data,
json=json,
headers=headers,
timeout=DiscourseClient.timeout
)
if r.status == 200
break
end
end
json_content = "application/json; charset=utf-8"
content_type = r.headers["content_type"]
decoded = r.json()
return decoded
end
Below is the python code for the endpoint I want to write.
class DiscourseClient(object):
"""Discourse API client"""
def __init__(self, host, api_username, api_key, timeout=None):
"""
Initialize the client
Args:
host: full domain name including scheme for the Discourse API
api_username: username to connect with
api_key: API key to connect with
timeout: optional timeout for the request (in seconds)
Returns:
"""
self.host = host
self.api_username = api_username
self.api_key = api_key
self.timeout = timeout
def user(self, username):
"""
Get user information for a specific user
TODO: include sample data returned
TODO: what happens when no user is found?
Args:
username: username to return
Returns:
dict of user information
"""
return self._get("/users/{0}.json".format(username))["user"]
def _get(self, path, **kwargs):
"""
Args:
path:
**kwargs:
Returns:
"""
return self._request(GET, path, params=kwargs)
def _request(self, verb, path, params={}, files={}, data={}, json={}):
"""
Executes HTTP request to API and handles response
Args:
verb: HTTP verb as string: GET, DELETE, PUT, POST
path: the path on the Discourse API
params: dictionary of parameters to include to the API
Returns:
dictionary of response body data or None
"""
url = self.host + path
headers = {
"Accept": "application/json; charset=utf-8",
"Api-Key": self.api_key,
"Api-Username": self.api_username,
}
# How many times should we retry if rate limited
retry_count = 4
# Extra time (on top of that required by API) to wait on a retry.
retry_backoff = 1
while retry_count > 0:
response = requests.request(
verb,
url,
allow_redirects=False,
params=params,
files=files,
data=data,
json=json,
headers=headers,
timeout=self.timeout,
)
log.debug("response %s: %s", response.status_code, repr(response.text))
if response.ok:
break
if not response.ok:
try:
msg = u",".join(response.json()["errors"])
except (ValueError, TypeError, KeyError):
if response.reason:
msg = response.reason
else:
msg = u"{0}: {1}".format(response.status_code, response.text)
if 400 <= response.status_code < 500:
if 429 == response.status_code:
# This codepath relies on wait_seconds from Discourse v2.0.0.beta3 / v1.9.3 or higher.
rj = response.json()
wait_delay = (
retry_backoff + rj["extras"]["wait_seconds"]
) # how long to back off for.
if retry_count > 1:
time.sleep(wait_delay)
retry_count -= 1
log.info(
"We have been rate limited and waited {0} seconds ({1} retries left)".format(
wait_delay, retry_count
)
)
log.debug("API returned {0}".format(rj))
continue
else:
raise DiscourseClientError(msg, response=response)
# Any other response.ok resulting in False
raise DiscourseServerError(msg, response=response)
if retry_count == 0:
raise DiscourseRateLimitedError(
"Number of rate limit retries exceeded. Increase retry_backoff or retry_count",
response=response,
)
if response.status_code == 302:
raise DiscourseError(
"Unexpected Redirect, invalid api key or host?", response=response
)
json_content = "application/json; charset=utf-8"
content_type = response.headers["content-type"]
if content_type != json_content:
# some calls return empty html documents
if not response.content.strip():
return None
raise DiscourseError(
'Invalid Response, expecting "{0}" got "{1}"'.format(
json_content, content_type
),
response=response,
)
try:
decoded = response.json()
except ValueError:
raise DiscourseError("failed to decode response", response=response)
if "errors" in decoded:
message = decoded.get("message")
if not message:
message = u",".join(decoded["errors"])
raise DiscourseError(message, response=response)
return decoded
Why does
julia> using Discourse
julia> plzwork = get_user("pseudocodenerd")
ERROR: function _request does not accept keyword arguments
Stacktrace:
[1] kwfunc(::Any) at ./boot.jl:321
[2] _get(::String) at /home/pseudocodenerd/.julia/dev/Discourse/src/utils/utils.jl:20
[3] get_user(::String) at /home/pseudocodenerd/.julia/dev/Discourse/src/Users/User.jl:11
[4] top-level scope at none:0
return an error ?