Implementing methods

Once we have a mock server, we could already provide an interface to external services mocking our replies.

This is very helpful to enable clients to test our API and enable quick feedbacks on data types and possible responses.

Now that we have the contract, we should start with the implementation!

OperationId

OperationId is the OAS3 fields with maps the resource-target with the python function to call.

paths:
  /status
    get:
      ...
      operationId: api.get_status
      ...

The method signature should reflect the function's one.

OAS allows to pass parameters to the resource target via:

  • query parameters
  • http headers
  • request body

Implement get_status

At first we'll just implement the get_status in api.py function that:

  • takes no input parameters;
  • returns a problem+json

In [1]:
# connexion provides a predefined problem object
from connexion import problem

    
# Exercise: write a get_status() returning a successful response to problem.
help(problem)


Help on function problem in module connexion.problem:

problem(status, title, detail, type=None, instance=None, headers=None, ext=None)
    Returns a `Problem Details <https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00>`_ error response.
    
    
    :param status: The HTTP status code generated by the origin server for this occurrence of the problem.
    :type status: int
    :param title: A short, human-readable summary of the problem type.  It SHOULD NOT change from occurrence to
                  occurrence of the problem, except for purposes of localisation.
    :type title: str
    :param detail: An human readable explanation specific to this occurrence of the problem.
    :type detail: str
    :param type: An absolute URI that identifies the problem type.  When dereferenced, it SHOULD provide human-readable
                 documentation for the problem type (e.g., using HTML).  When this member is not present its value is
                 assumed to be "about:blank".
    :type: type: str
    :param instance: An absolute URI that identifies the specific occurrence of the problem.  It may or may not yield
                     further information if dereferenced.
    :type instance: str
    :param headers: HTTP headers to include in the response
    :type headers: dict | None
    :param ext: Extension members to include in the body
    :type ext: dict | None
    :return: error response
    :rtype: ConnexionResponse


In [2]:
def get_status():
    return problem(
        status=200,
        title="OK",
        detail="The application is working properly"
    )

Now run the spec in a terminal using

cd /code/notebooks/oas3/
connexion run /code/notebooks/oas3/ex-03-02-path.yaml

play a bit with the Swagger UI

and try making a request!


In [3]:
!curl http://localhost:5000/datetime/v1/status -kv


*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 5000 (#0)





* HTTP 1.0, assume close after body







{
  "detail": "So far so good.",
  "status": 200,
  "title": "OK",
  "type": "about:blank"
}
* Curl_http_done: called premature == 0
* Closing connection 0

Returning errors.

Our api.get_status implementation always returns 200 OK, but in the real world APIs could return different kind of errors.

An interoperable API should:

  • fail fast, avoiding that application errors result in stuck connections;
  • implement a clean error semantic.

In our Service Management framework we expect that:

  • if the Service is unavailable, we must return 503 Service Unavailable http status
  • we must return the Retry-After header specifying the number of seconds when to retry.

TODO: ADD CIRCUIT_BREAKER IMAGE

To implement this we must:

  1. add the returned headers in the OAS3 interface;
  2. pass the headers to the flask Response

Exercise

Modify the OAS3 spec in ex-04-01-headers.yaml and:

  • add a 503 response to the /status path;
  • when a 503 is returned, the retry-after header is returned.

Hint: you can define a header in components/headers like that:

components:
  headers:
    Retry-After:
      description: |-
        Retry contacting the endpoint *at least* after seconds.
        See https://tools.ietf.org/html/rfc7231#section-7.1.3
    schema:
      format: int32
      type: integer

Or just $ref the Retry-After defined in https://teamdigitale.github.io/openapi/0.0.5/definitions.yaml#/headers/Retry-After

Modify api.py:get_status such that:

  • returns a 503 on 20% of the requests;
  • when a 503 is returned, the retry-after header is returned;
  • on each response, return the Cache-Control: no-store header to avoid caching on service status.

Bonus track: Google post on HTTP caching


In [6]:
from random import randint
from connexion import problem

def get_status():
    headers = {"Cache-Control": "no-store"}

    p = randint(1, 5)
    if p == 5:
        return problem(
            status=503,
            title="Service Temporarily Unavailable",
            detail="Retry after the number of seconds specified in the the Retry-After header.",
            headers=dict(**headers, **{'Retry-After': str(p)})
        )
    return problem(
        status=200,
        title="OK",
        detail="So far so good.",
        headers=headers
    )

Reusing default responses

As 503 is a quite recurring response, it's worth to define it in a reusable yaml file, so that every path can reuse it.

Exercise: reusable responses

In the following exercise you should edit ex-04-01-headers.yaml and:

  • move the 503 response from the /status path definition to the components/responses one, eg.
components:
  responses:
    503ServiceUnavailable:
      ...
  • reference #/components/responses/503ServiceUnavailable in the /status path

Your new file is semantically equivalent to the previous one: check that you can nicely connexion run your file in terminal!

connexion run /code/notebooks/oas3/ex-04-01-headers.yaml

In [ ]: