Skip to content

Commit cfb5edb

Browse files
olivermeyerclaude
andcommitted
feat(foundry): add FoundryContext and set_context()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 621dee0 commit cfb5edb

5 files changed

Lines changed: 563 additions & 19 deletions

File tree

ATTRIBUTIONS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ SOFTWARE.
360360

361361
```
362362

363-
## aignostics-foundry-core (0.1.0) - MIT License
363+
## aignostics-foundry-core (0.2.0) - MIT License
364364

365365
🏭 Foundational infrastructure for Foundry components.
366366

docs/decisions/0003-project-context-injection.md

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,11 @@ project_name = os.getenv("FOUNDRY_CORE_PROJECT_NAME")
5858
A one-time call at startup sets global library state; all functions then read from it. This is similar to configuration/init pattern used by logging libraries and the Sentry SDK.
5959

6060
```python
61-
foundry.set_context(project_name="bridge", version=__version__, ...)
61+
set_context(project_name="bridge", version=__version__, ...)
6262
locate_subclasses(BaseService) # reads from global state
6363

6464
def locate_subclasses(_class):
65-
project_name = foundry.context.name
65+
project_name = get_context().name
6666
...
6767
```
6868

@@ -83,23 +83,23 @@ locate_subclasses(BaseService, context=ctx)
8383
* Pros: requirements #1 and #2 satisfied; typed; derivation logic lives once in the library
8484
* Cons: requirement #3 only partially satisfied — projects must hold and thread their own `context` reference to read derived values, which doesn't fully eliminate `_constants.py`
8585

86-
**#5 `FoundryContext.from_package()` + `set_context()` + `foundry.context` accessor (combination of #3 and #4)**
86+
**#5 `FoundryContext.from_package()` + `set_context()` + `get_context()` (combination of #3 and #4)**
8787

88-
Extends #4 with a `set_context()` call that stores the context as library-level state, exposed back to callers via `foundry.context`. Library functions fall back to the configured default but accept an explicit `context` override for testing.
88+
Extends #4 with a `set_context()` call that stores the context as library-level state, retrieved via `get_context()`. Library functions fall back to the configured default but accept an explicit `context` override for testing.
8989

9090
```python
9191
# at startup — replaces _constants.py entirely
92-
foundry.set_context(FoundryContext.from_package("bridge"))
92+
set_context(FoundryContext.from_package("bridge"))
9393

9494
# library functions use the configured default
9595
locate_subclasses(BaseService)
9696

9797
def locate_subclasses(_class: type, context: FoundryContext | None = None) -> list:
98-
context = context or foundry.context
98+
context = context or get_context()
9999
...
100100

101101
# projects read derived values back from the library
102-
print(foundry.context.version_full)
102+
print(get_context().version_full)
103103

104104
# in tests — explicit override, no global state touched
105105
locate_subclasses(BaseService, context=FoundryContext(name="test-project", ...))
@@ -150,23 +150,30 @@ class FoundryContext(BaseModel):
150150
Each project calls `set_context()` once at startup. This single line replaces `_constants.py` entirely:
151151

152152
```python
153-
foundry.set_context(FoundryContext.from_package("bridge"))
153+
from aignostics_foundry_core.foundry import FoundryContext, set_context
154+
155+
set_context(FoundryContext.from_package("bridge"))
154156
```
155157

156-
The configured `FoundryContext` is accessible anywhere via `foundry.context`:
158+
The configured `FoundryContext` is accessible anywhere via `get_context()`:
157159

158160
```python
161+
from aignostics_foundry_core.foundry import get_context
162+
159163
# before: from bridge.utils._constants import __version_full__, __project_name__
160164
# after:
161-
foundry.context.version_full
162-
foundry.context.name
165+
get_context().version_full
166+
get_context().name
163167
```
164168

165-
All public library functions fall back to `foundry.context` but accept an explicit override:
169+
All public library functions fall back to `get_context()` but accept an explicit override:
166170

167171
```python
172+
from aignostics_foundry_core.foundry import get_context
173+
174+
168175
def locate_subclasses(_class: type, context: FoundryContext | None = None) -> list:
169-
context = context or foundry.context
176+
context = context or get_context()
170177
...
171178
```
172179

@@ -197,13 +204,15 @@ At startup the subclass instance is passed to `set_context()` as usual:
197204
foundry.set_context(BridgeContext.from_package("bridge"))
198205
```
199206

200-
`foundry.context` is typed as `FoundryContext` — sufficient for all library functions. Project code that needs access to the extended fields keeps its own reference to the concrete instance:
207+
`get_context()` returns `FoundryContext` — sufficient for all library functions. Project code that needs access to the extended fields keeps its own reference to the concrete instance:
201208

202209
```python
210+
from aignostics_foundry_core.foundry import set_context
211+
203212
bridge_context = BridgeContext.from_package("bridge")
204-
foundry.set_context(bridge_context)
213+
set_context(bridge_context)
205214

206-
# library uses foundry.context (FoundryContext) — no project-specific fields needed
215+
# library uses get_context() → FoundryContext — no project-specific fields needed
207216
# project code uses bridge_context directly for its own extended fields
208217
bridge_context.tenant_id
209218
```
@@ -212,9 +221,9 @@ This avoids module-level generics (which are awkward in Python) while keeping bo
212221

213222
## Consequences
214223

215-
- `_constants.py` is eliminated entirely across all projects; derivation logic lives once in the library and derived values are read back via `foundry.context`.
224+
- `_constants.py` is eliminated entirely across all projects; derivation logic lives once in the library and derived values are read back via `get_context()`.
216225
- New projects (API servers and CLI tools alike) require a single `set_context()` call and no boilerplate.
217226
- Production call sites are clean — no context threading.
218227
- Tests can pass a `FoundryContext` directly without touching or resetting global state.
219228
- `SentryContext` nesting makes it clear that the mode flags are Sentry-specific and not general-purpose project metadata.
220-
- Projects that need additional fields subclass `FoundryContext` and pass their subclass to `configure()`; they hold their own typed reference for project-specific access.
229+
- Projects that need additional fields subclass `FoundryContext` and pass their subclass to `set_context()`; they hold their own typed reference for project-specific access.

src/aignostics_foundry_core/AGENTS.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ This file provides an overview of all modules in `aignostics_foundry_core`, thei
2323
| **user_agent** | Parameterised HTTP user-agent string builder | `user_agent(project_name, version, repository_url)` — builds `{project_name}-python-sdk/{version} (…)` string including platform info, current test, and GitHub Actions run URL |
2424
| **gui** | NiceGUI page helpers, auth decorators, and nav builder | `GUINamespace` (configurable page decorator namespace), `gui` (default singleton), `page_public/authenticated/admin/internal/internal_admin` decorators, `get_gui_user`, `require_gui_user`, `BaseNavBuilder`, `NavItem`, `NavGroup`, `gui_get_nav_groups`, `BasePageBuilder`, `gui_register_pages`, `gui_run`; constants `WINDOW_SIZE`, `BROWSER_RECONNECT_TIMEOUT`, `RESPONSE_TIMEOUT` |
2525
| **console** | Themed terminal output | Module-level `console` object (Rich `Console`) with colour theme and `_get_console()` factory |
26+
| **foundry** | Project context injection | `FoundryContext`, `SentryContext`, `FoundryContext.from_package()`, `set_context()`, `get_context()` — centralised project-specific values (name, version, environment, env files, URLs, Sentry flags) derived from package metadata and environment variables |
2627
| **di** | Dependency injection | `locate_subclasses`, `locate_implementations`, `load_modules`, `discover_plugin_packages`, `clear_caches`, `PLUGIN_ENTRY_POINT_GROUP` for plugin and subclass discovery |
2728
| **health** | Service health checks | `Health` model and `HealthStatus` enum for tree-structured health status |
2829
| **settings** | Pydantic settings loading | `OpaqueSettings`, `load_settings`, `strip_to_none_before_validator`, `UNHIDE_SENSITIVE_INFO` for env-based settings with secret masking and user-friendly validation errors |
@@ -31,6 +32,49 @@ This file provides an overview of all modules in `aignostics_foundry_core`, thei
3132

3233
<!-- For each module, document its purpose, features, dependencies, and usage. -->
3334

35+
### foundry
36+
37+
**Project context injection — single startup call replaces all per-project `_constants.py` files**
38+
39+
- **Purpose**: Provides `FoundryContext` — a frozen Pydantic model that owns all derivation logic for
40+
project-specific values. One `set_context(FoundryContext.from_package("myproject"))` call at
41+
application startup makes the context available everywhere in the library without threading values
42+
through call sites. Tests pass an explicit context override and never touch global state.
43+
- **Key Features**:
44+
- `SentryContext(BaseModel)` — frozen; four bool flags (`is_container`, `is_cli`, `is_test`,
45+
`is_library`) all defaulting to `False`.
46+
- `FoundryContext(BaseModel)` — frozen; fields: `name`, `version`, `version_full`, `environment`,
47+
`env_file: list[Path]`, `repository_url`, `documentation_url`, `sentry: SentryContext`.
48+
- `FoundryContext.from_package(package_name)` — classmethod that derives all values from
49+
`importlib.metadata` and environment variables (`{NAME}_ENVIRONMENT`, `VCS_REF`, `COMMIT_SHA`,
50+
`BUILDER`, `BUILD_DATE`, `CI_RUN_ID`, `CI_RUN_NUMBER`, `{NAME}_ENV_FILE`,
51+
`{NAME}_RUNNING_IN_CONTAINER`, `PYTEST_RUNNING_{NAME}`). Environment fallback chain:
52+
`{NAME}_ENVIRONMENT``ENV``VERCEL_ENV``RAILWAY_ENVIRONMENT``"local"`.
53+
- `set_context(ctx)` — installs *ctx* as the process-level singleton.
54+
- `get_context()` — returns the installed context or raises `RuntimeError` with a helpful message
55+
if `set_context()` has not been called.
56+
- **Location**: `aignostics_foundry_core/foundry.py`
57+
- **Dependencies**: `pydantic>=2`, Python stdlib (`importlib.metadata`, `os`, `sys`, `pathlib`)
58+
- **Import**:
59+
```python
60+
from aignostics_foundry_core.foundry import FoundryContext, SentryContext, set_context, get_context
61+
```
62+
- **Usage example**:
63+
```python
64+
# Application startup (e.g. main.py or boot.py):
65+
from aignostics_foundry_core.foundry import FoundryContext, set_context, get_context
66+
67+
set_context(FoundryContext.from_package("myproject"))
68+
69+
# Library code — no threading of values through parameters:
70+
ctx = get_context() # raises RuntimeError if startup omitted set_context()
71+
logger.info(f"Starting {ctx.name} {ctx.version} in {ctx.environment}")
72+
73+
# Tests — pass context explicitly, do not call set_context():
74+
ctx = FoundryContext(name="test", version="0.0.0", version_full="0.0.0", environment="test")
75+
result = my_library_function(context=ctx)
76+
```
77+
3478
### api.exceptions
3579

3680
**API exception hierarchy and FastAPI exception handlers**

0 commit comments

Comments
 (0)