Metadata-Version: 2.4
Name: core-framework
Version: 1.2.0
Summary: Core framework package (import as core_framework)
Project-URL: Homepage, https://github.com/NepNepFFXIV/core-framework
Project-URL: Repository, https://github.com/NepNepFFXIV/core-framework
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.14
Requires-Dist: alembic>=1.18.4
Requires-Dist: arq>=0.28.0
Requires-Dist: async-lru>=2.1.0
Requires-Dist: asyncer>=0.0.14
Requires-Dist: asyncpg>=0.31.0
Requires-Dist: brotli-asgi>=1.6.0
Requires-Dist: cashews[redis]>=7.5.0
Requires-Dist: device-detector>=6.2.0
Requires-Dist: fastapi[standard]>=0.136.1
Requires-Dist: firebase-admin>=7.4.0
Requires-Dist: httpx[http2]>=0.28.1
Requires-Dist: itsdangerous>=2.2.0
Requires-Dist: logfire[asyncpg,fastapi,httpx,redis]>=4.32.1
Requires-Dist: mashumaro[orjson]>=3.20
Requires-Dist: orjson>=3.11.7
Requires-Dist: python-ulid>=3.1.0
Requires-Dist: structlog>=25.5.0
Requires-Dist: tenacity>=9.1.4
Provides-Extra: testing
Requires-Dist: asgi-lifespan>=2.1.0; extra == 'testing'
Requires-Dist: pytest-xdist>=3.6.0; extra == 'testing'
Requires-Dist: pytest>=9.0.3; extra == 'testing'
Requires-Dist: testcontainers[postgres,redis]>=4.13.2; extra == 'testing'
Description-Content-Type: text/markdown

# Core framework

Reusable backend core published as the Python distribution **`core-framework`** (`pyproject.toml`). Import the implementation as **`core_framework`** (see [Core framework migration](docs/core-framework-migration.md), [Package API](docs/package-api.md), and [CHANGELOG](CHANGELOG.md) for version history).

**Repository:** [github.com/NepNepFFXIV/core-framework](https://github.com/NepNepFFXIV/core-framework)

**License:** MIT — see [LICENSE](LICENSE).

## Install

Pin **`core-framework`** from **Git** (use a **tag** or commit for reproducible builds):

```bash
uv add "core-framework @ git+https://github.com/NepNepFFXIV/core-framework.git@v1.2.0"
```

```bash
pip install "core-framework @ git+https://github.com/NepNepFFXIV/core-framework.git@v1.2.0"
```

After it resolves, load **`Settings`**, call **`init_app(settings)`** from **`core_framework.main`** in your app factory, and apply bundled core schema from the host project root with **`uv run cf-alembic`** (see [Host integration](#host-integration) and [Package API](docs/package-api.md)).

To **hack on this repo**: clone it, run **`uv sync`** (or **`pip install -e .`**), keep a local **`config.toml`**, run **`uv run cf-alembic`**, then **`make run`** or serve **`core_framework.asgi:app`**.

## Host integration

Hosts depend on **`core-framework`**, own config and the DB, and import **`core_framework`**. Details: [Core framework migration](docs/core-framework-migration.md).

1. **Dependency** — Pin **`core-framework`** (path, Git tag, or index) and lock deploys (**`uv.lock`**, etc.).
1. **Settings** — Use **`Settings`** from **`core_framework.core.settings`**. Default loading uses **`config.toml`** and **environment variables**; **env wins** over TOML for the same field (see [Architecture decisions](docs/architecture-decisions.md#configuration-toml-primary-environment-overrides)). Subclass **only** if you need extra host-only fields; otherwise **`Settings()`** / **`model_validate`** is enough. Subclasses may need **`model_config`** tweaks (e.g. **`toml_file`**) if paths differ.
1. **App** — Call **`init_app(settings)`** from **`core_framework.main`** inside the host’s own **`main.py`** (or app factory). Importing **`core_framework.main`** has no side effects; only **`init_app(settings)`** runs the bootstrap. Do **not** import **`core_framework.asgi`** — that module exists so **`fastapi dev`** / **`uvicorn`** can serve the library’s own default-settings app standalone, and it eagerly loads **`core-framework`**’s own **`config.toml`** at import time. Do not mount **`router`** alone without the same bootstrap — see [Package API](docs/package-api.md).
1. **Database** — Apply core schema from the host root (where **`config.toml`** loads): **`uv run cf-alembic`**. Host-only tables: host migrations; order with core ([Database and Alembic](docs/core-framework-migration.md#database-and-alembic)).
1. **Worker** — **`WorkerSettings`** class in **`core_framework.worker.main`** (run with **`arq core_framework.worker.main.WorkerSettings`**). Bootstrap (**`configure_application_dependencies`**, pool connects, **`init_cashews_backend`**) runs in **`on_startup`**, not at import time. ARQ exposes **`ctx["redis"]`** for enqueue in cron handlers; **`CoreRuntime.redis_queue`** is a separate connected pool for runtime parity with the API — see **[Architecture — ARQ and CoreRuntime queue clients](docs/architecture.md#arq-and-coreruntime-queue-clients)**.
1. **API surface** — Only [Package API](docs/package-api.md) imports are stable; see [CHANGELOG](CHANGELOG.md).
1. **Upgrading / new `Settings` fields** — When you bump **`core-framework`**, read **[CHANGELOG](CHANGELOG.md)** for that release (and any required migration notes). New **`Settings`** keys are announced there: **required** keys must be added to the host’s **`config.toml`** / env before deploy; **optional** keys list defaults. Version bumps follow SemVer in **`pyproject.toml`** so hosts can judge impact.

## Testing in host projects

Use the **`core-framework[testing]`** optional extra in the host’s **dev** or **test** dependency group so pytest gets **`testcontainers`**, **`pytest-xdist`**, **`asgi-lifespan`**, and a compatible **`pytest`**. The package registers a pytest plugin automatically; you do **not** need **`pytest_plugins = (...)`** for **`core_framework.testing`**.

1. Add the extra (example with **`uv`**): in **`pyproject.toml`**, e.g. **`core-framework = { path = "…", extras = ["testing"] }`** or **`core-framework[testing]`** in **`[dependency-groups].dev`**.
1. In the host’s **root** **`tests/conftest.py`**, implement **`pytest_core_framework_config()`** returning **`TestConfig`** from **`core_framework.testing`** — pass **`settings_loader`** and **`app_factory`** (usually your **`load_default_settings`** and **`init_app`**). Optionally set **`alembic_root`**, **`firebase_project_id`**, **`include_arq_fixtures`**, etc.
1. In the same **`conftest.py`**, define a **session-scoped** **`anyio_backend`** fixture that returns **`"asyncio"`** (required so session-scoped async fixtures work with anyio’s pytest integration).

Minimal shape (adjust paths and flags to your host):

```python
from pathlib import Path

import pytest

from core_framework.testing import TestConfig
from my_host.settings import load_default_settings
from my_host.main import init_app


def pytest_core_framework_config() -> TestConfig:
    return TestConfig(
        settings_loader=load_default_settings,
        app_factory=init_app,
        # alembic_root=Path(__file__).resolve().parent.parent / "alembic",
        # firebase_project_id="test",
        # include_arq_fixtures=False,
    )


@pytest.fixture(scope="session")
def anyio_backend() -> str:
    return "asyncio"
```

**Stable API and fixture list:** **[Package API — Testing](docs/package-api.md#testing-pytest-plugin)**. **Design notes (xdist, Firebase hybrid, risks):** **[Testing plugin design](docs/testing-plugin-design.md)**.

## Commands

- `make run` — run the standalone core app locally.
- `make alembic` — run bundled core migrations via `cf-alembic`.
- `make ruff` — lint and format.
- `make ty` — type check.
- `make vulture` — scan for unused code.
- `make test` — run tests with the Firebase auth emulator.

## Adding Domains

For a new core domain with its own schema name, use `.cursor/skills/add-domain/SKILL.md`.
The initial scaffold creates settings, an empty Alembic tree, domain class shells, and bootstrap wiring; migration revisions and `ALEMBIC_DOMAINS` are added later with real DDL.

## TODO

- ETag
- py-spy
- Stale-while-revalidate
- Backpressure - uvicorn settings https://uvicorn.dev/settings/
- Backpressure - route level with semaphore
- https://github.com/rennf93/fastapi-guard
- https://github.com/TypeError/secure
- Outbox pattern
