From b80a10bb5cc773485166bd5b157631c2c0a222b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Contreras=20Guill=C3=A9n?= Date: Sun, 31 May 2026 19:34:06 +0200 Subject: [PATCH] fix: framework-wide hardening pass (v26.05.06) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A deep audit surfaced a class of silent wiring gaps and correctness bugs — features that existed but were never connected to the runtime path, so the suite passed while behaviour was broken. This fixes them. 3146 tests pass; ruff / ruff-format / mypy --strict all green. See CHANGELOG.md for the full list. Admin dashboard - traces are recorded (TraceCollectorFilter now joins the filter chain) - SSE streams instead of buffering (fixed all Server-Sent Events framework-wide) - server info resolves lazily (was "unknown") Dependency injection & AOP - same-type @bean methods no longer collapse; list[T] returns every bean - AOP woven regardless of registration order (two-pass post-processing) - weaving/scheduling/wiring scans no longer trigger @property getters at startup - RequestContextFilter wired by default (REQUEST scope + @pre/@post_authorize) Web - RequestLoggingFilter no longer crashes without structlog (pyfly.logging.get_logger) - FastAPI adapter: correct OpenAPI generation + @controller_advice handlers - /actuator health rescan now runs; /actuator/prometheus returns text format Transactional engine - workflow @compensation_step executes on failure (reverse-order rollback) - REST controllers (orchestration/dlq/workflow) are mounted - saga compensator records outcomes; recovery TypeError fixed - workflow @wait_for_all/@wait_for_any timeouts honoured; child correlation id CQRS / EDA / messaging / scheduling - query cache + domain-event publishing wired - EDA circuit breaker no longer stuck OPEN - Kafka/RabbitMQ subscribe-after-start; scheduler fixed_delay survives errors Data / config / eventsourcing / notifications / callbacks - derived-query stub detection; mongo LIKE wildcards - Config.bind() placeholders + nested dataclasses; ConfigServer save/fetch - projection cursor no longer skips failed events; SMTP BCC; HMAC canonical JSON Docs: CHANGELOG v26.05.06, new docs/modules/logging.md + config-server.md. Version synced (pyfly.__version__ was stale). 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude --- CHANGELOG.md | 96 +++++++ docs/modules/README.md | 2 + docs/modules/config-server.md | 105 ++++++++ docs/modules/logging.md | 106 ++++++++ pyproject.toml | 2 +- src/pyfly/__init__.py | 2 +- src/pyfly/actuator/adapters/starlette.py | 19 +- src/pyfly/admin/auto_configuration.py | 9 +- src/pyfly/admin/providers/server_provider.py | 31 ++- src/pyfly/aop/weaver.py | 8 + src/pyfly/callbacks/dispatcher.py | 9 +- .../cli/templates/cli/hello_command.py.j2 | 4 +- src/pyfly/config_server/backend.py | 34 ++- src/pyfly/container/container.py | 35 ++- src/pyfly/context/application_context.py | 101 +++---- src/pyfly/core/config.py | 61 +++-- src/pyfly/cqrs/config/auto_configuration.py | 33 ++- src/pyfly/cqrs/event/publisher.py | 30 ++- .../data/document/mongodb/query_compiler.py | 15 +- src/pyfly/data/post_processor.py | 8 + src/pyfly/eda/adapters/kafka.py | 11 +- src/pyfly/eda/adapters/postgres.py | 23 +- src/pyfly/eda/adapters/redis.py | 26 +- src/pyfly/eda/auto_configuration.py | 4 +- src/pyfly/eda/circuit_breaker.py | 5 + src/pyfly/eventsourcing/projection.py | 6 + src/pyfly/logging/__init__.py | 28 +- src/pyfly/messaging/adapters/kafka.py | 44 ++-- src/pyfly/messaging/adapters/rabbitmq.py | 49 ++-- src/pyfly/notifications/providers/smtp.py | 5 + src/pyfly/observability/correlation.py | 20 +- src/pyfly/scheduling/task_scheduler.py | 14 +- src/pyfly/transactional/rest/controllers.py | 100 +++++-- .../transactional/saga/engine/compensator.py | 10 +- .../transactional/saga/engine/saga_engine.py | 6 +- .../shared/persistence/memory.py | 21 +- .../workflow/child_workflow_service.py | 12 +- .../transactional/workflow/definition.py | 2 + src/pyfly/transactional/workflow/engine.py | 14 + src/pyfly/transactional/workflow/executor.py | 91 ++++++- src/pyfly/transactional/workflow/registry.py | 2 + src/pyfly/web/adapters/fastapi/app.py | 112 +++++--- src/pyfly/web/adapters/fastapi/controller.py | 54 +++- src/pyfly/web/adapters/starlette/app.py | 90 ++++--- .../web/adapters/starlette/filter_chain.py | 54 +++- .../adapters/starlette/filters/__init__.py | 2 + .../filters/request_logging_filter.py | 9 +- .../web/adapters/starlette/request_logger.py | 8 +- src/pyfly/web/adapters/starlette/resolver.py | 23 +- src/pyfly/web/mappings.py | 14 +- tests/actuator/test_health_rescan.py | 55 ++++ tests/admin/test_admin_create_app_wiring.py | 103 ++++++++ tests/aop/test_property_and_ordering.py | 94 +++++++ tests/callbacks/test_callbacks.py | 19 +- tests/config/test_bind_placeholders_nested.py | 70 +++++ tests/config_server/test_config_server.py | 20 ++ tests/container/test_same_type_beans.py | 74 ++++++ tests/cqrs/test_auto_configuration_wiring.py | 37 +++ tests/cqrs/test_event_publishing_wiring.py | 199 ++++++++++++++ tests/eda/test_auto_configuration.py | 48 ++-- tests/eda/test_circuit_breaker.py | 61 +++++ tests/eda/test_postgres_event_bus.py | 10 +- tests/logging/test_get_logger.py | 52 ++++ tests/observability/test_correlation.py | 17 +- tests/transactional/saga/test_recovery.py | 24 ++ .../test_rest_controllers_mounting.py | 121 +++++++++ .../workflow/test_compensation.py | 247 ++++++++++++++++++ tests/transactional/workflow/test_engine.py | 28 ++ tests/transactional/workflow/test_registry.py | 22 ++ tests/web/test_fastapi_adapter.py | 78 +++++- tests/web/test_filter_chain.py | 90 +++++++ uv.lock | 2 +- 72 files changed, 2683 insertions(+), 357 deletions(-) create mode 100644 docs/modules/config-server.md create mode 100644 docs/modules/logging.md create mode 100644 tests/actuator/test_health_rescan.py create mode 100644 tests/admin/test_admin_create_app_wiring.py create mode 100644 tests/aop/test_property_and_ordering.py create mode 100644 tests/config/test_bind_placeholders_nested.py create mode 100644 tests/container/test_same_type_beans.py create mode 100644 tests/cqrs/test_auto_configuration_wiring.py create mode 100644 tests/cqrs/test_event_publishing_wiring.py create mode 100644 tests/eda/test_circuit_breaker.py create mode 100644 tests/logging/test_get_logger.py create mode 100644 tests/transactional/test_rest_controllers_mounting.py create mode 100644 tests/transactional/workflow/test_compensation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ad6d239..935d97d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,102 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). --- +## v26.05.06 (2026-05-31) + +### Hardening pass — framework-wide bug fixes + +A deep audit of the whole framework surfaced a class of *silent wiring gaps* +and correctness bugs — features that existed but were never connected to the +runtime path, so the test suite passed while the behaviour was broken. This +release fixes them. The full test suite passes and CI (`ruff`, `ruff format`, +`mypy --strict`) is green. + +#### Admin dashboard + +- **HTTP traces are now recorded.** The `TraceCollectorFilter` was resolved + from the DI container at `create_app()` time — before beans are instantiated — + so it was always `None` and never joined the request filter chain. It is now + created and owned by `create_app` and wired into the chain; `/admin/api/traces` + and the SSE trace stream show real traffic. +- **Live updates (SSE) now stream.** `WebFilterChainMiddleware` buffered the + *entire* response body before returning, which hung every infinite SSE stream + (this broke **all** Server-Sent Events framework-wide, not just admin). The + filter chain now forwards streaming responses incrementally. +- **Server info** resolves lazily instead of showing `unknown`. + +#### Dependency injection & AOP + +- **Same-type beans no longer collapse.** Two `@bean` methods returning the same + concrete type overwrote each other in the type-keyed registry and vanished + from `list[T]` resolution. All registrations are now tracked and + `resolve_all`/`list[T]` returns every bean. +- **AOP advice is woven regardless of registration order.** Bean post-processing + is now two-pass (all `before_init`, then `post_construct`+`after_init`), so a + target initialised before its `@aspect` is still advised. +- **A side-effecting `@property` no longer aborts startup.** Weaving, scheduled- + task discovery and every context wiring/lifecycle scan now look attributes up + statically (`inspect.getattr_static`) instead of triggering property getters. +- **`RequestContextFilter` is wired by default**, so `REQUEST`-scoped beans and + `@pre_authorize`/`@post_authorize` work out of the box. + +#### Web + +- `RequestLoggingFilter`/middleware no longer crash on every request when + `structlog` is not installed (new `pyfly.logging.get_logger` shim). +- The FastAPI adapter now generates a correct OpenAPI document for controller + routes and honours `@controller_advice` global exception handlers. +- The health-indicator rescan hook now actually runs after startup, so + `/actuator/health` reflects `DOWN` subsystems instead of always reporting `UP`. +- `/actuator/prometheus` returns the Prometheus text exposition format (was JSON). + +#### Transactional engine + +- **Workflow `@compensation_step` now executes on failure** — completed + compensatable steps are rolled back in reverse order. +- **Transactional REST controllers** (`/api/orchestration`, `/dlq`, `/workflow`) + are now mounted as HTTP routes. +- The saga compensator records compensation outcomes, so + `SagaResult.compensated`/`compensation_result` are populated. +- Saga stale-recovery no longer raises `TypeError` (`started_at` is persisted as + a `datetime`; `get_stale` tolerates ISO strings). +- Workflow `@wait_for_all`/`@wait_for_any` timeouts are honoured (no unbounded waits). +- Fire-and-forget child workflows return the real child correlation id. + +#### CQRS + +- The query cache adapter now receives the `CacheAdapter`, so `@cacheable` + queries are actually cached. +- Domain-event publishing is wired when an EDA/messaging producer bean is + present (was a permanent no-op). + +#### EDA, messaging & scheduling + +- The EDA circuit breaker no longer gets permanently stuck `OPEN`. +- Kafka/RabbitMQ message-broker adapters handle `@message_listener` subscriptions + that arrive after `start()` (they previously never consumed). +- A scheduled `fixed_delay` task that raises no longer kills its loop. + +#### Data, config, event sourcing, notifications, callbacks + +- Derived-query stub detection no longer misclassifies documented repository + methods as real implementations (SQLAlchemy + MongoDB). +- MongoDB derived-query `LIKE` wildcards (`%`, `_`) are now translated to regex. +- `Config.bind()` resolves `${...}` placeholders and binds nested dataclass fields. +- `ConfigServer` filesystem backend writes back the file `fetch()` reads, so + saves are no longer silently shadowed by a stale `.yaml`. +- The event-sourcing `ProjectionRunner` no longer advances its cursor past a + failed event (at-least-once, in-order; was silent data loss). +- The SMTP notification provider no longer drops BCC recipients. +- Outbound callback/webhook HMAC signatures are computed over canonical JSON + (were computed over `str(dict)` — unverifiable). + +#### Internal + +- `pyfly.__version__` is back in sync with the packaged version (it was stale). +- Lint/format/type fixes across the EDA adapters and correlation surface. + +--- + ## v26.05.05 (2026-05-19) ### Fixed — `PostgresEventBus` is multi-worker safe diff --git a/docs/modules/README.md b/docs/modules/README.md index b3c789a..4536f03 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -29,6 +29,7 @@ The building blocks that every PyFly application relies on. | [Core & Lifecycle](core.md) | Application bootstrap with `@pyfly_application`, startup/shutdown sequence, configuration loading, profile overlays, banner rendering | | [Dependency Injection](dependency-injection.md) | `@service`, `@repository`, `@controller`, `@component` stereotypes, constructor injection, `Autowired()`, scopes (singleton, transient, request), `@bean` factories, `@primary`, `Qualifier`, conditional beans, lifecycle hooks | | [Configuration](configuration.md) | YAML/TOML config files, `Config` class, profile-specific overlays, `@config_properties` binding, environment variable overrides | +| [Config Server](config-server.md) | Centralized config server (`ConfigServer`, `ConfigClient`), `ConfigBackend` SPI, in-memory & filesystem backends, Spring-Cloud-Config-compatible responses | | [Error Handling](error-handling.md) | 25+ exception types, `ErrorResponse`, `ErrorCategory`, `ErrorSeverity`, HTTP status mapping, structured error responses, `@exception_handler` | --- @@ -152,6 +153,7 @@ Monitor, schedule, and observe your applications in production. | Guide | What You'll Learn | |-------|-------------------| | [Observability](observability.md) | `@timed`, `@counted`, `@span`, `MetricsRegistry`, `HealthChecker`, Prometheus metrics export, OpenTelemetry tracing, structured logging with correlation IDs | +| [Logging](logging.md) | `get_logger`, `LoggingPort`, structlog & stdlib adapters, structured `event + key=value` logging, runtime level control | | [Scheduling](scheduling.md) | `@scheduled`, `@async_method`, `CronExpression`, `TaskScheduler`, fixed-rate tasks, fixed-delay tasks, asyncio and thread pool executors | --- diff --git a/docs/modules/config-server.md b/docs/modules/config-server.md new file mode 100644 index 0000000..65356c2 --- /dev/null +++ b/docs/modules/config-server.md @@ -0,0 +1,105 @@ +# Config Server Guide + +A lightweight, Spring-Cloud-Config-style **centralized configuration server**: +serve versioned config bundles (keyed by application + profile + label) to many +client services over HTTP. + +--- + +## Table of Contents + +1. [Introduction](#introduction) +2. [ConfigSource](#configsource) +3. [Backends](#backends) +4. [ConfigServer](#configserver) +5. [ConfigClient](#configclient) + +--- + +## Introduction + +The module has three parts: + +- **`ConfigBackend`** — the storage SPI (a `Protocol`): `fetch`, `save`, `list`. +- **`ConfigServer`** — a framework-agnostic controller exposing config bundles + over HTTP (`base_path = "/config"`). +- **`ConfigClient`** — fetches a bundle from a remote `ConfigServer` on startup. + +--- + +## ConfigSource + +A bundle is identified by `application` + `profile` + `label`: + +```python +from pyfly.config_server import ConfigSource + +ConfigSource( + application="orders", + profile="prod", + label="main", # defaults to "main" + properties={"db.url": "..."}, +) +``` + +--- + +## Backends + +Two backends ship out of the box: + +```python +from pyfly.config_server import InMemoryConfigBackend, FilesystemConfigBackend + +backend = InMemoryConfigBackend() # great for tests +backend = FilesystemConfigBackend("/etc/pyfly") # reads/writes files +``` + +**`FilesystemConfigBackend`** stores each bundle as +`/