The HTTP Resource Class

Jivago uses Resource classes to define HTTP routes. Routable classes should be annotated with the @Resource decorator with an URL path on which it should be mounted. Each routing method should then be annotated with one of the HTTP verbs (@GET, @POST, etc.) to make them known to the framework, and can optionally define a subpath using the @Path annotation.

from jivago.wsgi.annotations import Resource, Path
from jivago.wsgi.invocation.parameters import PathParam, QueryParam
from jivago.wsgi.methods import GET, POST, PUT
from jivago.wsgi.request.request import Request
from jivago.wsgi.request.response import Response


@Resource("/hello")
class HelloWorldResource(object):

    @GET
    def get_hello(self) -> str:
        return "Hello"

    @POST
    @Path("/{name}")
    def post_hello(self, name: PathParam[str]) -> str:
        return "name: {}".format(name)

    @Path("/request/json")
    @POST
    def read_request_body_from_dict(self, body: dict) -> dict:
        return {"the body": body}

    @GET
    @Path("/query")
    def with_query(self, name: QueryParam[str]) -> str:
        return "Hello {}!".format(name)

    @GET
    @Path("/request/raw")
    def read_raw_request(self, request: Request) -> Response:
        return Response(200, {}, "body")

Allowed Parameter Types

When handling a specific request, Jivago reads declared parameter types before invoking the routing function. Passed arguments can come from the query itself, from the body, from the raw request or a combination. Below are all the allowed parameter types :

  • QueryParam[T] : Reads the parameter matching the variable name from the query string. T should be either str, int or float.

  • OptionalQueryParam[T] : Identical to the above, except that it allows None values to be passed in place of a missing one.

  • PathParam[T] : Reads the parameter from the url path. The variable name should match the declared name in the @Path or the @Resource annotation. Route definitions use the {path-parameter-name} to declare these parameters.

  • dict : The request body which has been deserialized to a dictionary. Requires the body to be deserializable to a dictionary. (e.g. JSON).

  • A user-defined DTO : Any declared @Serializable class will be instantiated before invoking. This effectively acts as a JSON-schema validation.

  • Request : The raw Request object, as handled by Jivago. Useful when direct access to headers, query strings or the body is required.

  • Headers : The raw Headers object, containing all request headers. This class is simply a case-insensitive dictionary.

Manual Route Registration

Additionnal URL routes can be registered by creating a new RoutingTable which references classes and their methods. Note that the appropriate classes should be imported beforehand. The referenced resource class can be either an instance, or the actual class. In that case, it will be instantiated by the ServiceLocator, and should therefore be registered manually in the configure_service_locator context method.

from jivago.wsgi.methods import GET, POST
from jivago.wsgi.routing.table.tree_routing_table import TreeRoutingTable

my_routing_table = TreeRoutingTable()

my_routing_table.register_route(GET, "/hello", MyResourceClass, MyResourceClass.get_hello)
my_routing_table.register_route(POST, "/hello", MyResourceClass, MyResourceClass.get_hello)

This new RoutingTable can then be used to configure the Router object, which is used to serve all requests. The recommended way of configuring your application is by inheriting from the ProductionJivagoContext class, and then overriding the create_router_config method.

from jivago.config.production_jivago_context import ProductionJivagoContext
from jivago.config.router.router_builder import RouterBuilder
from jivago.jivago_application import JivagoApplication
from jivago.wsgi.routing.routing_rule import RoutingRule


class MyApplicationContext(ProductionJivagoContext):

    def create_router_config(self) -> RouterBuilder:
        return super().create_router_config() \
            .add_rule(RoutingRule("/", my_routing_table))


app = JivagoApplication(my_package, context=MyApplicationContext)

Serving static files

While it is not generally recommended to serve static files from a WSGI application for performance reasons, Jivago supports static file serving. The StaticFileRoutingTable dynamically defines routes for serving files.

from jivago.config.production_jivago_context import ProductionJivagoContext
from jivago.config.router.router_builder import RouterBuilder
from jivago.lang.annotations import Override
from jivago.wsgi.routing.routing_rule import RoutingRule
from jivago.wsgi.routing.serving.static_file_routing_table import StaticFileRoutingTable


class MyApplicationContext(ProductionJivagoContext):

    @Override
    def create_router_config(self) -> RouterBuilder:
        return super().create_router_config() \
            .add_rule(RoutingRule("/", StaticFileRoutingTable("/var/www"))) \
            .add_rule(RoutingRule("/", StaticFileRoutingTable("/var/www", allowed_extensions=['.html', '.xml'])))

The StaticFileRoutingTable can also be used with a allowed_extensions parameter to explicitly allow or disallow specific file types.

HTTP Streaming responses

In cases where a streaming response is desired, Jivago provides the StreamingResponseBody object. Returning an instance of StreamingResponseBody will cause the Transfer-Encoding header to be automatically set to chunked. A StreamingResponseBody object requires an Iterable[bytes] object.

from jivago.wsgi.annotations import Resource
from jivago.wsgi.methods import GET
from jivago.wsgi.request.headers import Headers
from jivago.wsgi.request.response import Response
from jivago.wsgi.request.streaming_response_body import StreamingResponseBody


@Resource("/stream")
class MyStreamingResource(object):

    @GET
    def get_stream(self) -> StreamingResponseBody:
        # Returning the body object automatically sets the status code to 200 OK
        return StreamingResponseBody(self.generate_bytes())

    @GET
    def get_stream(self) -> Response:
        # A Response object can also be manually created to provide further control over transport parameters.
        return Response(202, Headers(), StreamingResponseBody(self.generate_bytes()))

    def generate_bytes(self) -> bytes:
        for i in range(0, 5):
            yield b"my bytes"

Note that chunked (streaming) requests and responses may not be supported by every wsgi server. Jivago has been tested with gunicorn.

HTTP Streaming requests

Similarly, requests using Transfer-Encoding: chunked will be mapped automatically to a StreamingRequestBody instance.

from jivago.wsgi.annotations import Resource
from jivago.wsgi.methods import POST
from jivago.wsgi.request.request import Request
from jivago.wsgi.request.streaming_request_body import StreamingRequestBody


@Resource("/stream")
class MyStreamingResource(object):

    @POST
    def post_stream(self, body: StreamingRequestBody):
        content = body.read()
        print(content)
        return "OK"

    @POST
    def post_stream2(self, request: Request):
        # When Transfer-Encoding is set to 'chunked', the request body will be an instance of StreamingRequestBody
        if isinstance(request.body, StreamingRequestBody):
            print(request.body.read())
            return "OK"
language:

python

Additional router configuration options, including specific filter and CORS rules, can be found at Router Configuration.