Skip to content

Commit 6f1bac4

Browse files
olivermeyerclaude
andcommitted
feat(di): drop project_name, use context only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent cfb5edb commit 6f1bac4

10 files changed

Lines changed: 239 additions & 109 deletions

File tree

src/aignostics_foundry_core/api/core.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@
66
- API initialization and metadata building
77
"""
88

9+
from __future__ import annotations
10+
911
from typing import TYPE_CHECKING, Any, ClassVar, Self, cast
1012

1113
from aignostics_foundry_core.di import load_modules
14+
from aignostics_foundry_core.foundry import get_context
15+
16+
if TYPE_CHECKING:
17+
from aignostics_foundry_core.foundry import FoundryContext
1218

1319
from .exceptions import (
1420
AccessDeniedException,
@@ -50,10 +56,10 @@ class VersionedAPIRouter:
5056
"""
5157

5258
# Class variable to track all created instances
53-
_instances: ClassVar[list["VersionedAPIRouter"]] = []
59+
_instances: ClassVar[list[VersionedAPIRouter]] = []
5460

5561
@classmethod
56-
def get_instances(cls) -> list["VersionedAPIRouter"]:
62+
def get_instances(cls) -> list[VersionedAPIRouter]:
5763
"""Get all created router instances.
5864
5965
Returns:
@@ -117,7 +123,7 @@ def create_public_router(
117123
prefix: str | None = None,
118124
extra_tags: list[str] | None = None,
119125
extra_dependencies: list[Any] | None = None,
120-
) -> "APIRouter":
126+
) -> APIRouter:
121127
"""Create a public API router (no authentication required).
122128
123129
Args:
@@ -143,7 +149,7 @@ def create_authenticated_router(
143149
prefix: str | None = None,
144150
extra_tags: list[str] | None = None,
145151
extra_dependencies: list[Any] | None = None,
146-
) -> "APIRouter":
152+
) -> APIRouter:
147153
"""Create an authenticated API router (requires valid Auth0 session).
148154
149155
Args:
@@ -173,7 +179,7 @@ def create_admin_router(
173179
prefix: str | None = None,
174180
extra_tags: list[str] | None = None,
175181
extra_dependencies: list[Any] | None = None,
176-
) -> "APIRouter":
182+
) -> APIRouter:
177183
"""Create an admin API router (requires admin role).
178184
179185
Args:
@@ -203,7 +209,7 @@ def create_internal_router(
203209
prefix: str | None = None,
204210
extra_tags: list[str] | None = None,
205211
extra_dependencies: list[Any] | None = None,
206-
) -> "APIRouter":
212+
) -> APIRouter:
207213
"""Create an internal API router (requires internal org membership).
208214
209215
Args:
@@ -233,7 +239,7 @@ def create_internal_admin_router(
233239
prefix: str | None = None,
234240
extra_tags: list[str] | None = None,
235241
extra_dependencies: list[Any] | None = None,
236-
) -> "APIRouter":
242+
) -> APIRouter:
237243
"""Create an internal admin API router (requires internal org + admin role).
238244
239245
Args:
@@ -344,28 +350,31 @@ def build_root_api_tags(base_url: str, versions: list[str]) -> list[dict[str, An
344350

345351

346352
def get_versioned_api_instances(
347-
project_name: str,
348353
versions: list[str],
349354
build_metadata: dict[str, Any] | None = None,
350-
) -> "dict[str, FastAPI]":
355+
*,
356+
context: FoundryContext | None = None,
357+
) -> dict[str, FastAPI]:
351358
"""Build per-version FastAPI instances and route registered routers to them.
352359
353-
Loads all modules in *project_name* so that ``VersionedAPIRouter`` instances
354-
created at module import time are registered. Each router whose ``version``
355-
attribute matches a name in *versions* is included in the corresponding
360+
Loads all modules in the configured project package so that ``VersionedAPIRouter``
361+
instances created at module import time are registered. Each router whose
362+
``version`` attribute matches a name in *versions* is included in the corresponding
356363
FastAPI sub-application.
357364
358365
Args:
359-
project_name: Package name whose modules should be loaded (to trigger router registration).
360366
versions: Ordered list of API version names (e.g., ``["v1", "v2"]``).
361367
build_metadata: Optional extra kwargs forwarded to each ``FastAPI()`` constructor.
368+
context: Project context supplying the package name. When ``None``,
369+
the global context installed via
370+
:func:`aignostics_foundry_core.foundry.set_context` is used.
362371
363372
Returns:
364373
Mapping from version name to its configured ``FastAPI`` instance.
365374
"""
366375
from fastapi import FastAPI # noqa: PLC0415
367376

368-
load_modules(project_name)
377+
load_modules(context=context or get_context())
369378
api_instances: dict[str, FastAPI] = {version: FastAPI(**(build_metadata or {})) for version in versions}
370379

371380
for router in VersionedAPIRouter.get_instances():
@@ -386,7 +395,7 @@ def init_api(
386395
lifespan: Any | None = None, # noqa: ANN401
387396
exception_handler_registrations: list[tuple[type[Exception], Any]] | None = None,
388397
**fastapi_kwargs: Any, # noqa: ANN401
389-
) -> "FastAPI":
398+
) -> FastAPI:
390399
"""Initialise a FastAPI application with standard exception handlers.
391400
392401
This is a generic factory that creates a ``FastAPI`` instance and registers

src/aignostics_foundry_core/cli.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,31 @@
11
"""Command-line interface (CLI) utilities."""
22

3+
from __future__ import annotations
4+
35
import sys
46
from pathlib import Path
7+
from typing import TYPE_CHECKING
58

69
import typer
710

811
from aignostics_foundry_core.di import locate_implementations
12+
from aignostics_foundry_core.foundry import get_context
13+
14+
if TYPE_CHECKING:
15+
from aignostics_foundry_core.foundry import FoundryContext
916

1017

11-
def prepare_cli(cli: typer.Typer, epilog: str, project_name: str) -> None:
18+
def prepare_cli(cli: typer.Typer, epilog: str, *, context: FoundryContext | None = None) -> None:
1219
"""Dynamically locate, register and prepare subcommands.
1320
1421
Args:
1522
cli (typer.Typer): Typer instance
1623
epilog (str): Epilog to add
17-
project_name (str): Project name used for subcommand discovery
24+
context: Project context used for subcommand discovery. When ``None``,
25+
the global context installed via
26+
:func:`aignostics_foundry_core.foundry.set_context` is used.
1827
"""
19-
for sub_cli in locate_implementations(typer.Typer, project_name):
28+
for sub_cli in locate_implementations(typer.Typer, context=context or get_context()):
2029
if sub_cli != cli:
2130
cli.add_typer(sub_cli)
2231

src/aignostics_foundry_core/di.py

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
"""Dependency injection using dynamic import and discovery of implementations and subclasses."""
22

3+
from __future__ import annotations
4+
35
import importlib
46
import pkgutil
5-
from collections.abc import Callable
67
from functools import lru_cache
78
from importlib.metadata import entry_points
89
from inspect import isclass
9-
from typing import Any
10+
from typing import TYPE_CHECKING, Any
11+
12+
from aignostics_foundry_core.foundry import get_context
13+
14+
if TYPE_CHECKING:
15+
from collections.abc import Callable
16+
17+
from aignostics_foundry_core.foundry import FoundryContext
1018

1119
_implementation_cache: dict[tuple[Any, str], list[Any]] = {}
1220
_subclass_cache: dict[tuple[Any, str], list[Any]] = {}
@@ -32,15 +40,19 @@ def discover_plugin_packages() -> tuple[str, ...]:
3240
return tuple(ep.value for ep in eps)
3341

3442

35-
def load_modules(project_name: str) -> None:
36-
"""Import all top-level submodules of the given project package.
43+
def load_modules(*, context: FoundryContext | None = None) -> None:
44+
"""Import all top-level submodules of the configured project package.
3745
3846
Args:
39-
project_name: The importable package name to scan (e.g. ``"bridge"``).
47+
context: Project context supplying the package name. When ``None``,
48+
the global context installed via :func:`aignostics_foundry_core.foundry.set_context`
49+
is used; :func:`~aignostics_foundry_core.foundry.get_context` raises
50+
``RuntimeError`` if no context has been configured.
4051
"""
41-
package = importlib.import_module(project_name)
52+
ctx = context or get_context()
53+
package = importlib.import_module(ctx.name)
4254
for _, name, _ in pkgutil.iter_modules(package.__path__):
43-
importlib.import_module(f"{project_name}.{name}")
55+
importlib.import_module(f"{ctx.name}.{name}")
4456

4557

4658
def _scan_packages_deep(
@@ -117,27 +129,29 @@ def _scan_packages_shallow(
117129
return results
118130

119131

120-
def locate_implementations(_class: type[Any], project_name: str) -> list[Any]:
132+
def locate_implementations(_class: type[Any], *, context: FoundryContext | None = None) -> list[Any]:
121133
"""Dynamically discover all instances of some class.
122134
123135
Searches plugin top-level exports first (shallow scan), then deep-scans all
124136
submodules of the main project package. Plugins are registered via entry
125137
points; only their top-level ``__init__.py`` exports are examined (submodules
126138
are not walked). The main package retains full deep-scan behaviour.
127139
128-
Cache keys include *project_name* to avoid cross-project cache pollution when
129-
multiple projects share this library.
140+
Cache keys include the context name to avoid cross-project cache pollution
141+
when multiple projects share this library.
130142
131143
Args:
132144
_class: Class to search for.
133-
project_name: Importable package name of the calling project
134-
(e.g. ``"bridge"``). Used as the deep-scan root and as part of the
135-
cache key.
145+
context: Project context supplying the package name. When ``None``,
146+
the global context installed via :func:`aignostics_foundry_core.foundry.set_context`
147+
is used; :func:`~aignostics_foundry_core.foundry.get_context` raises
148+
``RuntimeError`` if no context has been configured.
136149
137150
Returns:
138151
List of discovered instances of the given class.
139152
"""
140-
cache_key = (_class, project_name)
153+
ctx = context or get_context()
154+
cache_key = (_class, ctx.name)
141155
if cache_key in _implementation_cache:
142156
return _implementation_cache[cache_key]
143157

@@ -146,33 +160,35 @@ def predicate(member: object) -> bool:
146160

147161
results = [
148162
*_scan_packages_shallow(discover_plugin_packages(), predicate),
149-
*_scan_packages_deep(project_name, predicate),
163+
*_scan_packages_deep(ctx.name, predicate),
150164
]
151165
_implementation_cache[cache_key] = results
152166
return results
153167

154168

155-
def locate_subclasses(_class: type[Any], project_name: str) -> list[Any]:
169+
def locate_subclasses(_class: type[Any], *, context: FoundryContext | None = None) -> list[Any]:
156170
"""Dynamically discover all classes that are subclasses of some type.
157171
158172
Searches plugin top-level exports first (shallow scan), then deep-scans all
159173
submodules of the main project package. Plugins are registered via entry
160174
points; only their top-level ``__init__.py`` exports are examined (submodules
161175
are not walked). The main package retains full deep-scan behaviour.
162176
163-
Cache keys include *project_name* to avoid cross-project cache pollution when
164-
multiple projects share this library.
177+
Cache keys include the context name to avoid cross-project cache pollution
178+
when multiple projects share this library.
165179
166180
Args:
167181
_class: Parent class of subclasses to search for.
168-
project_name: Importable package name of the calling project
169-
(e.g. ``"bridge"``). Used as the deep-scan root and as part of the
170-
cache key.
182+
context: Project context supplying the package name. When ``None``,
183+
the global context installed via :func:`aignostics_foundry_core.foundry.set_context`
184+
is used; :func:`~aignostics_foundry_core.foundry.get_context` raises
185+
``RuntimeError`` if no context has been configured.
171186
172187
Returns:
173188
List of discovered subclasses of the given class.
174189
"""
175-
cache_key = (_class, project_name)
190+
ctx = context or get_context()
191+
cache_key = (_class, ctx.name)
176192
if cache_key in _subclass_cache:
177193
return _subclass_cache[cache_key]
178194

@@ -181,7 +197,7 @@ def predicate(member: object) -> bool:
181197

182198
results = [
183199
*_scan_packages_shallow(discover_plugin_packages(), predicate),
184-
*_scan_packages_deep(project_name, predicate),
200+
*_scan_packages_deep(ctx.name, predicate),
185201
]
186202
_subclass_cache[cache_key] = results
187203
return results

src/aignostics_foundry_core/gui/core.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,21 @@
77
- Constants: WINDOW_SIZE, BROWSER_RECONNECT_TIMEOUT, RESPONSE_TIMEOUT
88
"""
99

10-
from abc import ABC, abstractmethod
11-
from collections.abc import Callable
12-
from typing import Any
10+
from __future__ import annotations
1311

14-
from fastapi import FastAPI
15-
from fastapi.routing import APIRouter
12+
from abc import ABC, abstractmethod
13+
from typing import TYPE_CHECKING, Any
1614

1715
from aignostics_foundry_core.di import locate_subclasses
16+
from aignostics_foundry_core.foundry import get_context
17+
18+
if TYPE_CHECKING:
19+
from collections.abc import Callable
20+
21+
from fastapi import FastAPI
22+
from fastapi.routing import APIRouter
23+
24+
from aignostics_foundry_core.foundry import FoundryContext
1825

1926
WINDOW_SIZE = (1280, 768)
2027
BROWSER_RECONNECT_TIMEOUT = 60 * 60 * 24 * 7 # 7 days
@@ -42,16 +49,18 @@ def register_pages() -> None:
4249
"""Register NiceGUI pages."""
4350

4451

45-
def gui_register_pages(project_name: str) -> None:
52+
def gui_register_pages(*, context: FoundryContext | None = None) -> None:
4653
"""Register pages from all discovered PageBuilders.
4754
48-
Discovers all ``BasePageBuilder`` subclasses for the given project and calls
49-
``register_pages()`` on each one.
55+
Discovers all ``BasePageBuilder`` subclasses for the configured project and
56+
calls ``register_pages()`` on each one.
5057
5158
Args:
52-
project_name: Project name passed to locate_subclasses for discovery.
59+
context: Project context used for PageBuilder discovery. When ``None``,
60+
the global context installed via
61+
:func:`aignostics_foundry_core.foundry.set_context` is used.
5362
"""
54-
page_builders = locate_subclasses(BasePageBuilder, project_name)
63+
page_builders = locate_subclasses(BasePageBuilder, context=context or get_context())
5564
for page_builder in page_builders:
5665
page_builder: BasePageBuilder # type: ignore[no-redef]
5766
page_builder.register_pages()
@@ -103,7 +112,6 @@ def redirect_to_api_docs() -> RedirectResponse: # pyright: ignore[reportUnusedF
103112

104113

105114
def gui_run( # noqa: PLR0913, PLR0917
106-
project_name: str,
107115
show: bool = False,
108116
host: str | None = None,
109117
port: int | None = None,
@@ -113,15 +121,17 @@ def gui_run( # noqa: PLR0913, PLR0917
113121
auth_router: APIRouter | None = None,
114122
startup_callbacks: list[Callable[[], Any]] | None = None,
115123
shutdown_callbacks: list[Callable[[], Any]] | None = None,
124+
*,
125+
context: FoundryContext | None = None,
116126
) -> None:
117127
"""Start the NiceGUI application.
118128
119129
Args:
120-
project_name: Project name for page builder discovery.
121130
show: Whether to open a browser window on startup.
122131
host: Host to bind to. Defaults to NiceGUI's default.
123132
port: Port to listen on. Defaults to an open port found automatically.
124-
title: Title shown in the browser tab. Defaults to ``project_name``.
133+
title: Title shown in the browser tab. Defaults to the project name
134+
from *context*.
125135
watch: Whether to reload on source file changes.
126136
fastapi_app: Optional FastAPI application to mount at ``/api``. When
127137
provided, ``/docs`` is redirected to ``/api/docs``, and the
@@ -133,18 +143,23 @@ def gui_run( # noqa: PLR0913, PLR0917
133143
``app.on_startup``. Use this to initialise a database engine etc.
134144
shutdown_callbacks: Optional list of callables registered via
135145
``app.on_shutdown``. Use this to dispose resources on shutdown.
146+
context: Project context used for page builder discovery and window
147+
title. When ``None``, the global context installed via
148+
:func:`aignostics_foundry_core.foundry.set_context` is used.
136149
"""
137150
from nicegui import app, ui # noqa: PLC0415
138151
from nicegui import native as native_app # noqa: PLC0415
139152

153+
ctx = context or get_context()
154+
140155
_register_callbacks(app, startup_callbacks, shutdown_callbacks)
141-
gui_register_pages(project_name)
156+
gui_register_pages(context=ctx)
142157

143158
if fastapi_app is not None:
144159
_mount_fastapi_app(app, fastapi_app, auth_router)
145160

146161
ui.run( # pyright: ignore[reportUnknownMemberType]
147-
title=title or project_name,
162+
title=title or ctx.name,
148163
native=False,
149164
reload=watch,
150165
host=host,

0 commit comments

Comments
 (0)