Skip to main content

A declarative Python framework for orchestrating service logic with rhythm and precision

Project description

Cadence

PyPI version Python Versions License: MIT Tests codecov

A declarative Python framework for building service logic with explicit control flow.

Cadence lets you build complex service orchestration with a clean, readable API. Define your business logic as composable beats, handle errors gracefully, and scale with confidence.

Features

  • Declarative Cadence Definition - Build complex workflows with a fluent, chainable API
  • Parallel Execution - Run tasks concurrently with automatic context isolation and merging
  • Branching Logic - Conditional execution paths with clean syntax
  • Resilience Patterns - Built-in retry, timeout, fallback, and circuit breaker
  • Framework Integration - First-class support for FastAPI and Flask
  • Observability - Hooks for logging, metrics, and tracing
  • Type Safety - Full type hints and generics support
  • Zero Dependencies - Core library has no required dependencies

Installation

pip install cadence-flow

With optional integrations:

# FastAPI integration
pip install cadence-flow[fastapi]

# Flask integration
pip install cadence-flow[flask]

# OpenTelemetry tracing
pip install cadence-flow[opentelemetry]

# Prometheus metrics
pip install cadence-flow[prometheus]

# All integrations
pip install cadence-flow[all]

Quick Start

from dataclasses import dataclass
from cadence import Cadence, Context, beat

@dataclass
class OrderContext(Context):
    order_id: str
    items: list = None
    total: float = 0.0
    status: str = "pending"

@beat
async def fetch_items(ctx: OrderContext):
    # Fetch order items from database
    ctx.items = await db.get_items(ctx.order_id)

@beat
async def calculate_total(ctx: OrderContext):
    ctx.total = sum(item.price for item in ctx.items)

@beat
async def process_payment(ctx: OrderContext):
    await payment_service.charge(ctx.order_id, ctx.total)
    ctx.status = "paid"

# Build and run the cadence
cadence = (
    Cadence("checkout", OrderContext(order_id="ORD-123"))
    .then("fetch_items", fetch_items)
    .then("calculate_total", calculate_total)
    .then("process_payment", process_payment)
)

result = await cadence.run()
print(f"Order {result.order_id}: {result.status}")

Core Concepts

Sequential Beats

Execute beats one after another:

cadence = (
    Cadence("process", MyContext())
    .then("beat1", do_first)
    .then("beat2", do_second)
    .then("beat3", do_third)
)

Parallel Execution

Run independent tasks concurrently with automatic context isolation:

cadence = (
    Cadence("enrich", UserContext(user_id="123"))
    .sync("fetch_data", [
        fetch_profile,
        fetch_preferences,
        fetch_history,
    ])
    .then("merge_results", combine_data)
)

Conditional Branching

Route execution based on runtime conditions:

cadence = (
    Cadence("order", OrderContext())
    .then("validate", validate_order)
    .split("route",
        condition=is_premium_customer,
        if_true=[priority_processing, express_shipping],
        if_false=[standard_processing, regular_shipping]
    )
    .then("confirm", send_confirmation)
)

Child Cadences

Compose cadences for complex orchestration:

payment_cadence = Cadence("payment", PaymentContext())...
shipping_cadence = Cadence("shipping", ShippingContext())...

checkout_cadence = (
    Cadence("checkout", CheckoutContext())
    .then("prepare", prepare_order)
    .child("process_payment", payment_cadence, merge_payment)
    .child("arrange_shipping", shipping_cadence, merge_shipping)
    .then("complete", finalize_order)
)

Resilience Patterns

Retry with Backoff

from cadence import retry

@retry(max_attempts=3, delay=1.0, backoff=2.0)
@beat
async def call_external_api(ctx):
    response = await http_client.get(ctx.api_url)
    ctx.data = response.json()

Timeout

from cadence import timeout

@timeout(seconds=5.0)
@beat
async def slow_operation(ctx):
    ctx.result = await long_running_task()

Fallback

from cadence import fallback

@fallback(default={"status": "unknown"})
@beat
async def get_status(ctx):
    ctx.status = await status_service.get(ctx.id)

Circuit Breaker

from cadence import circuit_breaker

@circuit_breaker(failure_threshold=5, recovery_timeout=30.0)
@beat
async def call_fragile_service(ctx):
    ctx.data = await fragile_service.fetch()

Framework Integration

FastAPI

from fastapi import FastAPI
from cadence.integrations.fastapi import CadenceRouter

app = FastAPI()
router = CadenceRouter()

@router.cadence("/orders/{order_id}", checkout_cadence)
async def create_order(order_id: str):
    return OrderContext(order_id=order_id)

app.include_router(router)

Flask

from flask import Flask
from cadence.integrations.flask import CadenceBlueprint

app = Flask(__name__)
bp = CadenceBlueprint("orders", __name__)

@bp.cadence_route("/orders/<order_id>", checkout_cadence)
def create_order(order_id):
    return OrderContext(order_id=order_id)

app.register_blueprint(bp)

Observability

Hooks System

from cadence import Cadence, LoggingHooks, TimingHooks

cadence = (
    Cadence("monitored", MyContext())
    .with_hooks(LoggingHooks())
    .with_hooks(TimingHooks())
    .then("beat1", do_work)
)

Custom Hooks

from cadence import CadenceHooks

class MyHooks(CadenceHooks):
    async def before_beat(self, beat_name, context):
        print(f"Starting: {beat_name}")

    async def after_beat(self, beat_name, context, duration, error=None):
        print(f"Completed: {beat_name} in {duration:.2f}s")

    async def on_error(self, beat_name, context, error):
        alert_team(f"Error in {beat_name}: {error}")

Prometheus Metrics

from cadence.reporters import PrometheusReporter

reporter = PrometheusReporter(prefix="myapp")

cadence = (
    Cadence("tracked", MyContext())
    .with_reporter(reporter.report)
    .then("beat1", do_work)
)

OpenTelemetry Tracing

from cadence.reporters import OpenTelemetryReporter

reporter = OpenTelemetryReporter(service_name="my-service")

cadence = (
    Cadence("traced", MyContext())
    .with_reporter(reporter.report)
    .then("beat1", do_work)
)

Cadence Diagrams

Generate visual diagrams of your cadences:

from cadence import to_mermaid, to_dot

# Generate Mermaid diagram
print(to_mermaid(my_cadence))

# Generate DOT/Graphviz diagram
print(to_dot(my_cadence))

CLI

Cadence includes a CLI for scaffolding and utilities:

# Initialize a new project
cadence init my-project

# Generate a new cadence
cadence new cadence checkout

# Generate a new beat with resilience decorators
cadence new beat process-payment --retry 3 --timeout 30

# Generate cadence diagram
cadence diagram myapp.cadences:checkout_cadence --format mermaid

# Validate cadence definitions
cadence validate myapp.cadences

Context Management

Immutable Context

For functional-style cadences:

from cadence import ImmutableContext

@dataclass(frozen=True)
class Config(ImmutableContext):
    api_key: str
    timeout: int = 30

# Create new context with changes
new_config = config.with_field("timeout", 60)

Atomic Operations

Thread-safe context updates for parallel execution:

from cadence import Context, AtomicList, AtomicDict

@dataclass
class AggregatorContext(Context):
    results: AtomicList = None
    cache: AtomicDict = None

    def __post_init__(self):
        super().__post_init__()
        self.results = AtomicList()
        self.cache = AtomicDict()

# Safe concurrent updates
ctx.results.append(new_result)
ctx.cache["key"] = value

Error Handling

from cadence import CadenceError, BeatError

cadence = (
    Cadence("handled", MyContext())
    .then("risky", risky_operation)
    .on_error(handle_error, stop=False)  # Continue on error
    .then("cleanup", cleanup)
)

async def handle_error(context, error):
    if isinstance(error, BeatError):
        logger.error(f"Beat {error.beat_name} failed: {error}")
        context.errors.append(str(error))

Documentation

Contributing

We welcome contributions! Please see our Contributing Guide for details.

License

Cadence is released under the MIT License.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cadence_orchestration-0.3.0.tar.gz (127.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cadence_orchestration-0.3.0-py3-none-any.whl (51.5 kB view details)

Uploaded Python 3

File details

Details for the file cadence_orchestration-0.3.0.tar.gz.

File metadata

  • Download URL: cadence_orchestration-0.3.0.tar.gz
  • Upload date:
  • Size: 127.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for cadence_orchestration-0.3.0.tar.gz
Algorithm Hash digest
SHA256 004e0ceb954f4e88a4b1ad6c7deda24e7622a443fc4ec2ac41f269fa8fcf332b
MD5 87d383012ef6c8124de7484a6125fd03
BLAKE2b-256 691fa4f3c7ba4544ec34b785ed38c51156c70d794c8eb42d22d05c5b5e3ad1af

See more details on using hashes here.

Provenance

The following attestation bundles were made for cadence_orchestration-0.3.0.tar.gz:

Publisher: publish.yml on mauhpr/cadence

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cadence_orchestration-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for cadence_orchestration-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3086b5a1fe46c671250b16f706b5f5e2a7c670a5afefcc0c428059bab839c44f
MD5 c4b5cff7ab9754ac630464e11e1e6c35
BLAKE2b-256 63307e60547fb5fcd582d16fea6c733cae84814ad503435c39b15f551fd560b6

See more details on using hashes here.

Provenance

The following attestation bundles were made for cadence_orchestration-0.3.0-py3-none-any.whl:

Publisher: publish.yml on mauhpr/cadence

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page