Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

---

## v26.06.77 (2026-06-08)

### Docs (feature sync — v26.06.57-76 are now documented)

A sweep bringing the docs (and the companion plugin skills) in line with everything shipped since
v26.06.57, verified key-by-key against the source:

- **Scheduling** — distributed-lock providers (`pyfly.scheduling.lock.provider` = none|memory|redis|
**postgres** advisory lock) and the pluggable executor (`pyfly.scheduling.executor.type` =
asyncio|thread).
- **Session** — concurrency registry backends (`pyfly.session.concurrency.registry` = memory|redis|
**postgres**).
- **Security** — persistent OAuth2 token store (`pyfly.security.oauth2.token-store.provider` =
memory|redis|postgres) for multi-instance auth servers.
- **Config** — `Config.reload_from_sources()` + `POST /actuator/refresh` now re-reads config files.
- **Data/CLI** — run-on-startup Alembic migrations (`pyfly.data.relational.migrations.enabled`).
- **Web** — access-log opt-out (`pyfly.web.request-logging.enabled`).
- **Observability** — the `MetricsRecorder` port + `NoOpMetricsRecorder`.
- **DI** — the public container SPI (`register_instance` / `contains_type` / `get_registration` /
`registered_types` / `reset_instance`).
- Fixed the broken `@mongo_transactional(client)` example in the MongoDB adapter doc and the
stale `/actuator/refresh` description (now reflect the unified `@transactional` + config reload).

## v26.06.76 (2026-06-08)

### Tested (data — MongoDB query/projection execution coverage)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<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>
<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>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-green" alt="License: Apache 2.0"></a>
<a href="#"><img src="https://img.shields.io/badge/version-26.06.76-brightgreen" alt="Version: 26.06.76"></a>
<a href="#"><img src="https://img.shields.io/badge/version-26.06.77-brightgreen" alt="Version: 26.06.77"></a>
<a href="#"><img src="https://img.shields.io/badge/type--checked-mypy%20strict-blue?logo=python&logoColor=white" alt="Type Checked: mypy strict"></a>
<a href="#"><img src="https://img.shields.io/badge/code%20style-ruff-purple?logo=ruff&logoColor=white" alt="Code Style: Ruff"></a>
<a href="#"><img src="https://img.shields.io/badge/async-first-brightgreen" alt="Async First"></a>
Expand Down
25 changes: 17 additions & 8 deletions docs/adapters/mongodb.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,29 @@ Wires compiled query methods onto `MongoRepository` subclasses at startup — id

### Transactions

Use `@mongo_transactional` for multi-document transactions. The decorator
requires a `Motor` client (injected from the auto-configured bean):
Use the unified **`@transactional`** (from `pyfly.data`) for multi-document transactions — the
same annotation as the relational backend. On a service exposing a Motor client as
`self._motor_client`, it opens a session + transaction and injects it as the `session` keyword
argument (requires a MongoDB replica set):

```python
from pyfly.data.document.mongodb import mongo_transactional
from motor.motor_asyncio import AsyncIOMotorClient
from pyfly.container import service
from pyfly.data import transactional

client: AsyncIOMotorClient = ... # injected by DI

@mongo_transactional(client)
async def transfer(from_id: str, to_id: str, amount: float) -> None:
...
@service
class AccountService:
def __init__(self, motor_client) -> None:
self._motor_client = motor_client # selects the MongoDB transaction manager

@transactional()
async def transfer(self, from_id: str, to_id: str, amount: float, *, session=None) -> None:
...
```

> `from pyfly.data.document.mongodb import mongo_transactional` still works but is a **deprecated
> alias** of `@transactional`.

---

## Testing
Expand Down
20 changes: 20 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ The PyFly CLI provides command-line tools for project scaffolding, application m
- [pyfly db migrate](#pyfly-db-migrate)
- [pyfly db upgrade](#pyfly-db-upgrade)
- [pyfly db downgrade](#pyfly-db-downgrade)
- [Running Migrations on Startup](#running-migrations-on-startup)
- [pyfly license](#pyfly-license)
- [pyfly sbom](#pyfly-sbom)
- [Development Workflow](#typical-development-workflow)
Expand Down Expand Up @@ -758,6 +759,25 @@ pyfly db downgrade abc123 # Revert to specific revision
pyfly db downgrade base # Revert all migrations
```

### Running Migrations on Startup

Instead of (or in addition to) running `pyfly db upgrade` by hand, PyFly can apply migrations **automatically on application startup** — the Flyway-style auto-migrate equivalent. This reuses the same Alembic environment created by `pyfly db init`, so the CLI commands above continue to work unchanged.

It is **opt-in** via configuration, not a CLI flag. Enable it in `pyfly.yaml`:

```yaml
pyfly:
data:
relational:
url: postgresql+asyncpg://user:pass@localhost:5432/app
migrations:
enabled: true # run `alembic upgrade head` on startup
config: alembic.ini # Alembic config path (default: alembic.ini)
revision: head # target revision (default: head)
```

When enabled, the app runs `alembic upgrade <revision>` against the same datasource (`pyfly.data.relational.url`) during startup. If `alembic.ini` is not found, the migration step is skipped with a warning suggesting you run `pyfly db init` — startup is not aborted. See [Data Relational — Run Migrations on Startup](modules/data-relational.md#run-migrations-on-startup-flyway-style) for the full configuration reference.

---

## pyfly license
Expand Down
9 changes: 5 additions & 4 deletions docs/modules/actuator.md
Original file line number Diff line number Diff line change
Expand Up @@ -998,10 +998,11 @@ first, matching `traceback.extract_stack()` order.

**Endpoint:** `POST /actuator/refresh`

The refresh endpoint mirrors Spring Cloud's `POST /actuator/refresh`. It triggers a
context refresh — evicting all refresh-scoped beans and resetting
`@config_properties` beans so they re-bind against the live `Config` (which
re-reads environment variables and `${...}` placeholders) on next resolution.
The refresh endpoint mirrors Spring Cloud's `POST /actuator/refresh`. It first calls
`Config.reload_from_sources()` to **re-read the configuration files/profiles** (the exact merge
`from_sources` performed), then triggers a context refresh — evicting all refresh-scoped beans and
resetting `@config_properties` beans so they re-bind against the freshly-reloaded `Config` (which
also re-reads environment variables and `${...}` placeholders) on next resolution.
A `RefreshScopeRefreshedEvent` is published, and the response lists the cache keys
of the beans that were refreshed:

Expand Down
142 changes: 129 additions & 13 deletions docs/modules/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,38 @@ and the full reference of framework defaults.
- [get()](#get)
- [get_section()](#get_section)
- [bind()](#bind)
3. [YAML Configuration](#yaml-configuration)
4. [TOML Configuration](#toml-configuration)
5. [Profile System](#profile-system)
- [reload_from_sources()](#reload_from_sources)
3. [Runtime Configuration Refresh](#runtime-configuration-refresh)
4. [YAML Configuration](#yaml-configuration)
5. [TOML Configuration](#toml-configuration)
6. [Profile System](#profile-system)
- [Activating Profiles](#activating-profiles)
- [Profile-Specific Files](#profile-specific-files)
- [Profile Expressions in Beans](#profile-expressions-in-beans)
6. [Configuration Layering](#configuration-layering)
7. [Configuration Layering](#configuration-layering)
- [Layer 1: Framework Defaults](#layer-1-framework-defaults)
- [Layer 2: User Configuration File](#layer-2-user-configuration-file)
- [Layer 3: Profile Overlays](#layer-3-profile-overlays)
- [Layer 4: Environment Variables](#layer-4-environment-variables)
- [Deep Merge Behavior](#deep-merge-behavior)
- [Remote Config Import (Config Server)](#remote-config-import-config-server)
7. [Environment Variable Overrides](#environment-variable-overrides)
8. [Environment Variable Overrides](#environment-variable-overrides)
- [Naming Convention](#naming-convention)
- [Type Coercion](#type-coercion)
- [Examples](#environment-variable-examples)
8. [@config_properties](#config_properties)
9. [@config_properties](#config_properties)
- [Defining a Config Class](#defining-a-config-class)
- [Binding at Runtime](#binding-at-runtime)
- [Type Coercion in bind()](#type-coercion-in-bind)
9. [@Value (Field-Level Config Injection)](#value-field-level-config-injection)
- [Expression Syntax](#expression-syntax)
- [Usage in Beans](#usage-in-beans)
- [@Value vs @config_properties](#value-vs-config_properties)
10. [SpEL-lite Expressions](#spel-lite-expressions)
10. [@Value (Field-Level Config Injection)](#value-field-level-config-injection)
- [Expression Syntax](#expression-syntax)
- [Usage in Beans](#usage-in-beans)
- [@Value vs @config_properties](#value-vs-config_properties)
11. [SpEL-lite Expressions](#spel-lite-expressions)
- [The `#{ ... }` Form](#the-spel-form)
- [`@conditional_on_expression`](#conditional_on_expression)
- [Safety Model](#safety-model)
11. [Framework Defaults Reference](#framework-defaults-reference)
12. [Framework Defaults Reference](#framework-defaults-reference)
- [Application](#application-defaults)
- [Profiles](#profiles-defaults)
- [Banner](#banner-defaults)
Expand All @@ -58,7 +60,7 @@ and the full reference of framework defaults.
- [Admin](#admin-defaults)
- [Security](#security-defaults)
- [Observability](#observability-defaults)
12. [Complete Example: Multi-Environment Setup](#complete-example-multi-environment-setup)
13. [Complete Example: Multi-Environment Setup](#complete-example-multi-environment-setup)

---

Expand Down Expand Up @@ -216,6 +218,120 @@ print(web.port) # 8080

Raises `ValueError` if the class is not decorated with `@config_properties`.

### reload_from_sources()

```python
def reload_from_sources(self) -> bool:
```

Re-reads the original configuration sources and **atomically swaps in** the freshly
merged result, so a running application picks up edits to the config files and profile
overlays without a restart (Spring Cloud config refresh). It replays the exact merge
recorded by [`from_sources()`](#from_sources) — framework defaults, starter defaults,
`config/` files, project-root files, and profile overlays — under an internal lock, then
rebinds `_data` in a single assignment. Because `get()` reads that single attribute,
concurrent readers always see a consistent snapshot (the old tree or the new one, never a
half-merged mix).

Returns:

| Return | Meaning |
|---|---|
| `True` | The sources were re-read and the merged config was swapped in. |
| `False` | The instance was **not** built via `from_sources()` (e.g. a dict-constructed `Config`), so there is nothing to reload — a no-op. |

```python
config = Config.from_sources(".", active_profiles=["prod"])
# ... edit pyfly.yaml / pyfly-prod.yaml on disk ...
config.reload_from_sources() # True — files re-read, get() now returns new values
Config({"a": 1}).reload_from_sources() # False — dict-constructed, nothing to reload
```

> **Note:** environment-variable overrides and `${...}` placeholders are always resolved at
> *read time* in `get()`, so they reflect the current process environment regardless of
> reloading. `reload_from_sources()` is specifically about re-reading the **files**.

This method is invoked automatically by `POST /actuator/refresh` — see
[Runtime Configuration Refresh](#runtime-configuration-refresh) below.

---

## Runtime Configuration Refresh

PyFly supports Spring Cloud-style runtime configuration refresh: you can change config
files (or profile overlays) on disk and have a running application pick up the changes —
no restart — by issuing a single management request.

```bash
curl -X POST http://localhost:8080/actuator/refresh
# {"refreshed": ["FeatureFlags-singleton", "PricingProperties-singleton"]}
```

### What happens on POST /actuator/refresh

The endpoint resolves the injectable `ContextRefresher` and calls its `refresh()`, which
performs the following steps in order (`src/pyfly/context/refresh.py`):

1. **Re-reads the config sources.** It calls `Config.reload_from_sources()`, which replays
the original multi-source merge and atomically swaps in the new tree. This is the step
that lets edits to `pyfly.yaml` / `pyfly-{profile}.yaml` take effect at runtime. (For a
dict-constructed `Config` this is a no-op.)
2. **Evicts all refresh-scoped beans** (`@refresh_scope`). Each cached instance is dropped
so the next resolution rebuilds it — re-running constructor/field injection and
re-reading `@Value` placeholders against the now-refreshed `Config`.
3. **Resets `@config_properties` singletons.** Their backing instances are cleared so they
re-`bind()` from the live `Config` (which now reflects the re-read files, env-var
overrides, and resolved `${...}` placeholders) on next resolution.
4. **Publishes a `RefreshScopeRefreshedEvent`** on the application event bus.

The response is `{"refreshed": [...]}`, listing the cache keys of the evicted
refresh-scoped beans (an empty list when none are registered).

### Picking up file changes in your beans

Because step 1 now re-reads the files before steps 2–3 rebuild the affected beans, both
`@refresh_scope` and `@config_properties` beans see the **new file values** after a
refresh:

```python
from dataclasses import dataclass
from pyfly.container import component
from pyfly.core import config_properties
from pyfly.container.refresh_scope import refresh_scope
from pyfly.core.value import Value


@config_properties(prefix="pyfly.pricing")
@dataclass
class PricingProperties:
base_rate: float = 1.0 # edit pyfly.yaml + POST /actuator/refresh -> re-bound


@component
@refresh_scope
class FeatureFlags:
new_checkout: bool = Value("${features.new-checkout:false}")
# next resolution after refresh re-reads ${features.new-checkout} from the live Config
```

### Exposure (opt-in)

Like Spring Boot, the actuator is secure-by-default: only `health` and `info` are
reachable over HTTP. The `refresh` endpoint is registered whenever the actuator is enabled
with a context, but it is **not mounted** until you add it to the exposure include list:

```yaml
pyfly:
management:
endpoints:
web:
exposure:
include: "health,info,refresh" # or "*" to expose every enabled endpoint
```

With the default `health,info`, `POST /actuator/refresh` returns `404`. See the
[Actuator guide](actuator.md#refresh-endpoint) for the full endpoint reference.

---

## YAML Configuration
Expand Down
36 changes: 36 additions & 0 deletions docs/modules/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ depends on them.
- [Reading Values: get()](#reading-values-get)
- [Reading Sections: get_section()](#reading-sections-get_section)
- [Typed Binding: bind()](#typed-binding-bind)
- [Runtime Reload: reload_from_sources()](#runtime-reload-reload_from_sources)
5. [@config_properties Decorator](#config_properties-decorator)
6. [Configuration Layering](#configuration-layering)
- [Framework Defaults](#framework-defaults)
Expand Down Expand Up @@ -373,6 +374,41 @@ It reads the prefix from the decorator, fetches the effective config section (wi
`${...}` placeholders resolved, env overrides applied, and env-only keys injected),
and constructs the dataclass with relaxed key matching and type coercion.

### Runtime Reload: reload_from_sources()

```python
def reload_from_sources(self) -> bool:
```

Re-reads the original configuration sources and **atomically swaps in** the freshly merged
result, so a running application can pick up edits to its config files / profile overlays
without a restart (Spring Cloud config refresh). It replays the exact merge recorded by
`from_sources()` (framework defaults, starter defaults, `config/` files, project-root
files, and profile overlays) under an internal lock, then rebinds the backing data in a
single assignment — `get()` reads that one attribute, so concurrent readers always see a
consistent snapshot.

Returns `True` when the sources were re-read and swapped in; returns `False` for instances
not built via `from_sources()` (e.g. a dict-constructed `Config`), where there is nothing
to reload (a no-op).

```python
config = Config.from_sources(".", active_profiles=["prod"])
# ... edit pyfly.yaml / pyfly-prod.yaml on disk ...
config.reload_from_sources() # True — get() now returns the new file values
```

Environment-variable overrides and `${...}` placeholders are resolved at read time in
`get()`, so they already track the live process environment; `reload_from_sources()` is
specifically about re-reading the **files**.

This is what `POST /actuator/refresh` calls under the hood: the `ContextRefresher`
(`pyfly.context.refresh`) reloads config from sources *before* it evicts refresh-scoped
beans and resets `@config_properties` singletons, so those beans re-bind against the
re-read files on next resolution. See the
[Configuration guide](configuration.md#runtime-configuration-refresh) and the
[Actuator guide](actuator.md#refresh-endpoint).

---

## @config_properties Decorator
Expand Down
Loading
Loading