Metadata-Version: 2.4
Name: fractal-commands
Version: 1.0.1
Summary: A minimal command bus for building SOLID logic in your Python applications: plain-data commands, one handler per command, and no silent drops.
Keywords: command bus,cqrs,ddd,command,handler
Author-email: Douwe van der Meij <douwe@karibu-online.nl>
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
License-File: LICENSE
Requires-Dist: fractal-specifications
Requires-Dist: fractal-repositories
Requires-Dist: pytest>=7.0.0 ; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23.0 ; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0 ; extra == "dev"
Requires-Dist: black>=23.0.0 ; extra == "dev"
Requires-Dist: ruff>=0.1.0 ; extra == "dev"
Requires-Dist: mypy>=1.0.0 ; extra == "dev"
Requires-Dist: isort>=5.13.2 ; extra == "dev"
Project-URL: Documentation, https://github.com/Fractal-Forge/fractal-commands#readme
Project-URL: Homepage, https://github.com/Fractal-Forge/fractal-commands
Project-URL: Issues, https://github.com/Fractal-Forge/fractal-commands/issues
Project-URL: Repository, https://github.com/Fractal-Forge/fractal-commands
Provides-Extra: dev

# Fractal Commands

> Fractal Commands is a minimal command bus for building SOLID logic in your Python applications.

[![PyPI Version][pypi-image]][pypi-url]
[![Build Status][build-image]][build-url]

<!-- Badges -->

[pypi-image]: https://img.shields.io/pypi/v/fractal-commands
[pypi-url]: https://pypi.org/project/fractal-commands/
[build-image]: https://github.com/Fractal-Forge/fractal-commands/actions/workflows/build.yml/badge.svg
[build-url]: https://github.com/Fractal-Forge/fractal-commands/actions/workflows/build.yml

## Installation

```sh
pip install fractal-commands
```

## Background

A command is a plain data object describing an intent: *add this road*, *approve
this member*. A handler carries it out. The bus is the thin thing in between —
it knows which handler answers which command, and nothing else.

Keeping the two apart is what makes the pattern useful. The caller states what
it wants and stays ignorant of how it happens; the handler owns the how and
never has to know who asked. Commands cross that seam as data, so they are easy
to log, queue, replay, or map onto from events.

## Usage

```python
from dataclasses import dataclass

from fractal_commands import Command, CommandBus, CommandHandler


@dataclass
class Greet(Command):
    name: str


class GreetHandler(CommandHandler[Greet]):
    command = Greet

    def handle(self, command: Greet):
        return f"hello {command.name}"


bus = CommandBus()
bus.add_handler(GreetHandler())

bus.handle(Greet("world"))
# {'GreetHandler': 'hello world'}
```

Every handler registered for a command runs. `handle` collects the return
values of the ones that produced something, keyed by handler class name;
handlers that return `None` — most write handlers — simply do not appear.

`await bus.handle_async(command)` is the same thing for async handlers.

## A command with no handler is an error

Dispatching a command nobody handles raises `NoCommandHandlerError`:

```python
bus = CommandBus()
bus.handle(Greet("world"))
# NoCommandHandlerError: no handler is registered for Greet, so the command was
# dropped without being executed
```

This is deliberate, and it is worth explaining, because the obvious alternative
— shrug and return an empty result — is what this library was extracted to stop
doing.

Handlers usually register themselves through a decorator at import time. That
makes registration a side effect of the import graph, and import graphs lose
edges: a module stops being imported, and every command of that type silently
stops being handled. Nothing raises. No event is published, no row is written,
and the call returns normally. Tests that assert on the service layer see a
perfectly healthy no-op, so the failure surfaces days later somewhere else
entirely, as missing data.

Raising turns that into a one-line stack trace at the first dispatch.

### When silence is legitimate

A bus fed by fan-out is the honest exception. An event projector that maps
events onto commands will produce commands this particular deployment has no
handler for, and that is normal rather than broken. Pass `strict=False` to log
the miss at `ERROR` and carry on:

```python
bus = CommandBus(strict=False)
bus.handle(Greet("world"))
# ERROR:fractal_commands.command_bus:no handler is registered for Greet, so the
# command was dropped without being executed
# {}
```

It is also the migration path if you are adopting this library in an
application that has been relying on the old silence: start with
`strict=False`, fix what the logs show, then turn it on.

## Entity commands

The three commands generic CRUD needs are included, built on
[fractal-specifications](https://github.com/douwevandermeij/fractal-specifications)
and [fractal-repositories](https://github.com/douwevandermeij/fractal-repositories):

```python
from fractal_commands import AddEntityCommand, UpdateEntityCommand, DeleteEntityCommand
```

Each carries the specification the handler checks the entity against, so the
business rule travels with the intent instead of living in the handler.

## Development

```sh
make dev-install
make test
make lint
make format
```

