{"id":20354785,"url":"https://github.com/pgorecki/lato","last_synced_at":"2025-05-16T10:08:26.630Z","repository":{"id":203733754,"uuid":"708052397","full_name":"pgorecki/lato","owner":"pgorecki","description":"Python microframework for modular monoliths and loosely coupled apps","archived":false,"fork":false,"pushed_at":"2025-01-02T21:11:31.000Z","size":200,"stargazers_count":182,"open_issues_count":4,"forks_count":5,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-05-10T03:51:12.037Z","etag":null,"topics":["loose-coupling","loosely-coupled-design","microframework","modular","monolith"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pgorecki.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-10-21T11:33:05.000Z","updated_at":"2025-05-09T01:10:36.000Z","dependencies_parsed_at":null,"dependency_job_id":"5d1e3a9b-df5a-49ca-8aaa-19605beb4f3b","html_url":"https://github.com/pgorecki/lato","commit_stats":null,"previous_names":["pgorecki/lato"],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgorecki%2Flato","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgorecki%2Flato/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgorecki%2Flato/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgorecki%2Flato/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pgorecki","download_url":"https://codeload.github.com/pgorecki/lato/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254509477,"owners_count":22082892,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["loose-coupling","loosely-coupled-design","microframework","modular","monolith"],"created_at":"2024-11-14T23:09:43.551Z","updated_at":"2025-05-16T10:08:21.622Z","avatar_url":"https://github.com/pgorecki.png","language":"Python","readme":"[![](https://github.com/pgorecki/lato/workflows/Tests/badge.svg)](https://github.com/pgorecki/lato/actions?query=workflow%3ATests)\n[![Documentation Status](https://readthedocs.org/projects/lato/badge/?version=latest)](https://lato.readthedocs.io/en/latest/?badge=latest)\n[![PyPI version](https://img.shields.io/pypi/v/lato)](https://pypi.org/project/lato/)\n[![Python Versions](https://img.shields.io/pypi/pyversions/lato)](https://pypi.org/project/lato/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Downloads](https://static.pepy.tech/badge/lato/month)](https://pepy.tech/project/lato)\n\n# Lato\n\nLato is a Python microframework designed for building **modular monoliths** and **loosely coupled** applications.\nBased on dependency injection and Python 3.6+ type hints.\n\n---\n\n**Documentation**: \u003ca href=\"https://lato.readthedocs.io\" target=\"_blank\"\u003ehttps://lato.readthedocs.io\u003c/a\u003e\n\n**Source Code**: \u003ca href=\"https://github.com/pgorecki/lato\" target=\"_blank\"\u003ehttps://github.com/pgorecki/lato\u003c/a\u003e\n\n---\n\n## Features\n\n- **Modularity**: Organize your application into smaller, independent modules for better maintainability.\n\n- **Flexibility**: Loosely couple your application components, making them easier to refactor and extend.\n\n- **Testability**: Easily test your application components in isolation. \n\n- **Minimalistic**: Intuitive and lean API for rapid development without the bloat.\n\n- **Async Support**: Concurrency and async / await is supported.\n\n\n## Installation\n\nInstall `lato` using pip:\n\n```bash\npip install lato\n```\n\n## Quickstart\n\nHere's a simple example to get you started:\n\n```python\nfrom lato import Application, TransactionContext\nfrom uuid import uuid4\n\n\nclass UserService:\n    def create_user(self, email, password):\n        ...\n\n\nclass EmailService:\n    def send_welcome_email(self, email):\n        ...\n\n\napp = Application(\n    name=\"Hello World\",\n    # dependencies\n    user_service=UserService(),\n    email_service=EmailService(),\n)\n\n\ndef create_user_use_case(email, password, session_id, ctx: TransactionContext, user_service: UserService):\n    # session_id, TransactionContext and UserService are automatically injected by `ctx.call`\n    print(\"Session ID:\", session_id)\n    user_service.create_user(email, password)\n    ctx.publish(\"user_created\", email)\n\n\n@app.handler(\"user_created\")\ndef on_user_created(email, email_service: EmailService):\n    email_service.send_welcome_email(email)\n\n\nwith app.transaction_context(session_id=uuid4()) as ctx:\n    # session_id is transaction scoped dependency\n    result = ctx.call(create_user_use_case, \"alice@example.com\", \"password\")\n```\n\n\n## Example of a modular monolith\n\nLato is designed to help you build modular monoliths, with loosely coupled modules. This example shows how to \nintroduce a structure in your application and how to exchange messages (events) between modules.\n\nLet's imagine that we are building an application that allows the company to manage its candidates, \nemployees and projects. Candidates and employees are managed by the `employee` module, while projects are managed by\nthe `project` module. When a candidate is hired, the `employee` module publishes a `CandidateHired` event, which is handled\nby the `employee` module to send a welcome email. When an employee is fired, the `employee` module publishes an \n`EmployeeFired` event, which is handled by both the `employee` and `project` modules to send an exit email and \nto remove an employee from any projects, respectively.\n\nFirst, let's start with commands that holds all the required information to execute a use case:\n\n```python\n# commands.py\n\nfrom lato import Command\n\n\nclass AddCandidate(Command):\n    candidate_id: str\n    candidate_name: str\n\n\nclass HireCandidate(Command):\n    candidate_id: str\n\n\nclass FireEmployee(Command):\n    employee_id: str\n    \n    \nclass CreateProject(Command):\n    project_id: str\n    project_name: str\n    \n    \nclass AssignEmployeeToProject(Command):\n    employee_id: str\n    project_id: str\n```\n\nAnd the events that are published by the application (note that all events are expressed in past tense):\n\n```python\n# events.py\n\nfrom lato import Event\n\n\nclass CandidateHired(Event):\n    candidate_id: str\n\n\nclass EmployeeFired(Event):\n    employee_id: str\n    \n    \nclass EmployeeAssignedToProject(Event):\n    employee_id: str\n    project_id: str\n```\n\nNow let's define the employee module. Each function which is responsible for handling a specific command is decorated\nwith `employee_module.handler`. Similarly, each function which is responsible for handling a specific event is\ndecorated with `employee_module.on`.\n\n```python\n# employee_module.py\n\nfrom lato import ApplicationModule\nfrom commands import AddCandidate, HireCandidate, FireEmployee\nfrom events import CandidateHired, EmployeeFired\n\nemployee_module = ApplicationModule(\"employee\")\n\n\n@employee_module.handler(AddCandidate)\ndef add_candidate(command: AddCandidate, logger):\n    logger.info(f\"Adding candidate {command.candidate_name} with id {command.candidate_id}\")\n\n\n@employee_module.handler(HireCandidate)\ndef hire_candidate(command: HireCandidate, publish, logger):\n    logger.info(f\"Hiring candidate {command.candidate_id}\")\n    publish(CandidateHired(candidate_id=command.candidate_id))\n\n\n@employee_module.handler(FireEmployee)\ndef fire_employee(command: FireEmployee, publish, logger):\n    logger.info(f\"Firing employee {command.employee_id}\")\n    publish(EmployeeFired(employee_id=command.employee_id))\n\n\n@employee_module.handler(CandidateHired)\ndef on_candidate_hired(event: CandidateHired, logger):\n    logger.info(f\"Sending onboarding email to {event.candidate_id}\")\n\n\n@employee_module.handler(EmployeeFired)\ndef on_employee_fired(event: EmployeeFired, logger):\n    logger.info(f\"Sending exit email to {event.employee_id}\")\n```\n\nAs you can see, some functions have additional parameters (such as `logger` or `publish`) which are automatically \ninjected by the application (to be more specific, by a transaction context) upon command or event execution. This allows \nyou to test your functions in isolation, without having to worry about dependencies. \n\nThe structure of the project module is similar to the employee module:\n\n```python\n# project_module.py\n\nfrom lato.application_module import ApplicationModule\nfrom commands import CreateProject, AssignEmployeeToProject\nfrom events import EmployeeFired, EmployeeAssignedToProject\n\nproject_module = ApplicationModule(\"project\")\n\n\n@project_module.handler(EmployeeFired)\ndef on_employee_fired(event: EmployeeFired, logger):\n    logger.info(f\"Checking if employee {event.employee_id} is assigned to a project\")\n\n\n@project_module.handler(CreateProject)\ndef create_project(command: CreateProject, logger):\n    logger.info(f\"Creating project {command.project_name} with id {command.project_id}\")\n    \n    \n@project_module.handler(AssignEmployeeToProject)\ndef assign_employee_to_project(command: AssignEmployeeToProject, publish, logger):\n    logger.info(f\"Assigning employee {command.employee_id} to project {command.project_id}\")\n    publish(EmployeeAssignedToProject(employee_id=command.employee_id, project_id=command.project_id))\n    \n    \n@project_module.handler(EmployeeAssignedToProject)\ndef on_employee_assigned_to_project(event: EmployeeAssignedToProject, logger):\n    logger.info(f\"Sending 'Welcome to project {event.project_id}' email to employee {event.employee_id}\")\n```\n\nKeep in mind that the `employee_module` is not aware of the `project_module` and\nvice versa. The only way to communicate between modules is through events.\n\nFinally, let's put everything together:\n\n```python\n# application.py\n\nimport logging\nimport uuid\nfrom lato import Application, TransactionContext\nfrom employee_module import employee_module\nfrom project_module import project_module\nfrom commands import AddCandidate, HireCandidate, CreateProject, AssignEmployeeToProject, FireEmployee\n\nlogger = logging.getLogger()\nlogger.setLevel(logging.DEBUG)\nconsole_handler = logging.StreamHandler()\nconsole_handler.setLevel(logging.DEBUG)\nformatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')\nconsole_handler.setFormatter(formatter)\nlogger.addHandler(console_handler)\n\napp = Application(\"Modular Application\", logger=logger)\napp.include_submodule(project_module)\napp.include_submodule(employee_module)\n\n@app.on_enter_transaction_context\ndef on_enter_transaction_context(ctx: TransactionContext):\n    logger = ctx[logging.Logger]\n    transaction_id = uuid.uuid4()\n    logger = logger.getChild(f\"transaction-{transaction_id}\")\n    ctx.dependency_provider.update(logger=logger, transaction_id=transaction_id, publish=ctx.publish)\n    logger.debug(\"\u003c\u003c\u003c Begin transaction\")\n\n@app.on_exit_transaction_context\ndef on_exit_transaction_context(ctx: TransactionContext, exception=None):\n    logger = ctx[logging.Logger]\n    logger.debug(\"\u003e\u003e\u003e End transaction\")\n    \n@app.transaction_middleware\ndef logging_middleware(ctx: TransactionContext, call_next):\n    logger = ctx[logging.Logger]\n    description = f\"{ctx.current_action[1]} -\u003e {repr(ctx.current_action[0])}\" if ctx.current_action else \"\"\n    logger.debug(f\"Executing {description}...\")\n    result = call_next()\n    logger.debug(f\"Finished executing {description}\")\n    return result\n\n\napp.execute(command=AddCandidate(candidate_id=\"1\", candidate_name=\"Alice\"))\napp.execute(command=HireCandidate(candidate_id=\"1\"))\napp.execute(command=CreateProject(project_id=\"1\", project_name=\"Project 1\"))\napp.execute(command=AssignEmployeeToProject(employee_id=\"1\", project_id=\"1\"))\napp.execute(command=FireEmployee(employee_id=\"1\"))\n```\n\nThe first thing to notice is that the `Application` class is instantiated with a `logger`. This logger is used as\nan application level dependency. The `Application` class also provides a way to include submodules using the\n`include_submodule` method. This method will automatically register all the handlers and listeners defined in the\nsubmodule.\n\nNext, we have the `on_enter_transaction_context` and `on_exit_transaction_context` hooks. These hooks are called\nwhenever a transaction context is created or destroyed. The transaction context is automatically created when\n`app.execute` is called. The purpose of a transaction context is to hold all the dependencies that are required\nto execute a command or handle an event, and also to create any transaction level dependencies. In this example, we\nuse the `on_enter_transaction_context` hook to update the transaction context with a logger and a transaction id,\nbut in a real application you would probably want to use the hooks to begin a database transaction and commit/rollback \nany changes. If you need to get a dependency from the transaction context, you can use the `ctx[identifier]` syntax, \nwhere `identifier` is the name (i.e. `logger`) or type (i.e. `logging.Logger`) of the dependency.\n\n\nThere is also a `logging_middleware` which is used to log the execution of any commands and events. This middleware is\nautomatically called whenever a command or event is executed, and there may be multiple middlewares chained together.\n\nFinally, we have the `app.execute` calls which are used to execute commands and events. The `app.execute` method\nautomatically creates a transaction context and calls the `call` method of the transaction context. The `call` method\nis responsible for executing the command or event, and it will automatically inject any dependencies that are required.\n\nIn addition, you can use `app.publish` to publish any external event, i.e. from a webhooks or a message queue.\n\n\n## Dive deeper\n\nFor more examples check out:\n\n- [tutorial](https://lato.readthedocs.io/en/latest/tutorial/index.html)\n- [examples](https://github.com/pgorecki/lato/tree/main/examples)\n- [tests](https://github.com/pgorecki/lato/tree/main/tests)\n\n\n## Testing\n\nRun the tests using pytest:\n\n```bash\npytest tests\n```\n\n\n## What lato actually means?\n\n*Lato* is the Polish word for *\"summer\"*. And we all know that summer is more fun than spring ;)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpgorecki%2Flato","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpgorecki%2Flato","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpgorecki%2Flato/lists"}