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
61 changes: 55 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@
[![Pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/)
[![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-orange.json)](https://github.com/aignostics/foundry-python)

> [!NOTE]
> This is your project README - please feel free to update as you see fit.
> For first steps after scaffolding, check out [FOUNDRY_README.md](FOUNDRY_README.md).

---

Foundational infrastructure for Foundry components.

## Prerequisites
Expand All @@ -34,6 +28,61 @@ Or follow the [installation guide](https://mise.jdx.dev/getting-started.html) fo

## Usage

### Initialise the context at application startup

Call `set_context()` once, before any library code runs, then call `boot()` to
initialise logging, the SSL trust chain, and optional Sentry integration:

```python
# main.py
from aignostics_foundry_core.foundry import FoundryContext, set_context
from aignostics_foundry_core.boot import boot

set_context(FoundryContext.from_package("myproject"))
boot()
```

`FoundryContext.from_package()` derives everything from package metadata and
environment variables:

- `name`, `version`, `version_full` — from `importlib.metadata`
- `environment` — from `MYPROJECT_ENVIRONMENT` → `ENV` → `VERCEL_ENV` →
`RAILWAY_ENVIRONMENT` → `"local"`
- `is_container`, `is_cli`, `is_test`, `is_library` — detected automatically
- `env_prefix` (`"MYPROJECT_"`) — used by every settings class in the library

### Access the context from any module

```python
from aignostics_foundry_core.foundry import get_context

ctx = get_context()
print(f"Running {ctx.name} v{ctx.version_full} in {ctx.environment}")
# → Running myproject v1.2.3+main---run.12345 in staging
```

`get_context()` raises `RuntimeError` with a clear message if `set_context()`
was never called.

### Pass context explicitly in tests

Never call `set_context()` in tests. Pass a `FoundryContext` directly to each
function via its optional `context` parameter instead:

```python
from aignostics_foundry_core.foundry import FoundryContext
from aignostics_foundry_core.log import logging_initialize

ctx = FoundryContext(name="myproject", version="0.0.0", version_full="0.0.0", environment="test")
logging_initialize(context=ctx)
```

All public library functions (`logging_initialize`, `sentry_initialize`, `boot`,
`load_modules`, etc.) accept an optional `context` keyword argument and fall
back to `get_context()` when it is `None`.

### Health API

```python
from aignostics_foundry_core.health import Health, HealthStatus

Expand Down
21 changes: 7 additions & 14 deletions docs/decisions/0003-project-context-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,22 +118,12 @@ The central type is named `FoundryContext` (not `ProjectConfig` or `ProjectConte

- "Config" was rejected because it implies values loaded from env vars or files; this object is derived at startup from `importlib.metadata`, `sys.argv`, and env vars — it is computed context, not configuration input. The existing `SentrySettings` type already uses the "settings/config" pattern for env-based values.
- "Project" prefix was considered but doesn't communicate which library owns the type. Since `FoundryContext` is specifically the library's handle on a project, naming it after the library makes the dependency explicit and aids discoverability.
- The name is consistent with `SentryContext` (also runtime-computed, also nested within the same design).

### Structure

`FoundryContext` is a frozen Pydantic model, making all instances immutable after construction. Runtime mode flags (`is_container`, `is_cli`, `is_test`, `is_library`) are only consumed by `sentry_initialize()`, so they live in a nested `SentryContext` rather than on `FoundryContext` directly:
`FoundryContext` is a frozen Pydantic model, making all instances immutable after construction. Runtime mode flags (`is_container`, `is_cli`, `is_test`, `is_library`) live directly on `FoundryContext`. They describe process runtime mode — not Sentry internals — and are consumed by `boot()`, `sentry_initialize()`, and any other code that needs to know how the process is running. `SentrySettings` (env-based SDK configuration) remains separate.

```python
class SentryContext(BaseModel):
model_config = ConfigDict(frozen=True)

is_container: bool
is_cli: bool
is_test: bool
is_library: bool


class FoundryContext(BaseModel):
model_config = ConfigDict(frozen=True)

Expand All @@ -144,7 +134,10 @@ class FoundryContext(BaseModel):
env_file: list[Path]
repository_url: str = ""
documentation_url: str = ""
sentry: SentryContext = Field(default_factory=SentryContext)
is_container: bool = False
is_cli: bool = False
is_test: bool = False
is_library: bool = False
```

Each project calls `set_context()` once at startup. This single line replaces `_constants.py` entirely:
Expand Down Expand Up @@ -177,7 +170,7 @@ def locate_subclasses(_class: type, context: FoundryContext | None = None) -> li
...
```

`SentryContext` is kept separate from `SentrySettings` (which holds SDK configuration loaded from env vars). `SentryContext` is runtime-computed; `SentrySettings` is env-based.
The four runtime mode flags are kept separate from `SentrySettings` (which holds SDK configuration loaded from env vars). The flags are runtime-computed; `SentrySettings` is env-based.

### Extending FoundryContext

Expand Down Expand Up @@ -225,5 +218,5 @@ This avoids module-level generics (which are awkward in Python) while keeping bo
- New projects (API servers and CLI tools alike) require a single `set_context()` call and no boilerplate.
- Production call sites are clean — no context threading.
- Tests can pass a `FoundryContext` directly without touching or resetting global state.
- `SentryContext` nesting makes it clear that the mode flags are Sentry-specific and not general-purpose project metadata.
- Runtime mode flags live directly on `FoundryContext`, making them accessible to any code that cares about how the process is running — not just Sentry. `boot()` uses `context.is_library` directly without a separate parameter.
- Projects that need additional fields subclass `FoundryContext` and pass their subclass to `set_context()`; they hold their own typed reference for project-specific access.
Loading
Loading