Framework for building REST API clients.
Reason this release was yanked:
Misconfigured setup
Project description
REST Client Framework
This package attempts to simplify the process of connecting to REST APIs by providing a flexible and extensible framework that handles most of the common and/or tedious tasks involved in performing the necessary HTTP requests and working with responses. Its goal is to minimize the time you must spend reinventing the wheel while providing sufficient flexibility to handle the sorts of quirks one sees in the real world. Useful features that this library either supports outright or provides hooks to facilitate your implementation include the following:
- Configurable logging of request and response content
- Automatic repetition of failed requests
- Simulation/interruption of requests in selected contexts (e.g. test environments)
- Automatic JSON encoding/decoding
- Conversion of REST objects into rich Python object instances (and back again)
- Consuming paged API responses
Quickstart
Here's an example of a quick-and-dirty client for the Google Chrome UX Report API built using this framework:
from rest_client_framework import Client
from rest_client_framework.request import JsonRequest as BaseRequest
from rest_client_framework.response import JsonResponse
class ChromeUXReportAPIClient(Client):
base_url = 'https://chromeuxreport.googleapis.com/v1/'
response_class = JsonResponse
def __init__(self, key):
self.key = key
super().__init__()
class Request(BaseRequest):
def __init__(self, client, *args, **kwargs):
kwargs['key'] = client.key
super().__init__(client, *args, **kwargs)
This code accomplishes the following:
- It declares the API's base URL by setting
ChromeUXReportAPIClient.base_url
- It specifies the default response class for the API client by setting
ChromeUXReportAPIClient.response_class
- It overrides
Client.__init__()
to accept an API key, which is stored as an attribute of the client instance - It overrides
Request.__init__()
to retrieve the API key from the client instance and pass it to the parent constructor as a keyword argument
We can now create and use an instance of this client:
>>> key = 'my API key'
>>> client = ChromeUXReportAPIClient(key)
>>> response = client.request(Request(
... client,
... 'records:queryRecord',
... method='POST',
... body={'url': 'https://www.python.org/'}
... ))
>>> type(response)
<class 'rest_client_framework.response.JsonResponse'>
>>> # The underlying HTTP response is available
>>> response.response.status_code
200
>>> # The response's JSON is available as a property
>>> response.json['record']['metrics']['largest_contentful_paint']['percentiles']
{'p75': 2539}
>>> # A response code outside the 200 range triggers an exception
>>> response = client.request(Request(
... client,
... 'asdf',
... method='POST',
... body={'url': 'https://www.python.org/'}
... ))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/rest_client_framework/client.py", line 166, in request
result = self.handle_response(
File "/rest_client_framework/client.py", line 96, in handle_response
raise ServiceResponseError(request, response)
rest_client_framework.exceptions.ServiceResponseError: The request failed with HTTP status 404.
>>> response = client.request(Request(
... client,
... 'records:queryRecord',
... method='POST',
... body={'url': 'https://foo.bar/'}
... ))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/rest_client_framework/client.py", line 166, in request
result = self.handle_response(
File "/rest_client_framework/client.py", line 96, in handle_response
raise ServiceResponseError(request, response)
rest_client_framework.exceptions.ServiceResponseError: The request failed with HTTP status 404.
Of course, this isn't all that much easier than simply using the requests
library directly. The real benefits of using this framework come from going a bit deeper. For simplicity's sake, all of the following definitions will take place in the same scope, although in the real world it may be desirable to create separate modules for the Client
subclass, the Request
subclass(es), and the Response
subclass(es).
from rest_client_framework import Client
from rest_client_framework.request import JsonRequest
from rest_client_framework.response import JsonRestResponse
from rest_client_framework.rest import RestObject
class HistogramBin(RestObject):
property_map = {
'start': None,
'end': None,
'density': None
}
class Metric(RestObject):
property_map = {
RestObject.__types__: {
'histogram': HistogramBin
},
'histogram': None,
'percentiles': {
'p75': 'percentile75'
}
}
class ChromeUXRecord(RestObject):
property_map = {
RestObject.__types__: {
'first_contentful_paint': Metric,
'largest_contentful_paint': Metric,
'cumulative_layout_shift': Metric,
'first_input_delay': Metric
},
'record': {
'key': {
'url': None,
'formFactor': None,
'effectiveConnectionType': None
},
'metrics': {
'first_contentful_paint': None,
'largest_contentful_paint': None,
'cumulative_layout_shift': None,
'first_input_delay': None
}
}
}
class QueryRecordRequest(JsonRequest):
def __init__(self, client, url, *args, **kwargs):
kwargs['key'] = client.key
super().__init__(client, 'records:queryRecord', method='POST', body={
'url': url
}, *args, **kwargs)
class QueryRecordResponse(JsonRestResponse):
REST_CLASS = ChromeUXRecord
class ChromeUXReportAPIClient(Client):
base_url = 'https://chromeuxreport.googleapis.com/v1/'
def __init__(self, key):
self.key = key
super().__init__()
def query_record(self, url):
return self.request(QueryRecordRequest(self, url), QueryRecordResponse)
In this version of the client, we've added a query_record()
method that eliminates the need to deal with request()
directly. We've also simplified the process of making the request by defining QueryRecordRequest
, which always uses the appropriate request URL and HTTP verb, so the user only needs to pass the URL for which she wants to obtain a record. We also implemented QueryRecordResponse
, which will not only convert the response JSON data to a dict
, but also use that data to instantiate a ChromeUXRecord
instance, which provides access to the response data as Python attributes. Here's how that ends up working out:
>>> from pprint import pprint
>>> key = 'my API key'
>>> client = ChromeUXReportAPIClient(key)
>>> response = client.query_record('https://www.cnn.com')
>>> response.instance.url
'https://www.cnn.com/'
>>> # This property was not in the response, so it was initialized as None
>>> response.instance.form_factor
>>> # The value of this attribute is a Metric instance
>>> response.instance.first_contentful_paint
<__main__.Metric object at 0x103dfee50>
>>> response.instance.first_contentful_paint.percentile75
3162
>>> # The value of this attribute is a list of HistogramBin instances
>>> pprint(response.instance.first_contentful_paint.histogram)
[<__main__.HistogramBin object at 0x103e1d1f0>,
<__main__.HistogramBin object at 0x103e1d190>,
<__main__.HistogramBin object at 0x103e1d220>]
>>> response.instance.first_contentful_paint.histogram[0].start
0
>>> response.instance.first_contentful_paint.histogram[0].density
0.45352676338169273
>>> pprint(response.instance.first_contentful_paint.as_rest())
{'histogram': [{'density': 0.45352676338169273, 'end': 1800, 'start': 0},
{'density': 0.2813406703351688, 'end': 3000, 'start': 1800},
{'density': 0.26513256628313814, 'start': 3000}],
'percentiles': {'p75': 3162}}
With just a little more code than in the quick-and-dirty version, we have a more semantically friendly API client. The goal of this framework is to let you easily implement REST objects in the Python domain that have features like rich typing, default values, or anything else you can do with Python.
The Client
class
The rest_client_framework.client.Client
class handles the mechanics of issuing requests and receiving responses.
Attributes
The Client
class has the following class-level attributes that are intended to be user-customizable:
base_url
Default value: None
This should contain the common base portion of the API's URLs, which will be joined with paths specified per-request. Note that it is possible to specify full or root-relative URLs per request, so the value of this attribute can be overridden, though that isn't the general intention.
max_attempts
Default value: 3
This specifies the maximum number of times that a Client
instance will attempt a failing request. Note that the default Client
behavior is to never repeat any requests; subclasses must define the circumstances under which this takes place.
response_class
Default value: None
This specifies the default class that will be used to instantiate responses, which should be a subclass of rest_client_framework.response.Response
. If this is left unspecified, the client will look for a module named response
one level up from the module that defines the Client
subclass. If found, it will attempt to import it and look for an object named Response
in that module, which will be used as the default response class. If this operation fails, rest_client_framework.response.Response
will be used as the default response class.
request_log_level
Default value: logging.DEBUG
This specifies the logging level for basic request information, which includes the request method, URL, and body (unless the user overrides this behavior; see the documentation on the Request
class) as well as the current attempt count.
request_verbose_log_level
Default value: logging.NOTSET
This specifies the level at which to log each request's HTTP headers.
response_log_level
Default value: logging.DEBUG
This specifies the logging level for basic response information, which includes the content length and HTTP status code.
response_verbose_log_level
Default value: logging.NOTSET
This specifies the logging level for detailed response information, including all response HTTP headers and the full body content.
verbose_name
Default value: None
This attribute, if set, will be used as the return value of the Client.get_verbose_name()
method; otherwise the value of Client.__module__
will be used. This framework does not use this feature, but users may find it helpful.
Methods
The behavior of Client
subclasses may be modified by overriding certain methods, the most important of which are documented here.
__init__()
Most concrete Client
subclasses will probably need to override the initializer to accept parameters such as authorization keys, either by accepting them as arguments or extracting them from a configuration object.
get_transport()
This method should return the mechanism to be used to perform requests, which may be anything that defines a method named request()
that accepts the same arguments as requests.request()
. By default, this is the requests
module, but subclasses may override this to do things like return instances of requests.Session
containing persistent authorization headers, or even to prevent requests from taking place at all by returning an instance of rest_client_framework.client.NoOpTransport
.
get_logger()
This method should return the logging.Logger
instance that the subclass should use to perform its logging. By default, this instance is identified by the subclass' module, as is standard Python practice.
request(request, response_class_or_instance=None, **response_class_kwargs)
This method is the centerpiece of the Client
class. Subclasses may wish to override this to examine or modify requests in some way prior to calling the base method.
should_skip_request(request)
If this method returns a true value, the request will not take place. The default implementation is a no-op, but subclasses may use this to short-circuit a request based on external factors.
handle_response(request, response, response_class_or_instance, **response_class_kwargs)
This method is called after the completion of each HTTP request. If it returns an instance of rest_client_framework.response.Response
, that value is used in turn as the return value of the Client.request()
method. Subclasses may override this method to return Client.RETRY
if the response has a certain characteristic (for example, if its HTTP status is in the 500 range); in this scenario, the request will be repeated, provided the total number of attempts has not yet been exceeded. The arguments passed to this method are 1) an instance of rest_client_framework.request.Request
, 2) an instance of requests.Response
, 3) a subclass of rest_client_framework.response.Response
or an instance thereof, and finally any keyword arguments necessary to instantiate the final response (note that these are only used when a class is passed as the third argument, not an instance).
prepare_for_retry(request)
This hook is called after Client.handle_response()
returns Client.RETRY
and the number of attempts has not yet been exhausted. The default implementation is a no-op, but subclasses may use this to do things like implementing delays.
finalize_request(request)
This hook is called after a completed request that will not be retried. The default implementation is a no-op.
simulate_requests(request_type=Client.REQUEST_TYPE_WRITE)
This context manager declares a context in which read and/or write requests may be simulated for testing purposes, which means that Client.request()
will behave as if they succeeded without actually performing any HTTP request (naturally, the response will not contain any content). The argument to this context manager should be a bitmask of the constants Client.REQUEST_TYPE_READ
and Client.REQUEST_TYPE_WRITE
, the value of which will be set on the Client
instance's simulation_context
attribute. Concrete subclasses must define behavior that examines this attribute and behaves as desired. The principal purpose of this feature is to facilitate testing.
The Request
class
The rest_client_framework.request.Request
class encapsulates the characteristics of an API request.
Properties
The Request
class defines the following dynamically-computed properties:
url
This returns the full request URL, resolved against the base URL specified in the Client
subclass.
signature
This returns a binary SHA1 hash incorporating the request's GET parameters (as returned by Request.get_serializable_args()
) and body (as returned by Request.get_serializable_body()
). This is intended to facilitate deduplication. Note that by default, the hash does not consider the request URL.
Methods
The following methods are those most likely to require overriding in subclasses.
__init__(client, path, *, method='GET', body=None, headers=None, **kwargs)
Instantiates a request. Typically, the value of path
will be relative to client.base_url
, but absolute URLs may be passed as well. The value of body
will be transformed by the get_formatted_body()
method before it is passed to the client's transport. If headers
is provided, it will be passed directly to the client's transport, so typically it should be a dict
instance as expected by requests.request()
. Any additional keyword arguments will be used as GET parameters.
__str__()
The default return value of this method includes the request method, the full request URL, and the request body (if present). The latter two attributes are returned by the Request.get_printable_url()
and Request.get_printable_body()
methods, which subclasses may override to remove sensitive components such as API keys.
get_serializable_args()
This method should return the request's GET parameters as suitable for deduplication hash evaluation, which may mean the removal of temporary authentication tokens. The default implementation returns the GET parameters unaltered.
get_serializable_body()
This method should return the request's body as suitable for deduplication hash evaluation, which may mean the removal of temporary authentication tokens. The default implementation returns the body unaltered.
get_printable_url()
This method should return a version of the request URL suitable for representation in logs and the like, which may require the removal of sensitive parameters like API keys. The default implementation returns the full unaltered URL.
get_printable_body()
This method should return the request body as suitable for representation in logs and the like, which may require the removal of sensitive parameters like API keys. The default implementation returns the unaltered body.
get_formatted_body()
This method should return the request body as suitable for submission in API requests. The default implementation returns the unaltered body, while rest_client_framework.request.JsonRequest
overrides this method to convert the body to a JSON string.
get_headers()
This method should return HTTP headers to be included in the request. The default implementation returns the headers provided upon instantiation; subclasses may use this as a hook to modify them.
set_basic_authentication(user, password)
Sets a username and password for services that use basic HTTP authentication.
Response classes
The rest_client_framework.response
module provides a variety of classes and mixins for working with HTTP responses from REST APIs. All concrete response subclasses should inherit from rest_client_framework.response.Response
. The base class itself does nothing more than serve as a wrapper for the Client
instance, the Request
instance, and the underlying HTTP response (a requests.Response
instance). This package's subclasses provide additional functionality, and users should extend these as needed.
JsonResponse
This class adds the cached property json
, which provides more convenient access to the underlying json()
method of the requests.Response
instance.
JsonRestResponse
In addition to the behavior defined in JsonResponse
, this class also supports the declaration of a REST type via the REST_CLASS
class attribute. This should be a subclass of rest_client_framework.rest.RestObject
, and the cached property instance
will return an instance of this class constructed from the JSON data in the HTTP response.
JsonSequenceRestResponse
In addition to the behavior defined in JsonRestResponse
, this class facilitates working with HTTP responses that contain a series of REST objects (as opposed to a single object). Subclasses should set the class attribute CONTAINER_PROPERTY
to the key that maps to the object series in the JSON data. The get_series_from_response()
method will iterate through this sequence to create a series of instances of the appropriate RestObject
subclass. This class also defines __len__()
, __getitem__()
, and __iter__()
.
CachingJsonSequenceRestResponse
Like JsonSequenceRestResponse
, but caches the instantiated objects in memory.
PagingMixin
This mixin provides some basic logic for handling paged API responses. When a response subclass that includes this mixin is used, no HTTP request takes place until the response's pages()
method is called. This method is a generator that performs an HTTP request on each iteration. The get_next_request()
method, which concrete subclasses must define, will be called at the end of each iteration. If it returns a Request
instance, it will be issued on the next iteration. If it returns None
, the loop terminates.
The RestObject
class
The rest_client_framework.rest.RestObject
class is the base class for working with REST objects in the Python domain.
Attributes
The following configuration attributes govern the behavior of RestObject
subclasses.
property_map
This is the single most important RestObject
attribute. It should be a dict
whose keys correspond to REST objects returned by the API. Each key should map to None
, a string, or a nested dict
. When a key maps to a string, it will be used as an instance attribute name whose value will contain the corresponding content in the remote object. If the key maps to None
, the behavior is the same, but the attribute name will be based on the corresponding key in the remote object; depending on the value of the class-level use_pythonic_attribute_names
attribute, the attribute name will be either the unaltered key from the remote object or an automatically-generated equivalent with Python semantics. When a key maps to a dict
, the remote object will also be expected to contain a mapping under the corresponding key; this feature may be used to flatten remote structures. Some examples will make this easier to understand. Suppose a REST service returns the following JSON:
{
"monty": "python",
"cheeseTypes": [
"Wensleydale",
"Gouda",
"Edam"
],
"parrot": {
"breed": "Norwegian Blue",
"plumage": "beautiful",
"pinesFor": "fjords"
},
"extraData": "foo"
}
Then, consider the following RestObject
subclass:
class MyRestObject(RestObject):
property_map = {
'monty': 'my_attribute_name',
'cheeseTypes': None,
'parrot': {
'breed': None,
'plumage': 'feathers',
'pinesFor': None
},
'lumberjackStatus': None
}
When the REST data is converted to a Python dict
and passed as keyword arguments to MyRestObject.__init__()
via the **
operator, we have the following:
>>> obj = MyRestObject(**data)
>>> obj.my_attribute_name
'python'
>>> obj.cheese_types
['Wensleydale', 'Gouda', 'Edam']
>>> obj.breed
'Norwegian Blue'
>>> obj.feathers
'beautiful'
>>> obj.pines_for
'fjords'
>>> obj.lumberjack_status
>>> obj.extra_data
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/rest_client_framework/rest.py", line 456, in __getattr__
raise AttributeError(name)
AttributeError: extra_data
>>> obj.extraData
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/rest_client_framework/rest.py", line 456, in __getattr__
raise AttributeError(name)
AttributeError: extraData
The REST property monty
maps to the local attribute name my_attribute_name
, as provided in the property_map
configuration. Because cheeseTypes
maps to None
, and the default value of RestObject.use_pythonic_attribute_names
is True
, the camel-cased name is converted to the Pythonic cheese_types
. Because parrot
maps to a nested mapping, the contents of the remote property are flattened, and the resulting Python attributes have the same scope as the others. Although the example REST object did not contain the lumberjackStatus
property, because the property_map
configuration defines it, it was initialized to a default value of None
in the MyRestObject
instance. Finally, although the REST data contained the property extraData
, because it was omitted from the property_map
configuration, it was silently discarded.
RestObject
instances can be converted back to their REST form as well, minus any properties that were discarded upon instantiation, via the as_rest()
method:
>>> from pprint import pprint
>>> pprint(obj.as_rest())
{'cheeseTypes': ['Wensleydale', 'Gouda', 'Edam'],
'monty': 'python',
'parrot': {'breed': 'Norwegian Blue',
'pinesFor': 'fjords',
'plumage': 'beautiful'}}
The RestObject
class defines a series of objects that have special meanings when used as property_map
keys.
RestObject.__defaults__
This should map to a dict
that associates names with default values. The preferred way to declare these is to map Python attribute names to default values at the top level of property_map
, but it's possible to nest this in a deeper node or to use remote REST property names. For example:
class MyRestObject(RestObject):
property_map = {
RestObject.__defaults__: {
'lumberjack_status': 'ok'
},
'monty': 'my_attribute_name',
'cheeseTypes': None,
'parrot': {
RestObject.__defaults__: {
'pinesFor': 'Oslo'
},
'breed': None,
'plumage': 'feathers',
'pinesFor': None
},
'lumberjackStatus': None
}
>>> obj = MyRestObject(**{
... 'cheeseTypes': ['Cheddar', 'Limburger'],
... 'parrot': {
... 'breed': 'Ex-parrot',
... 'plumage': 'withering'
... }
... })
>>> obj.lumberjack_status
'ok'
>>> obj.pines_for
'Oslo'
RestObject.__types__
This should map to a dict
that associates attributes with required types. As with RestObject.__defaults__
, the preferred practice is to set this at the top level and key it on Python attribute names, but the same flexibility is supported. This is useful for sanity checking on input data. In the examples above, cheese_types
has been a list
, because the instantiation data provided it as such, but the configurations we have used so far do not enforce this:
>>> obj = MyRestObject(cheeseTypes='Mozzarella')
>>> obj.cheese_types
'Mozzarella'
However, we can require that this property be a list as follows:
class MyRestObject(RestObject):
property_map = {
RestObject.__types__: {
'cheese_types': list
},
'monty': 'my_attribute_name',
'cheeseTypes': None,
'parrot': {
'breed': None,
'plumage': 'feathers',
'pinesFor': None
},
'lumberjackStatus': None
}
>>> obj = MyRestObject(cheeseTypes='Mozzarella')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/rest_client_framework/rest.py", line 411, in __init__
set_attrs = self._set_data(kwargs or {}, self._resolved_property_map)
File "/rest_client_framework/rest.py", line 539, in _set_data
setattr(self, attr_name, val)
File "/rest_client_framework/rest.py", line 441, in __setattr__
getattr(
File "/rest_client_framework/rest.py", line 694, in set_list_attribute
raise TypeError('The attribute "{}" requires a list value.'.format(attr_name.lstrip('_')))
TypeError: The attribute "cheese_types" requires a list value.
This mechanism can also be used to instantiate nested data via RestObject
subclasses:
class ParrotInfo(RestObject):
property_map = {
'breed': None,
'plumage': 'feathers',
'pinesFor': None
}
class MyRestObject(RestObject):
property_map = {
RestObject.__types__: {
'cheese_types': list,
'parrot': ParrotInfo
},
'monty': 'my_attribute_name',
'cheeseTypes': None,
'parrot': None,
'lumberjackStatus': None
}
>>> obj = MyRestObject(cheeseTypes=['Gouda', 'Cheddar'], parrot={
... 'breed': 'African Grey',
... 'pinesFor': 'Serengeti'
... })
>>> obj.parrot
<__main__.ParrotInfo object at 0x102f3bd00>
>>> obj.parrot.pines_for
'Serengeti'
In order to avoid circular dependency resolution issues, the REST type name may be provided as a string. If it is not a fully-qualified Python path, the discovery mechanism will look for the class in the current module:
class MyRestObject(RestObject):
property_map = {
RestObject.__types__: {
'cheese_types': list,
'parrot': 'ParrotInfo'
},
'monty': 'my_attribute_name',
'cheeseTypes': None,
'parrot': None,
'lumberjackStatus': None
}
class ParrotInfo(RestObject):
property_map = {
'breed': None,
'plumage': 'feathers',
'pinesFor': None
}
>>> obj = MyRestObject(parrot={'plumage': 'Majestic'})
>>> obj.parrot.feathers
'Majestic'
Nested REST objects are converted back to their REST form when the parent object's as_rest()
method is executed:
>>> obj = MyRestObject(cheeseTypes=['Gouda', 'Cheddar'], parrot={
... 'breed': 'African Grey',
... 'pinesFor': 'Serengeti'
... })
>>> pprint(obj.as_rest())
{'cheeseTypes': ['Gouda', 'Cheddar'],
'parrot': {'breed': 'African Grey', 'pinesFor': 'Serengeti'}}
Note that there is a similar as_dict()
method, which casts the parent object as a dict
, but not any nested REST objects:
>>> pprint(obj.as_dict())
{'cheeseTypes': ['Gouda', 'Cheddar'],
'parrot': <__main__.ParrotInfo object at 0x102f41be0>}
Internally, to perform type-checking for types other than RestObject
subclasses, the setter will attempt to execute a method named set_TYPE_attribute()
, with "TYPE" replaced with the lowercase type name. RestObject
provides setters for the following types:
list
dict
bool
int
float
datetime.datetime
Users may, of course, implement their own behavior for other types.
RestObject.__readonly__
This should be a list of REST properties to be treated as read-only, which means that they are excluded from the REST representation. The list may contain any Python attribute name when defined at the top level. The list may also specify REST property names, in which case it should be defined at the same level as the corresponding name in property_map
. For example:
class MyRestObject(RestObject):
property_map = {
RestObject.__readonly__: ['lumberjack_status'],
'monty': 'my_attribute_name',
'cheeseTypes': None,
'parrot': {
RestObject.__readonly__: ['pinesFor'],
'breed': None,
'plumage': 'feathers',
'pinesFor': None
},
'lumberjackStatus': None
}
>>> obj = MyRestObject(
... lumberjackStatus='ok',
... monty='python',
... parrot={
... 'breed': 'Norwegian Blue',
... 'pinesFor': 'fjords'
... }
... )
>>> obj.lumberjack_status
'ok'
>>> obj.pines_for
'fjords'
>>> obj.as_rest()
{'monty': 'python', 'parrot': {'breed': 'Norwegian Blue'}}
Note that even though obj.lumberjack_status
and obj.pines_for
were initialized with the expected values, they are excluded when converting the object back to its REST form.
RestObject.__order__
This feature can be used to control the order in which attributes are set. Consider the following RestObject
subclass:
class MyRestObject(RestObject):
property_map = {
'monty': 'my_attribute_name',
'cheeseTypes': None,
'parrot': {
'breed': None,
'plumage': 'feathers',
'pinesFor': None
},
'lumberjackStatus': None
}
@property
def feathers(self):
return self._feathers
@feathers.setter
def feathers(self, val):
self._feathers = val
if self._feathers:
self.description = 'Lovely bird, the {}! {} plumage.'.format(
self.breed, self._feathers
)
As a side effect of setting the feathers
attribute (which is now a Python dynamic property), this object attempts to set the description
attribute. However, this will fail to work as expected if the breed
attribute has not been set. Because the order in which attributes are set is non-deterministic by default, this could cause the code to fail. However, we can ensure that breed
is always set first as follows:
class MyRestObject(RestObject):
property_map = {
'monty': 'my_attribute_name',
'cheeseTypes': None,
'parrot': {
RestObject.__order__: ['breed'],
'breed': None,
'plumage': 'feathers',
'pinesFor': None
},
'lumberjackStatus': None
}
@property
def feathers(self):
return self._feathers
@feathers.setter
def feathers(self, val):
self._feathers = val
if self._feathers:
self.description = 'Lovely breed, the {}! {} plumage.'.format(
self.breed, self._feathers
)
In this situation, it isn't necessary to declare where feathers
should fall in the order, since the only requirement is that it be set after breed
. RestObject.__order__
should map to a list or tuple of names. These may be either Python attribute names or REST property names, but in either case, the order must be declared at the same node level as the corresponding REST properties.
use_pythonic_attribute_names
If true, camel-case REST property names whose names are not specified in property_map
will be converted to Python-style underscore-separated lowercase names. If false, no such conversion will take place.
include_null_properties
By default, when RestObject
instances are converted to their REST form via as_rest()
, attributes whose value is None
or an empty list
or dict
are excluded:
class MyRestObject(RestObject):
property_map = {
RestObject.__types__: {
'cheeseTypes': list
},
'monty': 'my_attribute_name',
'cheeseTypes': None,
'parrot': {
'breed': None,
'plumage': 'feathers',
'pinesFor': None
},
'lumberjackStatus': None
}
>>> MyRestObject().as_rest()
{}
However, if a REST property name is declared in include_null_properties
, it will be included in the output even if its value is empty. To exercise this preference for nested properties, use dotted path syntax. For example:
class MyRestObject(RestObject):
include_null_properties = ('cheeseTypes', 'parrot.plumage')
property_map = {
RestObject.__types__: {
'cheeseTypes': list
},
'monty': 'my_attribute_name',
'cheeseTypes': None,
'parrot': {
'breed': None,
'plumage': 'feathers',
'pinesFor': None
},
'lumberjackStatus': None
}
>>> MyRestObject().as_rest()
{'cheeseTypes': [], 'parrot': {'plumage': None}}
datetime_formats
For RestObject
subclasses that enforce the datetime.datetime
type for any attribute values, this tuple should contain one or more strptime
-compatible format strings. The set_datetime_attribute()
method will iterate through these to attempt to convert string values to datetime.datetime
instances, stopping at the first one that succeeds. For example:
from datetime import datetime
class MyRestObject(RestObject):
datetime_formats = ('%Y-%m-%d', '%m/%d/%Y %H:%M:%S')
property_map = {
RestObject.__types__: {
'timestamp': datetime
},
'monty': 'my_attribute_name',
'cheeseTypes': None,
'parrot': {
RestObject.__types__: {
'birthday': datetime
},
'breed': None,
'plumage': 'feathers',
'pinesFor': None,
'birthday': None
},
'timestamp': None
}
>>> obj = MyRestObject(parrot={'birthday': '1976-04-10'}, timestamp='10/22/2021 13:45:00')
>>> obj.birthday
datetime.datetime(1976, 4, 10, 0, 0)
>>> obj.timestamp
datetime.datetime(2021, 10, 22, 13, 45)
When converting back to the REST representation, the same format that was successful at parsing a datetime will be used to convert it back to a string:
>>> obj.as_rest()
{'timestamp': '10/22/2021 13:45:00', 'parrot': {'birthday': '1976-04-10'}}
Methods
The following are some of the important RestObject
methods to know about.
exclude_properties(*names, merge_contexts=False)
This is a context manager for selectively omitting properties when converting to REST format. Property names should be specified via dotted path syntax. For example:
class MyRestObject(RestObject):
property_map = {
'monty': 'my_attribute_name',
'cheeseTypes': None,
'parrot': {
'breed': None,
'plumage': 'feathers',
'pinesFor': None
},
'lumberjackStatus': None
}
>>> obj = MyRestObject(**{
... "monty": "python",
... "cheeseTypes": [
... "Wensleydale",
... "Gouda",
... "Edam"
... ],
... "parrot": {
... "breed": "Norwegian Blue",
... "plumage": "beautiful",
... "pinesFor": "fjords"
... },
... "extraData": "foo"
... })
>>> with obj.exclude_properties('monty', 'parrot.pinesFor'):
... obj.as_rest()
...
{'cheeseTypes': ['Wensleydale', 'Gouda', 'Edam'], 'parrot': {'breed': 'Norwegian Blue', 'plumage': 'beautiful'}}
The merge_contexts
parameter governs what happens when nesting contexts. By default, the inner context overrides the outer one entirely, but they can also be merged:
>>> with obj.exclude_properties('monty', 'parrot.pinesFor'):
... with obj.exclude_properties('cheeseTypes'):
... obj.as_rest()
...
{'monty': 'python', 'parrot': {'breed': 'Norwegian Blue', 'pinesFor': 'fjords', 'plumage': 'beautiful'}}
>>> with obj.exclude_properties('monty', 'parrot.pinesFor'):
... with obj.exclude_properties('cheeseTypes', merge_contexts=True):
... obj.as_rest()
...
{'parrot': {'breed': 'Norwegian Blue', 'plumage': 'beautiful'}}
include_readonly()
This is a context manager that allows REST properties that would otherwise be read-only to be included in REST representation. For example:
class MyRestObject(RestObject):
property_map = {
RestObject.__readonly__: ['lumberjack_status'],
'monty': 'my_attribute_name',
'cheeseTypes': None,
'parrot': {
RestObject.__readonly__: ['pinesFor'],
'breed': None,
'plumage': 'feathers',
'pinesFor': None
},
'lumberjackStatus': None
}
>>> obj = MyRestObject(lumberjackStatus='ok', parrot={'breed': 'Norwegian Blue', 'pinesFor': 'fjords'})
>>> obj.as_rest()
{'parrot': {'breed': 'Norwegian Blue'}}
>>> with obj.include_readonly():
... obj.as_rest()
...
{'lumberjackStatus': 'ok', 'parrot': {'breed': 'Norwegian Blue', 'pinesFor': 'fjords'}}
is_empty(prop, val)
Returns True
if the given val
should be considered empty for the given prop
, False
otherwise. Subclasses may wish to override this method to take their own nuances into account.
format_rest_property(name, value)
Formats the property value
(identified by name
) for REST representation. Subclasses may wish to override this method to take their own nuances into account.
as_rest()
Converts the instance to REST representation. Internally, this method activates a context that sets the value of the instance's __rest__
attribute to True
; subclasses can refer to this to make decisions about behavior.
set_excluded_properties(*names)
This method is the non-contextual equivalent of the exclude_properties()
context manager.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for rest-client-framework-0.1.tar.gz
Algorithm | Hash digest | |
---|---|---|
SHA256 | 3337c697343f6c229880effa85895ace929b10c7946b8b1ce2d35de8248c0d27 |
|
MD5 | de83b5b73c92fec9ed982853fef6b094 |
|
BLAKE2b-256 | e33e3dae9c659742856a2f3ff414115aedd369e209bb57097eeb14153d833e3f |
Hashes for rest_client_framework-0.1-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | f95115adbd913727fd3f32942c03e73a5892516d71fd3d8b818811098327a137 |
|
MD5 | e89f774380f878cc7a09b581d23e6dbc |
|
BLAKE2b-256 | 0a162e8c3a571032255792af221811166e5c6f94f9747d6a628f022f3fcbf42d |