Skip to content

Commit

Permalink
change composer interface. update documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
pgorecki committed Mar 24, 2024
1 parent 5a07b24 commit c954c28
Show file tree
Hide file tree
Showing 28 changed files with 499 additions and 159 deletions.
11 changes: 8 additions & 3 deletions docs/concurrency.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@ Below is an example of async application:

import asyncio
import logging
import sys

from lato import Application, TransactionContext

logging.basicConfig(level=logging.DEBUG)
logging.basicConfig(level=logging.INFO, format='%(name)s%(message)s')
root_logger = logging.getLogger("toy")

stream_handler = logging.StreamHandler(stream=sys.stdout)
stream_handler.setFormatter(logging.Formatter('%(levelname)s:%(name)s:%(message)s'))
root_logger.addHandler(stream_handler)


app = Application()


class Counter:
def __init__(self):
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
doc_root = dirname(__file__)
tutorial_src_root = sep.join([doc_root, "tutorial", "src"])

sys.path.insert(0, doc_root)
sys.path.insert(0, "..")
sys.path.insert(0, doc_root)

# Configuration file for the Sphinx documentation builder.
#
Expand Down
2 changes: 2 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Core Features

- **Minimalistic**: Intuitive and lean API for rapid development without the bloat.

- **Concurrency support**: built-in support for coroutines declared with async / await syntax.

Contents
--------

Expand Down
16 changes: 15 additions & 1 deletion docs/key_concepts/cq_composition.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,18 @@
Message Composition and Decomposition
=====================================

lorem ipsum
If there are multiple command handlers (i.e. in different modules) for the same Command, all handlers for that
command will be executed (decomposition), and the results of command handlers will be merged into single response
(composition).

.. literalinclude:: ../../examples/example5.py

By default, handler responses of type ``dict`` will be recursively merged into a single dict, and responses of type
``list`` will be merged a into single list.


You can use ``Application.compose`` decorator to declare a custom composer. A custom composer will receive kwargs
with names of the modules handling the response.

.. literalinclude:: ../../examples/example6.py

46 changes: 37 additions & 9 deletions docs/key_concepts/dependency_injection.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,42 @@
Dependency Injection
====================

work in progress
:func:`~lato.DependencyProvider` is an interface for any concrete provider capable of resolving and matching function
parameters. Both :func:`~lato.Application` and :func:`~lato.TransactionContext` internally use dependency provider
to resolve handler parameters. By default use dict based implementation
:func:`~lato.dependency_provider.BasicDependencyProvider`.

Example:
--------
>>> from lato.dependency_provider import BasicDependencyProvider
>>>
>>> def my_func(a: str) -> str:
>>> print(f"{a} {b}")
>>>
>>> assert BasicDependencyProvider(a="foo").resolve_func_params(my_func) == {}

This code demonstrates basic functionality of a dependency provider

.. testcode::

from lato.dependency_provider import BasicDependencyProvider

class FooService():
pass

def a_handler(service: FooService):
pass

foo_service = FooService()
dp = BasicDependencyProvider(foo_service=foo_service)
assert dp[FooService] is foo_service
assert dp["foo_service"] is foo_service

assert dp.resolve_func_params(a_handler) == {'service': foo_service}

``lagom`` integration
---------------------

This code showcases a dependency provider based on ``lagom``:

.. literalinclude:: ../../examples/example3/lagom_integration.py


``dependency_injector`` integration
-----------------------------------

This code showcases a dependency provider based on ``dependency_injector``:

.. literalinclude:: ../../examples/example3/dependency_injector_integration.py
44 changes: 44 additions & 0 deletions docs/key_concepts/events.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
Events and publish-subscribe
============================

Application modules can interact by publishing and subscribing to messages. Events can be published
using a ``TransactionContext``, and received by an event handler. There can be multiple handlers bound to
a single event. If the application is receiving an event from an external source,
it should be processed using ``Application.publish()``:


.. testcode::

from lato import Application, ApplicationModule, Event, Command, TransactionContext

class SampleCommand(Command):
pass

class FooHappened(Event):
source: str

foo_module = ApplicationModule(name="foo")
@foo_module.handler(SampleCommand)
def call_foo(command: SampleCommand, ctx: TransactionContext):
print("handling foo")
ctx.publish(FooHappened(source="foo"))

bar_module = ApplicationModule(name="bar")
@bar_module.handler(FooHappened)
def on_foo_happened(event: FooHappened):
print(f"handling event from {event.source}")

foobar = Application()
foobar.include_submodule(foo_module)
foobar.include_submodule(bar_module)

foobar.execute(SampleCommand())
foobar.publish(FooHappened(source="external source"))

And the output is:

.. testoutput::

handling foo
handling event from foo
handling event from external source
42 changes: 22 additions & 20 deletions docs/key_concepts/handlers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,11 @@ In this approach, a function is passed to a :func:`Application.call` as is:
from lato import Application

def foo():
print("called directly")
return "called directly"

app = Application("example")

app.call(foo)

And the output is:

.. testoutput::

called
assert app.call(foo) == "called directly"


Calling a function using an *alias*
Expand All @@ -38,26 +32,34 @@ Calling a function using an *alias*
In this approach, a function is first decorated with :func:`ApplicationM.handler`, and then called using an alias:

.. testcode::
from lato import ApplicationModule

from lato import Application
app = Application("example")

@app.handler("alias_of_foo")
def foo():
print("called via alias")
@app.handler("alias_of_bar")
def bar():
return "called via alias"

app.call("alias_of_foo")
app.call("alias_of_bar") == "called via alias"

And the output is:

.. testoutput::
Calling the function using a command
--------------------------------------------

called via alias
In this approach, a command is declared, then a :func:`Application.handler` decorator is used to
associate the command with its handler.

.. testcode::

Calling the function using a message handler
--------------------------------------------
from lato import Application, Command
app = Application("example")

class SampleCommand(Command):
x: int

In this approach, a message is declared, then a :func:`Application.handler` decorator is used to
associate the message with its handler.

@app.handler(SampleCommand)
def sample_command_handler(command: SampleCommand):
return f"called sample command with x={command.x}"

app.execute(SampleCommand(x=1)) == "called sample command with x=1"
4 changes: 3 additions & 1 deletion docs/key_concepts/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ Key Concepts
:maxdepth: 3

handlers
modularity
events
transaction_context
dependency_injection
middlewares
dependency_injection
cq_composition

76 changes: 73 additions & 3 deletions docs/key_concepts/middlewares.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,74 @@
Transaction Middleware
======================
Transaction callbacks and middlewares
=====================================

...
In most cases, the handler is executed by calling one of the Application methods: :func:`~lato.Application.call`,
:func:`~lato.Application.execute`, or :func:`~lato.Application.publish`, which creates the transaction context under the
hood.

When :func:`~lato.Application.call` or similar method is executed, the lifecycle of handler execution is as following:

1. ``on_enter_transaction_context`` callback is invoked

2. ``transaction_middleware`` functions, wrapping the handler are invoked

3. handler function is invoked

4. ``on_exit_transaction_context`` callback is invoked

The application can be configured to use any of the callbacks by using decorators:

.. testcode::

from typing import Optional, Callable
from lato import Application, TransactionContext


app = Application()

@app.on_enter_transaction_context
def on_enter_transaction_context(ctx: TransactionContext):
print("starting transaction")

@app.on_exit_transaction_context
def on_exit_transaction_context(ctx: TransactionContext,
exception: Optional[Exception]=None):
print("exiting transaction context")

@app.transaction_middleware
def middleware1(ctx: TransactionContext, call_next: Callable):
print("entering middleware1")
result = call_next() # will call middleware2
print("exiting middleware1")
return result

@app.transaction_middleware
def middleware2(ctx: TransactionContext, call_next: Callable):
print("entering middleware2")
result = call_next() # will call the handler
print("exiting middleware2")
return f"[{result}]"

def to_uppercase(s):
print("calling handler")
return s.upper()


print(app.call(to_uppercase, "foo"))

This will generate the output:

.. testoutput::

starting transaction
entering middleware1
entering middleware2
calling handler
exiting middleware2
exiting middleware1
exiting transaction context
[FOO]

Any of the callbacks is optional, and there can be multiple ``transaction_middleware`` callbacks.
``on_enter_transaction_context`` is a good place to set up the transaction level dependencies, i.e. the dependencies
that change with every transaction - correlation id, current time, database session, etc. ``call_next`` is a ``functools.partial`` object,
so you can inspect is arguments using ``call_next.args`` and ``call_next.keywords``.
Loading

0 comments on commit c954c28

Please sign in to comment.