Skip to content

Commit

Permalink
emitting and handling events
Browse files Browse the repository at this point in the history
  • Loading branch information
pgorecki committed Oct 26, 2023
1 parent 063d535 commit e4a56f4
Show file tree
Hide file tree
Showing 26 changed files with 1,684 additions and 3 deletions.
19 changes: 19 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

[*.yaml]
indent_size = 2

[{Makefile,**.mk}]
# Use tabs for indentation (Makefiles require tabs)
indent_style = tab
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,4 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/
31 changes: 31 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
default_language_version:
python: python3.10

repos:
# native hints instead of `from typing` | List -> list
- repo: https://github.com/sondrelg/pep585-upgrade
rev: 'v1.0' # Version to check
hooks:
- id: upgrade-type-hints

# Only for removing unused imports > Other staff done by Black
- repo: https://github.com/myint/autoflake
rev: "v1.4" # Version to check
hooks:
- id: autoflake
args:
- --in-place
- --remove-all-unused-imports
- --ignore-init-module-imports

- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
name: isort (python)
args: ["--profile", "black"]

- repo: https://github.com/ambv/black
rev: 22.3.0
hooks:
- id: black
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
publish:
python3 -m build
python3 -m twine upload --repository pypi dist/*
69 changes: 67 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,67 @@
# lato
Python microframework for modular monoliths and loosely coupled application
# Lato

## Overview
Lato is a Python microframework designed for building modular monoliths and loosely coupled applications.

## Features

- **Modularity**: Organize your application into smaller, independent modules for better maintainability.

- **Flexibility**: Loosely couple your application components, making them easier to refactor and extend.

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

- **Testability**: Easily test your application components in isolation.

## Installation

Install `lato` using pip:

```bash
pip install lato
```

## Quickstart

Here's a simple example to get you started:

```python
from lato import Application, TransactionContext
from uuid import uuid4


class UserService:
def create_user(self, email, password):
...


class EmailService:
def send_welcome_email(self, email):
...


app = Application(
name="Hello World",
# dependencies
user_service=UserService(),
email_service=EmailService(),
)


def create_user_use_case(email, password, session_id, ctx: TransactionContext, user_service: UserService):
# session_id, TransactionContext and UserService are automatically injected by `ctx.call`
print("Session ID:", session_id)
user_service.create_user(email, password)
ctx.emit("user_created", email)


@app.on("user_created")
def on_user_created(email, email_service: EmailService):
email_service.send_welcome_email(email)


with app.transaction_context(session_id=uuid4()) as ctx:
# session_id is transaction scoped dependency
result = ctx.call(create_user_use_case, "[email protected]", "password")
```

Empty file added docs/.gitkeep
Empty file.
Empty file added examples/example1/main.py
Empty file.
5 changes: 5 additions & 0 deletions lato/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from lato.application import Application
from lato.application_module import ApplicationModule
from lato.transaction_context import TransactionContext

__all__ = [Application, ApplicationModule, TransactionContext]
76 changes: 76 additions & 0 deletions lato/application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from typing import Any, Callable

from lato.application_module import ApplicationModule
from lato.dependency_provider import DependencyProvider
from lato.transaction_context import TransactionContext


class Application(ApplicationModule):
dependency_provider_class = DependencyProvider

def __init__(self, name=__name__, dependency_provider=None, **kwargs):
super().__init__(name)
self.dependency_provider = (
dependency_provider or self.dependency_provider_class(**kwargs)
)
self._on_enter_transaction_context = lambda ctx: None
self._on_exit_transaction_context = lambda ctx, exception=None: None
self._transaction_middlewares = []

def get_dependency(self, identifier: Any) -> Any:
"""Get a dependency from the dependency provider"""
return self.dependency_provider.get_dependency(identifier)

def __getitem__(self, item) -> Any:
return self.get_dependency(item)

def call(self, func: Callable | str, *args, **kwargs):
with self.transaction_context() as ctx:
result = ctx.call(func, *args, **kwargs)
return result

def on_enter_transaction_context(self, func):
"""
Decorator for registering a function to be called when entering a transaction context
:param func:
:return:
"""
self._on_enter_transaction_context = func
return func

def on_exit_transaction_context(self, func):
"""
Decorator for registering a function to be called when exiting a transaction context
:param func:
:return:
"""
self._on_exit_transaction_context = func
return func

def transaction_middleware(self, middleware_func):
"""
Decorator for registering a middleware function to be called when executing a function in a transaction context
:param middleware_func:
:return:
"""
self._transaction_middlewares.insert(0, middleware_func)
return middleware_func

def transaction_context(self, **dependencies) -> TransactionContext:
"""
Creates a transaction context with the application dependencies
:param dependencies:
:return:
"""
dp = self.dependency_provider.copy(**dependencies)
ctx = TransactionContext(dependency_provider=dp)
ctx.configure(
on_enter_transaction_context=self._on_enter_transaction_context,
on_exit_transaction_context=self._on_exit_transaction_context,
middlewares=self._transaction_middlewares,
handlers_iterator=self.iterate_handlers_for,
)
return ctx
64 changes: 64 additions & 0 deletions lato/application_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from collections import defaultdict

from utils import OrderedSet


class ApplicationModule:
def __init__(self, name: str):
self.name: str = name
self._handlers: dict[str, set[callable]] = defaultdict(OrderedSet)
self._submodules: OrderedSet[ApplicationModule] = OrderedSet()

def include_submodule(self, a_module):
assert isinstance(
a_module, ApplicationModule
), f"Can only include {ApplicationModule} instances, got {a_module}"
self._submodules.add(a_module)

def handler(self, alias):
"""
Decorator for registering use cases by name
"""
if callable(alias):
func = alias
alias = func.__name__
assert len(self._handlers[alias]) == 0
self._handlers[alias].add(func)
return func

def decorator(func):
"""
Decorator for registering use cases by name
"""
assert len(self._handlers[alias]) == 0
self._handlers[alias].add(func)
return func

return decorator

def iterate_handlers_for(self, alias: str):
if alias in self._handlers:
for handler in self._handlers[alias]:
yield handler
for submodule in self._submodules:
try:
yield from submodule.iterate_handlers_for(alias)
except KeyError:
pass

def on(self, event_name):
# TODO: add matcher parameter
def decorator(func):
"""
Decorator for registering an event handler
:param event_handler:
:return:
"""
self._handlers[event_name].add(func)
return func

return decorator

def __repr__(self):
return f"<{self.name} {object.__repr__(self)}>"
Loading

0 comments on commit e4a56f4

Please sign in to comment.