Skip to content

fireflyframework/fireflyframework-pyfly

PyFly Logo

The Official Python Implementation of the Firefly Framework

CI Firefly Framework Python 3.12+ License: Apache 2.0 Version: 26.05.04 Type Checked: mypy strict Code Style: Ruff Async First

Build production-grade Python applications with the patterns you trust — dependency injection, CQRS, event-driven architecture, and more — powered by the Firefly Framework.


The Problem

You've been here before. A new Python microservice needs to ship. Before writing a single line of business logic, you spend the first two weeks making choices:

  • Which web framework? (FastAPI, Flask, Starlette, Django...)
  • Which ORM? (SQLAlchemy, Tortoise, Django ORM...)
  • Which message broker? (aiokafka, aio-pika, kombu...)
  • How do you wire dependencies? (dependency-injector, python-inject, manual...)
  • How do you structure the project? (Everyone invents their own layout)

You assemble a bespoke stack, glue it together, and move on. Six months later, another team builds a second service — and makes entirely different choices. Now you have two codebases with different conventions, different testing strategies, different deployment patterns, and no shared understanding of how things work.

Python gives you infinite choice. What it doesn't give you is cohesion.


What is PyFly?

PyFly makes these decisions for you.

It is a cohesive, full-stack framework for building production-grade Python applications — microservices, monoliths, and libraries — where every module is designed to work together seamlessly. Dependency injection, HTTP routing, database access, messaging, caching, security, observability, and more — all integrated, all consistent, all with production-ready defaults from day one.

from pyfly.container import rest_controller, service
from pyfly.web import request_mapping, post_mapping, Body, Valid

@service
class OrderService:
    def __init__(self, repo: OrderRepository, events: EventPublisher) -> None:
        self._repo = repo
        self._events = events

    async def place_order(self, order: Order) -> Order:
        saved = await self._repo.save(order)
        await self._events.publish(OrderPlaced(order_id=saved.id))
        return saved

@rest_controller
@request_mapping("/orders")
class OrderController:
    def __init__(self, service: OrderService) -> None:
        self._service = service

    @post_mapping("", status_code=201)
    async def create(self, order: Valid[Body[Order]]) -> Order:
        return await self._service.place_order(order)

No boilerplate. No manual wiring. The DI container resolves OrderRepository and EventPublisher from type hints, validates the request body, and publishes domain events — all out of the box.

PyFly is the official Python implementation of the Firefly Framework, a battle-tested enterprise platform originally built on Spring Boot for Java (40+ modules in production). PyFly brings the same cohesive programming model to Python 3.12+ — not as a port, but as a native implementation reimagined for async/await, type hints, protocols, and the full power of modern Python.

Who is PyFly for?

  • Python developers who want enterprise-grade patterns without reinventing the wheel for every project
  • Teams tired of assembling bespoke stacks and want every service to follow the same conventions
  • Architects building polyglot platforms who need consistency across Java and Python services
  • Anyone migrating from Spring Boot who wants familiar concepts expressed natively in Python

Coming from Spring Boot? See the Spring Boot Comparison Guide for a side-by-side concept mapping.


Philosophy

Four principles shape every design decision in PyFly. Together, they answer a single question: how do you build applications that are easy to start, easy to change, and ready for production from the first commit?

Convention Over Configuration

Starting a new project should take seconds, not days. PyFly ships with production-ready defaults for every module — logging formats, connection pool sizes, retry policies, security headers, health endpoints — so a new service works immediately with minimal configuration:

# A complete, production-ready web service:
pyfly:
  web:
    port: 8080

When you need to customize, you override only what matters. Everything else stays sensible.

Your Code, Not Ours

Your business logic should never import sqlalchemy, redis, aiokafka, or any other infrastructure library. PyFly enforces this through hexagonal architecture — the same ports-and-adapters pattern used across all Firefly Framework modules:

  • Ports are Python Protocol classes that define contracts
  • Adapters are concrete implementations that fulfill those contracts
  • Your services depend on ports. The DI container wires the adapters at startup.

The result: you can swap your database from PostgreSQL to MongoDB, your broker from Kafka to RabbitMQ, or your cache from Redis to in-memory — without touching a single line of business logic.

Async-Native, Type-Safe

Every PyFly API is designed for asyncio from the ground up — no sync-to-async bridges, no thread pool workarounds. Every public surface has complete type annotations validated by mypy in strict mode. If it compiles, it's consistent.

Production-Ready from Day One

The first time you run pyfly run, your application already has structured logging with correlation IDs, health check endpoints, Prometheus metrics, OWASP security headers, and graceful shutdown. These aren't features you opt into — they're the baseline.


How It Works

Dependency Injection

PyFly's DI container resolves dependencies from type hints — no XML, no service locators, just decorators and Python annotations. The container scans packages listed in scan_packages, discovers all decorated classes, and builds a complete dependency graph at startup.

from pyfly.container import Autowired, service

@service
class OrderService:
    metrics: MetricsCollector = Autowired(required=False)  # field injection

    def __init__(self, repo: OrderRepository, events: EventPublisher) -> None:
        self._repo = repo      # constructor injection (preferred)
        self._events = events

How resolution works: When the container creates OrderService, it inspects the __init__ type hints, finds OrderRepository and EventPublisher in the bean registry, resolves them recursively (including their dependencies), and injects the fully-initialized instances. After construction, it sets any Autowired() fields via setattr. The entire graph is resolved before your application handles its first request.

Stereotypes mark classes with their architectural role and register them with the container:

Stereotype Purpose Layer
@component Generic managed bean Any
@service Business logic Service
@repository Data access Data
@controller Web controller (template responses) Web
@rest_controller REST endpoints (JSON) Web
@shell_component CLI commands (import from pyfly.shell) Shell
@configuration + @bean Bean factory methods Infrastructure

All stereotypes default to singleton scope (one instance per application). You can override with @service(scope=Scope.TRANSIENT) for a new instance on every injection, or Scope.REQUEST for one instance per HTTP request.

Advanced capabilities: Optional[T] resolves to None when no bean is registered. list[T] collects all implementations of a type. Qualifier("name") selects a specific named bean when multiple candidates exist. @primary marks the default when there are multiple implementations of the same port. The container detects circular dependencies at startup and reports them clearly rather than deadlocking at runtime.

Hexagonal Architecture

Every PyFly module that touches external systems is split into two halves: ports and adapters. Ports are abstract Protocol interfaces that your business logic depends on. Adapters are concrete implementations backed by real libraries. The DI container connects them at startup.

This separation is not conceptual — it is enforced by package structure:

┌──────────────────────────────────────────────────────────┐
│                    APPLICATION LAYER                     │
│                                                          │
│  Your services, controllers, and domain logic.           │
│  They depend ONLY on ports.                              │
│                                                          │
│    @service                                              │
│    class OrderService:                                   │
│        repo: RepositoryPort[Order, int]                  │
│        events: EventPublisher                            │
│        cache: CacheAdapter                               │
│                                                          │
└────────────────────────────┬─────────────────────────────┘
                             │ depends on
┌────────────────────────────┴─────────────────────────────┐
│                 PORTS  (Python Protocols)                │
│                                                          │
│  pyfly.data           RepositoryPort[T, ID]              │
│  pyfly.messaging      MessageBrokerPort                  │
│  pyfly.cache          CacheAdapter                       │
│  pyfly.eda            EventPublisher                     │
│  pyfly.client         HttpClientPort                     │
│  pyfly.scheduling     TaskExecutorPort                   │
│  pyfly.shell          ShellRunnerPort                    │
│  pyfly.web            WebServerPort                      │
│                                                          │
└────────────────────────────┬─────────────────────────────┘
                             │ implements
┌────────────────────────────┴─────────────────────────────┐
│            ADAPTERS  (Concrete Implementations)          │
│                                                          │
│  pyfly.data.relational.sqlalchemy                        │
│  pyfly.data.document.mongodb                             │
│  pyfly.messaging.adapters.kafka                          │
│  pyfly.messaging.adapters.rabbitmq                       │
│  pyfly.cache.adapters.redis                              │
│  pyfly.eda.adapters.memory                               │
│  pyfly.client.adapters.httpx_adapter                     │
│  pyfly.scheduling.adapters.asyncio_executor              │
│  pyfly.shell.adapters.click_adapter                      │
│  pyfly.web.adapters.starlette                            │
│                                                          │
└──────────────────────────────────────────────────────────┘

The practical result — swap any adapter without changing a single line of business logic:

# Your service depends on the port, never on the adapter
@service
class OrderService:
    def __init__(self, repo: RepositoryPort[Order, int]) -> None:
        self._repo = repo

    async def place_order(self, cmd: PlaceOrder) -> Order:
        return await self._repo.save(Order(name=cmd.name))

# The @repository stereotype wires the adapter at startup.
# Switch from SQL to MongoDB by changing one class declaration:

# SQL:     class OrderRepo(Repository[OrderEntity, int]): ...
# MongoDB: class OrderRepo(MongoRepository[OrderDoc, str]): ...
# Custom:  class OrderRepo(DynamoRepository[OrderItem, str]): ...
#
# OrderService never changes. Tests never change. Controllers never change.

Auto-Configuration

PyFly detects installed libraries at startup and wires the right adapters automatically — no manual bean registration needed.

This works through two complementary mechanisms:

1. Declarative auto-configuration@configuration classes guarded by conditions. They act as "default with override" factories:

from pyfly.context.conditions import auto_configuration, conditional_on_class, conditional_on_missing_bean
from pyfly.container.bean import bean

@auto_configuration
@conditional_on_missing_bean(CacheAdapter)    # only if user hasn't registered one
@conditional_on_class("redis.asyncio")        # only if redis is installed
class RedisCacheAutoConfig:
    @bean
    def cache(self) -> CacheAdapter:
        return RedisCacheAdapter(url=self._props.redis.url)

This bean is created only when (1) no user-provided CacheAdapter exists and (2) the redis library is installed. If the user registers their own CacheAdapter via @bean, the auto-configuration is silently skipped.

2. Decentralized entry-point discovery — Each subsystem owns its own @auto_configuration class, registered as a pyfly.auto_configuration entry point in pyproject.toml. At startup, discover_auto_configurations() uses importlib.metadata.entry_points(group="pyfly.auto_configuration") to find and load them — no hardcoded imports, no central engine:

Entry Point Class Detects Binds Fallback
web_fastapi FastAPIAutoConfiguration fastapi FastAPIWebAdapter none
web WebAutoConfiguration starlette StarletteWebAdapter none
server_granian GranianServerAutoConfiguration granian GranianServerAdapter none
server_uvicorn UvicornServerAutoConfiguration uvicorn UvicornServerAdapter none
server_hypercorn HypercornServerAutoConfiguration hypercorn HypercornServerAdapter none
event-loop EventLoopAutoConfiguration uvloop / winloop Event loop policy asyncio
relational RelationalAutoConfiguration sqlalchemy Repository[T, ID] none
document DocumentAutoConfiguration motor, beanie MongoRepository[T, ID] none
messaging MessagingAutoConfiguration aiokafka / aio-pika KafkaAdapter / RabbitMQAdapter InMemoryMessageBroker
cache CacheAutoConfiguration redis.asyncio RedisCacheAdapter InMemoryCache
client ClientAutoConfiguration httpx HttpxClientAdapter none
shell ShellAutoConfiguration click ClickShellAdapter none
cqrs CqrsAutoConfiguration CQRS handlers none
admin AdminAutoConfiguration Admin dashboard none
transactional TransactionalEngineAutoConfiguration Saga/TCC engines none
security-jwt JwtAutoConfiguration pyjwt JWTService none
security-password PasswordEncoderAutoConfiguration bcrypt BcryptPasswordEncoder none
scheduling SchedulingAutoConfiguration croniter TaskScheduler none
metrics MetricsAutoConfiguration prometheus_client MetricsRegistry none
tracing TracingAutoConfiguration opentelemetry TracerProvider none
actuator ActuatorAutoConfiguration ActuatorRegistry, HealthAggregator none
actuator-metrics MetricsActuatorAutoConfiguration prometheus_client MetricsEndpoint, PrometheusEndpoint none
aop AopAutoConfiguration AspectBeanPostProcessor none

Third-party packages can register their own auto-configurations by adding entries to the same entry-point group — the same extensibility model as Spring Boot's META-INF/spring.factories:

# In a third-party pyproject.toml:
[project.entry-points."pyfly.auto_configuration"]
my-addon = "my_package.auto_configuration:MyAutoConfiguration"

The practical workflow: During development, install uv add "pyfly[full]" (or pip install pyfly[full]) and everything auto-wires. In production Docker images, install only the extras you need (e.g., pyfly[web,data-relational,cache]) and the discovered auto-configurations bind exactly those adapters. You can always override any auto-configured adapter with explicit provider settings in pyfly.yaml or by registering your own bean.


Featured Patterns

PyFly is more than a web framework — it ships production-grade implementations of the distributed patterns that power real microservices: distributed transactions, durable workflows, event sourcing, identity, content management, multi-channel notifications, inbound/outbound webhooks, business rules, and more. Each one is a first-class module with a port-and-adapter design, CLI scaffolding, REST controllers (where applicable), metrics, tracing, and persistence.

The sections below show one representative example per pattern. The full guides live under docs/modules/.

Saga — Distributed Transaction with Compensation

Coordinate work across multiple services with automatic compensation on failure. Sagas are declared with decorators on a class; the engine builds the DAG, executes steps in dependency order, and rolls back via compensation steps if anything fails.

from pyfly.transactional.saga import saga, saga_step
from pyfly.transactional.saga.core.context import SagaContext

@saga(name="place-order", timeout_ms=30_000)
class PlaceOrderSaga:
    def __init__(self, payments: PaymentService, inventory: InventoryService, ship: ShippingService) -> None:
        self._payments = payments
        self._inventory = inventory
        self._ship = ship

    # `compensate=` names a method on this class to invoke if a later step fails.
    @saga_step(id="reserve-inventory", retry=3, backoff_ms=500, compensate="release_inventory")
    async def reserve(self, ctx: SagaContext) -> str:
        return await self._inventory.reserve(ctx.input["order_id"])

    async def release_inventory(self, ctx: SagaContext) -> None:
        await self._inventory.release(ctx.input["order_id"])

    @saga_step(id="charge-payment", depends_on=["reserve-inventory"], compensate="refund_payment")
    async def charge(self, ctx: SagaContext) -> str:
        return await self._payments.charge(ctx.input["order_id"], ctx.input["amount"])

    async def refund_payment(self, ctx: SagaContext) -> None:
        await self._payments.refund(ctx.input["order_id"])

    @saga_step(id="ship-order", depends_on=["charge-payment"])
    async def ship_order(self, ctx: SagaContext) -> None:
        await self._ship.dispatch(ctx.input["order_id"])

Highlights: parallel-by-default DAG execution, per-step retries with jitter, backpressure, idempotency keys, metrics + tracing, pluggable persistence (in-memory, Redis, SQLAlchemy, Cache), DLQ with RecoveryService, REST controllers for list/start/retry, and OrchestrationHealthIndicator. Programmatic API also available via SagaBuilder(). See docs/modules/transactional.md.

Workflow — Durable, Signal-Driven Orchestration

Long-running processes that can wait for external events, sleep for hours, spawn child workflows, and be queried while running. Inspired by Temporal/Cadence, native to PyFly.

from pyfly.transactional.workflow import (
    workflow, workflow_step, wait_for_signal, wait_for_timer,
    child_workflow, workflow_query, on_workflow_complete,
)

@workflow(id="loan-approval", version=2, timeout_ms=7 * 24 * 3600 * 1000)
class LoanApprovalWorkflow:
    def __init__(self, scoring: ScoringService, kyc: KycService) -> None:
        self._scoring = scoring
        self._kyc = kyc
        self._decision: str | None = None

    @workflow_step(id="run-scoring")
    async def run_scoring(self, ctx) -> int:
        return await self._scoring.score(ctx.input["applicant_id"])

    @child_workflow(workflow_id="kyc-verification", wait_for_completion=True, timeout_ms=3600_000)
    @workflow_step(id="kyc", depends_on=["run-scoring"])
    async def run_kyc(self, ctx) -> dict:
        return {"applicant_id": ctx.input["applicant_id"]}

    @wait_for_signal("manual-decision", timeout_ms=48 * 3600 * 1000)
    @workflow_step(id="await-officer", depends_on=["kyc"])
    async def await_officer(self, ctx, signal_payload: dict) -> str:
        self._decision = signal_payload["decision"]
        return self._decision

    @wait_for_timer(delay_ms=24 * 3600 * 1000)
    @workflow_step(id="cooling-off", depends_on=["await-officer"])
    async def cooling_off(self, ctx) -> None: ...

    @workflow_query(name="status")
    def get_status(self) -> str:
        return self._decision or "pending"

    @on_workflow_complete
    async def notify(self, ctx) -> None:
        ctx.publish("LoanDecided", {"id": ctx.input["applicant_id"], "decision": self._decision})

Highlights: @wait_for_signal / @wait_for_timer / @wait_for_all / @wait_for_any, child workflows, queries, scheduling via cron, lifecycle hooks (@on_workflow_complete, @on_workflow_error, @on_step_complete), SignalService + TimerService + ContinueAsNewService, WorkflowController REST API. See docs/modules/transactional.md.

TCC — Try / Confirm / Cancel

Strong-consistency three-phase distributed transactions for financial workloads where compensation alone isn't enough.

from pyfly.transactional.tcc import tcc, tcc_participant, try_method, confirm_method, cancel_method
from pyfly.transactional.tcc.context import TccContext
from typing import Annotated
from pyfly.transactional.tcc.annotations import FromTry

@tcc(name="transfer-funds", timeout_ms=10_000, retry_enabled=True, max_retries=3)
class TransferFundsTcc: ...

@tcc_participant(id="debit-source", order=1)
class DebitSource:
    def __init__(self, ledger: Ledger) -> None: self._ledger = ledger

    @try_method(timeout_ms=5_000)
    async def try_debit(self, ctx: TccContext) -> str:
        return await self._ledger.hold(ctx.input["from"], ctx.input["amount"])

    @confirm_method()
    async def confirm(self, hold_id: Annotated[str, FromTry()], ctx: TccContext) -> None:
        await self._ledger.commit_hold(hold_id)

    @cancel_method()
    async def cancel(self, hold_id: Annotated[str, FromTry()], ctx: TccContext) -> None:
        await self._ledger.release_hold(hold_id)

@tcc_participant(id="credit-target", order=2)
class CreditTarget:
    def __init__(self, ledger: Ledger) -> None: self._ledger = ledger

    @try_method()
    async def try_credit(self, ctx: TccContext) -> str:
        return await self._ledger.reserve(ctx.input["to"], ctx.input["amount"])

    @confirm_method()
    async def confirm(self, reservation_id: Annotated[str, FromTry()], ctx: TccContext) -> None:
        await self._ledger.commit_reservation(reservation_id)

    @cancel_method()
    async def cancel(self, reservation_id: Annotated[str, FromTry()], ctx: TccContext) -> None:
        await self._ledger.release_reservation(reservation_id)

The TCC engine runs Try across all participants in order. If every Try succeeds, it runs Confirm. If any Try fails, it runs Cancel for every participant whose Try succeeded. Try-results flow into Confirm/Cancel via Annotated[T, FromTry()]. See docs/modules/transactional.md.

Event Sourcing — Aggregates, Event Store, Outbox

Persist state as an append-only event log. Aggregates rebuild from history; projections update read models; the outbox reliably publishes events to brokers.

from dataclasses import dataclass
from pyfly.eventsourcing import (
    AggregateRoot, DomainEvent, domain_event,
    EventStore, SqlAlchemyEventStore, TransactionalOutbox,
)

@domain_event
@dataclass(frozen=True)
class AccountOpened(DomainEvent):
    account_id: str
    owner: str
    initial_balance: int

@domain_event
@dataclass(frozen=True)
class MoneyDeposited(DomainEvent):
    account_id: str
    amount: int

class Account(AggregateRoot):
    def __init__(self) -> None:
        super().__init__()
        self.owner: str = ""
        self.balance: int = 0
        self.when(AccountOpened, Account._on_opened)
        self.when(MoneyDeposited, Account._on_deposit)

    @classmethod
    def open(cls, account_id: str, owner: str, initial_balance: int) -> "Account":
        agg = cls()
        agg.id = account_id
        agg.apply(AccountOpened(account_id=account_id, owner=owner, initial_balance=initial_balance))
        return agg

    def deposit(self, amount: int) -> None:
        if amount <= 0:
            raise ValueError("amount must be positive")
        self.apply(MoneyDeposited(account_id=self.id, amount=amount))

    def _on_opened(self, e: AccountOpened) -> None:
        self.owner, self.balance = e.owner, e.initial_balance

    def _on_deposit(self, e: MoneyDeposited) -> None:
        self.balance += e.amount

# Persisting and rebuilding
store: EventStore = SqlAlchemyEventStore(session_factory)
account = Account.open("acc-42", "Alice", 100)
account.deposit(25)
await store.append(account.id, account.pending_events(), expected_version=account.version - len(account.pending_events()))
account.mark_committed()

# Later — reconstruct from the log:
events = await store.load("acc-42")
rebuilt = Account()
rebuilt.id = "acc-42"
for envelope in events:
    rebuilt.replay(envelope.event_type, envelope.event)
assert rebuilt.balance == 125

Highlights: AggregateRoot with when()/apply()/replay(), optimistic concurrency via expected_version, snapshots (SnapshotStore), TransactionalOutbox for at-least-once publishing, Projection + ProjectionRunner for read models, EventUpcaster for schema evolution. Adapters: InMemoryEventStore, SqlAlchemyEventStore. See docs/modules/eventsourcing.md.

CQRS — Command/Query Buses with Validation, Authorization, Caching

from pydantic import BaseModel
from pyfly.cqrs import command, query, CommandHandler, QueryHandler, CommandBus, QueryBus

class CreateUser(BaseModel):
    name: str
    email: str

class FindUserById(BaseModel):
    user_id: int

@command(CreateUser)
class CreateUserHandler(CommandHandler[CreateUser, int]):
    def __init__(self, repo: UserRepository) -> None:
        self._repo = repo

    async def handle(self, cmd: CreateUser) -> int:
        return (await self._repo.save(User(name=cmd.name, email=cmd.email))).id

@query(FindUserById, cache_ttl=60)
class FindUserByIdHandler(QueryHandler[FindUserById, User]):
    def __init__(self, repo: UserRepository) -> None: self._repo = repo
    async def handle(self, q: FindUserById) -> User:
        return await self._repo.find_by_id(q.user_id)

# Dispatch:
async def usage(commands: CommandBus, queries: QueryBus) -> None:
    user_id = await commands.dispatch(CreateUser(name="Ada", email="[email protected]"))
    user = await queries.dispatch(FindUserById(user_id=user_id))

Command/query are auto-wired via the CqrsAutoConfiguration entry point. Cross-cutting concerns (@validates, @authorizes, @cacheable) compose declaratively. See docs/modules/cqrs.md.

Inbound Webhooks — Verify · Dedupe · Dispatch

from pyfly.webhooks import (
    AbstractWebhookEventListener, HmacSignatureValidator,
    InMemoryWebhookEventStore, WebhookProcessor,
)
from pyfly.container import service

@service
class StripePaymentListener(AbstractWebhookEventListener):
    source = "stripe"

    async def handle(self, event_type: str, payload: dict) -> None:
        match event_type:
            case "payment_intent.succeeded":
                await self._mark_paid(payload["data"]["object"]["id"])
            case "charge.refunded":
                await self._mark_refunded(payload["data"]["object"]["id"])

# Auto-wired by AdapterAutoConfiguration:
processor = WebhookProcessor(
    validator=HmacSignatureValidator(secret=os.environ["STRIPE_SIGNING_SECRET"]),
    store=InMemoryWebhookEventStore(),
    listeners=[stripe_payment_listener],
)

The WebhookProcessor validates the signature, deduplicates by event id, persists to the WebhookEventStore, and routes to AbstractWebhookEventListener instances by source. Plug it into any controller (@post_mapping("/webhooks/stripe")) and you get production-grade ingestion. See docs/modules/webhooks.md.

Outbound Callbacks — HMAC-Signed Webhook Dispatch

from pyfly.callbacks import CallbackDispatcher, CallbackSubscription, CallbackConfig

dispatcher: CallbackDispatcher  # @autowired

await dispatcher.dispatch(
    event_type="OrderShipped",
    payload={"order_id": "ord-42", "tracking": "1Z..."},
    correlation_id="corr-1",
)
# Reads subscriptions, filters authorized domains, signs with HMAC,
# retries with exponential backoff, persists CallbackExecution records.

Subscriptions, authorized domains, and execution history are first-class persisted entities. Configure retry policies, secret rotation, and per-subscription filters declaratively. See docs/modules/callbacks.md.

Notifications — Email · SMS · Push (Provider-Agnostic)

from pyfly.notifications import EmailMessage, EmailService, SmsMessage, SmsService

email_service: EmailService  # auto-wired with SendGrid / Resend / SMTP / dummy
sms_service:   SmsService    # auto-wired with Twilio / dummy

await email_service.send(EmailMessage(
    to=["[email protected]"],
    subject="Welcome to Acme",
    html="<h1>Hi Alice</h1>",
))

await sms_service.send(SmsMessage(to="+34611222333", body="Your code is 4242"))

Configuration in pyfly.yaml selects the provider:

pyfly:
  notifications:
    email:
      provider: sendgrid     # sendgrid | resend | smtp | dummy
      api-key: ${SENDGRID_API_KEY}
    sms:
      provider: twilio       # twilio | dummy
      account-sid: ${TWILIO_ACCOUNT_SID}
      auth-token: ${TWILIO_AUTH_TOKEN}
    push:
      provider: firebase     # firebase | dummy
      credentials-file: /run/secrets/firebase.json

See docs/modules/notifications.md.

Identity Provider (IDP) — Multi-Provider Auth

Single port, four interchangeable adapters (Keycloak, AWS Cognito, Azure AD, internal-DB).

from pyfly.idp import IdpAdapter, LoginRequest

idp: IdpAdapter  # auto-wired

result = await idp.login(LoginRequest(username="[email protected]", password="hunter2"))
if result.requires_mfa:
    result = await idp.verify_mfa(result.mfa_challenge_id, code="123456")

session = await idp.introspect(result.access_token)
print(session.user.email, session.roles)
pyfly:
  idp:
    provider: keycloak       # keycloak | aws-cognito | azure-ad | internal-db
    realm: acme
    server-url: https://auth.acme.com
    client-id: acme-app
    client-secret: ${KEYCLOAK_CLIENT_SECRET}

Switching providers is a YAML one-liner — your business code keeps depending on IdpAdapter. See docs/modules/idp.md.

ECM — Documents · Folders · E-Signature

from pyfly.ecm import DocumentService, ESignatureService, SignatureRequest, Recipient

documents: DocumentService     # auto-wired (S3 / Azure Blob / local-fs)
esig: ESignatureService        # auto-wired (DocuSign / Adobe Sign / Logalty / no-op)

doc = await documents.upload("contracts/2026/acme.pdf", file_bytes, mime_type="application/pdf")

envelope = await esig.send(SignatureRequest(
    document_id=doc.id,
    recipients=[
        Recipient(email="[email protected]", name="Alice", role="signer"),
        Recipient(email="[email protected]", name="Legal", role="approver"),
    ],
    subject="Please sign the SaaS agreement",
))

status = await esig.status(envelope.id)

Storage and e-signature are independent ports — combine S3 storage with DocuSign, or Azure Blob with Adobe Sign, or local-fs with the no-op signer for tests. See docs/modules/ecm.md.

Rule Engine — YAML DSL with AST Evaluation

Externalize business rules so non-developers can change them without redeploys.

# rules/credit_approval.yaml
name: credit_approval
description: Decision rules for personal loans
inputs: [income, debt, credit_score, employment_years]
rules:
  - id: high_credit_fast_track
    when: credit_score >= 750 and income >= 50000
    then: { decision: "approve", limit: 50000 }
  - id: standard_review
    when: credit_score >= 650 and (debt / income) < 0.4
    then: { decision: "review", limit: 25000 }
  - id: reject_low_credit
    when: credit_score < 600 or employment_years < 1
    then: { decision: "reject" }
from pyfly.rule_engine import RuleEngine, RuleSetRepository

rules: RuleEngine  # auto-wired

decision = await rules.evaluate("credit_approval", {
    "income": 75000, "debt": 12000, "credit_score": 770, "employment_years": 5,
})
# {'decision': 'approve', 'limit': 50000, '_rule_id': 'high_credit_fast_track'}

Audit trails (which rule fired, why), batch evaluation, hot-reload from RuleSetRepository. See docs/modules/rule-engine.md.

Plugin SPI — @plugin / @extension_point / @extension

Build extensible products: define extension points, let third-party packages contribute extensions.

from pyfly.plugins import plugin, extension_point, extension

@extension_point
class PaymentMethod:
    def display_name(self) -> str: ...
    async def charge(self, amount: int, customer_id: str) -> str: ...

@plugin(name="acme-stripe", version="1.0.0", depends_on=[])
class StripePlugin: ...

@extension(point=PaymentMethod, plugin="acme-stripe")
class StripePayment(PaymentMethod):
    def display_name(self) -> str: return "Credit Card (Stripe)"
    async def charge(self, amount: int, customer_id: str) -> str:
        return await self._stripe.charges.create(amount=amount, customer=customer_id)

The plugin manager resolves the dependency graph, loads plugins in order, and registers extensions with the DI container. See docs/modules/plugins.md.

Domain — DDD Building Blocks

pyfly.domain ships the foundational types every domain-driven design codebase ends up reinventing — Entity, ValueObject, AggregateRoot, DomainEvent, Specification, DomainRepository, and domain-flavoured exceptions. The module is pure standard-library Python with zero runtime dependencies.

from dataclasses import dataclass
from pyfly.domain import AggregateRoot, BusinessRuleViolation, DomainEvent, ValueObject

@dataclass(frozen=True, slots=True)
class Money(ValueObject):
    amount: int
    currency: str

@dataclass(frozen=True)
class OrderShipped(DomainEvent):
    order_id: str = ""
    tracking_number: str = ""

class Order(AggregateRoot[str]):
    def __init__(self, id: str, total: Money) -> None:
        super().__init__(id)
        self.total = total
        self.status = "placed"

    def ship(self, tracking_number: str) -> None:
        if self.status == "shipped":
            raise BusinessRuleViolation("order-already-shipped")
        self.status = "shipped"
        assert self.id is not None
        self.raise_event(OrderShipped(order_id=self.id, tracking_number=tracking_number))

# Application service:
order = Order("o-1", Money(100, "EUR"))
order.ship("trk-42")

events = order.clear_events()      # drained by the repository
# repository.save(order); for e in events: bus.publish(e)

For domain-tier microservices, the @enable_domain_stack starter activates CQRS, the transactional engine (saga/workflow/TCC), event sourcing, the rule engine, and the relational data layer in a single decorator — mirroring fireflyframework-starter-domain (Java) and AddFireflyDomain (.NET):

from pyfly.core import pyfly_application
from pyfly.starters.domain import enable_domain_stack

@enable_domain_stack
@pyfly_application(name="my-service", scan_packages=["my_service"])
class Application:
    pass

The full DDD primitives are also re-exported from pyfly.starters.domain so a single import line is enough:

from pyfly.starters.domain import (
    AggregateRoot, BusinessRuleViolation, DomainEvent, DomainRepository,
    Entity, Specification, ValueObject, enable_domain_stack,
)

See samples/order_service/ for an end-to-end DDD microservice that uses every primitive: layered split (interfaces / models / core / web / sdk), a real Order aggregate that protects its invariants, CQRS handlers, and a ConfirmOrderSaga that walks the order through PLACED → INVENTORY_RESERVED → PAID → SHIPPED with full compensation. See docs/modules/domain.md.


Installation

Note: PyFly is distributed exclusively via GitHub Releases. It is not published to PyPI.

Install from GitHub Release (Recommended)

# Install the latest release (uv)
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.4-py3-none-any.whl"

# Or with pip
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)

# Via get.pyfly.io
curl -fsSL https://get.pyfly.io/ | bash

# Or directly from GitHub
curl -fsSL https://raw.githubusercontent.com/fireflyframework/fireflyframework-pyfly/main/install.sh | bash

The installer clones the repo, creates a virtual environment, installs PyFly with all extras, and adds pyfly to your PATH. You can customize with environment variables:

# Install to a custom directory
PYFLY_HOME=/opt/pyfly curl -fsSL https://get.pyfly.io/ | bash

# Install with specific extras only
PYFLY_EXTRAS=web,data-relational,security curl -fsSL https://get.pyfly.io/ | bash

Install from Source

# Clone the repository
git clone https://github.com/fireflyframework/fireflyframework-pyfly.git
cd pyfly

# Run the interactive installer
bash install.sh

# Or install manually with uv
uv sync --all-extras --group dev

# Or with pip
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[full]"

Verify Installation

pyfly --version
pyfly doctor
pyfly info

Create Your First Project

# Quick start — create a REST API with all the batteries
pyfly new my-service --archetype web-api
cd my-service
pyfly run --reload

# Visit http://localhost:8080/health

See the Installation Guide for detailed options, Docker examples, and CI/CD setup.


CLI & Project Scaffolding

The pyfly CLI generates production-ready project structures with DI stereotypes, Docker support, and layered architecture out of the box.

Archetypes

Command What you get
pyfly new my-app Minimal microservice (core archetype)
pyfly new my-api --archetype web-api REST API with controllers, services, repositories
pyfly new my-api --archetype fastapi-api REST API with FastAPI and native OpenAPI
pyfly new my-site --archetype web Server-rendered HTML with Jinja2 templates
pyfly new my-svc --archetype hexagonal Hexagonal architecture with ports & adapters
pyfly new my-lib --archetype library Reusable library with py.typed marker
pyfly new my-tool --archetype cli CLI application with interactive shell and DI

Feature Selection

Choose which PyFly extras to include with --features:

# REST API with database and caching
pyfly new order-service --archetype web-api --features web,data-relational,cache

Available features: web, data-relational, data-document, eda, cache, client, security, scheduling, observability, cqrs, shell

Interactive Mode

Run pyfly new without arguments for a guided experience:

$ pyfly new

  ╭──────────────────────────────────╮
  │   PyFly Project Generator        │
  ╰──────────────────────────────────╯

  Step 1 of 4 — Project Details
  ? Project name: order-service
  ? Package name: order_service

  Step 2 of 4 — Architecture
  ? Select archetype: (use arrow keys)
    ❯ core          Minimal microservice with DI container and config
      web-api       Full REST API with controller/service/repository layers
      web           Server-rendered HTML with Jinja2 templates and static assets
      hexagonal     Clean architecture with domain isolation
      library       Reusable library with py.typed and packaging best practices
      cli           Command-line application with interactive shell and DI

  Step 3 of 4 — Features
  ? Select features: (space to toggle, enter to confirm)
    ❯ [x] web          HTTP server, REST controllers, OpenAPI docs
      [ ] data-relational  Data Relational — SQL databases (SQLAlchemy ORM)
      ...

  Step 4 of 4 — Review & Create
  ? Create this project? Yes

Generated Web API Structure

order-service/
├── Dockerfile              # Multi-stage production build
├── README.md               # Project docs with quick start
├── pyfly.yaml              # Framework configuration
├── pyproject.toml          # Dependencies based on selected features
├── .gitignore
├── .env.example
├── src/order_service/
│   ├── __init__.py
│   ├── app.py              # @pyfly_application entry point
│   ├── main.py             # ASGI app factory
│   ├── controllers/
│   │   ├── __init__.py
│   │   ├── health_controller.py   # @rest_controller — /health
│   │   └── todo_controller.py     # @rest_controller — CRUD /todos
│   ├── services/
│   │   ├── __init__.py
│   │   └── todo_service.py        # @service — business logic
│   ├── models/
│   │   ├── __init__.py
│   │   └── todo.py                # Pydantic DTOs
│   └── repositories/
│       ├── __init__.py
│       └── todo_repository.py     # @repository — data access
└── tests/
    ├── __init__.py
    ├── conftest.py
    └── test_todo_service.py

Other CLI Commands

Command Description
pyfly run --reload Start the application server with auto-reload
pyfly info Show installed framework version and extras
pyfly doctor Diagnose your development environment
pyfly db init Initialize Alembic migration environment
pyfly db migrate -m "msg" Auto-generate a database migration
pyfly db upgrade Apply pending migrations
pyfly license Display the Apache 2.0 license
pyfly sbom Software Bill of Materials (table or JSON)

See the full CLI Reference for details.


Modules

PyFly ships with 39 fully-implemented modules organized into five layers — covering everything from HTTP routing and database access to distributed transactions, event sourcing, identity, content management, observability, and DDD building blocks:

Foundation Layer

Module Description Firefly Java Equivalent
Core Application bootstrap, lifecycle, banner, configuration fireflyframework-starter-core
Kernel Exception hierarchy, structured error types fireflyframework-kernel
Container Dependency injection, stereotypes, bean factories Spring DI (built-in)
Context ApplicationContext, events, lifecycle hooks, conditions Spring ApplicationContext
Config Decentralized auto-configuration via @auto_configuration entry points Spring Auto-Configuration
Logging Structured logging port and adapters fireflyframework-observability

Application Layer

Module Description Firefly Java Equivalent
Web HTTP routing, controllers, middleware, OpenAPI (Starlette and FastAPI adapters) fireflyframework-web
Server Pluggable ASGI servers (Granian, Uvicorn, Hypercorn) and event loops (uvloop, asyncio) Embedded Tomcat/Jetty/Undertow
Data Repository ports, derived queries, pagination, sorting, entity mapping Spring Data Commons
Data Relational SQLAlchemy adapter — specifications, transactions, custom queries fireflyframework-r2dbc
Data Document MongoDB adapter — Beanie ODM, document repositories fireflyframework-mongodb
CQRS Command/Query segregation with CommandBus/QueryBus, validation, authorization, caching fireflyframework-cqrs
Validation Input validation with Pydantic fireflyframework-validators

Infrastructure Layer

Module Description Firefly Java Equivalent
Security JWT, password encoding, authorization Part of fireflyframework-starter-application
Messaging Kafka, RabbitMQ, in-memory broker fireflyframework-eda
EDA Event-driven architecture, event bus fireflyframework-eda
Cache Caching decorators, Redis adapter fireflyframework-cache
Client HTTP client, circuit breaker, retry fireflyframework-client
Scheduling Cron jobs, fixed-rate tasks Spring Scheduling
Resilience Rate limiter, bulkhead, timeout, fallback Resilience4j (in fireflyframework-client)
Shell CLI commands, interactive REPL, runners Spring Shell
Transactional Saga + Workflow + TCC orchestration: signal-driven, DAG, compensation, multi-backend persistence, DLQ, recovery fireflyframework-orchestration
Event Sourcing AggregateRoot, EventStore, snapshots, transactional outbox, projections, upcasting fireflyframework-eventsourcing
Domain (DDD) Entity, ValueObject, AggregateRoot, DomainEvent, Specification, DomainRepository, BusinessRuleViolation fireflyframework-starter-domain
Plugins @plugin / @extension_point / @extension, dependency-ordered lifecycle fireflyframework-plugins
Rule Engine YAML DSL, AST evaluator, batch evaluation, rule-set repository fireflyframework-rule-engine
Config Server Spring Cloud Config Server analogue + client fireflyframework-config-server

Integration Layer

Module Description Firefly Java Equivalent
IDP Identity-provider port + Keycloak / AWS Cognito / Azure AD / internal-DB adapters fireflyframework-idp + adapters
ECM Document storage / metadata / folders / e-signature ports + AWS S3 / Azure Blob / DocuSign / Adobe Sign / Logalty / local-fs adapters fireflyframework-ecm + adapters
Notifications Email / SMS / push ports + SendGrid / Twilio / Firebase / Resend / SMTP / dummy adapters fireflyframework-notifications + adapters
Callbacks Outbound webhook dispatcher with HMAC signing, retries, execution tracking fireflyframework-callbacks
Webhooks Inbound webhook ingestion with signature validation, idempotency, listener pattern fireflyframework-webhooks
Starters Meta-packages (enable_core_stack / application / data / domain) fireflyframework-starter-*

Cross-Cutting Layer

Module Description Firefly Java Equivalent
AOP Aspect-oriented programming Spring AOP
Observability Prometheus metrics, OpenTelemetry tracing fireflyframework-observability
Actuator Health checks, monitoring endpoints fireflyframework-starter-core (actuator)
Admin Embedded management dashboard with 15 views, SSE streams, server mode fleet monitoring Spring Boot Admin
Testing Test fixtures and assertions Spring Test
CLI Command-line tools fireflyframework-cli

Documentation

Full documentation lives in the docs/ directory:

Module Guides

Browse all guides in the Module Guides Index:

Adapter Reference

Browse the Adapter Catalog for setup and configuration of each concrete backend:

Browse the full list in the Documentation Table of Contents.


Roadmap

See ROADMAP.md for the full roadmap toward feature parity with the Firefly Framework Java ecosystem (40+ modules).

Phase Focus Key Modules Status
Phase 1 Core Distributed Patterns Saga/TCC, Workflow, Event Sourcing Complete (v26.05.01)
Phase 2 Business Logic Rule Engine, Plugins Complete (v26.05.01)
Phase 3 Enterprise Integrations Notifications, IDP, ECM, Webhooks, Callbacks, Config Server Complete (v26.05.01)
Phase 4 Administrative & DDD Backoffice, Utils, DDD starters (done in v26.05.02) DDD complete; backoffice / utils planned

v26.05.01 closes the parity gap with the Java Firefly Framework: the transactional engine has been rewritten from scratch (Saga + Workflow + TCC), nine new modules have been added (Event Sourcing, Callbacks, Webhooks, Notifications, IDP, ECM, Plugins, Rule Engine, Config Server), 12 third-party adapters were added, four new client protocols (SOAP/gRPC/GraphQL/WebSocket) were introduced, and the validation library now ships 16 domain validators. The framework is feature-complete for production microservice workloads.


Versioning

PyFly uses Calendar Versioning (CalVer) — YY.MM.PATCH — to stay aligned with the rest of the Firefly Framework family (Java, .NET, Go).

Component Meaning
YY Two-digit year (e.g., 26 = 2026)
MM Two-digit month of the release
PATCH Patch number within the month (01, 02, …)

Examples: 26.05.01, 26.05.02, 26.06.01.

The git tag and human-readable display use the leading-zero form (v26.05.01); the pyproject.toml version field uses PEP 440's normalized form (26.5.1) so Python tooling (uv, pip, hatchling) accepts it without warnings. Both reference the same release. See docs/versioning.md for full details, including the migration from the previous SemVer-with-milestone scheme.


Changelog

See CHANGELOG.md for detailed release notes.

Current: v26.05.04 (2026-05-08) — pyfly.security import-chain fix:

  • Bug fixpyfly.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.
  • Imperative API for parity with .NET — every starter now ships a register_*_stack(app) function (register_core_stack, register_web_stack, register_application_stack, register_data_stack, register_domain_stack) — the Pythonic counterpart to .NET's services.AddFireflyXxx(...) extension methods. Imperative registration is authoritative (last-call-wins).
  • One-import-line ergonomics — every starter re-exports the most commonly used decorators and types of its tier. from pyfly.starters.web import rest_controller, post_mapping, Body, Valid, ...; from pyfly.starters.domain import AggregateRoot, BusinessRuleViolation, Command, CommandHandler, command_handler, ....
  • Layered docs — new docs/modules/starters.md explains the property-layering model (framework defaults < starter defaults < user yaml < profile overlays < env vars) and shows the cross-language correspondence table.

Previous: v26.05.02 (2026-05-08) — DDD primitives + OrderService sample + async-saga fix:

  • pyfly.domain — pure-Python DDD building blocks: Entity, ValueObject, AggregateRoot, DomainEvent, Specification (with & / | / ~ combinators), DomainRepository protocol, DomainException / BusinessRuleViolation / AggregateNotFound. Mirrors fireflyframework-starter-domain (Java) and FireflyFramework.Starter.Domain (.NET).
  • OrderService samplesamples/order_service/ is a complete DDD-flavoured microservice with the same layered split (interfaces / models / core / web / sdk) used by the firefly-oss Java services and the .NET OrdersService sample. Includes a real Order aggregate, CQRS handlers, and a ConfirmOrderSaga that walks the order through PLACED → INVENTORY_RESERVED → PAID → SHIPPED with full compensation. 13/13 tests pass end-to-end.
  • Async-saga fix@saga_step / @try_method / @confirm_method / @cancel_method no longer wrap the function with a sync adapter that masked inspect.iscoroutinefunction. async def saga and TCC steps are now correctly awaited by the engine. Regression test pinned in tests/transactional/saga/test_async_steps.py.

Previous: v26.05.01 (2026-05-07) — Full Java framework parity:

  • Transactional engine rewritepyfly.transactional now ships Saga + Workflow + TCC patterns on a shared core (DAG topology, retries with jitter, backpressure, idempotency, DLQ, recovery, REST controllers, health indicators)
  • Nine new moduleseventsourcing, callbacks, webhooks, notifications, idp, ecm, plugins, rule_engine, config_server
  • 12 new third-party adapters — Keycloak / AWS Cognito / Azure AD (IDP); AWS S3 / Azure Blob (ECM storage); DocuSign / Adobe Sign / Logalty (e-signature); SendGrid / Resend / Twilio / Firebase (notifications)
  • Four new client protocols — SOAP, gRPC, GraphQL, WebSocket (joining the existing HTTP / OpenAPI generators)
  • 16 domain validators — including IBAN, BIC, ISO country/currency codes, phone numbers, dates, national IDs, sort codes, interest rates
  • CalVer migrationYY.MM.PATCH aligning with all Firefly Framework siblings (Java, .NET, Go)

Firefly Framework Ecosystem

PyFly is part of the Firefly Framework ecosystem:

Platform Repository Status
Java / Spring Boot fireflyframework-* (40+ modules) Production
.NET 9 fireflyframework-dotnet Beta (CalVer 26.05+)
Python fireflyframework-pyfly Beta (CalVer 26.05+)
Frontend (Angular) flyfront Active Development
GenAI fireflyframework-genai Active Development
CLI (Go) fireflyframework-cli Active Development

Requirements

Requirement Version
Python >= 3.12
uv >= 0.5 recommended (pip also supported)
Git For cloning the repository
OS macOS, Linux (Windows support planned)

License

Apache License 2.0 — Firefly Software Foundation.

About

Official Python implementation of the Firefly Framework — async-first, type-checked microservice framework with DI, hexagonal architecture, distributed transactions (Saga/Workflow/TCC), event sourcing, IDP, ECM, notifications, and 38 fully-implemented modules

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors