HTTP Microservice
Flask-API
HTTP implementation of microservices based on Flask-API
Firstly, you need to initialize your microservice app object:
from microservices.http.service import Microservice
app = Microservice(__name__)
app
is in fact just a standard flask-api application.
You can add route the same way like in flask-api
@app.route('/')
def hello_world():
return 'Hello, world'
And run it in debug mode:
if __name__ == '__main__':
app.run(debug=True)
Start app:
python hello_world.py
You will see following output:
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger pin code: 301-234-464
Let's open http://127.0.0.1:5000/ in the browser.
You will see:
This is a standard browsable api page generated by flask-api
ResourceMarker
Let's add ResourceMarker to function hello_world
from microservices.http.resources import ResourceMarker
@app.route(
'/',
resource=ResourceMarker(),
)
def hello_world():
return 'Hello, world'
After page reloading you will see this:
As you can see, this page contains some additional information:
- status
- resource
- methods
- status_code
- headers
- response, of course
Everything is fine and dict 'response' field contains our "Hello, world" string. This is default behavior.
Let's change it.
We can add dictionary for update response data:
resource=ResourceMarker(
update={
'resource_created': datetime.datetime.now().isoformat()
},
)
Now reload our API page.
New key "resource_created" just appeared in the response. If you try to reload page again, datetime will
not be changed - it is immutable resource
attribute.
Note:
status, resource, methods, status_code, headers 1 in browser, if you run curl or another http client, not browser, you will see only "response" and "update" dictionary, like this:
{"response": "Hello, world", "resource_created": "2016-06-17T18:49:36.782267"}
ResourceSchema
Ok, it's time to add some more customizations using ResourceSchema.
ResourceSchema
can be imported from microservices.http.resources
from microservices.http.resources import ResourceMarker, ResourceSchema
For example, we can change response name
resource=ResourceMarker(
update={
'resource_created': datetime.datetime.now().isoformat()
},
schema=ResourceSchema(
response='result',
)
)
Response:
"response": "Hello, world"
=> "result": "Hello, world"
Want a more? OK!
If your response is a dict, by default response will be updated from your response
def hello_world():
return {'hello': 'Hello, world'}
If you want reponse all data in your response key name, you can change a policy:
resource=ResourceMarker(
update={
'resource_created': datetime.datetime.now().isoformat()
},
schema=ResourceSchema(
response='result',
response_update=False,
)
),
And you will see:
"result": {
"hello": "Hello, world"
},
By default for non-browser clients technical information (as status, headers, url) will be ignored. You can change this and rename response keys
Lets importing BrowserResourceSchema
from microservices.http.resources import ResourceMarker, ResourceSchema, BrowserResourceSchema
And add some modifications:
resource=ResourceMarker(
update={
'resource_created': datetime.datetime.now().isoformat()
},
schema=ResourceSchema(
response='result',
response_update=False,
status_code='status',
browser=BrowserResourceSchema(
status=None,
)
)
),
In browser you will see
Was status_code
- now - status
In real client you will see:
{"status": 200, "result": {"hello": "Hello, world"}, "resource_created": "2016-06-20T14:02:50.684756"}
Settings
If you want to use your custom ResourceSchema everywhere, you can change default settings:
app.config['SCHEMA'] = ResourceSchema(
response='result',
response_update=False,
status_code='status',
browser=BrowserResourceSchema(
status=None,
)
)
resource=ResourceMarker(
update={
'resource_created': datetime.datetime.now().isoformat()
}
)
More resources
Let's add new resource
from flask import request
@app.route(
'/<string:one>/<string:two>/<string:three>/',
methods=['GET', 'POST'],
resource=ResourceMarker()
)
def one_two_three(one, two, three):
response = {'one': one, 'two': two, 'three': three}
if request.method == 'POST':
response['data'] = request.data
return response
Open in browser http://localhost:5000/1/2/3/
Response now have one new field called resources
It's an information about all available resources in Microservice instance.
You can see url
(clickable), methods
and schema
If you open http://localhost:5000, there would be information about resource "/<string:one>/<string:two>/<string:three>/"
The url
field gone missing because microservice don't know how to create url dynamically,
but you know.
So, it would be a good idea to tell microservice how to build url.
@app.route(
'/<string:one>/<string:two>/<string:three>/',
methods=['GET', 'POST'],
resource=ResourceMarker(
url_params={'one': 'one', 'two': 'two', 'three': 'three'}
)
)
def one_two_three(one, two, three):
Result:
Client
Let's write a client for our microservice
Create a hello_world_client.py
And add this code
from microservices.http.client import Client
hello_world = Client('http://localhost:5000')
response = hello_world.get()
print(response)
and run it python hello_world_client.py
You will see
{u'status': 200, u'result': {u'hello': u'Hello, world'}, u'resource_created': u'2016-06-20T17:51:22.358575'}
If you want to get a result, you can use key
in method get
response = hello_world.get(key='result')
You will get
{u'hello': u'Hello, world'}
What if there is no such a key?
response = hello_world.get(key='bad_key')
...microservices.http.client.ResponseError
exception would be thrown:
Traceback (most recent call last):
File "/home/viator/coding/code/microservices/examples/http/hello_world_client.py", line 5, in <module>
response = hello_world.get(key='bad_key')
File "/home/viator/coding/code/microservices/microservices/http/client.py", line 89, in __call__
return self.client.handle_response(response, response_key=response_key)
File "/home/viator/coding/code/microservices/microservices/http/client.py", line 198, in handle_response
raise ResponseError(response, 'Response key not found!')
microservices.http.client.ResponseError: Error status code: 200. Description: Response key not found!
What we can get from exception?
from microservices.http.client import Client
from microservices.http.client import ResponseError
from six import print_
hello_world = Client('http://localhost:5000')
try:
response = hello_world.get(key='bad_key')
except ResponseError as error:
print_('Data:', error.response.json())
print_('Status code:', error.status_code)
print_('Description:', error.description)
print_('Content:', error.content)
Answer:
Data: {u'status': 200, u'result': {u'hello': u'Hello, world'}, u'resource_created': u'2016-06-20T17:51:22.358575'}
Status code: 200
Description: Response key not found!
Content: {"status": 200, "result": {"hello": "Hello, world"}, "resource_created": "2016-06-20T17:51:22.358575"}
What Client can do as yet? Well, http methods - GET/POST/PUT/PATCH/DELETE/etc...
Here's example for the POST method
We can create a new resource from a Client:
one_two_three = hello_world.resource('one', 'two', 'three')
'one', 'two', 'three'
=> http://localhost:5000/one/two/three/
Let's see how it works:
one_two_three = hello_world.resource('one', 'two', 'three')
response = one_two_three.post(data={'post': 'test'})
print_(response)
result = one_two_three.post(data={'post': 'test'}, key='result')
print_(result)
Result:
{u'status': 200, u'result': {u'one': u'one', u'data': {u'post': u'test'}, u'three': u'three', u'two': u'two'}}
{u'one': u'one', u'data': {u'post': u'test'}, u'three': u'three', u'two': u'two'}
You can write your own Client class and override method handle_response
for specific purposes.
Production
Microservice app is a fully WSGI application, so you can use it with any of wsgi servers.
Library also provide you with runners to simplify deployment.
Gevent
from microservices.http.runners import gevent_run
from basic import microservice
from microservices.utils import set_logging
set_logging()
gevent_run(microservice)
Tornado
Single server with gevent for async duties
from microservices.http.runners import tornado_run
from basic import microservice
from microservices.utils import set_logging
set_logging()
tornado_run(microservice, use_gevent=True)
Multiple servers in one process using gevent for async
from microservices.http.runners import tornado_combiner
from basic import microservice
from microservices.utils import set_logging
set_logging()
tornado_combiner(
[
{'app': microservice, 'port': 5000},
{'app': microservice, 'port': 5001}
],
use_gevent=True,
)