API¶
Request structure¶
Allowed requests¶
Blargh currently implements following requests:
from blargh.api.basic import get, post, put, patch, get
data = {'foo': 'bar'}
name = 'cookies'
id_ = 1
get(name, depth=1, filter_={})
get(name, id_, depth=1)
post(name, data)
put(name, id_, data)
patch(name, id_, data)
detele(name, id_)
Other possible requests (PATCH/DELETE on a collection, OPTIONS, HEAD etc) have to be implemented separately, e.g. like this: Patch on a collection.
GET args¶
Besides resource name and optional id, get()
accepts also:
depth: | Non-negative integer, default 1, controlling relational fields behaviour. Related objects will be displayed
with value >>> for depth in range(0, 4):
... pprint(get('cookie', 1, depth=depth)[0])
...
1
{'id': 1, 'jar': 1, 'type': 'biscuit'}
{'id': 1, 'jar': {'cookies': [1, 2], 'id': 1}, 'type': 'biscuit'}
{'id': 1,
'jar': {'cookies': [{'id': 1, 'jar': 1, 'type': 'biscuit'},
{'id': 2, 'jar': 1, 'type': 'muffin'}],
'id': 1},
'type': 'biscuit'}
Note: depth is by default not limited, and calls with |
---|---|
filter_: | Dictionary that will be a subset of each returned dictionary. Example again: >>> pprint(get('cookie')[0])
[{'id': 1, 'jar': 1, 'type': 'biscuit'},
{'id': 2, 'jar': 1, 'type': 'muffin'},
{'id': 3, 'jar': 2, 'type': 'shortbread'}]
>>> pprint(get('cookie', filter_={'jar': 1})[0])
[{'id': 1, 'jar': 1, 'type': 'biscuit'},
{'id': 2, 'jar': 1, 'type': 'muffin'}]
>>> pprint(get('cookie', filter_={'jar': 1, 'type': 'biscuit'})[0])
[{'id': 1, 'jar': 1, 'type': 'biscuit'}]
>>> pprint(get('cookie', filter_={'type': 'shortbread'})[0])
[{'id': 3, 'jar': 2, 'type': 'shortbread'}]
>>> pprint(get('cookie', filter_={'type': 'gingerbread'})[0])
[]
|
sort: | List of field names with possible ‘-’ prefixes. Not prefixed field denotes ascending sort, prefixed - descending. >>> pprint(get('cookie', sort=['type'])[0])
[{'id': 1, 'jar': 1, 'type': 'biscuit'},
{'id': 2, 'jar': 1, 'type': 'muffin'},
{'id': 3, 'jar': 2, 'type': 'shortbread'}]
>>> pprint(get('cookie', sort=['-id'])[0])
[{'id': 3, 'jar': 2, 'type': 'shortbread'},
{'id': 2, 'jar': 1, 'type': 'muffin'},
{'id': 1, 'jar': 1, 'type': 'biscuit'}]
>>> pprint(get('cookie', sort=['jar', '-type'])[0])
[{'id': 2, 'jar': 1, 'type': 'muffin'},
{'id': 1, 'jar': 1, 'type': 'biscuit'},
{'id': 3, 'jar': 2, 'type': 'shortbread'}]
|
limit: | Non-negative integer determining max number of returned objects. |
Other arguments (pagination, field selection, more advanced search etc.) will probably be added in the future.
POST/PATCH/PUT data¶
patch()
/put()
data is a dictionary, post()
data is either a dictionary or a list of dictionaries. List of dictionaries is treated
the same way as set of subsequent post()
calls, that either all succeed or all fail. Dictionary keys are fields external names, values -
anything that given field can parse. For standard field types:
- Scalar
- Anything, if fields
type_ is None
, or anything that can be reversibly casted totype_
iftype_ is not None
. - Calc
- If field has defined
setter
- any value accepted by thesetter
function (i.e. any value that passed tosetter
doesn’t raise an exception). Ifsetter is None
, nothing is allowed. - Rel
Id or a dictionary if not
field.multi
, or list of those otherwise. Id has to be a valid id of an object stored in this field, dictionary will be used as POST data.For example, let’s assume we want to create a new jar with a new cookie. We can do this in two separate requests:
post('cookie', {'type': 'gingerbread'}) # Let's assume POSTed cookie has id 4 post('jar', {'cookies': [4]})
but also in one request, either posting a jar:
post('jar', {'cookies': [{'type': 'gingerbread'}]})
or a cookie:
post('cookie', {'type': 'gingerbread', 'jar': {}})
Check Data model fields for more information.
Vague requests¶
It is possible to make a call with more-or-less conflicting data, for example (assuming example.cookies.dm
is used):
>>> patch('jar', 1, {'cookies': [{'jar': 2, 'type': 'gingerbread'}]})
Here we patch a jar, setting its cookies to include a new cookie (thus creating it), but this new cookie
already is set to be in another jar. The result is cookie in jar 1, so 'jar': 2
is just ignored.
This could get worse with bit more complicated datamodel (example.family.dm
):
>>> print(get('female', 2)[0].get('husband'))
2
>>> patch('female', 1, {'husband': {'wife': 2}})[0]
{'id': 1, 'name': 'f1', 'husband': 3, 'children': [1], 'url': 'female/1'}
>>> print(get('female', 2)[0].get('husband'))
None
New male is created with wife = 2
, so male 2 (previous husband of female 2) gets divorced first, and later this new male’s wife is set to be female 1.
This is equivalent to:
# creates male with ID 3 and set's it as new husband of female 2
post('male', {'wife': 2})
# sets male 3 as husband of female 1 - so female 2 becomes single
patch('female', 1, {'husband': 3})
Such requests are currently allowed, but this will probably change in future and they will return 422.
Authentication¶
Blargh user is advised to implement authentication in a following way:
Authentication data (should be a dictionary) is somehow obtained by the application - e.g. from JWT, cookies, env variables, hardcoded - whatever.
This dictionary is passed to
get()/post()/put()/patch()/delete()
with keyword ‘auth’, e.g.get('cookie', 1, auth={'user_id': 42})
Storage has access to the authentication data and deals with it in a desired way.
API layers¶
Api has few nested layers, user should choose the one most appropriate.
blargh.engine.Engine¶
Deepest layer. All requests that would result in status other than 2** end in exceptions. Useful for debugging, or when we want to deal with “incorrect” request in some special way.
# ... (data model, storage, engine.setup())
from blargh.engine import Engine
# returns ({'id': 7, 'type': 'shortbread'}, 201)
Engine.put('cookie', 7, {'type': 'shortbread'})
# raises blargh.exceptions.client.e404
Engine.delete('cookie', 8)
blargh.api.basic¶
The same behaviour as blargh.engine.Engine
, but blargh errors are captured.
Also adds “headers” to returned tuple, to maintain the same interface as blargh.api.flask
.
# ... (data model, storage, engine.setup())
from blargh.api.basic import put, delete
# returns ({'id': 7, 'type': 'shortbread'}, 201, {})
put('cookie', {'type': 'shortbread'})
# returns ({'error': {'code': 'OBJECT_DOES_NOT_EXIST',
# 'details': {'object_name': 'cookie',
# 'object_id': 8}}},
# 404, {})
delete('cookie', 8)
blargh.api.flask¶
Flask + REST = Flask-RESTful.
When you replace two Flask-RESTful classes with their blargh counterparts:
flask_restful.Resource
->blargh.api.flask.Resource
flask_restful.Api
->blargh.api.flask.Api
.
you should be able to use all Flask-RESTful features together with blargh.
So, complete Flask + Flask-RESTful + blargh application code is a compilation of Flask-RESTful minimal api and Blargh basic usage:
from flask import Flask
# this replaces `from flask_restful import Resource, Api`
from blargh.api.flask import Api, Resource
# blargh initialization
from blargh import engine
from example.cookies import dm
storage = engine.DictStorage({})
engine.setup(dm, storage)
# this does not change
app = Flask(__name__)
api = Api(app)
# blargish classes
class Cookie(Resource):
model = dm.object('cookie')
class Jar(Resource):
model = dm.object('jar')
# blargish api has the same interface as Flask-RESTful api
api.add_resource(Cookie, '/cookie')
api.add_resource(Jar, '/jar')
if __name__ == '__main__':
app.run(debug=True)
After saving this in app.py
and starting debug server with python3 app.py
our cookie managment system is ready:
$ curl -d '{"type":"shortbread"}' -H "Content-Type: application/json" \
-X POST http://0.0.0.0:5000/cookie
{
"id": 1,
"type": "shortbread"
}
$ curl -d '{"type":"muffin"}' -H "Content-Type: application/json" \
-X POST http://0.0.0.0:5000/cookie
{
"id": 2,
"type": "muffin"
}
$ curl -d '{"cookies":[1,2]}' -H "Content-Type: application/json" \
-X POST http://0.0.0.0:5000/jar
{
"id": 1,
"cookies": [
1,
2
]
}
$ curl -X GET http://0.0.0.0:5000/jar?depth=2
[
{
"id": 1,
"cookies": [
{
"id": 1,
"type": "shortbread",
"jar": 1
},
{
"id": 2,
"type": "muffin",
"jar": 1
}
]
}
]