pulp_smash.api¶
Location: Pulp Smash → API Documentation → pulp_smash.api
A client for working with Pulp’s API.
Working with an API can require repetitive calls to perform actions like check HTTP status codes. In addition, Pulp’s API has specific quirks surrounding its handling of href paths and HTTP 202 status codes. This module provides a customizable client that makes it easier to work with the API in a safe and concise manner.
-
class
pulp_smash.api.
Client
(cfg, response_handler=None, request_kwargs=None, pulp_host=None)¶ Bases:
object
A convenience object for working with an API.
This class is a wrapper around the
requests.api
module provided by Requests. Each of the functions from that module are exposed as methods here, and each of the arguments accepted by Requests’ functions are also accepted by these methods. The difference between this class and the Requests functions lies in its configurable request and response handling mechanisms.This class is flexible enough that it should be usable with any API, but certain defaults have been set to work well with Pulp.
As an example of basic usage, let’s say that you’d like to create a user, then read that user’s information back from the server. This is one way to do it:
>>> from pulp_smash.api import Client >>> from pulp_smash.config import get_config >>> client = Client(get_config()) >>> response = client.post('/pulp/api/v2/users/', {'login': 'Alice'}) >>> response = client.get(response.json()['_href']) >>> print(response.json())
Notice how we never call
response.raise_for_status()
? We don’t need to because, by default,Client
instances do this. Handy!How does this work? Each
Client
object has a callback function,response_handler
, that is given a chance to munge each server response. How else might this callback be useful? Well, notice how we calljson()
on each server response? That’s kludgy. Let’s write our own callback that takes care of this for us:>>> from pulp_smash.api import Client >>> from pulp_smash.config import get_config >>> def response_handler(client, response): ... response.raise_for_status() ... return response.json() >>> client = Client(get_config(), response_handler=response_handler) >>> response = client.post('/pulp/api/v2/users/', {'login': 'Alice'}) >>> response = client.get(response['_href']) >>> print(response)
Pulp Smash ships with several response handlers. In order of increasing complexity, see:
pulp_smash.api.echo_handler()
pulp_smash.api.code_handler()
pulp_smash.api.safe_handler()
pulp_smash.api.json_handler()
pulp_smash.api.page_handler()
pulp_smash.api.task_handler()
pulp_smash.api.smart_handler()
As mentioned, this class has configurable request and response handling mechanisms. We’ve covered response handling mechanisms — let’s move on to request handling mechanisms.
When a client is instantiated, a
pulp_smash.config.PulpSmashConfig
must be passed to the constructor, and configuration options are copied from thePulpSmashConfig
to the client. These options can be overridden on a per-object or per-request basis. Here’s an example:>>> from pulp_smash.api import Client >>> from pulp_smash.config import PulpSmashConfig >>> cfg = config.PulpSmashConfig( ... pulp_auth=('username', 'password'), ... pulp_version='1!0', ... pulp_selinux_enabled=True, ... hosts=[ ... config.PulpHost( ... hostname='example.com', ... roles={'api': { ... 'scheme': 'https', ... 'verify': '~/Documents/my.crt', ... }} ... ) ... ] ... ) >>> client = api.Client(cfg) >>> client.request_kwargs['url'] == 'https://example.com' True >>> client.request_kwargs['verify'] == '~/Documents/my.crt' True >>> response = client.get('/index.html') # Use my.crt for SSL verification >>> response = client.get('/index.html', verify=False) # Disable SSL >>> response = client.get('/index.html') # Use my.crt for SSL verification >>> client.request_kwargs['verify'] = None >>> response = client.get('/index.html') # Do default SSL verification
As shown above, an argument that’s passed to one of this class’ methods is passed to the corresponding Requests method. And an argument that’s set in
requests_kwargs
is passed to Requests during every call.The
url
argument is special. When making an HTTP request with Requests, an absolute URL is required. But when making an HTTP request with one of this class’ methods, either an absolute or a relative URL may be passed. If a relative URL is passed, it’s joined to this class’ default URL like so:>>> urljoin(self.request_kwargs['url'], passed_in_url)
This allows one to easily use the hrefs returned by Pulp in constructing new requests.
Parameters: - cfg (pulp_smash.config.PulpSmashConfig) – Information about a Pulp app.
- response_handler – A callback function, invoked after each request is
made. Must accept two arguments: a
pulp_smash.config.PulpSmashConfig
object, and arequests.Response
object. Defaults tosmart_handler()
. - request_kwargs – A dict of parameters to send with each request. This dict is merged into the default dict of parameters that’s sent with each request.
- pulp_host (pulp_smash.config.PulpHost) – The host with which to communicate. Defaults to the first host that fulfills the “api” role.
Supplementary information on writing response handlers.
This class accepts a
pulp_smash.config.PulpSmashConfig
parameter. This object may be accessed via the_cfg
attribute. This attribute should be used sparingly, as careless accesses can be an easy way to inadverdently create bugs. For example, if given the choice between callingself._cfg.get_request_kwargs()
or referencingself.request_kwargs
, reference the latter. To explain why, consider this scenario:>>> from pulp_smash import api, config >>> client = api.Client(config.get_config()) >>> client.request_kwargs['verify'] == '~/Documents/my.crt' >>> client.get('https://example.com')
The API client has been told to use an SSL certificate for verification. Yet if the client uses
self._cfg.get_requests_kwargs()
when making an HTTP GET call, the SSL certificate won’t be used.If this attribute is so problematic, why does it exist? It exists so that each API client may share context with its response handler. For example, a response handler might need to know which version of Pulp it is communicating with:
>>> def example_handler(client, response): ... if client._cfg.pulp_version < Version('3'): ... return pulp_2_procedure(response) ... else: ... return pulp_3_procedure(response)
However, this same logic could also be implemented by calling
pulp_smash.config.get_config()
:>>> def example_handler(client, response): ... if config.get_config().pulp_version < Version('3'): ... return pulp_2_procedure(response) ... else: ... return pulp_3_procedure(response)
Given this, why lug around a
pulp_smash.config.PulpSmashConfig
object? This is done because it is fundamentally correct for a response handler to learn about its calling API client’s state by accessing the calling API client, and it is fundamentally incorrect for a response handler to learn about its calling API client’s state by accessing a global cache. To illustrate, consider one possible failure scenario:- No settings file exists at any of the default load paths, e.g.
~/.config/pulp_smash/settings.json
. - An API client is created by reading a non-default configuration file.
- The API client makes a request, and a response handler is invoked to handle the response.
- The response handler needs to learn which version of Pulp is being
targeted.
- If it invokes
pulp_smash.config.get_config()
, no configuration file will be found, and an exception will be raised. - If it accesses the calling API client, it will find what it needs.
- If it invokes
Letting a response handler access its calling API client prevents incorrect behaviour in other scenarios too, such as when working with multi-threaded code.
Supplementary information on method signatures.
requests.post has the following signature:
requests.post(url, data=None, json=None, **kwargs)
However,
post()
has a different signature. Why? Pulp supports only JSON for most of its API endpoints, so it makes sense for us to demotedata
to being a regular kwarg and listjson
as the one and only positional argument.We make
json
a positional argument forpost()
,put()
, andpatch()
, but not the other methods. Why? Because HTTP OPTIONS, GET, HEAD and DELETE must not have bodies. This is stated by the HTTP/1.1 specification, and network intermediaries such as caches are at liberty to drop such bodies.Why is a sentinel object used in several function signatures? Imagine the following scenario: a user provides a default JSON payload in
self.request_kwargs
, but they want to skip sending that payload for just one request. How can they do that? Withclient.post(url, json=None)
.http://docs.python-requests.org/en/master/api/#requests.post
-
delete
(url, **kwargs)¶ Send an HTTP DELETE request.
-
get
(url, **kwargs)¶ Send an HTTP GET request.
-
head
(url, **kwargs)¶ Send an HTTP HEAD request.
-
options
(url, **kwargs)¶ Send an HTTP OPTIONS request.
-
patch
(url, json=<object object>, **kwargs)¶ Send an HTTP PATCH request.
-
post
(url, json=<object object>, **kwargs)¶ Send an HTTP POST request.
-
put
(url, json=<object object>, **kwargs)¶ Send an HTTP PUT request.
-
request
(method, url, **kwargs)¶ Send an HTTP request.
Arguments passed directly in to this method override (but do not overwrite!) arguments specified in
self.request_kwargs
.
-
using_handler
(response_handler)¶ Return a copy this same client changing specific handler dependency.
This method clones and injects a new handler dependency in to the existing client instance and then returns it.
This method is offered just as a ‘syntax-sugar’ for:
from pulp_smash import api, config def function(client): # This function needs to use a different handler other_client = api.Client(config.get_config(), other_handler) other_client.get(url)
with this method the above can be done in fewer lines:
def function(client): # already receives a client here client.using_handler(other_handler).get(url)
-
pulp_smash.api.
check_pulp3_restriction
(client)¶ Check if running system is running on Pulp3 otherwise raise error.
-
pulp_smash.api.
code_handler
(client, response)¶ Check the response status code, and return the response.
Unlike
safe_handler()
, this method doesn’t wait for asynchronous tasks to complete ifresponse
has an HTTP 202 status code.Raises: requests.exceptions.HTTPError
if the response status code is in the 4XX or 5XX range.
-
pulp_smash.api.
echo_handler
(client, response)¶ Immediately return
response
.
-
pulp_smash.api.
json_handler
(client, response)¶ Like
safe_handler
, but also return a JSON-decoded response body.Do what
pulp_smash.api.safe_handler()
does. In addition, decode the response body as JSON and return the result.
-
pulp_smash.api.
page_handler
(client, response)¶ Call
json_handler()
, optionally collect results, and return.Do the following:
- If
response
has an HTTP No Content (204) status code, returnresponse
. - Call
json_handler()
. - If the response appears to be paginated, walk through each page of results, and collect them into a single list. Otherwise, do nothing. Return either the list of results or the single decoded response.
Raises: ValueError
if the target Pulp application under test is older than version 3 or at least version 4.- If
-
pulp_smash.api.
poll_spawned_tasks
(cfg, call_report, pulp_host=None)¶ Recursively wait for spawned tasks to complete. Yield response bodies.
Recursively wait for each of the spawned tasks listed in the given call report to complete. For each task that completes, yield a response body representing that task’s final state.
Parameters: - cfg – A
pulp_smash.config.PulpSmashConfig
object. - call_report – A dict-like object with a call report structure.
- pulp_host – The host to poll. If
None
, a host will automatically be selected byClient
.
Returns: A generator yielding task bodies.
Raises: Same as
poll_task()
.- cfg – A
-
pulp_smash.api.
poll_task
(cfg, href, pulp_host=None)¶ Wait for a task and its children to complete. Yield response bodies.
Poll the task at
href
, waiting for the task to complete. When a response is received indicating that the task is complete, yield that response body and recursively poll each child task.Parameters: - cfg – A
pulp_smash.config.PulpSmashConfig
object. - href – The path to a task you’d like to monitor recursively.
- pulp_host – The host to poll. If
None
, a host will automatically be selected byClient
.
Returns: An generator yielding response bodies.
Raises: pulp_smash.exceptions.TaskTimedOutError – If a task takes too long to complete.
- cfg – A
-
pulp_smash.api.
safe_handler
(client, response)¶ Check status code, wait for tasks to complete, and check tasks.
Inspect the response’s HTTP status code. If the response has an HTTP Accepted status code, inspect the returned call report, wait for each task to complete, and inspect each completed task.
Raises: requests.exceptions.HTTPError
if the response status code is in the 4XX or 5XX range.Raises: - pulp_smash.exceptions.CallReportError – If the call report contains an error.
- pulp_smash.exceptions.TaskReportError – If the task report contains an error.
-
pulp_smash.api.
smart_handler
(client, response)¶ Decides which handler to call based on response content.
Do the following:
- Pass response through safe_handler to handle 202 and raise_for_status.
- Return the response if it is not Pulp 3.
- Return the response if it is not application/json type.
- Pass response through task_handler if is JSON 202 with ‘task’.
- Pass response through page_handler if is JSON but not 202 with ‘task’.
-
pulp_smash.api.
task_handler
(client, response)¶ Wait for tasks to complete and then collect resources.
Do the following:
- Call
json_handler()
to handle 202 and get call_report. - Raise error if response is not a task.
- Re-read the task by its _href to get the final state and metadata.
- Return the task’s created or updated resource or task final state.
Raises: ValueError
if the target Pulp application under test is older than version 3 or at least version 4.Usage examples:
Create a distribution using meth:json_handler:
client = Client(cfg, api.json_handler) spawned_task = client.post(DISTRIBUTION_PATH, body) # json_handler returns the task call report not the created entity spawned_task == {'task': ...} # to have the distribution it is needed to get the task's resources
Create a distribution using meth:task_handler:
client = Client(cfg, api.task_handler) distribution = client.post(DISTRIBUTION_PATH, body) # task_handler resolves the created entity and returns its data distribution == {'_href': ..., 'base_path': ...}
Having an existent client it is possible to use the shortcut:
client.using_handler(api.task_handler).post(DISTRIBUTION_PATH, body)
- Call