Skip to content

Commit

Permalink
Merge pull request #111 from keotl/develop
Browse files Browse the repository at this point in the history
0.6.0 release
  • Loading branch information
keotl committed Jan 11, 2021
2 parents fe9d5fd + b6070e1 commit 8618d05
Show file tree
Hide file tree
Showing 74 changed files with 1,183 additions and 176 deletions.
6 changes: 6 additions & 0 deletions .travis.yml
Expand Up @@ -8,6 +8,12 @@ matrix:
- os: linux
python: 3.7
dist: xenial
- os: linux
python: 3.8
dist: bionic
# - os: linux
# python: 3.9
# dist: focal

install:
- pip install -r requirements.txt
Expand Down
6 changes: 6 additions & 0 deletions docs/dependency_injection/index.rst
Expand Up @@ -31,6 +31,12 @@ By default, all components are re-instantiated when a request is received. Howev

A *singleton* component will be instantiated when it is first requested, and reused for subsequent calls.

Jivago also provides the ``@RequestScoped`` annotation for components which should be re-used for the lifetime of a single HTTP request. Instances will be destroyed after the resource class returns and the filter chain is unwound.
Using request-scoped components outside of an HTTP request lifecycle (e.g. async event bus, background worker, init hooks ...) is not supported and may lead to unexpected results.

.. literalinclude:: request_scoped.py
:language: python

Factory Functions
-------------------
When complex scoping is required for a given component, for example when handling a database connection, factory functions can be used to instantiate and cache components using the ``@Provider`` annotation. In this case, the return type hint defines the class to which the function is registered.
Expand Down
47 changes: 47 additions & 0 deletions docs/dependency_injection/request_scoped.py
@@ -0,0 +1,47 @@
from jivago.inject.annotation import Component, RequestScoped
from jivago.lang.annotations import Inject, Override
from jivago.wsgi.annotations import Resource
from jivago.wsgi.filter.filter import Filter
from jivago.wsgi.filter.filter_chain import FilterChain
from jivago.wsgi.request.request import Request
from jivago.wsgi.request.response import Response


@Component
@RequestScoped
class UserSession(object):
"""A single instance will be shared across the request lifecycle,
from the filter chain to the resource class and any synchronous call it makes."""

def __init__(self):
self.user_id = None

def set(self, user_id: str):
self.user_id = user_id

def get(self) -> str:
return self.user_id


@Component
class UserSessionInitializationFilter(Filter):

@Inject
def __init__(self, session: UserSession):
self.session = session

@Override
def doFilter(self, request: Request, response: Response, chain: FilterChain):
self.session.set(request.headers["Authorization"])
chain.doFilter(request, response)


@Resource("/")
class MyResourceClass(object):

@Inject
def __init__(self, user_session: UserSession):
# This is the same instance that was initialized in the request filter class.
self.user_session = user_session

# ...
15 changes: 15 additions & 0 deletions docs/resource/index.rst
Expand Up @@ -44,7 +44,22 @@ While it is not generally recommended to serve static files from a WSGI applicat

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.

.. literalinclude:: streaming_response.py
:language: python

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.

.. literalinclude:: streaming_request.py
:language: python

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

Expand Down
21 changes: 21 additions & 0 deletions docs/resource/streaming_request.py
@@ -0,0 +1,21 @@
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"
23 changes: 23 additions & 0 deletions docs/resource/streaming_response.py
@@ -0,0 +1,23 @@
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"
9 changes: 7 additions & 2 deletions e2e_test/app/application.py
Expand Up @@ -7,9 +7,11 @@
import e2e_test.app.static
from e2e_test.app import components
from jivago.config.debug_jivago_context import DebugJivagoContext
from jivago.config.router.cors_rule import CorsRule
from jivago.config.router.filtering.filtering_rule import FilteringRule
from jivago.config.router.router_builder import RouterBuilder
from jivago.jivago_application import JivagoApplication
from jivago.wsgi.filter.system_filters.default_filters import JIVAGO_DEFAULT_FILTERS
from jivago.wsgi.routing.routing_rule import RoutingRule
from jivago.wsgi.routing.serving.static_file_routing_table import StaticFileRoutingTable
from jivago.wsgi.routing.table.auto_discovering_routing_table import AutoDiscoveringRoutingTable
Expand All @@ -25,10 +27,13 @@ def configure_service_locator(self):

def create_router_config(self) -> RouterBuilder:
return RouterBuilder() \
.add_rule(FilteringRule("*", self.get_default_filters())) \
.add_rule(FilteringRule("*", JIVAGO_DEFAULT_FILTERS)) \
.add_rule(RoutingRule("/api", AutoDiscoveringRoutingTable(self.registry, self.root_package_name))) \
.add_rule(RoutingRule("/static", StaticFileRoutingTable(os.path.dirname(e2e_test.app.static.__file__),
allowed_extensions=['.txt'])))
allowed_extensions=['.txt']))) \
.add_rule(CorsRule("/", {'Access-Control-Allow-Origin': 'http://jivago.io',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Methods': '*'}))


application = JivagoApplication(components, context=DemoContext)
Expand Down
37 changes: 37 additions & 0 deletions e2e_test/app/components/event/async_event_handler.py
@@ -0,0 +1,37 @@
import anachronos
from anachronos import Anachronos

from e2e_test.testing_messages import ASYNC_INSTANTIATED_EVENT_HANDLER, \
ASYNC_FUNCTION_EVENT_HANDLER, ASYNC_RUNNABLE_EVENT_HANDLER
from jivago.event.config.annotations import EventHandler, EventHandlerClass
from jivago.lang.annotations import Inject, Override
from jivago.lang.runnable import Runnable


@EventHandler("async-event")
class MyHandler(Runnable):

@Inject
def __init__(self, anachronos: Anachronos):
self.anachronos = anachronos

@Override
def run(self):
self.anachronos.store(ASYNC_RUNNABLE_EVENT_HANDLER)


@EventHandlerClass
class MyHandlerClass(object):

@Inject
def __init__(self, anachronos: Anachronos):
self.anachronos = anachronos

@EventHandler("async-event")
def handle(self):
self.anachronos.store(ASYNC_INSTANTIATED_EVENT_HANDLER)


@EventHandler("async-event")
def my_event_handler_function():
anachronos.get_instance().store(ASYNC_FUNCTION_EVENT_HANDLER)
1 change: 1 addition & 0 deletions e2e_test/app/components/event/event_handler.py
Expand Up @@ -34,3 +34,4 @@ def handle(self):
@EventHandler("event")
def my_event_handler_function():
anachronos.get_instance().store(FUNCTION_EVENT_HANDLER)

16 changes: 12 additions & 4 deletions e2e_test/app/components/event/event_resource.py
@@ -1,17 +1,25 @@
from jivago.event.async_event_bus import AsyncEventBus
from jivago.event.event_bus import EventBus
from jivago.lang.annotations import Inject
from jivago.wsgi.annotations import Resource
from jivago.wsgi.methods import GET
from jivago.wsgi.annotations import Resource, Path
from jivago.wsgi.methods import POST


@Resource("/event")
class EventResource(object):

@Inject
def __init__(self, event_bus: EventBus):
def __init__(self, event_bus: EventBus, async_event_bus: AsyncEventBus):
self.async_event_bus = async_event_bus
self.event_bus = event_bus

@GET
@POST
def send_event(self) -> str:
self.event_bus.emit("event")
return "OK"

@POST
@Path("/async")
def send_event_async(self) -> str:
self.async_event_bus.emit("async-event")
return "OK"
43 changes: 40 additions & 3 deletions e2e_test/app/components/resource/dependency_resource.py
@@ -1,7 +1,10 @@
import random

import time
from anachronos import Anachronos

from e2e_test.testing_messages import INSTANTIATED_LAZY_BEAN
from jivago.inject.annotation import Provider, Singleton
from e2e_test.testing_messages import INSTANTIATED_LAZY_BEAN, INSTANTIATED_REQUEST_SCOPED_BEAN
from jivago.inject.annotation import Provider, Singleton, Component, RequestScoped
from jivago.lang.annotations import Inject
from jivago.wsgi.annotations import Resource, Path
from jivago.wsgi.methods import GET
Expand All @@ -11,18 +14,52 @@ class ALazyProvidedBean(object):
pass


@Component
@RequestScoped
class ARequestScopedBean(object):

@Inject
def __init__(self, anachronos: Anachronos):
self.value = random.randint(0, 1000)
anachronos.store(INSTANTIATED_REQUEST_SCOPED_BEAN + str(self.value))


@Component
class ADependency(object):

@Inject
def __init__(self, request_scoped_bean: ARequestScopedBean):
self.request_scoped_bean = request_scoped_bean

def get_value(self) -> int:
return self.request_scoped_bean.value


@Resource("/dependency")
class DependencyResource(object):

@Inject
def __init__(self, lazy_component: ALazyProvidedBean):
def __init__(self, lazy_component: ALazyProvidedBean,
some_request_scoped_bean: ARequestScopedBean,
some_dependency: ADependency,
anachronos: Anachronos):
self.anachronos = anachronos
self.some_dependency = some_dependency
self.some_request_scoped_bean = some_request_scoped_bean
self.lazy_component = lazy_component

@GET
@Path("/lazybean")
def get_lazybean(self) -> str:
return "OK"

@GET
@Path("/request-scoped")
def check_request_scoped_bean(self):
assert self.some_request_scoped_bean.value == self.some_dependency.get_value()
time.sleep(0.5)
return str(self.some_request_scoped_bean.value)


@Provider
@Singleton
Expand Down
31 changes: 31 additions & 0 deletions e2e_test/app/components/resource/stream_resource.py
@@ -0,0 +1,31 @@
import time
from anachronos import Anachronos

from e2e_test.testing_messages import POST_HTTP_STREAM
from jivago.lang.annotations import Inject
from jivago.wsgi.annotations import Resource
from jivago.wsgi.methods import GET, POST
from jivago.wsgi.request.streaming_request_body import StreamingRequestBody
from jivago.wsgi.request.streaming_response_body import StreamingResponseBody


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

@Inject
def __init__(self, anachronos: Anachronos):
self.anachronos = anachronos

@GET
def get_stream(self) -> StreamingResponseBody:
return StreamingResponseBody(self.generate_bytes())

def generate_bytes(self) -> bytes:
for i in range(0, 5):
yield f"test-{i}\r\n".encode("utf-8")
time.sleep(0.1)

@POST
def post_stream(self, body: StreamingRequestBody) -> str:
self.anachronos.store(POST_HTTP_STREAM + f" {body.readall()}")
return "OK"

0 comments on commit 8618d05

Please sign in to comment.