Skip to content

Commit 82f8d3a

Browse files
anconguiAndrés Contreras Guillén
andauthored
feat: starter parity with Java/.NET (v26.05.03) (#9)
Brings the starter system to feature parity with fireflyframework-starter-* (Java) and FireflyFramework.Starter.* (.NET). Three big wins: 1. The @enable_*_stack decorators are now functional. They used to set a marker attribute that nothing read at boot, so the bundle they advertised did not actually take effect. They now inject their property defaults into the live config between framework defaults and the user's pyfly.yaml: framework defaults < starter defaults < user yaml < profiles < env Config.from_sources() accepts a new starter_defaults parameter that PyFlyApplication.__init__ populates by scanning the application class for __pyfly_starter_* attributes. Auto-configs guarded on pyfly.X.enabled = "true" (CQRS, EDA, cache, event sourcing, transactional engine, IDP, etc.) wire up automatically when the matching starter is applied. 2. New @enable_web_stack for the web tier specifically. Activates pyfly.web, pyfly.server, pyfly.observability, pyfly.actuator and pyfly.resilience -- useful for HTTP/REST APIs that don't need EDA, CQRS, or cache. Java rolls these into starter-core; .NET rolls them into Starter.Core; pyfly keeps them split so a non-HTTP service (worker, scheduler, CLI tool) can opt out of the web stack entirely. 3. Imperative register_*_stack(app) for parity with .NET's services.AddFireflyXxx(...) extension methods: - register_core_stack(app) - register_web_stack(app) - register_application_stack(app) - register_data_stack(app) - register_domain_stack(app) Imperative registration is authoritative -- it merges starter properties on top of whatever's already in the config (last call wins), matching .NET DI semantics. The decorator route is preferred when you want the user's pyfly.yaml to keep winning. Re-exports per starter Each starter now re-exports the most commonly used types and decorators of its tier so a controller / service file needs only one import line. Examples: from pyfly.starters.web import ( rest_controller, post_mapping, Body, Valid, PathVar, ... ) from pyfly.starters.domain import ( AggregateRoot, BusinessRuleViolation, DomainEvent, Entity, Specification, ValueObject, Command, CommandHandler, command_handler, rest_controller, service, ... ) Documentation New module guide docs/modules/starters.md explains the property- layering model, shows the cross-language correspondence table (Java / .NET / Python), and documents both the declarative (decorator) and imperative (function) usage patterns. Versioning - pyproject.toml: 26.5.2 -> 26.5.3 - pyfly.__version__: 26.05.02 -> 26.05.03 - install.sh PYFLY_VERSION: 26.05.02 -> 26.05.03 - README badge, install URLs, "Current" section - ROADMAP marks the starter-parity work as complete in v26.05.03; Backoffice and Utils still planned Tests 13 new starter tests (web decorator + register_*_stack imperative API + functional injection + user-config-overrides). 2799 framework tests pass. mypy --strict and ruff clean across all changed files. Co-authored-by: Andrés Contreras Guillén <ancongui@Andress-MacBook-Pro.local>
1 parent bbd546f commit 82f8d3a

17 files changed

Lines changed: 1276 additions & 69 deletions

File tree

CHANGELOG.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,84 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

77
---
88

9+
## v26.05.03 (2026-05-08)
10+
11+
### Changed — starter decorators are now functional
12+
13+
The ``@enable_*_stack`` decorators (`enable_core_stack`,
14+
`enable_application_stack`, `enable_data_stack`,
15+
`enable_domain_stack`) used to set a marker attribute that nothing
16+
read at boot, so the bundle they advertised did not actually take
17+
effect. They now inject their property defaults between framework
18+
defaults and the user's ``pyfly.yaml``:
19+
20+
```
21+
framework defaults < starter defaults < user pyfly.yaml < profiles < env
22+
```
23+
24+
`Config.from_sources()` accepts a new ``starter_defaults`` parameter
25+
that ``PyFlyApplication.__init__`` populates by scanning the
26+
application class for ``__pyfly_starter_*__`` attributes. Auto-configs
27+
guarded on ``pyfly.X.enabled = "true"`` (CQRS, EDA, cache,
28+
event sourcing, transactional, IDP, etc.) now wire up automatically
29+
when the matching starter is applied.
30+
31+
### Added — `@enable_web_stack` (new)
32+
33+
Pure web-tier bundle separate from `@enable_core_stack`. Activates
34+
``pyfly.web``, ``pyfly.server``, ``pyfly.observability``,
35+
``pyfly.actuator`` and ``pyfly.resilience`` — useful for HTTP/REST
36+
APIs that don't need EDA, CQRS, or cache. Java rolls these into
37+
``starter-core``; .NET rolls them into ``Starter.Core``; pyfly keeps
38+
them split so a non-HTTP service (worker, scheduler, CLI tool) can
39+
opt out of the web stack entirely.
40+
41+
### Added — imperative `register_*_stack(app)` API
42+
43+
Every starter now ships an imperative counterpart for parity with
44+
.NET's ``services.AddFireflyXxx(...)`` extension methods:
45+
46+
* ``register_core_stack(app)``
47+
* ``register_web_stack(app)``
48+
* ``register_application_stack(app)``
49+
* ``register_data_stack(app)``
50+
* ``register_domain_stack(app)``
51+
52+
Imperative registration is **authoritative** — it merges starter
53+
properties on top of whatever's already in the config, including the
54+
user's ``pyfly.yaml``. Mirrors .NET's last-call-wins DI semantics.
55+
56+
### Added — re-exports
57+
58+
Each starter now re-exports the most commonly used types and
59+
decorators of its tier so a controller / service file needs only a
60+
single import line:
61+
62+
* `pyfly.starters.core` re-exports `service`, `component`,
63+
`configuration`, `rest_controller`, `Autowired`, `Scope`,
64+
`pyfly_application`, `Command`, `CommandBus`, `CommandHandler`,
65+
`command_handler`, `Query`, `QueryBus`, `QueryHandler`,
66+
`query_handler`.
67+
* `pyfly.starters.web` re-exports `rest_controller`, `controller`,
68+
`controller_advice`, `exception_handler`, `request_mapping`,
69+
`get_mapping`, `post_mapping`, `put_mapping`, `patch_mapping`,
70+
`delete_mapping`, `sse_mapping`, `Body`, `PathVar`, `QueryParam`,
71+
`Header`, `Cookie`, `File`, `UploadedFile`, `Valid`.
72+
* `pyfly.starters.domain` re-exports the full DDD primitive set
73+
(`Entity`, `ValueObject`, `AggregateRoot`, `DomainEvent`,
74+
`Specification`, `DomainRepository`, `DomainException`,
75+
`BusinessRuleViolation`, `AggregateNotFound`) plus the core
76+
re-exports above.
77+
78+
### Documentation
79+
80+
New module guide [`docs/modules/starters.md`](docs/modules/starters.md)
81+
explains the property-layering model, shows the cross-language
82+
correspondence table (Java / .NET / Python), and documents both the
83+
declarative (decorator) and imperative (function) usage patterns.
84+
85+
---
86+
987
## v26.05.02 (2026-05-08)
1088

1189
### Added — `pyfly.domain` DDD building blocks

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<a href="https://github.com/fireflyframework"><img src="https://img.shields.io/badge/Firefly_Framework-official-ff6600?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMiAyQzYuNDggMiAyIDYuNDggMiAxMnM0LjQ4IDEwIDEwIDEwIDEwLTQuNDggMTAtMTBTMTcuNTIgMiAxMiAyeiIvPjwvc3ZnPg==" alt="Firefly Framework"></a>
1212
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.12%2B-blue?logo=python&logoColor=white" alt="Python 3.12+"></a>
1313
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-green" alt="License: Apache 2.0"></a>
14-
<a href="#"><img src="https://img.shields.io/badge/version-26.05.02-brightgreen" alt="Version: 26.05.02"></a>
14+
<a href="#"><img src="https://img.shields.io/badge/version-26.05.03-brightgreen" alt="Version: 26.05.03"></a>
1515
<a href="#"><img src="https://img.shields.io/badge/type--checked-mypy%20strict-blue?logo=python&logoColor=white" alt="Type Checked: mypy strict"></a>
1616
<a href="#"><img src="https://img.shields.io/badge/code%20style-ruff-purple?logo=ruff&logoColor=white" alt="Code Style: Ruff"></a>
1717
<a href="#"><img src="https://img.shields.io/badge/async-first-brightgreen" alt="Async First"></a>
@@ -804,13 +804,13 @@ See **[`samples/order_service/`](samples/order_service/README.md)** for an end-t
804804

805805
```bash
806806
# Install the latest release (uv)
807-
uv add "pyfly @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.5.2-py3-none-any.whl"
807+
uv add "pyfly @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.5.3-py3-none-any.whl"
808808
809809
# Install with specific extras
810-
uv add "pyfly[web,data-relational,cache] @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.5.2-py3-none-any.whl"
810+
uv add "pyfly[web,data-relational,cache] @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.5.3-py3-none-any.whl"
811811
812812
# Or with pip
813-
pip install "pyfly @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.5.2-py3-none-any.whl"
813+
pip install "pyfly @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.5.3-py3-none-any.whl"
814814
```
815815

816816
### One-Line Install (CLI + Framework)
@@ -1081,6 +1081,7 @@ Browse all guides in the [Module Guides Index](docs/modules/README.md):
10811081
- [Transactional Engine](docs/modules/transactional.md) — Saga, Workflow, and TCC distributed transaction patterns
10821082
- [Event Sourcing](docs/modules/eventsourcing.md) — Aggregates, event store, snapshots, outbox, projections
10831083
- [Domain (DDD primitives)](docs/modules/domain.md) — Entity, ValueObject, AggregateRoot, DomainEvent, Specification, DomainRepository, exceptions
1084+
- [Starters](docs/modules/starters.md) — Layered bundles (`@enable_core_stack`, `@enable_web_stack`, `@enable_application_stack`, `@enable_data_stack`, `@enable_domain_stack`) with one-line imperative APIs for .NET parity
10841085
- [Plugins](docs/modules/plugins.md) — Plugin SPI, extension points, lifecycle
10851086
- [Rule Engine](docs/modules/rule-engine.md) — YAML DSL, AST evaluator, batch evaluation
10861087
- [Callbacks (outbound)](docs/modules/callbacks.md) — Dispatch domain events to external HTTP endpoints
@@ -1135,7 +1136,15 @@ The git tag and human-readable display use the leading-zero form (`v26.05.01`);
11351136
11361137
See **[CHANGELOG.md](CHANGELOG.md)** for detailed release notes.
11371138
1138-
**Current:** `v26.05.02` (2026-05-08) — DDD primitives + OrderService sample + async-saga fix:
1139+
**Current:** `v26.05.03` (2026-05-08) — Functional starters + Java/.NET parity:
1140+
1141+
- **Starters now actually do something** — `@enable_*_stack` decorators no longer just set a marker attribute. They now inject their property defaults between framework defaults and the user's `pyfly.yaml`, so the bundle activates the modules it promises (`pyfly.cqrs.enabled`, `pyfly.transactional.enabled`, etc.) while explicit user values still win.
1142+
- **`@enable_web_stack` (new)** — dedicated web-tier starter for HTTP/REST APIs that don't need EDA, CQRS, or cache. Activates web framework adapter (Starlette/FastAPI), ASGI server, validation, actuator, observability, and resilience filters.
1143+
- **Imperative API for parity with .NET** — every starter now ships a `register_*_stack(app)` function (`register_core_stack`, `register_web_stack`, `register_application_stack`, `register_data_stack`, `register_domain_stack`) — the Pythonic counterpart to .NET's `services.AddFireflyXxx(...)` extension methods. Imperative registration is authoritative (last-call-wins).
1144+
- **One-import-line ergonomics** — every starter re-exports the most commonly used decorators and types of its tier. `from pyfly.starters.web import rest_controller, post_mapping, Body, Valid, ...`; `from pyfly.starters.domain import AggregateRoot, BusinessRuleViolation, Command, CommandHandler, command_handler, ...`.
1145+
- **Layered docs** — new [`docs/modules/starters.md`](docs/modules/starters.md) explains the property-layering model (framework defaults < starter defaults < user yaml < profile overlays < env vars) and shows the cross-language correspondence table.
1146+
1147+
**Previous:** `v26.05.02` (2026-05-08) — DDD primitives + OrderService sample + async-saga fix:
11391148
11401149
- **`pyfly.domain`** — pure-Python DDD building blocks: `Entity`, `ValueObject`, `AggregateRoot`, `DomainEvent`, `Specification` (with `&` / `|` / `~` combinators), `DomainRepository` protocol, `DomainException` / `BusinessRuleViolation` / `AggregateNotFound`. Mirrors `fireflyframework-starter-domain` (Java) and `FireflyFramework.Starter.Domain` (.NET).
11411150
- **OrderService sample** — `samples/order_service/` is a complete DDD-flavoured microservice with the same layered split (interfaces / models / core / web / sdk) used by the firefly-oss Java services and the .NET OrdersService sample. Includes a real `Order` aggregate, CQRS handlers, and a `ConfirmOrderSaga` that walks the order through `PLACED → INVENTORY_RESERVED → PAID → SHIPPED` with full compensation. 13/13 tests pass end-to-end.

ROADMAP.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ PyFly's roadmap is driven by achieving feature parity with the full [Firefly Fra
44

55
---
66

7-
## Current State (v26.05.02)
7+
## Current State (v26.05.03)
88

9-
PyFly ships with **39 fully-implemented modules** covering the foundation, application, infrastructure, integration, and cross-cutting layers — including the rewritten transactional engine (Saga + Workflow + TCC), Event Sourcing, IDP, ECM, Notifications, Webhooks, Callbacks, Plugins, Rule Engine, Config Server, and the new **`pyfly.domain` DDD primitives** (`v26.05.02`). See the [Changelog](CHANGELOG.md) for full details on what's included.
9+
PyFly ships with **39 fully-implemented modules** covering the foundation, application, infrastructure, integration, and cross-cutting layers — including the rewritten transactional engine (Saga + Workflow + TCC), Event Sourcing, IDP, ECM, Notifications, Webhooks, Callbacks, Plugins, Rule Engine, Config Server, and the **`pyfly.domain` DDD primitives** (`v26.05.02`). The starter system reached Java/.NET parity in `v26.05.03`: declarative `@enable_*_stack` decorators now actually activate the bundle's property defaults at boot, an imperative `register_*_stack(app)` API mirrors .NET's `services.AddFireflyXxx(...)`, and a new `@enable_web_stack` ships dedicated web-tier wiring. See the [Changelog](CHANGELOG.md) for full details.
1010

11-
Phases 1, 2, and 3 of the original roadmap landed in `v26.05.01`. The **DDD starters** portion of Phase 4 landed in `v26.05.02`. Backoffice and Utils remain planned.
11+
Phases 1, 2, and 3 of the original roadmap landed in `v26.05.01`. The **DDD starters** portion of Phase 4 landed in `v26.05.02`, and the **layered-bundle starter system** reached parity with the Java and .NET ports in `v26.05.03`. Backoffice and Utils remain planned.
1212

1313
---
1414

docs/modules/starters.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# Starters — Layered Bundles
2+
3+
Starters are **opinionated bundles** that activate every framework
4+
module a given service tier needs in a single decorator. They mirror
5+
``org.fireflyframework.starter.*`` (Java) and
6+
``FireflyFramework.Starter.*`` (.NET) so a service that's been written
7+
on one platform reads the same way on every other.
8+
9+
## Available starters
10+
11+
| Starter | Activates | Use it for |
12+
|---------|-----------|-----------|
13+
| **Core** (`enable_core_stack`) | web, server, observability, metrics, tracing, cache, EDA, CQRS, resilience, actuator (+ metrics), AOP | Any infra-tier service. The foundation every other starter pulls in. |
14+
| **Web** (`enable_web_stack`) | web, server, observability, metrics, tracing, actuator (+ metrics), resilience | Pure HTTP/REST APIs that don't need EDA, CQRS or cache. |
15+
| **Application** (`enable_application_stack`) | core stack + plugins, security (JWT + password), sessions, i18n, scheduling, transactional engine, IDP, callbacks, webhooks, notifications | Application/orchestration tier with auth, scheduling and integrations. |
16+
| **Data** (`enable_data_stack`) | core stack + relational, document, HTTP client, scheduling, resilience | Data ingestion / enrichment / batch services. |
17+
| **Domain** (`enable_domain_stack`) | core stack + event sourcing, transactional engine, rule engine, relational, HTTP client, plugins. Re-exports every `pyfly.domain` DDD primitive. | DDD-style domain microservices. |
18+
19+
## Two ways to use a starter
20+
21+
### 1. Declarative (preferred) — decorate the application class
22+
23+
```python
24+
from pyfly.core import pyfly_application
25+
from pyfly.starters.domain import enable_domain_stack
26+
27+
@enable_domain_stack
28+
@pyfly_application(name="my-service", scan_packages=["my_service"])
29+
class Application:
30+
pass
31+
```
32+
33+
`PyFlyApplication.__init__` sees the `__pyfly_starter_*__` attributes,
34+
expands every dotted key into the nested config dictionary, and
35+
**merges the result between framework defaults and the user's
36+
`pyfly.yaml`**. So:
37+
38+
- Framework default `pyfly.cqrs.enabled=false` → starter sets `true`.
39+
- User `pyfly.yaml` says `pyfly.cqrs.enabled=false` → user wins
40+
(explicit user choice always beats the bundle).
41+
42+
### 2. Imperative (explicit) — call `register_*_stack(app)`
43+
44+
This mirrors .NET's `services.AddFireflyCore(...)`:
45+
46+
```python
47+
from pyfly.core.application import PyFlyApplication
48+
from pyfly.starters.core import register_core_stack
49+
50+
@pyfly_application(name="my-service", scan_packages=["my_service"])
51+
class Application: pass
52+
53+
app = PyFlyApplication(Application)
54+
register_core_stack(app) # explicit registration — overrides config files
55+
await app.startup()
56+
```
57+
58+
Imperative registration is **authoritative**: starter values win over
59+
anything already in the config (including a user `pyfly.yaml`). The
60+
last `register_*_stack(...)` call wins for a given key, matching .NET's
61+
`services.AddX(...)` semantics.
62+
63+
## Re-exports — single import line per layer
64+
65+
Each starter re-exports the most commonly used types and decorators of
66+
its tier so a controller / service file needs only one import line.
67+
68+
### Core layer
69+
70+
```python
71+
from pyfly.starters.core import (
72+
Autowired, Scope, component, configuration, rest_controller, service,
73+
Command, CommandBus, CommandHandler, command_handler,
74+
Query, QueryBus, QueryHandler, query_handler,
75+
pyfly_application,
76+
enable_core_stack, register_core_stack,
77+
)
78+
```
79+
80+
### Web layer
81+
82+
```python
83+
from pyfly.starters.web import (
84+
rest_controller, controller, controller_advice, exception_handler,
85+
request_mapping, get_mapping, post_mapping, put_mapping,
86+
patch_mapping, delete_mapping, sse_mapping,
87+
Body, PathVar, QueryParam, Header, Cookie, File, UploadedFile, Valid,
88+
enable_web_stack, register_web_stack,
89+
)
90+
```
91+
92+
### Domain layer
93+
94+
```python
95+
from pyfly.starters.domain import (
96+
# DDD primitives
97+
Entity, ValueObject, AggregateRoot, DomainEvent, Specification,
98+
DomainRepository, DomainException, BusinessRuleViolation,
99+
AggregateNotFound,
100+
# Carried through from the core re-exports
101+
Command, CommandHandler, command_handler,
102+
Query, QueryHandler, query_handler,
103+
rest_controller, service, configuration,
104+
pyfly_application,
105+
enable_domain_stack, register_domain_stack,
106+
)
107+
```
108+
109+
## Cross-language correspondence
110+
111+
| Java | .NET | Python |
112+
|------|------|--------|
113+
| `fireflyframework-starter-core` | `services.AddFireflyCore(...)` | `@enable_core_stack` / `register_core_stack(app)` |
114+
| (web bundled in core) | (web bundled in core) | `@enable_web_stack` / `register_web_stack(app)` *(new in v26.05.03)* |
115+
| `fireflyframework-starter-application` | `services.AddFireflyApplication(...)` | `@enable_application_stack` / `register_application_stack(app)` |
116+
| `fireflyframework-starter-data` | `services.AddFireflyData(...)` | `@enable_data_stack` / `register_data_stack(app)` |
117+
| `fireflyframework-starter-domain` | `services.AddFireflyDomain(...)` | `@enable_domain_stack` / `register_domain_stack(app)` |
118+
119+
## Composing starters
120+
121+
Starters compose. A typical pattern: stack `@enable_application_stack`
122+
*on top of* a vendor- or product-specific decorator that adds your own
123+
beans or property defaults. Multiple `@enable_*_stack` decorators on
124+
the same class union their property dicts, with later decorators
125+
winning on key overlaps.
126+
127+
```python
128+
@enable_web_stack # add the web tier on top of the application bundle
129+
@enable_application_stack # base bundle: core + security + scheduling + …
130+
@pyfly_application(name="acme-api", scan_packages=["acme"])
131+
class Application:
132+
pass
133+
```
134+
135+
## Property layering recap
136+
137+
```
138+
┌─────────────────────────────────────────────────────────────┐
139+
│ framework defaults (pyfly-defaults.yaml — most modules │
140+
│ disabled by default for safety) │
141+
├─────────────────────────────────────────────────────────────┤
142+
│ starter defaults (@enable_*_stack — turns the bundle on) │
143+
├─────────────────────────────────────────────────────────────┤
144+
│ user pyfly.yaml (your project root + config/) │
145+
├─────────────────────────────────────────────────────────────┤
146+
│ profile overlays (pyfly-{profile}.yaml) │
147+
├─────────────────────────────────────────────────────────────┤
148+
│ environment vars (PYFLY_X_Y at read time) │
149+
└─────────────────────────────────────────────────────────────┘
150+
151+
each layer overrides the one above
152+
```
153+
154+
`register_*_stack(app)` is the only exception — it stamps starter
155+
values **on top** of everything (including the user yaml) because it's
156+
called explicitly.

docs/versioning.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,11 @@ shipped, with the version metadata updated.
8787

8888
```python
8989
import pyfly
90-
print(pyfly.__version__) # → "26.05.02"
90+
print(pyfly.__version__) # → "26.05.03"
9191
```
9292

9393
```bash
94-
pyfly --version # → 26.05.02
94+
pyfly --version # → 26.05.03
9595
```
9696

9797
The startup banner displays the leading-zero form:

install.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ set -euo pipefail
2626

2727
# ── Constants ──────────────────────────────────────────────────────────────────
2828

29-
PYFLY_VERSION="26.05.02"
29+
PYFLY_VERSION="26.05.03"
3030
PYFLY_REPO="https://github.com/fireflyframework/fireflyframework-pyfly.git"
3131
DEFAULT_INSTALL_DIR="$HOME/.pyfly"
3232
MIN_PYTHON_MAJOR=3

0 commit comments

Comments
 (0)