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:

http_1

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: http_2

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.

http_3

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:

http_4

"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'}

http_5

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"
},

http_6

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

http_7

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/

http_8

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:

http_9

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,
)