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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

---

## v26.09.11 (2026-09-30)

### Fixed

- `TaskScheduler.discover()` skips custom descriptors before binding
scheduled methods. Scanning a Pydantic settings instance no longer evaluates
`model_fields` or `model_computed_fields`, which emitted
`PydanticDeprecatedSince211` warnings in 26.09.10. Discovery still finds and
executes ordinary, inherited, static and class methods marked `@scheduled`.
- Regression coverage verifies both the absence of Pydantic warnings and that
discovery never executes custom descriptor, property or cached-property getters.

### Upgrading

- No configuration or public API changes. No warning filters are required.
Custom descriptors are not evaluated to obtain scheduled callables; expose
scheduled jobs as methods.

## v26.09.10 (2026-09-29)

Reusable infrastructure hardening discovered while building Firefly Weave.
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,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="CHANGELOG.md"><img src="https://img.shields.io/badge/version-26.09.10-brightgreen" alt="Version: 26.09.10"></a>
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/version-26.09.11-brightgreen" alt="Version: 26.09.11"></a>
<a href="https://mypy-lang.org/"><img src="https://img.shields.io/badge/type--checked-mypy%20strict-blue?logo=python&logoColor=white" alt="Type Checked: mypy strict"></a>
<a href="https://docs.astral.sh/ruff/"><img src="https://img.shields.io/badge/code%20style-ruff-purple?logo=ruff&logoColor=white" alt="Code Style: Ruff"></a>
<a href="#philosophy"><img src="https://img.shields.io/badge/async-first-brightgreen" alt="Async First"></a>
Expand Down Expand Up @@ -850,13 +850,13 @@ See **[`samples/lumen/`](samples/lumen/README.md)** for an end-to-end DDD micros

```bash
# Install the latest release (uv)
uv add "pyfly @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.9.10-py3-none-any.whl"
uv add "pyfly @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.9.11-py3-none-any.whl"

# Install with specific extras
uv add "pyfly[web,data-relational,cache] @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.9.10-py3-none-any.whl"
uv add "pyfly[web,data-relational,cache] @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.9.11-py3-none-any.whl"

# Or with pip
pip install "pyfly @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.9.10-py3-none-any.whl"
pip install "pyfly @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.9.11-py3-none-any.whl"
```

### One-Line Install (CLI + Framework)
Expand Down Expand Up @@ -1184,6 +1184,7 @@ The git tag and human-readable display use the leading-zero form (`v26.05.01`);

The full release history lives in **[CHANGELOG.md](CHANGELOG.md)** ([Keep a Changelog](https://keepachangelog.com/) format). Recent highlights:

- **`v26.09.11`** (2026-09-30) — scheduled-method discovery skips custom descriptors, avoiding Pydantic instance-field deprecation warnings.
- **`v26.09.10`** (2026-09-29) — bounded JWKS rotation and HTTP responses, standalone OAuth acquisition, complete failed-start cleanup, substitutable observability and exact-byte webhook validation.
- **`v26.09.09`** (2026-09-28) — a restarted context no longer resolves an interface through the previous run's binding (a second `start()` of the same context failed with a `KeyError`).
- **`v26.09.08`** (2026-09-28) — **the data layer on a unit of work**: Spring `@transactional` semantics on every backend (all seven propagations, rollback-only, isolation, read-only replica routing, timeouts), one `DataSourceRegistry` for every engine, repositories that commit per call outside a transaction, the framework's stores in the business transaction, MongoDB transactions on a replica set, and a tested matrix of SQLite, PostgreSQL, MySQL, MariaDB and MongoDB. Several changes are breaking: read the upgrade guide in the [CHANGELOG](CHANGELOG.md).
Expand Down
2 changes: 1 addition & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,7 @@ Missing optional tools are shown with a `-` dash indicator (dimmed), while missi
Verifies that PyFly itself is importable and displays the installed version:

```
✓ pyfly v26.09.10
✓ pyfly v26.09.11
```

### Summary
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ ______ ___.__._/ ____\ | ___.__.
| __// ____| |__| |____/ ____|
|__| \/ \/

PyFly v26.09.10 | Python 3.12.0
PyFly v26.09.11 | Python 3.12.0

2026-01-15T10:30:00Z [info] starting_application app=my-service version=0.1.0
2026-01-15T10:30:00Z [info] no_active_profiles message=No active profiles set, falling back to default
Expand Down
2 changes: 1 addition & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ PyFly Doctor
✓ mypy — Type checker

PyFly packages:
✓ pyfly v26.09.10
✓ pyfly v26.09.11

All checks passed!
```
Expand Down
4 changes: 2 additions & 2 deletions docs/modules/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ class BannerMode(enum.Enum):
| Mode | Behavior |
|---|---|
| `TEXT` | Full ASCII art banner (default) with a framework version line. |
| `MINIMAL` | Single line: `:: PyFly :: (v26.09.10)` |
| `MINIMAL` | Single line: `:: PyFly :: (v26.09.11)` |
| `OFF` | No banner output at all. |

### BannerPrinter Class
Expand Down Expand Up @@ -640,7 +640,7 @@ ______ ___.__._/ ____\ | ___.__.
| __// ____| |__| |____/ ____|
|__| \/ \/

:: PyFly Framework :: (v26.09.10)
:: PyFly Framework :: (v26.09.11)
```

### Custom Banner Files
Expand Down
7 changes: 6 additions & 1 deletion docs/modules/scheduling.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,12 @@ scheduler = TaskScheduler(lock=RedisLock(redis_client))

Call `discover()` with a list of bean instances. It scans every public attribute
(names not starting with `_`) and records those marked with
`__pyfly_scheduled__ = True`. Returns the number of scheduled methods found:
`__pyfly_scheduled__ = True`. Since v26.09.11, discovery skips custom
descriptors as well as properties and cached properties before binding methods.
Pydantic settings can be included without instance-field deprecation warnings.
Ordinary, inherited, static and class methods remain discoverable; expose jobs
as methods rather than descriptor-produced callables. Returns the number of
scheduled methods found:

```python
beans = [metrics_collector, data_syncer, report_generator]
Expand Down
7 changes: 4 additions & 3 deletions docs/versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ rare case where a substantial change needs an additional review window.

| Version | Date | Notes |
|---------|------|-------|
| `26.09.11` | 2026-09-30 | Scheduled-method discovery avoids evaluating Pydantic and custom descriptors. |
| `26.09.10` | 2026-09-29 | Bounded identity and HTTP primitives, lifecycle ownership, substitutable observability and exact-byte webhooks. |
| `26.09.09` | 2026-09-28 | A restarted context drops the previous run's interface bindings with its registrations, so a second `start()` of the same context no longer fails with a `KeyError` (a test suite whose modules share an application). |
| `26.09.08` | 2026-09-28 | The data layer on a unit of work: Spring `@transactional` semantics on every backend, one `DataSourceRegistry` for every engine, repositories that commit per call outside a transaction, the framework stores in the business transaction, MongoDB transactions, and a backend matrix (SQLite, PostgreSQL, MySQL, MariaDB, MongoDB replica set). Several breaking changes: see the changelog. |
Expand Down Expand Up @@ -93,17 +94,17 @@ shipped, with the version metadata updated.

```python
import pyfly
print(pyfly.__version__) # → "26.09.10"
print(pyfly.__version__) # → "26.09.11"
```

```bash
pyfly --version # → 26.09.10
pyfly --version # → 26.09.11
```

The startup banner displays the leading-zero form:

```
:: PyFly Framework :: (v26.09.10) (Python 3.13.9)
:: PyFly Framework :: (v26.09.11) (Python 3.13.9)
```

---
Expand Down
2 changes: 1 addition & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ set -euo pipefail

# ── Constants ──────────────────────────────────────────────────────────────────

PYFLY_VERSION="26.09.10"
PYFLY_VERSION="26.09.11"
PYFLY_REPO="https://github.com/fireflyframework/fireflyframework-pyfly.git"
DEFAULT_INSTALL_DIR="$HOME/.pyfly"
MIN_PYTHON_MAJOR=3
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.9.10"
version = "26.9.11"
description = "The official Python implementation of the Firefly Framework — DI, CQRS, EDA, hexagonal architecture, and more."
readme = "README.md"
license = "Apache-2.0"
Expand Down
2 changes: 1 addition & 1 deletion src/pyfly/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
# limitations under the License.
"""PyFly — Enterprise Python Framework."""

__version__ = "26.09.10"
__version__ = "26.09.11"
14 changes: 12 additions & 2 deletions src/pyfly/scheduling/task_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,9 @@ def executor(self) -> TaskExecutorPort:
def discover(self, beans: list[Any]) -> int:
"""Scan beans for @scheduled methods. Return number of scheduled methods found.

For each bean, inspects all attributes. If an attribute is callable and
has ``__pyfly_scheduled__ == True``, it is recorded for later scheduling.
For each bean, inspects public attributes without evaluating custom
descriptors. Callable attributes marked ``__pyfly_scheduled__ == True``
are recorded for later scheduling.
"""
count = 0
for bean in beans:
Expand All @@ -137,6 +138,15 @@ def discover(self, beans: list[Any]) -> int:
static_attr = inspect.getattr_static(bean, name, None)
if isinstance(static_attr, (property, functools.cached_property)):
continue
# Custom descriptors (including Pydantic's deprecated instance
# properties) can look like method descriptors on Python 3.12.
if hasattr(type(static_attr), "__get__") and not (
inspect.isfunction(static_attr)
or isinstance(static_attr, (staticmethod, classmethod))
or inspect.ismethoddescriptor(static_attr)
and type(static_attr).__module__ == "builtins"
):
continue
try:
attr = getattr(bean, name)
except Exception: # noqa: BLE001
Expand Down
100 changes: 100 additions & 0 deletions tests/scheduling/test_discovery_descriptors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Copyright 2026 Firefly Software Foundation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Discovery must not evaluate bean descriptors while finding scheduled methods."""

from __future__ import annotations

import asyncio
import warnings
from datetime import timedelta
from functools import cached_property
from typing import Any

import pytest
from pydantic import BaseModel
from pydantic.warnings import PydanticDeprecatedSince211

from pyfly.scheduling.decorators import scheduled
from pyfly.scheduling.task_scheduler import TaskScheduler


@pytest.mark.asyncio
async def test_pydantic_settings_do_not_warn_and_scheduled_bindings_still_execute() -> None:
observed = {name: asyncio.Event() for name in ("model", "inherited", "static", "class")}

class Settings(BaseModel):
name: str = "settings"

@scheduled(fixed_delay=timedelta(milliseconds=10))
async def model_job(self) -> None:
assert self.name == "settings"
observed["model"].set()

class ParentJobs:
@scheduled(fixed_delay=timedelta(milliseconds=10))
async def inherited_job(self) -> None:
observed["inherited"].set()

class Jobs(ParentJobs):
@staticmethod
@scheduled(fixed_delay=timedelta(milliseconds=10))
async def static_job() -> None:
observed["static"].set()

@classmethod
@scheduled(fixed_delay=timedelta(milliseconds=10))
async def class_job(cls) -> None:
assert cls is Jobs
observed["class"].set()

scheduler = TaskScheduler()
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always", PydanticDeprecatedSince211)
count = scheduler.discover([Settings(), Jobs()])
assert not caught, [str(warning.message) for warning in caught]
assert count == 4
try:
await scheduler.start()
await asyncio.wait_for(asyncio.gather(*(event.wait() for event in observed.values())), timeout=1)
finally:
await scheduler.stop()


def test_discovery_does_not_evaluate_custom_descriptors_or_properties() -> None:
accessed: list[str] = []

class Descriptor:
def __get__(self, instance: Any, owner: type | None = None) -> Any:
accessed.append("descriptor")
raise RuntimeError("must not execute during discovery")

class Bean:
metadata = Descriptor()

@property
def value(self) -> str:
accessed.append("property")
raise RuntimeError("must not execute during discovery")

@cached_property
def cached(self) -> str:
accessed.append("cached_property")
raise RuntimeError("must not execute during discovery")

@scheduled(fixed_delay=timedelta(seconds=1))
async def job(self) -> None:
pass

assert TaskScheduler().discover([Bean()]) == 1
assert accessed == []
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ <h1 class="hero__title">
<span class="badge">Apache 2.0</span>
<span class="badge">mypy strict</span>
<span class="badge">async-first</span>
<span class="badge badge--ver">v26.09.10</span>
<span class="badge badge--ver">v26.09.11</span>
</div>
</div>
</section>
Expand Down
Loading