diff --git a/CHANGELOG.md b/CHANGELOG.md index b1caf700..7f8bf6fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). --- +## v26.06.112 (2026-06-16) + +### Changed + +- **"PyFly by Example" — all diagrams redesigned.** Every content figure in both + the English and Spanish editions was rebuilt in a single, polished design + language (consistent cards, gradient headers, numbered step flows, monospace + code tokens, brand palette, vector iconography). This fixes real defects in the + previous artwork — broken/tofu glyphs (font-dependent arrows, circled numbers + and check marks) and clipped/overflowing content — by drawing all arrows, + checkmarks and numbered badges as vector shapes and keeping figure text to + ASCII/Latin-1 only. +- **Two new figures** where they help most: *Page / Pageable / Sort* (Chapter 5) + and *Value Object vs Entity* (Chapter 6). Both editions rebuilt and re-attached + to the release. + +--- + ## v26.06.111 (2026-06-16) ### Added diff --git a/book/art/figures/01-choice.svg b/book/art/figures/01-choice.svg index 02635927..54c41080 100644 --- a/book/art/figures/01-choice.svg +++ b/book/art/figures/01-choice.svg @@ -1,81 +1,100 @@ - + + + + + + + + + + + + + + + + - - + + - + + HAND-WIRED + a dozen libraries, your glue code - - - - FastAPI - + + - - - - Flask + + + + + FastAPI + + + + Flask + + + + SQLAlchemy + + + + Redis + + + + aiokafka + + + + pydantic-settings + + + + dependency-injector + - - - - SQLAlchemy - - - - - - aiokafka - + + CHOOSE + - - - - Redis - + + COHESIVE + one framework, integrated - - - - dependency-injector + + + + + PyFly + one cohesive stack - - - - - - - + + + + HTTP routing + + data access - - - - - - - - + + messaging + + caching - - PyFly - - - one cohesive stack + + DI container + + config + + + + production-ready defaults diff --git a/book/art/figures/01-layers.svg b/book/art/figures/01-layers.svg index b04a75ea..0909336e 100644 --- a/book/art/figures/01-layers.svg +++ b/book/art/figures/01-layers.svg @@ -1,75 +1,63 @@ - - - - - - - - - - + + + + + + + + + - - - - - depends on - - - - - Cross-Cutting - - aop · observability · logging · testing - - - - Infrastructure - web · data · messaging · cache · security - - - - Application - validation · cqrs · eda - - - - Foundation - kernel · core · container · context · config - - - - - - - + + + + + + DEPENDS ON + + + + + + + + Cross-Cutting + + aop · observability · logging · testing + 1 + + + + + + Infrastructure + + web · data · messaging · cache · security + 2 + + + + + + Application + + validation · cqrs · eda + 3 + + + + + + Foundation + BASE LAYER + + kernel · core · container · context · config + 4 diff --git a/book/art/figures/02-di.svg b/book/art/figures/02-di.svg index 87f997a7..c561854d 100644 --- a/book/art/figures/02-di.svg +++ b/book/art/figures/02-di.svg @@ -1,84 +1,83 @@ - + + + + + + + + + + + + + + + - - - - - - - ApplicationContext + + - + + + AUTOWIRING + ApplicationContext + the DI container - - - - - - WalletRepository - @repository + + + + + + WalletRepository + + @repository + CRUD bean + 2 - - - - - - EventPublisher - @component + + + + + EventPublisher + + @component + event bus bean + 2 - - - - - - - - - + + + + + WalletService + + 3 + def __init__(self, + repo: WalletRepository, + publisher: EventPublisher, + ) -> None: + declares what it needs by type - - - - - WalletService - __init__( - repo, publisher - ) + + reads type hints + 1 + - - - - - injected + + + - - - - injected + + + injected + + injected + + + build & inject bean + + inspect hints diff --git a/book/art/figures/02-lifecycle.svg b/book/art/figures/02-lifecycle.svg index dee442a5..50ea2540 100644 --- a/book/art/figures/02-lifecycle.svg +++ b/book/art/figures/02-lifecycle.svg @@ -1,94 +1,108 @@ - - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - scan - - - - - - - register - - - - - - - resolve - - - - - - - @post_construct - - - - - - - ready - - - - - - - - - - @pre_destroy - (shutdown) - - - classpath - definitions - wire deps - init hook - serving - cleanup - + + + + + STARTUP + + + SHUTDOWN + + + + + + + + + + + + scan + + scan_packages + import each package + 1 + + + + + + register + + bean definitions + stereotypes collected + 2 + + + + + + resolve + + wire dependencies + construct singletons + 3 + + + + + + + + + + + + + @post_construct + + init hook + runs after injection + 4 + + + + + + ready + + serving requests + application_started + 5 + + + + + + + + + @pre_destroy + + cleanup hook + reverse init order + 6 diff --git a/book/art/figures/03-config.svg b/book/art/figures/03-config.svg index 071160f9..da9658ee 100644 --- a/book/art/figures/03-config.svg +++ b/book/art/figures/03-config.svg @@ -1,71 +1,95 @@ - - - - - - - - + + + + + + + + + + + + + - - + + + + + Configuration precedence + four layers deep-merged bottom to top - later layers override earlier ones + + + + + HIGHER PRECEDENCE + + - - precedence + + + + - + + + + + Framework defaults + + pyfly-defaults.yaml + bundled baseline - complete working values for every key + + 1 - - - Framework defaults - built-in values baked into PyFly + + + + + User config file + + pyfly.yaml + project base - only the keys that differ from defaults + + 2 - - - pyfly.yaml - project-level config file · overrides defaults + + + + + Profile overlays + + pyfly-{profile}.yaml + per active profile - dev / staging / prod + + 3 - - - pyfly-{profile}.yaml - profile-specific overrides (dev / staging / prod) + + + + + Environment variables + checked at read time + + highest precedence - always wins, even when set after startup + + 4 - - - Environment variables - highest precedence · always wins - - - wins + + + wins + + Deep merge: nested keys merge key-by-key; scalars are replaced - you write only what changes. diff --git a/book/art/figures/04-request.svg b/book/art/figures/04-request.svg index d20d1df6..4f29f398 100644 --- a/book/art/figures/04-request.svg +++ b/book/art/figures/04-request.svg @@ -1,129 +1,92 @@ - - - - - - - - + + + + + + - - + + - - + + - - - - - request - - - - - - 200 / JSON + + - - - - - - Client - browser / - curl + + REQUEST + + + + RESPONSE - - - - - Filter chain - auth · logging - rate-limit + + + + + + - - - - - Controller - @get_mapping - route handler + + + + + Client + + browser / curl + sends the request + 1 - - - - - Service - @service - business logic + + + + + Filter chain + + auth · logging + rate-limit, CORS + 2 - - - - - Repository - @repository - data access + + + + + Controller + + @get_mapping + route handler + 3 - - - - - - + + + + + Service + + @service + business logic + 4 - - - - - - + + + + + Repository + + @repository + data access + 5 + + + + 200 / JSON diff --git a/book/art/figures/05-pagination.svg b/book/art/figures/05-pagination.svg new file mode 100644 index 00000000..49b76d4f --- /dev/null +++ b/book/art/figures/05-pagination.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + ONE ROUND-TRIP + the slice and the counts come back together + + + + + + + + + + Pageable + + page = 2 + size = 20 + sort = balance DESC + what slice do you want? + 1 + + + + + + Repository + + find_all(pageable) + SELECT ... LIMIT 20 OFFSET 40 + COUNT(*) + 2 + + + + + + Page[WalletDto] + + CONTENT + + + + 20 rows (this page) + METADATA + + total = 137 + + pages = 7 + + number = 2 + + has_next + 3 + diff --git a/book/art/figures/05-repository.svg b/book/art/figures/05-repository.svg index 5e1a0064..2be76833 100644 --- a/book/art/figures/05-repository.svg +++ b/book/art/figures/05-repository.svg @@ -1,102 +1,90 @@ - - - - - - - - - - + + + + + + - - + + + + + - - - - - - WalletService - transfer() - deposit() - - - + + - - - - - - RepositoryPort - «interface» + + YOUR CODE + FRAMEWORK - - your code depends on the port + + + + the port - - + + + + depends on - - + + + + implements - - - - - - SQLAlchemy Adapter - save() / find() - implements port + + + SQL - - + + + + + WalletService + + domain logic + transfer() · deposit() + 1 - - - - - - - - - + + + + + Repository + + «interface» + save() · find_by_id() + 2 - - DB + + + + + SQLAlchemy + + adapter + framework-supplied + 3 - - infrastructure - domain + + + + + + + DB + wallets table + 4 + + you depend on the port + the framework supplies the implementation diff --git a/book/art/figures/06-aggregate.svg b/book/art/figures/06-aggregate.svg index 51bc6747..5e0606ea 100644 --- a/book/art/figures/06-aggregate.svg +++ b/book/art/figures/06-aggregate.svg @@ -1,96 +1,132 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - - Wallet «aggregate root» - - - owner_id: OwnerId - - balance: Money - - - - - - - - Money «value object» - amount, currency - - - - - - - «invariant» - balance ≥ 0 - - - - deposit(amount) - withdraw(amount) - open(owner_id) - - - - - + + + + + + + + Wallet «aggregate root» - WalletOpened - - - + + STATE + + + owner_id: OwnerId + + + + balance: Money + + + + + + + Money «value object» + + amount, currency + + + + + + + + + + + + «invariant» + balance >= 0 + + + + + + BEHAVIOUR - the only way in + + + 1 + open(owner_id, currency) + + 2 + deposit(amount) + + 3 + withdraw(amount) + + + enforce the + invariant on + every change + + + EMITS + domain events + + + + + + + + + + + + + + WalletOpened + + + + + + + + + FundsDeposited + + + + + + + - FundsDeposited - - - - - - - emits - emits + FundsWithdrawn + + appended to the + _pending_events + buffer; drained after save diff --git a/book/art/figures/06-vo-entity.svg b/book/art/figures/06-vo-entity.svg new file mode 100644 index 00000000..8aa03765 --- /dev/null +++ b/book/art/figures/06-vo-entity.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + TWO KINDS OF DOMAIN OBJECT + + + + + + Value Object + + Money(amount, currency) + + + + + No identity - it just is a value + Immutable - frozen, never changes + Equality by all fields (10 EUR == 10 EUR) + Interchangeable - replace, do not mutate + euro replaced by euro: same value, no history + + + + + + Entity + + Wallet(id, balance) + + + + Stable id - a thread of identity + Mutable - balance changes over time + Equality by id (same wallet, new balance) + Has a lifecycle - opened, used, closed + balance 10 then 750: still the same wallet + diff --git a/book/art/figures/07-cqrs.svg b/book/art/figures/07-cqrs.svg index 3764136a..af52a817 100644 --- a/book/art/figures/07-cqrs.svg +++ b/book/art/figures/07-cqrs.svg @@ -1,140 +1,118 @@ - - - - - - - - - - + + + + + + + + + + - - + + + + + - - - - - - - - - - - writes - reads - - - - - - - Controller - POST / GET - - - - - - - - - - - - - CommandBus - dispatch(cmd) - - - - - - - - - - TransferFundsHandler - handle(cmd) - - - - - - - - - - write DB - - - - - - - QueryBus - dispatch(qry) - - - - - - - - - - GetBalanceHandler - handle(qry) - - - - - - - - - read model - view / cache - + + + + + + + + + WRITES · COMMANDS + + + READS · QUERIES + + + + + + + Controller + + POST / GET + route handler + 1 + + + + + + + + + + + + + + + + + CommandBus + + send(cmd) + 2 + + + + + + TransferFundsHandler + + handle(cmd) + mutates state + 3 + + + + + + + write DB + source of truth + + + + + + + + + + + QueryBus + + query(qry) + 2 + + + + + + GetBalanceHandler + + handle(qry) + returns a DTO + 3 + + + + + + read model + + view / cache + read-optimized diff --git a/book/art/figures/08-eda.svg b/book/art/figures/08-eda.svg index e0eb2f55..07997146 100644 --- a/book/art/figures/08-eda.svg +++ b/book/art/figures/08-eda.svg @@ -1,106 +1,95 @@ - - - - - - - - - - - - - + + + + + + + + + - - - - - EventPublisher - publish(event) - - - + + - - no listener knowledge + + PUBLISH + + FAN-OUT - - - - - InMemoryEventBus - dispatch → listeners + + + + + + + + + - - - + + + + + EventPublisher - FundsDeposited - - - + publish(event) + aggregate raises a fact + 1 - - - - - + + + + + InMemoryEventBus + + dispatch() + routes to listeners + 2 - - - - - BalanceProjection - on_funds_deposited() + + + + + + FundsDeposited + + - - - - - Notifier - send_email() + + + + + BalanceProjection + + on_funds_deposited() + 3 - - - - - AuditLog - append_entry() + + + + + Notifier + + send_email() + 3 - - publisher ↔ listeners: zero coupling + + + + + AuditLog + + append_entry() + 3 + + + Zero coupling: the publisher never names a listener - add one, no edits. diff --git a/book/art/figures/09-eventsourcing.svg b/book/art/figures/09-eventsourcing.svg index 86097362..80fe6d87 100644 --- a/book/art/figures/09-eventsourcing.svg +++ b/book/art/figures/09-eventsourcing.svg @@ -1,170 +1,156 @@ - - - - - - - - - - + + + + + + + + + + - - + + + + + + + + - - ① command - ② replay → state - ③ projection - - - - - - TransferFunds - Command - - - - append - - - EventStore (append-only / immutable) - - - - - - WalletOpened - seq 1 · owner_id - - - - - - FundsDeposited - seq 2 · amount - - - - - - FundsWithdrawn - seq 3 · amount - - - - immutable ✓ - - - - - - - replay all events - - - - WalletOpened - - - - - FundsDeposited - - - - - FundsWithdrawn - - - - - - - - - Wallet - balance: £750 - - - - - snapshot @N - - - - - - - - fan-out - - - - - - Projection - on_event(e) - - - - - - - read model - + + + + + WRITE + + + + + + + TransferFunds + + command + applies new events + + 1 + + + + append + + + + + + EventStore - append-only log + + + 2 + + + + WalletOpened + seq 1 · owner_id + + + + + + FundsDeposited + seq 2 · amount + + + + + + FundsWithdrawn + seq 3 · amount + + + + + immutable + + + + + replay() + + REBUILD STATE + + 3 + + + fold the stream, event by event + + + + WalletOpened + + + + FundsDeposited + + + + FundsWithdrawn + + + + + + + + + Wallet + + rebuilt state + balance: 750 + + + + snapshot every N events + optional - skips early replay + + + + + + + + READ + + + 4 + + + + fan-out + + + + + + Projection + + on_event(e) + + + + + + + + + Read model + + fast queries diff --git a/book/art/figures/10-messaging.svg b/book/art/figures/10-messaging.svg index afb3c6ce..da9b9205 100644 --- a/book/art/figures/10-messaging.svg +++ b/book/art/figures/10-messaging.svg @@ -1,135 +1,112 @@ - - - - - - - - - - + + + + + + + + + + + + + + - - - - - + + + + + - - happy path - - on failure - - - - - - - - - Wallet - service - - - - write - - - - - - outbox - DB table - - - - poll - - - - - - - - - - - - - - wallet-events - - - - - - - - + + + + + PUBLISH + + domain events cross the network to other services + + + + + write + + consume + + + + + + Wallet - - - - consume - - - - - - Payments - consumer - - - - - - max retries exceeded - - - - - - - - - DLQ - dead-letter queue - + service + emits domain events + 1 + + + + + + MessageBrokerPort + + one abstraction, many brokers + + + Kafka adapter + + RabbitMQ adapter + 2 + + + + + + + + + + + + + + + topic + wallet-events + durable, replayable log + 3 + + + + + + Payments + + @message_listener + consumer in another service + 4 + + + + ON FAILURE + a poisoned message cannot be processed + + + + max retries exceeded + + + + + + DLQ + + dead-letter queue + holds messages for later inspection diff --git a/book/art/figures/11-client.svg b/book/art/figures/11-client.svg index 08890244..59aa0018 100644 --- a/book/art/figures/11-client.svg +++ b/book/art/figures/11-client.svg @@ -1,96 +1,87 @@ - - - - - - - - - - + + + + + + - - + + + + + - - - - - WalletService - calls client + + - - + + TYPED CLIENT - NO HAND-ROLLED HTTP - - - - @service_client - - - - - PaymentsClient - typed interface proxy - - (not hand-rolled httpx) + + + + - - + - - HTTP + + - - - circuit breaker + + + HTTP - - - retry + + + + + WalletService + + caller / BFF + injects the client + 1 - - - - - Payments - service + + + + @service_client + + + + PaymentsClient + + typed interface proxy + no hand-rolled httpx + 2 - - - + + + + + Payments + external service + over the network + 3 - - typed client — no hand-rolled HTTP + + + circuit breaker + + retry x3 + + + + + + + HttpClientBeanPostProcessor + generates the implementation at startup diff --git a/book/art/figures/12-saga.svg b/book/art/figures/12-saga.svg index 3b617fa0..a7987e3a 100644 --- a/book/art/figures/12-saga.svg +++ b/book/art/figures/12-saga.svg @@ -1,130 +1,103 @@ - - - - - - - - - - + + + + + + + + + + - - - - - + + + + + - - forward - compensation (undo) - - - - - - - - - Orchestrator - saga engine - - - - - - - - - Debit Wallet - reserve funds - - - + + - - - - - Capture Payment - payment gateway + + FORWARD + - - - - - FAIL + + + COMPENSATION (UNDO) - - + + + + + - - - - - Notify - not reached - - - - - - - - - - - - - Refund Wallet - release reserved funds - - - + + + + + Orchestrator + + saga engine + runs steps, rolls back + + + + + + Debit Wallet + + reserve funds + compensate=refund + 1 + + + + + + Capture Payment + + payment gateway + step fails + 2 + + + + + + + + Notify + send receipt + not reached + + + + + + + + + + + + Refund Wallet + + release reserved funds + undo of step 1 - - ← compensate + + + compensate - - - + + + - diff --git a/book/art/figures/13-cache.svg b/book/art/figures/13-cache.svg index 680a6078..73a3c64d 100644 --- a/book/art/figures/13-cache.svg +++ b/book/art/figures/13-cache.svg @@ -1,112 +1,131 @@ - - - - - - - - - - + + + + + + + + + + - - + + - - + + + + + - - - - - Service - lookup(key) - - - - check - - - - - - Cache - Redis / memory - - - - - - HIT - fast return - - - - - MISS - - - - - - - - - - - - - Database - - - - - populate cache - - - - return data - - - - + + + + + CACHE-ASIDE READ PATH + + + + + + + + + + + + Request + + balance(w-001) + read the wallet + 1 + + + + + + Cache decorator + @cacheable + wraps the service method + 2 + + + + + + Cache lookup + + get(key) + Redis / in-memory + 3 + + + found? - - cache-aside pattern + + + + HIT + + + + cached value - function body skipped + + + + + MISS + + + + + + + + + Service + source + + function body runs + SQL query / read replica + 4 + + + + + + Store in cache + + put(key, value, ttl) + 5 + + + + result + + + + populate + + + + + computed value + + + + + forward / hit return + + value to caller + + populate cache (miss) + diff --git a/book/art/figures/13-resilience.svg b/book/art/figures/13-resilience.svg index 2697661e..426ea923 100644 --- a/book/art/figures/13-resilience.svg +++ b/book/art/figures/13-resilience.svg @@ -1,134 +1,109 @@ - - - - - - - - - - - - - + + + + + + + + + + - - + + + + + - - Request - + + - - - - - - Rate - limiter + + RESILIENCE CHAIN + - - + + Request + - - - - Bulk- - head - - - - - - - - Timeout - - - - - - - - Circuit - breaker + + + + + - - + - - - - - Service - business logic - - - - - - - - open / rejected - - - - - - Fallback - default / cached resp. + + + + + Rate limiter + + @rate_limiter + drops excess + 1 - - - + + + + + Bulkhead + @bulkhead + caps concurrency + 2 - - resilience chain + + + + + Timeout + + @time_limiter + cancels slow calls + 3 + + + + + Circuit breaker + + closed -> open + -> half-open + trips on failure + 4 + + + + + + Service + + downstream call + AccountService + + + + + + + + open / rejected / timed out + + + + + + Fallback + + default / cached response diff --git a/book/art/figures/14-security.svg b/book/art/figures/14-security.svg index b5377b34..db6ee907 100644 --- a/book/art/figures/14-security.svg +++ b/book/art/figures/14-security.svg @@ -1,122 +1,122 @@ - - - - - - - - - - + + + + + + + + + + - - + + - - + + + + + - - security filter chain — JWT auth + RBAC - - - - - - Request - Bearer token - - - - - - - - - - - - - JWT auth - verify signature - decode claims - - - - principal - - - - - - @secure - role check - expression eval - - - - allowed - - - - - - Service - business logic - - - - - - IDP - token issuer / OIDC - - - - issues JWT - - - - 403 Forbidden - - - - + + + + + AUTHENTICATE + + AUTHORIZE + + + + + + + + + token + claims + allowed + + + + + + Bearer JWT - - \ No newline at end of file + Authorization: + Bearer eyJ... + signed token per request + 1 + + + + + + Resource filter + + validate signature + JWKS · issuer · audience + check exp + 2 + + + + + + claims -> roles + + SecurityContext + sub · roles · permissions + readable by handlers + 3 + + + + + + Authz gate + + @secure + role / permission + expression eval + 4 + + + + + + Identity provider + + OIDC issuer + publishes JWKS keys + + + + signing keys + + + + + + Service + + @service + business logic runs + + + + + + + + 403 Forbidden + diff --git a/book/art/figures/15-observability.svg b/book/art/figures/15-observability.svg index 3ac21a5e..4c87574d 100644 --- a/book/art/figures/15-observability.svg +++ b/book/art/figures/15-observability.svg @@ -1,98 +1,165 @@ - - - - - - - - - - + + + + + + + + + + - - + + + + + - - observability pillars — logs · metrics · traces → dashboard - - - - - - Logs - - - - - - structured JSON - - - - - - Metrics - - - - - - - - Prometheus-style - - - - - - Traces - - - root span - - db query - - redis get - OpenTelemetry - - - - - - - - - - - Admin dashboard - /admin · actuator endpoints - - - - + + + + + OBSERVABILITY + metrics · traces · logs · health, served on the management port + + + + + + PyFly app + create_app() + business API + + port 8080 + + + + + user traffic stays on 8080 + + + + + + + + + + + - \ No newline at end of file + + + + + Metrics + + Prometheus + + + + + + + + + + + Traces + + OpenTelemetry + + + + + + + + + Logs + + structured JSON + + + + + + + + + Health + + UP / DOWN + + + + + + + + + + + + + + Management port + + + port 9090 + Actuator endpoints + /actuator/health + /actuator/prometheus + /actuator/loggers + /actuator/beans + + + + + + + + + + Admin dashboard + + /admin + + + + + + Ops tooling + + Prometheus scrape + Grafana / kubectl + + + + + + + + Kubernetes probes + + liveness and readiness hit the dedicated health sub-paths on 9090 + + + + /actuator/health/liveness + + + /actuator/health/readiness + + + + + + diff --git a/book/art/figures/16-testing.svg b/book/art/figures/16-testing.svg index 4489e738..2f3c3b23 100644 --- a/book/art/figures/16-testing.svg +++ b/book/art/figures/16-testing.svg @@ -1,103 +1,124 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + - - testing — pytest + Testcontainers → real containers → ✓ + + + + + PyFly testing pyramid + every layer runs fast - no Docker, SQLite only - - - - - pytest - test suite - conftest / fixtures + + + + + MANY / FAST + FEWER + - - - spins up + + - - - - - Testcontainers + + + + + + + 4 + Booted-context - - - - Postgres - real DB + + + + + + + 3 + Repository + derived queries + pagination + Specification - - - Redis - real cache + + + + + + + 2 + CQRS flow + open / deposit / withdraw / query + real bus + repository - - - Kafka - real broker + + + + + + + 1 + Unit + Money + Wallet domain model + no dependencies - plain pytest - - - @service_connection — auto-wired to fixtures + + + + + + - - - asserts + + + + + Full ApplicationContext + DI scan + CQRS + transactional + EDA + monkeypatch env - - - - + + + + + SQLite + aiosqlite + real DB, no mocks + tmp_path + SQLAlchemy - all tests pass + + + + + Real bus + repo over SQLite + wired once, shared by tests + conftest.py fixtures - - - + + + + No dependencies + construct, call, assert + no fixtures, no async - \ No newline at end of file + + PYRAMID + DEPENDENCIES + LUMEN APPROACH + diff --git a/book/art/figures/17-integrations.svg b/book/art/figures/17-integrations.svg index bb5910ec..6c2a91a3 100644 --- a/book/art/figures/17-integrations.svg +++ b/book/art/figures/17-integrations.svg @@ -1,153 +1,169 @@ - - - - - - - - - - + + + + + + - - + + - - + + + + + - - integrations — schedule · notify · webhook · callback - - - - - - - - - - - - - - - - - @scheduled - - - - - - - - - Job - cron / interval - - - - - - - - - Notifier - - - - - - - - - email - - SMS - - push - - - - - - - - - - - - - Webhook ↓ inbound - HMAC-SHA256 verify - dispatch to handler - - - - POST /webhook - - - - - - Callback ↑ outbound - HMAC-SHA256 sign - exponential retry - - - - POST callback - - - - - - 3rd party - external service - - - - + + + + + OUTBOUND INTEGRATIONS + schedule · notify · webhook · callback - reaching the outside world + + + + INSIDE THE APP + + + + THE OUTSIDE WORLD + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + Scheduler + + @scheduled + cron / interval - fires internally + + + + + + + + + + + + 2 + + + + + + + + + Notifier + + @notification + flows outward to users + + + + email + + SMS + + push + + + + + + + + + + + 3 + + + + + + Webhook + + verify HMAC - dispatch + + + + + + 4 + + + + + + Callback + + sign HMAC - retry + + + + + + + + Partner + + external service + users · SaaS · 3rd party + + + + + + + + + + - \ No newline at end of file + + + + + + POST /webhook + + + + POST + + + retry + diff --git a/book/art/figures/18-production.svg b/book/art/figures/18-production.svg index fe3e3afb..8f26ba3e 100644 --- a/book/art/figures/18-production.svg +++ b/book/art/figures/18-production.svg @@ -1,120 +1,120 @@ - - - - - - - - - - + + + + + + - - + + + + + - - production — plugins · rule engine · Lumen → deploy → cloud - - - - - - Plugins - hook registry - extension points - - - - - - Config server - remote config reload - - - - - - Rule Engine - hot-reload rules - - - - - - - - - - - Lumen - PyFly runtime core - DI · web · data · events - - - - ship - - - - - - Docker - - - container - - - - - - - - - - - - - cloud - AWS / GCP / bare metal - - - - + + + + + PUBLIC TRAFFIC + + port 8080 only + + + cluster-internal · 9090 + + + + + + + + Ingress + + internet edge + exposes 8080 only + TLS · routing + 1 + + + + + + PyFly process (one process) + two in-process listeners - not extra workers + 2 + + + + + + application port + :8080 + business API + WebSocket feeds + OpenAPI spec + config-server routes + pyfly.server.port + + + + + + management port + :9090 + /actuator/* + /admin + health probes + Prometheus scrape + management.server.port + + + + + + + + open by default - keep inside the cluster + + + + + + + + Observability + + 3 + + + + Prometheus + /actuator/prometheus + + + K8s probes + liveness · readiness + + + Admin console + /admin + + + Metrics drill-down + /actuator/metrics + + + + public request flow (8080, exposed to internet) + + + management access (9090, cluster-internal only) - \ No newline at end of file + Set management.server.port equal to server.port to collapse onto one port, or -1 to disable management endpoints. + diff --git a/book/dist/pyfly-by-example-es.epub b/book/dist/pyfly-by-example-es.epub index a12314a3..633a000e 100644 Binary files a/book/dist/pyfly-by-example-es.epub and b/book/dist/pyfly-by-example-es.epub differ diff --git a/book/dist/pyfly-by-example-es.pdf b/book/dist/pyfly-by-example-es.pdf index 73adcd20..9c69f4aa 100644 Binary files a/book/dist/pyfly-by-example-es.pdf and b/book/dist/pyfly-by-example-es.pdf differ diff --git a/book/dist/pyfly-by-example.epub b/book/dist/pyfly-by-example.epub index 663ec5f0..da03f5c5 100644 Binary files a/book/dist/pyfly-by-example.epub and b/book/dist/pyfly-by-example.epub differ diff --git a/book/dist/pyfly-by-example.pdf b/book/dist/pyfly-by-example.pdf index e3b63631..10d2219f 100644 Binary files a/book/dist/pyfly-by-example.pdf and b/book/dist/pyfly-by-example.pdf differ diff --git a/book/manuscript-es/05-persistence.md b/book/manuscript-es/05-persistence.md index c7e603b8..86523612 100644 --- a/book/manuscript-es/05-persistence.md +++ b/book/manuscript-es/05-persistence.md @@ -269,6 +269,8 @@ El prefijo decide la *forma* del resultado: `find_by` devuelve una lista, `count Un endpoint de listado nunca debería devolver *todos* los monederos. La *paginación* es la solución estándar: devolver una **página** de filas de tamaño fijo a la vez, más los metadatos suficientes para que el cliente pueda pedir la siguiente. Los tipos de paginación de PyFly —`Pageable` (qué página, qué tamaño, qué orden), `Sort` (la ordenación) y `Page[T]` (el fragmento más los metadatos)— se heredan directamente de la superficie CRUD a través de `find_all(pageable)`. +::: figure art/figures/05-pagination.svg | Figura 5.2 — Un solo viaje de ida y vuelta: un Pageable (página, tamaño, orden) entra en find_all y vuelve un Page con el fragmento de filas más los totales que el cliente necesita para dibujar los controles de paginación. + Hay tres piezas pequeñas que ensamblar: el manejador que llama a `find_all(pageable)`, el `Page[T]` que devuelve y el controlador que construye el `Pageable` a partir de la petición. Las veremos en ese orden. El manejador de consulta `ListWallets` de Lumen es toda la historia en tres líneas: diff --git a/book/manuscript-es/06-domain-driven-design.md b/book/manuscript-es/06-domain-driven-design.md index d8afcc19..2d550d0b 100644 --- a/book/manuscript-es/06-domain-driven-design.md +++ b/book/manuscript-es/06-domain-driven-design.md @@ -30,6 +30,8 @@ El DDD da nombre a estos dos roles: **entidades** y **objetos de valor**, y el m Las entidades transitorias (las que tienen `id=None`) se comparan iguales solo por la identidad de objeto de Python, así que puedes meter entidades en conjuntos y diccionarios sin preocuparte por colisiones de hash de objetos sin guardar. +::: figure art/figures/06-vo-entity.svg | Figura 6.1 — Dos clases de objeto de dominio: un objeto de valor Money (sin identidad, inmutable, comparado por sus campos) frente a una entidad Wallet (con identidad estable, mutable, comparada por id). + El dinero es el objeto de valor de manual. Un importe de cien euros no es un objeto específico que sigues en el tiempo; es un valor. Dos instancias separadas de `Money(100, "EUR")` son iguales. Un depósito no muta el importe existente: produce uno nuevo, dejando el original intacto y el modelo libre de efectos secundarios ocultos. Aquí está el objeto de valor `Money` para Lumen: @@ -367,7 +369,7 @@ uv run --extra dev pytest tests/test_wallet_aggregate.py -q El diagrama de abajo muestra el panorama completo: estado, invariantes y los eventos que el monedero emite. -::: figure art/figures/06-aggregate.svg | Figura 6.1 — El agregado Wallet: estado, invariantes y los eventos que emite. +::: figure art/figures/06-aggregate.svg | Figura 6.2 — El agregado Wallet: estado, invariantes y los eventos que emite. !!! spring "Equivalencia con Spring" `AggregateRoot[str]` se corresponde con `org.jmolecules.ddd.types.AggregateRoot` de jMolecules y con `AbstractAggregateRoot` de Spring Data, que ofrece el mismo mecanismo `registerEvent()` / `@DomainEvents` / `@AfterDomainEventPublication`. El patrón es idéntico en espíritu: el agregado acumula eventos en un búfer; el repositorio los vacía tras un guardado exitoso; un `DomainEventPublisher` los despacha. El `raise_event` + `clear_events` de PyFly es el equivalente Python de `registerEvent` + `@AfterDomainEventPublication`. diff --git a/book/manuscript/05-persistence.md b/book/manuscript/05-persistence.md index c3b04677..7b9169c0 100644 --- a/book/manuscript/05-persistence.md +++ b/book/manuscript/05-persistence.md @@ -269,6 +269,8 @@ The prefix decides the *shape* of the result: `find_by` returns a list, `count_b A list endpoint should never return *every* wallet. *Pagination* is the standard fix: return one fixed-size **page** of rows at a time, plus enough metadata for the client to ask for the next one. PyFly's pagination types — `Pageable` (what page, what size, what sort), `Sort` (the ordering), and `Page[T]` (the slice plus metadata) — are inherited straight from the CRUD surface via `find_all(pageable)`. +::: figure art/figures/05-pagination.svg | Figure 5.2 — One round-trip: a Pageable (page, size, sort) goes into find_all, and a Page comes back carrying the row slice plus the total counts the client needs to render pagination controls. + There are three small pieces to assemble: the handler that calls `find_all(pageable)`, the `Page[T]` it returns, and the controller that builds the `Pageable` from the request. We will take them in that order. Lumen's `ListWallets` query handler is the whole story in three lines: diff --git a/book/manuscript/06-domain-driven-design.md b/book/manuscript/06-domain-driven-design.md index 52170ede..a0169537 100644 --- a/book/manuscript/06-domain-driven-design.md +++ b/book/manuscript/06-domain-driven-design.md @@ -30,6 +30,8 @@ DDD names these two roles **entities** and **value objects**, and PyFly's `pyfly Transient entities (those with `id=None`) compare equal only by Python's object identity, so you can safely put entities in sets and dicts without worrying about hash collisions from unsaved objects. +::: figure art/figures/06-vo-entity.svg | Figure 6.1 — Two kinds of domain object: a Money value object (no identity, immutable, compared by its fields) versus a Wallet entity (a stable identity, mutable, compared by id). + Money is the textbook value object. An amount of one hundred euros is not a specific object you track over time; it is a value. Two separate `Money(100, "EUR")` instances are equal. A deposit does not mutate the existing amount — it produces a new one, leaving the original untouched and the model free of hidden side-effects. Here is the `Money` value object for Lumen: @@ -367,7 +369,7 @@ uv run --extra dev pytest tests/test_wallet_aggregate.py -q The diagram below shows the complete picture: state, invariants, and the events the wallet emits. -::: figure art/figures/06-aggregate.svg | Figure 6.1 — The Wallet aggregate: state, invariants, and the events it emits. +::: figure art/figures/06-aggregate.svg | Figure 6.2 — The Wallet aggregate: state, invariants, and the events it emits. !!! spring "Spring parity" `AggregateRoot[str]` maps to jMolecules's `org.jmolecules.ddd.types.AggregateRoot` and to Spring Data's `AbstractAggregateRoot`, which offers the same `registerEvent()` / `@DomainEvents` / `@AfterDomainEventPublication` mechanism. The pattern is identical in spirit: the aggregate accumulates events in a buffer; the repository drains them after a successful save; a `DomainEventPublisher` dispatches them. PyFly's `raise_event` + `clear_events` is the Python equivalent of `registerEvent` + `@AfterDomainEventPublication`. diff --git a/pyproject.toml b/pyproject.toml index e23d62ab..2fa80297 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "pyfly" # CalVer YY.MM.PATCH — package metadata uses PEP 440 normalized form (26.5.4); # git tag, GitHub release and human-readable display use leading-zero form # (v26.05.04) to match the Java/.NET/Go siblings. -version = "26.6.111" +version = "26.6.112" description = "The official Python implementation of the Firefly Framework — DI, CQRS, EDA, hexagonal architecture, and more." readme = "README.md" license = "Apache-2.0" diff --git a/src/pyfly/__init__.py b/src/pyfly/__init__.py index 82f07d35..b7fe128d 100644 --- a/src/pyfly/__init__.py +++ b/src/pyfly/__init__.py @@ -13,4 +13,4 @@ # limitations under the License. """PyFly — Enterprise Python Framework.""" -__version__ = "26.06.111" +__version__ = "26.06.112"