REST-ORM Framework¶
Part of the client library might be suitable to break out into a separate framework for building ORM clients to different API:s
We try to not couple the functions to hard to the Visma e-Accounting API.
Inspiration¶
When looking at the Visma e-Accouting API with the use of OData as filtering query parameters it looks very much like accessing a database. Just over HTTP REST. Normally in Python applications you have an ORM to access a database which usually looks like:
>>> Model.objects.all() # Django
We have written many integration where we just have a class that uses request to get the relevant recource and parse the JSON into a dict for further processing. To do this on this enrire API would be annoying. Especially since the Visma e-Accounting API uses Pascal casing in their JSON.
So first we looked around for something to handle the Pascal to snake-case serializing and found marshmallow. It did the job well but we had alot of duplicate code when we wanted to parse data that looked exactly like our python objects.
So with some inspiration from how Django hadles Model creation we made a Metaclass that will use Marshmallow fields to generate a Python object with a simmilar Marshmallow Schema attached.
Models¶
Models inherit from VismaModel.
MyClass(VismaModel):
id = field.UUID()
name = field.String()
Model Meta¶
To set up how the class interacts with the API we specify the model meta
- endpoint
- Specifies the main enpoint for a class.
- allowed_methods
- Specifies the allowed methods on the class.
- envelopes
- Specifies how to handle enveloped schemas on endpoints.
Endpoints and methods¶
We assume for now that one is following normal REST API behaviour. We have some thoughts on how in add specific handling but we have not had time to test other more important features
If you specify and endpoint for example /customer, you will get the following behaviour
- list
- accessible via .all(). Does a get and get everything from /customers
- get
- given an id it will do a GET on /customers/{id}
- create
- if no id is set on the model it will POST to /customers to create the object on .save()
- update
- if id is set on the model it will PUT to /customers/{id} to update the object on .save()
- delete
- can be called on the object or on the model with an id to issue a DELETE to /customers/{id}
Some API endpoint does not have support of all methods so you have to list them in the Meta as a list.
Enveloped Schemas¶
When communicating with an API the data sent might be more than the data you want to get or change. For example getting a list of resources on an endpoint that supports pagination you might get meta data like number of pages and current page in a metadata section.
By defining a model for the outer data it is possible to handle this in a simple way by adding the envelope settings in the Meta data of the classes that uses envelope.
class PaginatedResponse(VismaModel):
meta = fields.Nested('PaginationMetadataSchema', data_key='Meta')
class PaginationMetadata(VismaModel):
current_page = fields.Integer(data_key='CurrentPage')
page_size = fields.Integer(data_key='PageSize')
total_number_of_pages = fields.Integer(data_key='TotalNumberOfPages')
total_number_of_results = fields.Integer(data_key='TotalNumberOfResults')
server_time_utc = fields.DateTime(data_key='ServerTimeUtc')
class Customer(VismaModel):
name = fields.String(data_key='Name')
class Meta:
endpoint = '/customers'
allowed_methods = ['list', 'create', 'delete']
envelopes = {'list': {'class': PaginatedResponse,
'data_attr': 'Data'}}
This will allow the following data response to return an object of Customer.
{'Data': [{'Name': 'Customer-Name'},],
'Meta': {'CurrentPage': 1,
'PageSize': 50,
'ServerTimeUtc': '2018-06-21T16:23:13.1083743Z',
'TotalNumberOfPages': 1,
'TotalNumberOfResults': 37}}