Getting Started

In this section, we’ll cover the basics with Uplink. To illustrate usage with an existent service, we mainly provide examples with GitHub’s API. To try them out yourself, you can simply copy the code snippets into a script or the Python console.

Making a Request

The simplest API consumer method requires only an HTTP method decorator.

For example, let’s define an GitHub API consumer that can retrieve all the public repositories hosted on the site:

import uplink

class GitHub(uplink.Consumer):
    @uplink.get("/repositories")
    def get_repos(self): pass

Now, to fetch the list of public repositories hosted on github.com, we simply invoke the get_repos() with a consumer instance:

github = GitHub(base_url="https://api.github.com")
response = github.get_repos().execute()
print(response.json()) # [{u'issues_url': u'https://api.github.com/repos/mojombo/grit/issues{/number}', ...

To summarize, we used the get decorator to indicate that the get_repos() method handles an HTTP GET request targeting the /repositories endpoint.

Further, Uplink currently supports get, post, put, patch, and delete.

Creating Consumer Instances

As illustrated in the previous example, to define a consumer, simply inherit the base class uplink.Consumer. Notably, the constructor affords us the ability to reuse consumers in different contexts.

For instance, by simply changing the function’s base_url parameter, we could use the same GitHub API consumer against the main website, github.com, and any GitHub Enterprise instance, since they offer identical APIs.

Setting the URL

To set a static URL, use the the leading parameter, path, of the HTTP method decorator:

 class GitHub(uplink.Consumer):
     @uplink.get("/repositories")
     def get_repos(self): pass

Alternatively, you can provide the URL at runtime as a method argument. To set a dynamic URL, omit the decorator parameter path and annotate the corresponding method argument with uplink.Url:

 class GitHub(uplink.Consumer);
     @uplink.get
     def get_commit(self, commit_url: uplink.Url): pass

Path Variables

For both static and dynamic URLs, Uplink supports URI templates. These templates can contain parameters enclosed in braces (e.g., {name}) for method arguments to handle at runtime.

To map a method argument to a declared URI path parameter for expansion, use the uplink.Path annotation. For instance, we can define a consumer method to query any GitHub user’s metadata by declaring the path segment parameter {/username} in the method’s URL.

class GitHub(uplink.Consumer):
    @get("users{/username}")
    def get_user(self, username: Path("username")): pass

With an instance of this consumer, we can invoke the get_user method like so

github.get_user("prkumar")

to create an HTTP request with a URL ending in users/prkumar.

Implicit Path Annotations

When building the consumer instance, Uplink will try to resolve unannotated method arguments by matching their names with URI path parameters.

For example, consider the consumer defined below, in which the method get_user() has an unannotated argument, username. Since its name matches the URI path parameter {username}, uplink will auto-annotate the argument with Path for us:

class GitHub(uplink.Consumer):
    @uplink.get("users{/username}")
    def get_user(self, username): pass

Important to note, failure to resolve all unannotated function arguments raises an InvalidRequestDefinitionError.

Query Parameters

To set unchanging query parameters, you can append a query string to the static URL. For instance, GitHub offers the query parameter q for adding keywords to a search. With this, we can define a consumer that queries all GitHub repositories written in Python:

 class GitHub(uplink.Consumer):
     @uplink.get("/search/repositories?q=language:python")
     def search_python_repos(self): pass

Note that we have hard-coded the query parameter into the URL, so that all requests that this method handles include that search term.

Alternatively, we can set query parameters at runtime using method arguments. To set dynamic query parameters, use the uplink.Query and uplink.QueryMap argument annotations.

For instance, to set the search term q at runtime, we can provide a method argument annotated with uplink.Query:

 class GitHub(uplink.Consumer):
     @uplink.get("/search/repositories")
     def search_repos(self, q: uplink.Query)

Further, the uplink.QueryMap annotation indicates that an argument handles a mapping of query parameters. For example, let’s use this annotation to transform keyword arguments into query parameters:

class GitHub(uplink.Consumer):
    @uplink.get("/search/repositories")
    def search_repos(self, **params: uplink.QueryMap)

This serves as a nice alternative to adding a uplink.Query annotated argument for each supported query parameter. For instance, we can now optionally modify how the GitHub search results are sorted, leveraging the sort query parameter:

# Search for Python repos and sort them by number of stars.
github.search_repos(q="language:python", sort="stars").execute()

Note

Another approach for setting dynamic query parameters is to use path variables in the static URL, with “form-style query expansion”.

HTTP Headers

To add literal headers, use the uplink.headers method annotation, which has accepts the input parameters as dict:

 class GitHub(uplink.Consumer):
     # This header explicitly requests version v3 of the GitHub API.
     @uplink.headers({"Accept": "application/vnd.github.v3.full+json"})
     @uplink.get("/repositories")
     def get_repos(self): pass

Alternatively, we can use the uplink.Header argument annotation to pass a header as a method argument at runtime:

 class GitHub(uplink.Consumer):
     @uplink.get("/users{/username}")
     def get_user(
         self,
         username,
         last_modified: uplink.Header("If-Modified-Since")
     ):
         """Fetch a GitHub user if modified after given date."""

Further, you can annotate an argument with uplink.HeaderMap to accept a mapping of header fields.

URL-Encoded Request Body

For POST/PUT/PATCH requests, the format of the message body is an important detail. A common approach is to url-encode the body and set the header Content-Type: application/x-www-form-urlencoded to notify the server.

To submit a url-encoded form with Uplink, decorate the consumer method with uplink.form_url_encoded and annotate each argument accepting a form field with uplink.Field. For instance, let’s provide a method for reacting to a specific GitHub issue:

 class GitHub(uplink.Consumer):
     @uplink.form_url_encoded
     @uplink.patch("/user")
     def update_blog_url(
         self,
         access_token: uplink.Query,
         blog_url: uplink.Field
     ):
         """Update a user's blog URL."""

Further, you can annotate an argument with uplink.FieldMap to accept a mapping of form fields.

Send Multipart Form Data

Multipart requests are commonly used to upload files to a server.

To send a multipart message, decorate a consumer method with uplink.multipart. Moreover, use the uplink.Part argument annotation to mark a method argument as a form part.

Todo

Add a code block that illustrates an example of how to define a consumer method that sends multipart requests.

Further, you can annotate an argument with uplink.PartMap to accept a mapping of form fields to parts.

JSON Requests, and Other Content Types

Nowadays, many HTTP webservices nowadays accept JSON requests. (GitHub’s API is an example of such a service.) Given the format’s growing popularity, Uplink provides the decorator uplink.json.

When using this decorator, you should annotate a method argument with uplink.Body, which indicates that the argument’s value should become the request’s body. Moreover, this value is expected to be an instance of dict or a subclass of uplink.Mapping.

Note that uplink.Body can annotate the keyword argument, which often enables concise method signatures:

 class GitHub(uplink.Consumer):
     @uplink.json
     @uplink.patch("/user")
     def update_user(
         self,
         access_token: uplink.Query,
         **info: uplink.Body
     ):
         """Update an authenticated user."""

Further, you may be able to send other content types by using uplink.Body and setting the Content-Type header appropriately with the decorator uplink.header.