HTTP/Json API¶
Reliure permits to build json api for simple processing function (or Optionable)
as well as for more complex Engine. The idea of the reliure API
mechanism is : you manage data processing logic, reliure manages the “glue” job.
Reliure web API are based on Flask.
A reliure API (ReliureAPI) is a Flask Blueprint where you plug view of your
processing modules.
Let’s see how it works on some simple examples !
Table of contents
Component or simple function¶
Expose a simple function¶
Let’s imagine that we have the following hyper-complex data procesing method:
>>> def count_a_and_b(chaine):
... return chaine.count("a") + chaine.count("b")
and you want it to be accessible on an HTTP/json supercool-powered API...
In ohter word we just want that a GET on http://myapi.me.com/api/count_ab/totocotata
returns 2 and eventualy some other metadata (processing time for instance).
Here is how we can do that with reliure.
First you need to build a “view” (a ComponentView) on this function:
>>> from reliure.web import ComponentView
>>> view = ComponentView(count_a_and_b)
Then you have to define the type of the input (the type will manage parsing from string/json):
>>> from reliure.types import Text
>>> view.add_input("in", Text())
>>> # Note that, by default, the output will be named with the function name
You can also specify a short url patern to reach your function, this is done with flask route paterns syntax. Here we will simply indicate that the url (note that there will be url prefix) should match our uniq input:
>>> view.play_route("<in>")
Then you can create a ReliureAPI object and register this view on it:
>>> from reliure.web import ReliureAPI
>>> api = ReliureAPI("api")
>>> api.register_view(view, url_prefix="count_ab")
This api object can be plug to a flask app (it is a Flask Blueprint):
>>> from flask import Flask
>>> app = Flask("my_app")
>>> app.register_blueprint(api, url_prefix="/api")
To illustrate API call, let’s use Flask testing mechanism:
>>> resp = client.get("/api/count_ab/abcdea") # call our API
>>> results = json.loads(resp.data.decode("utf-8"))
>>> pprint(results["results"])
{'count_a_and_b': 3}
>>>
>>> resp = client.get("/api/count_ab/abcdea__bb_aaa")
>>> results = json.loads(resp.data.decode("utf-8"))
>>> pprint(results["results"])
{'count_a_and_b': 8}
Note that meta information is also available:
>>> pprint(results["meta"])
{'details': [{'errors': [],
'name': 'count_a_and_b',
'time': 3.314018249511719e-05,
'warnings': []}],
'errors': [],
'name': 'count_a_and_b:[count_a_and_b]',
'time': 3.314018249511719e-05,
'warnings': []}
Managing options and multiple inputs¶
Let’s mouv on a more complex exemple...
First, write your processing component¶
One can imagine the following component that merge two string with two possible methods (choice is made with an option):
>>> from reliure import Optionable
>>> from reliure.types import Text
>>>
>>> class StringMerge(Optionable):
... """ Stupid component that merge to string together
... """
... def __init__(self):
... super(StringMerge, self).__init__()
... self.add_option("method", Text(
... choices=[u"concat", u"altern"],
... default=u"concat",
... help="How to merge the inputs"
... ))
...
... @Optionable.check
... def __call__(self, left, right, method=None):
... if method == u"altern":
... merge = "".join("".join(each) for each in zip(left, right))
... else:
... merge = left + right
... return merge
One can use this directly in python:
>>> merge_component = StringMerge()
>>> merge_component("aaa", "bbb")
'aaabbb'
>>> merge_component("aaa", "bbb", method=u"altern")
'ababab'
Then create a view on it, and register it on your API¶
If you want to expose this component on a HTTP API,
as for our first exemple,
you need to build a “view” (a ComponentView) on it:
>>> view = ComponentView(merge_component)
>>> # you need to define the type of the input
>>> from reliure.types import Text
>>> view.add_input("in_lft", Text())
>>> view.add_input("in_rgh", Text(default=u"ddd"))
>>> # ^ Note that it is possible to give default value for inputs
>>> view.add_output("merge")
>>> # we specify two short urls to reach the function:
>>> view.play_route("<in_lft>/<in_rgh>", "<in_lft>")
Warning
Note that for a ComponentView the order of the inputs
matters to match with component (or function) arguments.
It is not the name of that permits the match.
Warning
when you define default value for inputs, None can not be a default value.
Then we can register this new view to a reliure API object:
>>> api.register_view(view, url_prefix="merge")
Finaly, just use it !¶
And then we can use it:
>>> resp = client.get("/api/merge/aaa/bbb")
>>> results = json.loads(resp.data.decode("utf-8"))
>>> results["results"]
{'merge': 'aaabbb'}
As we have specify a route that require only one argument, and a default value
for this second input (in_rgh), it is also possible to do:
>>> resp = client.get("/api/merge/aaa")
>>> results = json.loads(resp.data.decode("utf-8"))
>>> results["results"]
{'merge': 'aaaddd'}
It is also possible to call the API with options:
>>> resp = client.get("/api/merge/aaa/bbb?method=altern")
>>> results = json.loads(resp.data.decode("utf-8"))
>>> results["results"]
{'merge': 'ababab'}
Alternatively you can use a POST to send inputs. There is two posibility to provide inputs and options. First by using direct form encoding:
>>> resp = client.post("/api/merge", data={"in_lft":"ee", "in_rgh":"hhhh"})
>>> results = json.loads(resp.data.decode("utf-8"))
>>> results["results"]
{'merge': 'eehhhh'}
And with options in the url:
>>> resp = client.post("/api/merge?method=altern", data={"in_lft":"ee", "in_rgh":"hhhh"})
>>> results = json.loads(resp.data.decode("utf-8"))
>>> results["results"]
{'merge': 'eheh'}
The second option is to use a json payload:
>>> data = {
... "in_lft":"eeee",
... "in_rgh":"gg",
... "options": {
... "name": "StringMerge",
... "options": {
... "method": "altern",
... }
... }
... }
>>> json_data = json.dumps(data)
>>> resp = client.post("/api/merge", data=json_data, content_type='application/json')
>>> # note that it is important to specify content_type to 'application/json'
>>> results = json.loads(resp.data.decode("utf-8"))
>>> results["results"]
{'merge': 'egeg'}
Note that a GET call on the root /api/merge returns a json that specify
the API. With this, it is possible do list all the options of the component:
>>> resp = client.get("/api/merge")
>>> results = json.loads(resp.data.decode("utf-8"))
>>> pprint(results)
{'args': ['in_lft', 'in_rgh'],
'components': [{'default': True,
'name': 'StringMerge',
'options': [{'name': 'method',
'otype': {'choices': ['concat', 'altern'],
'default': 'concat',
'encoding': 'utf8',
'help': 'How to merge the inputs',
'multi': False,
'type': 'Text',
'uniq': False,
'vtype': 'unicode'},
'type': 'value',
'value': 'concat'}]}],
'multiple': False,
'name': 'StringMerge',
'required': True,
'returns': ['merge']}
Complex processing engine¶
Define your engine¶
Here is a simple reliure engine that we will expose as an HTTP API.
>>> from reliure.engine import Engine
>>> engine = Engine("vowel", "consonant", "concat")
>>> engine.vowel.setup(in_name="text")
>>> engine.consonant.setup(in_name="text")
>>> engine.concat.setup(in_name=["vowel", "consonant"], out_name="merge")
>>>
>>> from reliure import Composable
>>> vowels = u"aiueoéèàùêôûîï"
>>> @Composable
... def extract_vowel(text):
... return "".join(char for char in text if char in vowels)
>>> engine.vowel.set(extract_vowel)
>>>
>>> @Composable
... def extract_consonant(text):
... return "".join(char for char in text if char not in vowels)
>>> engine.consonant.set(extract_consonant)
>>>
>>> # for the merge we re-use the component defined in previous section:
>>> engine.concat.set(StringMerge())
The Figure Engine schema. draw the processing schema of this small engine.
Engine schema.
(See
engine_schema() to see how to generate such schema from an engine)Create a view and register it on your api¶
As for a simple component we need to create a view over our engine :
>>> from reliure.web import EngineView
>>> view = EngineView(engine)
And then to define the input and output types:
>>> view.add_input("text", Text())
>>> view.add_output("merge", Text())
We can also specify a short url patern to run the engine:
>>> view.play_route("<text>")
Then you can create a ReliureAPI object and register this view on it:
Then we can register this new view to a reliure API object:
>>> api = ReliureAPI("api")
>>> api.register_view(view, url_prefix="process")
>>> # and register thi api to our flask app :
>>> app.register_blueprint(api, url_prefix="/api")
Use it !¶
>>> resp = client.get("/api/process/abcdea")
>>> results = json.loads(resp.data.decode("utf-8"))
>>> pprint(results["results"])
{'merge': 'aeabcd'}
>>>
>>> resp = client.get("/api/process/abcdea__bb_aaa")
>>> results = json.loads(resp.data.decode("utf-8"))
>>> pprint(results["results"])
{'merge': 'aeaaaabcd__bb_'}
Note that meta information is also available:
>>> pprint(results["meta"])
{'details': [{'details': [{'errors': [],
'name': 'extract_vowel',
'time': 3.695487976074219e-05,
'warnings': []}],
'errors': [],
'name': 'vowel:[extract_vowel]',
'time': 3.695487976074219e-05,
'warnings': []},
{'details': [{'errors': [],
'name': 'extract_consonant',
'time': 3.0040740966796875e-05,
'warnings': []}],
'errors': [],
'name': 'consonant:[extract_consonant]',
'time': 3.0040740966796875e-05,
'warnings': []},
{'details': [{'errors': [],
'name': 'StringMerge',
'time': 5.507469177246094e-05,
'warnings': []}],
'errors': [],
'name': 'concat:[StringMerge]',
'time': 5.507469177246094e-05,
'warnings': []}],
'errors': [],
'name': 'engine:[vowel:[extract_vowel], consonant:[extract_consonant], concat:[StringMerge]]',
'time': 0.0001220703125,
'warnings': []}