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

---

## v26.05.04 (2026-05-08)

### Fixed — `pyfly.security` no longer needs `pyjwt` to import

`pyfly.security/__init__.py` used to eagerly re-export
`SecurityMiddleware`, which transitively imported the starlette
adapter and `pyjwt` at module load time. The chain meant that
`import pyfly` itself failed when those optional packages were
missing — even for non-HTTP services that just want the kernel + DDD
primitives.

The import is now wrapped in `try / except ImportError`, matching the
pattern already used for `JWTService` and `BcryptPasswordEncoder`.
Optional symbols (`SecurityMiddleware`, `JWTService`,
`BcryptPasswordEncoder`) only land in the `__all__` export list when
their underlying packages (`starlette`, `pyjwt`, `bcrypt`) are present.

Regression test pinned in
[`tests/security/test_optional_imports.py`](tests/security/test_optional_imports.py).

### Verified — bare-wheel install works end-to-end

* `pip install pyfly` (no extras) → `pyfly.domain` primitives
importable, no infra deps required.
* `pip install "pyfly[web,cqrs,transactional,eventsourcing]"` →
`PyFlyApplication`, `@enable_*_stack` decorators, and
`register_*_stack(app)` imperative API all work without `pyjwt`
installed.

---

## v26.05.03 (2026-05-08)

### Changed — starter decorators are now functional
Expand Down
15 changes: 10 additions & 5 deletions 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.05.03-brightgreen" alt="Version: 26.05.03"></a>
<a href="#"><img src="https://img.shields.io/badge/version-26.05.04-brightgreen" alt="Version: 26.05.04"></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 Expand Up @@ -804,13 +804,13 @@ See **[`samples/order_service/`](samples/order_service/README.md)** for an end-t

```bash
# Install the latest release (uv)
uv add "pyfly @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.5.3-py3-none-any.whl"
uv add "pyfly @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.5.4-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.5.3-py3-none-any.whl"
uv add "pyfly[web,data-relational,cache] @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/latest/download/pyfly-26.5.4-py3-none-any.whl"

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

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

See **[CHANGELOG.md](CHANGELOG.md)** for detailed release notes.

**Current:** `v26.05.03` (2026-05-08) — Functional starters + Java/.NET parity:
**Current:** `v26.05.04` (2026-05-08) — `pyfly.security` import-chain fix:

- **Bug fix** — `pyfly.security/__init__.py` no longer eagerly imports a starlette-specific `SecurityMiddleware` that transitively pulls in `pyjwt`. Importing `pyfly` (and instantiating `PyFlyApplication`) now works without `[security]` extras installed. Optional symbols (`SecurityMiddleware`, `JWTService`, `BcryptPasswordEncoder`) only export when their underlying packages (`starlette`, `pyjwt`, `bcrypt`) are present. Regression test pinned in `tests/security/test_optional_imports.py`.
- Verified: bare wheel install (`pip install pyfly`) now exposes `pyfly.domain` immediately; the `[web,cqrs,transactional,eventsourcing]` extras unblock the full application bootstrap path.

**Previous:** `v26.05.03` (2026-05-08) — Functional starters + Java/.NET parity:

- **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.
- **`@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.
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.05.03"
PYFLY_VERSION="26.05.04"
PYFLY_REPO="https://github.com/fireflyframework/fireflyframework-pyfly.git"
DEFAULT_INSTALL_DIR="$HOME/.pyfly"
MIN_PYTHON_MAJOR=3
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ build-backend = "hatchling.build"

[project]
name = "pyfly"
# CalVer YY.MM.PATCH — package metadata uses PEP 440 normalized form (26.5.3);
# 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.03) to match the Java/.NET/Go siblings.
version = "26.5.3"
# (v26.05.04) to match the Java/.NET/Go siblings.
version = "26.5.4"
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.05.03"
__version__ = "26.05.04"
23 changes: 20 additions & 3 deletions src/pyfly/security/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,43 @@
# 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.
"""PyFly Security — Authentication, authorization, and JWT integration."""
"""PyFly Security — Authentication, authorization, and JWT integration.

The starlette-specific :class:`SecurityMiddleware`, :class:`JWTService`
and :class:`BcryptPasswordEncoder` are *optional* — they are only
exported when their underlying third-party dependency
(``starlette`` / ``pyjwt`` / ``bcrypt``) is installed. This keeps the
security package importable from non-HTTP applications (workers, CLI
tools, data jobs) that don't pull in the ``[web]`` / ``[security]``
extras.
"""

from pyfly.security.context import SecurityContext
from pyfly.security.decorators import secure
from pyfly.security.http_security import AccessRule, AccessRuleType, HttpSecurity, SecurityRule
from pyfly.security.method_security import post_authorize, pre_authorize
from pyfly.security.middleware import SecurityMiddleware

__all__ = [
"AccessRule",
"AccessRuleType",
"HttpSecurity",
"SecurityContext",
"SecurityMiddleware",
"SecurityRule",
"post_authorize",
"pre_authorize",
"secure",
]

try:
from pyfly.security.middleware import SecurityMiddleware

__all__ += ["SecurityMiddleware"]
except ImportError:
# ``pyfly.security.middleware`` re-exports a starlette adapter that
# transitively requires ``pyjwt``; skip it when those extras aren't
# installed.
pass

try:
from pyfly.security.jwt import JWTService

Expand Down
85 changes: 85 additions & 0 deletions tests/security/test_optional_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Copyright 2026 Firefly Software Foundation.
# Licensed under the Apache License, Version 2.0.
"""Regression test: ``pyfly.security`` must import without infra extras.

Before v26.05.04 ``pyfly.security.__init__`` unconditionally re-exported
``SecurityMiddleware``, which transitively imported ``starlette`` and
``jwt`` at module load time. That made ``import pyfly`` itself fail
when those optional packages were missing — even for non-HTTP services
that just want the kernel + DDD primitives.

The fix wraps the ``SecurityMiddleware`` import in ``try / except
ImportError`` so it's only exposed when its dependencies are
satisfied. This test pins the behaviour by checking that the
package's exports degrade gracefully.
"""

from __future__ import annotations

import importlib

import pytest


def test_pyfly_security_imports_without_starlette_or_jwt() -> None:
"""The package must import even if ``starlette`` / ``jwt`` are missing.

We can't easily uninstall packages mid-test, so this test merely
verifies that the ``__init__`` doesn't *unconditionally* depend on
them — the fix is to check that ``SecurityMiddleware`` is wrapped
in a ``try / except`` import block, and that the package can be
fully reloaded without crashing.
"""
import pyfly.security as sec

# Reload to exercise the fresh import path again.
importlib.reload(sec)

# The unconditional symbols must always be available.
for name in (
"AccessRule",
"AccessRuleType",
"HttpSecurity",
"SecurityContext",
"SecurityRule",
"post_authorize",
"pre_authorize",
"secure",
):
assert hasattr(sec, name), f"missing required symbol: {name}"


def test_security_middleware_import_is_optional() -> None:
"""Confirm the import is guarded — same source-shape used by
JWTService and BcryptPasswordEncoder, our reference patterns.
"""
spec = importlib.util.find_spec("pyfly.security")
assert spec is not None
src = spec.origin
assert src is not None
with open(src) as fh:
body = fh.read()

# The eager import on the canonical symbol must be wrapped in a
# ``try`` block (matching the JWTService / password helpers).
assert "from pyfly.security.middleware import SecurityMiddleware" in body
middleware_idx = body.index("from pyfly.security.middleware import SecurityMiddleware")
preceding_try = body.rfind("try:", 0, middleware_idx)
assert preceding_try != -1, (
"SecurityMiddleware import must be wrapped in a try / except ImportError "
"block so the package stays importable without starlette/pyjwt."
)
# The matching except clause must appear after the import:
after = body[middleware_idx:]
assert "except ImportError" in after, "missing except ImportError after SecurityMiddleware import"


@pytest.mark.parametrize(
"extra_symbol",
["JWTService", "BcryptPasswordEncoder", "PasswordEncoder", "SecurityMiddleware"],
)
def test_optional_security_symbols_are_in_all_when_available(extra_symbol: str) -> None:
import pyfly.security as sec

if hasattr(sec, extra_symbol):
assert extra_symbol in sec.__all__
Loading