Help debug 2 functions in my package

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 ?

Since the definition of _request is function _request(verb, path, params::Dict, files::Dict, data::Dict, json::Dict) it means, that it accepts only positional arguments, so _request call in _get should look like

    return _request(GET, path, kwargs)

If you want to use keyword arguments you should write them after ; and provide some default value. For example

function _request(verb, path, files::Dict, data::Dict, json::Dict; params = Dict())

More information can be found in functions documentation.

3 Likes

Thank you kind sir.
I must say, the community support of julia is just like a family nurturing small kids.

1 Like