Package: rest-client-framework
REST Client FrameworkThis 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 contentAutomatic repetition of failed requestsSimulation/interruption of requests in selected contexts (e.g. test environments)Automatic JSON encoding/decodingConversion of REST objects into rich Python object instances (and back again)Consuming paged API responsesQuickstartHere's an example of a quick-and-dirty client for theGoogle Chrome UX Report APIbuilt using this framework:fromrest_client_frameworkimportClientfromrest_client_framework.requestimportJsonRequestasBaseRequestfromrest_client_framework.responseimportJsonResponseclassChromeUXReportAPIClient(Client):base_url='https://chromeuxreport.googleapis.com/v1/'response_class=JsonResponsedef__init__(self,key):self.key=keysuper().__init__()classRequest(BaseRequest):def__init__(self,client,*args,**kwargs):kwargs['key']=client.keysuper().__init__(client,*args,**kwargs)This code accomplishes the following:It declares the API's base URL by settingChromeUXReportAPIClient.base_urlIt specifies the default response class for the API client by settingChromeUXReportAPIClient.response_classIt overridesClient.__init__()to accept an API key, which is stored as an attribute of the client instanceIt overridesRequest.__init__()to retrieve the API key from the client instance and pass it to the parent constructor as a keyword argumentWe 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 therequestslibrary 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 theClientsubclass, theRequestsubclass(es), and theResponsesubclass(es).fromrest_client_frameworkimportClientfromrest_client_framework.requestimportJsonRequestfromrest_client_framework.responseimportJsonRestResponsefromrest_client_framework.restimportRestObjectclassHistogramBin(RestObject):property_map={'start':None,'end':None,'density':None}classMetric(RestObject):property_map={RestObject.__types__:{'histogram':HistogramBin},'histogram':None,'percentiles':{'p75':'percentile75'}}classChromeUXRecord(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}}}classQueryRecordRequest(JsonRequest):def__init__(self,client,url,*args,**kwargs):kwargs['key']=client.keysuper().__init__(client,'records:queryRecord',method='POST',body={'url':url},*args,**kwargs)classQueryRecordResponse(JsonRestResponse):REST_CLASS=ChromeUXRecordclassChromeUXReportAPIClient(Client):base_url='https://chromeuxreport.googleapis.com/v1/'def__init__(self,key):self.key=keysuper().__init__()defquery_record(self,url):returnself.request(QueryRecordRequest(self,url),QueryRecordResponse)In this version of the client, we've added aquery_record()method that eliminates the need to deal withrequest()directly. We've also simplified the process of making the request by definingQueryRecordRequest, 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 implementedQueryRecordResponse, which will not only convert the response JSON data to adict, but also use that data to instantiate aChromeUXRecordinstance, 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.ExceptionsOther than Python built-in exceptions, all exceptions raised byrest_client_frameworkare defined inrest_client_framework.exceptions. The base class for all of these isRuntimeError. The class hierarchy is as follows:RuntimeError
 +-- RestRuntimeError
      +-- ServiceResponseError
      +-- MaximumAttemptsExceeded
 +-- ConfigurationError
      +-- WebServiceDefinitionError
      +-- RestDefinitionError
           +-- AttributeCollisionError
           +-- AmbiguousDatetimeFormatError
           +-- AmbiguousOrderedSequenceErrorServiceResponseErrorThis is the error raised byrest_client_framework.Clientwhen the remote service responds with an HTTP status of 400 or greater.MaximumAttemptsExceededThis is the error raised byrest_client_framework.Clientafter retrying a failing request the maximum allowable number of times without receiving a successful response.WebServiceDefinitionErrorThis error is raised when the user attempts to extend certain base classes without providing required configuration values.RestDefinitionErrorThis is the common base class for several more specific errors that may result from misconfiguration ofrest_client_framework.rest.RestObjectsubclasses.TheClientclassTherest_client_framework.client.Clientclass handles the mechanics of issuing requests and receiving responses.AttributesTheClientclass has the following class-level attributes that are intended to be user-customizable:base_urlDefault value:NoneThis 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_attemptsDefault value: 3This specifies the maximum number of times that aClientinstance will attempt a failing request. Note that the defaultClientbehavior is to never repeat any requests; subclasses must define the circumstances under which this takes place.response_classDefault value:NoneThis specifies the default class that will be used to instantiate responses, which should be a subclass ofrest_client_framework.response.Response. If this is left unspecified, the client will look for a module namedresponseone level up from the module that defines theClientsubclass. If found, it will attempt to import it and look for an object namedResponsein that module, which will be used as the default response class. If this operation fails,rest_client_framework.response.Responsewill be used as the default response class.request_log_levelDefault value:logging.DEBUGThis 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 theRequestclass) as well as the current attempt count.request_verbose_log_levelDefault value:logging.NOTSETThis specifies the level at which to log each request's HTTP headers.response_log_levelDefault value:logging.DEBUGThis specifies the logging level for basic response information, which includes the content length and HTTP status code.response_verbose_log_levelDefault value:logging.NOTSETThis specifies the logging level for detailed response information, including all response HTTP headers and the full body content.verbose_nameDefault value:NoneThis attribute, if set, will be used as the return value of theClient.get_verbose_name()method; otherwise the value ofClient.__module__will be used. This framework does not use this feature, but users may find it helpful.MethodsThe behavior ofClientsubclasses may be modified by overriding certain methods, the most important of which are documented here.__init__()Most concreteClientsubclasses 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 namedrequest()that accepts the same arguments asrequests.request(). By default, this is therequestsmodule, but subclasses may override this to do things like return instances ofrequests.Sessioncontaining persistent authorization headers, or even to prevent requests from taking place at all by returning an instance ofrest_client_framework.client.NoOpTransport.get_logger()This method should return thelogging.Loggerinstance 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 theClientclass. 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 ofrest_client_framework.response.Response, that value is used in turn as the return value of theClient.request()method. Subclasses may override this method to returnClient.RETRYif 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 ofrest_client_framework.request.Request, 2) an instance ofrequests.Response, 3) a subclass ofrest_client_framework.response.Responseor 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 afterClient.handle_response()returnsClient.RETRYand 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 thatClient.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 constantsClient.REQUEST_TYPE_READandClient.REQUEST_TYPE_WRITE, the value of which will be set on theClientinstance'ssimulation_contextattribute. Concrete subclasses must define behavior that examines this attribute and behaves as desired. The principal purpose of this feature is to facilitate testing.TheRequestclassTherest_client_framework.request.Requestclass encapsulates the characteristics of an API request.PropertiesTheRequestclass defines the following dynamically-computed properties:urlThis returns the full request URL, resolved against the base URL specified in theClientsubclass.signatureThis returns a binary SHA1 hash incorporating the request's GET parameters (as returned byRequest.get_serializable_args()) and body (as returned byRequest.get_serializable_body()). This is intended to facilitate deduplication. Note that by default, the hash does not consider the request URL.MethodsThe 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 ofpathwill be relative toclient.base_url, but absolute URLs may be passed as well. The value ofbodywill be transformed by theget_formatted_body()method before it is passed to the client's transport. Ifheadersis provided, it will be passed directly to the client's transport, so typically it should be adictinstance as expected byrequests.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 theRequest.get_printable_url()andRequest.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, whilerest_client_framework.request.JsonRequestoverrides 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 classesTherest_client_framework.responsemodule provides a variety of classes and mixins for working with HTTP responses from REST APIs. All concrete response subclasses should inherit fromrest_client_framework.response.Response. The base class itself does nothing more than serve as a wrapper for theClientinstance, theRequestinstance, and the underlying HTTP response (arequests.Responseinstance). This package's subclasses provide additional functionality, and users should extend these as needed.JsonResponseThis class adds the cached propertyjson, which provides more convenient access to the underlyingjson()method of therequests.Responseinstance.JsonRestResponseIn addition to the behavior defined inJsonResponse, this class also supports the declaration of a REST type via theREST_CLASSclass attribute. This should be a subclass ofrest_client_framework.rest.RestObject, and the cached propertyinstancewill return an instance of this class constructed from the JSON data in the HTTP response.JsonSequenceRestResponseIn addition to the behavior defined inJsonRestResponse, 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 attributeCONTAINER_PROPERTYto the key that maps to the object series in the JSON data. Theget_series_from_response()method will iterate through this sequence to create a series of instances of the appropriateRestObjectsubclass. This class also defines__len__(),__getitem__(), and__iter__().CachingJsonSequenceRestResponseLikeJsonSequenceRestResponse, but caches the instantiated objects in memory.PagingMixinThis 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'spages()method is called. This method is a generator that performs an HTTP request on each iteration. Theget_next_request()method, which concrete subclasses must define, will be called at the end of each iteration. If it returns aRequestinstance, it will be issued on the next iteration. If it returnsNone, the loop terminates.TheRestObjectclassTherest_client_framework.rest.RestObjectclass is the base class for working with REST objects in the Python domain.AttributesThe following configuration attributes govern the behavior ofRestObjectsubclasses.property_mapThis is the single most importantRestObjectattribute. It should be adictwhose keys correspond to REST objects returned by the API. Each key should map toNone, a string, or a nesteddict. 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 toNone, 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-leveluse_pythonic_attribute_namesattribute, 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 adict, 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 followingRestObjectsubclass:classMyRestObject(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 Pythondictand passed as keyword arguments toMyRestObject.__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: extraDataThe REST propertymontymaps to the local attribute namemy_attribute_name, as provided in theproperty_mapconfiguration. BecausecheeseTypesmaps toNone, and the default value ofRestObject.use_pythonic_attribute_namesisTrue, the camel-cased name is converted to the Pythoniccheese_types. Becauseparrotmaps 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 thelumberjackStatusproperty, because theproperty_mapconfiguration defines it, it was initialized to a default value ofNonein theMyRestObjectinstance. Finally, although the REST data contained the propertyextraData, because it was omitted from theproperty_mapconfiguration, it was silently discarded.RestObjectinstances can be converted back to their REST form as well, minus any properties that were discarded upon instantiation, via theas_rest()method:>>> from pprint import pprint
>>> pprint(obj.as_rest())
{'cheeseTypes': ['Wensleydale', 'Gouda', 'Edam'],
 'monty': 'python',
 'parrot': {'breed': 'Norwegian Blue',
            'pinesFor': 'fjords',
            'plumage': 'beautiful'}}TheRestObjectclass defines a series of objects that have special meanings when used asproperty_mapkeys.RestObject.__defaults__This should map to adictthat associates names with default values. The preferred way to declare these is to map Python attribute names to default values at the top level ofproperty_map, but it's possible to nest this in a deeper node or to use remote REST property names. For example:classMyRestObject(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 adictthat associates attributes with required types. As withRestObject.__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_typeshas been alist, 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:classMyRestObject(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 viaRestObjectsubclasses:classParrotInfo(RestObject):property_map={'breed':None,'plumage':'feathers','pinesFor':None}classMyRestObject(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:classMyRestObject(RestObject):property_map={RestObject.__types__:{'cheese_types':list,'parrot':'ParrotInfo'},'monty':'my_attribute_name','cheeseTypes':None,'parrot':None,'lumberjackStatus':None}classParrotInfo(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'sas_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 similaras_dict()method, which casts the parent object as adict, 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 thanRestObjectsubclasses, the setter will attempt to execute a method namedset_TYPE_attribute(), with "TYPE" replaced with the lowercase type name.RestObjectprovides setters for the following types:listdictboolintfloatdatetime.datetimeUsers 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 inproperty_map. For example:classMyRestObject(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 thoughobj.lumberjack_statusandobj.pines_forwere 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 followingRestObjectsubclass:classMyRestObject(RestObject):property_map={'monty':'my_attribute_name','cheeseTypes':None,'parrot':{'breed':None,'plumage':'feathers','pinesFor':None},'lumberjackStatus':None}@propertydeffeathers(self):returnself._feathers@feathers.setterdeffeathers(self,val):self._feathers=valifself._feathers:self.description='Lovely bird, the{}!{}plumage.'.format(self.breed,self._feathers)As a side effect of setting thefeathersattribute (which is now a Python dynamic property), this object attempts to set thedescriptionattribute. However, this will fail to work as expected if thebreedattribute 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 thatbreedis always set first as follows:classMyRestObject(RestObject):property_map={'monty':'my_attribute_name','cheeseTypes':None,'parrot':{RestObject.__order__:['breed'],'breed':None,'plumage':'feathers','pinesFor':None},'lumberjackStatus':None}@propertydeffeathers(self):returnself._feathers@feathers.setterdeffeathers(self,val):self._feathers=valifself._feathers:self.description='Lovely breed, the{}!{}plumage.'.format(self.breed,self._feathers)In this situation, it isn't necessary to declare wherefeathersshould fall in the order, since the only requirement is that it be set afterbreed.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_namesIf true, camel-case REST property names whose names are not specified inproperty_mapwill be converted to Python-style underscore-separated lowercase names. If false, no such conversion will take place.include_null_propertiesBy default, whenRestObjectinstances are converted to their REST form viaas_rest(), attributes whose value isNoneor an emptylistordictare excluded:classMyRestObject(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 ininclude_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:classMyRestObject(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_formatsForRestObjectsubclasses that enforce thedatetime.datetimetype for any attribute values, this tuple should contain one or morestrptime-compatible format strings. Theset_datetime_attribute()method will iterate through these to attempt to convert string values todatetime.datetimeinstances, stopping at the first one that succeeds. For example:fromdatetimeimportdatetimeclassMyRestObject(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'}}MethodsThe following are some of the importantRestObjectmethods 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:classMyRestObject(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'}}Themerge_contextsparameter 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:classMyRestObject(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)ReturnsTrueif the givenvalshould be considered empty for the givenprop,Falseotherwise. Subclasses may wish to override this method to take their own nuances into account.format_rest_property(name, value)Formats the propertyvalue(identified byname) 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 toTrue; subclasses can refer to this to make decisions about behavior.set_excluded_properties(*names)This method is the non-contextual equivalent of theexclude_properties()context manager.InheritanceThe configuration resolution in theRestObjectclass offers full support for inheritance. Classes descending from concreteRestObjectsubclasses will respect parents'property_mapconfigurations as well as their own. Depending on their types, values in the configuration will either override or be merged with those from their parent class(es). Consider the following two class definitions:classParentRestObject(RestObject):property_map={RestObject.__defaults__:{'lumberjack_status':'ok'},RestObject.__order__:['monty','lumberjackStatus'],'monty':'my_attribute_name','cheeseTypes':None,'parrot':{RestObject.__defaults__:{'pinesFor':'Oslo'},'breed':None,'plumage':'feathers','pinesFor':None},'lumberjackStatus':None}classChildRestObject(ParentRestObject):property_map={RestObject.__defaults__:{'cheeseTypes':['Wensleydale']},RestObject.__order__:['lumberjackStatus','nightActivity','dayActivity'],'nightActivity':None,'dayActivity':None}@propertydefday_activity(self):returnself._day_activity@day_activity.setterdefday_activity(self,value):self._day_activity=valueprint("""I'm a lumberjack and I'm{}I{}all night and I{}all day""".format(self.lumberjack_status,self.night_activity,self.day_activity))>>> obj = ChildRestObject(nightActivity='sleep', dayActivity='work', parrot={
...     'pinesFor': 'Saskatchewan'
... })
I'm a lumberjack and I'm ok
I sleep all night and I work all day

>>> obj.cheese_types
['Wensleydale']
>>> obj.my_attribute_name
>>> obj.pines_for
'Saskatchewan'Instances ofChildRestObjectwill have all the attributes defined inParentRestObject's configuration, plus several others.ChildRestObjectwill apply a default value during instantiation not only forcheeseTypes/cheese_types, but alsolumberjackStatus/lumberjack_status. Because of the nature of theRestObject.__order__configuration attribute, the internal configuration mechanism attempts to compute a merged result that respects the preferences of both the parent and the child; in this case, the ultimate order is['monty', 'lumberjackStatus', 'nightActivity', 'dayActivity']. If a child configuration specifies an order incompatible with the parent, an error is raised:classParentRestObject(RestObject):property_map={RestObject.__defaults__:{'lumberjack_status':'ok'},RestObject.__order__:['monty','lumberjackStatus'],'monty':'my_attribute_name','cheeseTypes':None,'parrot':{RestObject.__defaults__:{'pinesFor':'Oslo'},'breed':None,'plumage':'feathers','pinesFor':None},'lumberjackStatus':None}classChildRestObject(ParentRestObject):property_map={RestObject.__defaults__:{'cheeseTypes':['Wensleydale']},RestObject.__order__:['lumberjackStatus','nightActivity','monty'],'nightActivity':None,'dayActivity':None}Traceback (most recent call last):
  File "/rest_client_framework/rest.py", line 366, in resolve_property_map
    new_class._resolved_property_map[meta_key] = cls.merge_ordered_sequences(
  File "/rest_client_framework/rest.py", line 186, in merge_ordered_sequences
    raise AmbiguousOrderedSequenceError(metadata_key)
rest_client_framework.exceptions.AmbiguousOrderedSequenceError: __order__

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/rest_client_framework/rest.py", line 83, in __new__
    cls.resolve_property_map(new_class)
  File "/rest_client_framework/rest.py", line 370, in resolve_property_map
    raise RestDefinitionError(
rest_client_framework.exceptions.RestDefinitionError: Could not merge ChildRestObject.__order__ with parents due to order conflict.