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
.