Skip to content

Commit 69b8234

Browse files
olivermeyerclaude
andcommitted
refactor(api): derive metadata from context in api.core helpers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6340feb commit 69b8234

File tree

3 files changed

+48
-39
lines changed

3 files changed

+48
-39
lines changed

src/aignostics_foundry_core/AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,10 @@ This file provides an overview of all modules in `aignostics_foundry_core`, thei
135135
- `API_TAG_PUBLIC`, `API_TAG_AUTHENTICATED`, `API_TAG_ADMIN`, `API_TAG_INTERNAL`, `API_TAG_INTERNAL_ADMIN` — string constants for OpenAPI tagging
136136
- `create_public_router(module_tag, *, version, prefix, …)` — public (unauthenticated) router
137137
- `create_authenticated_router`, `create_admin_router`, `create_internal_router`, `create_internal_admin_router` — router factories that inject the appropriate `require_*` dependency from `api.auth`
138-
- `build_api_metadata(title, description, author_name, author_email, repository_url, documentation_url, version)` — returns a `dict` suitable for `FastAPI(**metadata)`
139-
- `build_versioned_api_tags(version_name, repository_url)` — OpenAPI tags for a single versioned sub-app
138+
- `build_api_metadata(version=None, *, context=None)` — returns a `dict` suitable for `FastAPI(**metadata)`; derives title, description, author, and URLs from *context* (falls back to global context)
139+
- `build_versioned_api_tags(version_name, *, context=None)` — OpenAPI tags for a single versioned sub-app; reads `repository_url` from *context*
140140
- `build_root_api_tags(base_url, versions)` — OpenAPI tags for the root app linking to each version's docs
141-
- `get_versioned_api_instances(versions, build_metadata=None, *, context=None)` — loads project modules (resolved via context), creates one `FastAPI` per version, routes registered `VersionedAPIRouter` instances to the matching version
141+
- `get_versioned_api_instances(versions, *, context=None)` — loads project modules (resolved via context), calls `build_api_metadata(context=ctx)` to configure each `FastAPI` instance, routes registered `VersionedAPIRouter` instances to the matching version
142142
- `init_api(root_path, lifespan, exception_handler_registrations, versions=None, version_exception_handler_registrations=None, **fastapi_kwargs)` — creates a `FastAPI` with the standard Foundry exception handlers (`ApiException`, `RequestValidationError`, `ValidationError`, `Exception`) pre-registered; when *versions* is supplied, calls `get_versioned_api_instances` internally, optionally applies *version_exception_handler_registrations* to each sub-app, and mounts them at `/{version}` on the root app
143143
- **Location**: `aignostics_foundry_core/api/core.py`
144144
- **Dependencies**: `fastapi>=0.110,<1` (mandatory); `aignostics_foundry_core.di` (`load_modules`)

src/aignostics_foundry_core/api/core.py

Lines changed: 26 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -262,58 +262,53 @@ def create_internal_admin_router(
262262
return cast("APIRouter", VersionedAPIRouter(version, prefix=actual_prefix, tags=tags, dependencies=dependencies))
263263

264264

265-
def build_api_metadata( # noqa: PLR0913, PLR0917
266-
title: str,
267-
description: str = "",
268-
author_name: str = "",
269-
author_email: str = "",
270-
repository_url: str = "",
271-
documentation_url: str = "",
272-
version: str | None = None,
273-
) -> dict[str, Any]:
265+
def build_api_metadata(version: str | None = None, *, context: FoundryContext | None = None) -> dict[str, Any]:
274266
"""Build a metadata dictionary suitable for passing to a FastAPI instance.
275267
268+
All fields (title, description, author, URLs) are derived from *context*.
269+
276270
Args:
277-
title: The API title (project name).
278-
description: Human-readable description of the API.
279-
author_name: Contact person or team name.
280-
author_email: Contact email address.
281-
repository_url: URL to the source repository.
282-
documentation_url: URL to the documentation or terms of service.
283271
version: Optional API version string.
272+
context: Project context supplying the title, description, author, and URLs.
273+
When ``None``, the global context installed via
274+
:func:`aignostics_foundry_core.foundry.set_context` is used.
284275
285276
Returns:
286277
Dictionary containing FastAPI metadata keys.
287278
"""
279+
ctx = context or get_context()
288280
metadata: dict[str, Any] = {
289-
"title": title,
290-
"description": description,
281+
"title": ctx.name,
282+
"description": ctx.metadata.description,
291283
"contact": {
292-
"name": author_name or "Unknown",
293-
"email": author_email,
294-
"url": repository_url,
284+
"name": ctx.metadata.author_name or "Unknown",
285+
"email": ctx.metadata.author_email or "",
286+
"url": ctx.metadata.repository_url,
295287
},
296-
"terms_of_service": documentation_url,
288+
"terms_of_service": ctx.metadata.documentation_url,
297289
"license_info": {
298290
"name": "Aignostics Commercial License",
299-
"url": f"{repository_url}/blob/main/LICENSE",
291+
"url": f"{ctx.metadata.repository_url}/blob/main/LICENSE",
300292
},
301293
}
302294
if version is not None:
303295
metadata["version"] = version
304296
return metadata
305297

306298

307-
def build_versioned_api_tags(version_name: str, repository_url: str = "") -> list[dict[str, Any]]:
299+
def build_versioned_api_tags(version_name: str, *, context: FoundryContext | None = None) -> list[dict[str, Any]]:
308300
"""Build ``openapi_tags`` for a versioned API instance.
309301
310302
Args:
311303
version_name: The version name (e.g., "v1").
312-
repository_url: URL to the source repository (used for external docs link).
304+
context: Project context supplying the repository URL for the external docs link.
305+
When ``None``, the global context installed via
306+
:func:`aignostics_foundry_core.foundry.set_context` is used.
313307
314308
Returns:
315309
List of OpenAPI tag dictionaries for the versioned API.
316310
"""
311+
repository_url = (context or get_context()).metadata.repository_url
317312
return [
318313
{
319314
"name": version_name,
@@ -351,7 +346,6 @@ def build_root_api_tags(base_url: str, versions: list[str]) -> list[dict[str, An
351346

352347
def get_versioned_api_instances(
353348
versions: list[str],
354-
build_metadata: dict[str, Any] | None = None,
355349
*,
356350
context: FoundryContext | None = None,
357351
) -> dict[str, FastAPI]:
@@ -364,18 +358,19 @@ def get_versioned_api_instances(
364358
365359
Args:
366360
versions: Ordered list of API version names (e.g., ``["v1", "v2"]``).
367-
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.
361+
context: Project context supplying the package name, title, description, author,
362+
and URLs for each ``FastAPI`` instance. When ``None``, the global context
363+
installed via :func:`aignostics_foundry_core.foundry.set_context` is used.
371364
372365
Returns:
373366
Mapping from version name to its configured ``FastAPI`` instance.
374367
"""
375368
from fastapi import FastAPI # noqa: PLC0415
376369

377-
load_modules(context=context or get_context())
378-
api_instances: dict[str, FastAPI] = {version: FastAPI(**(build_metadata or {})) for version in versions}
370+
ctx = context or get_context()
371+
load_modules(context=ctx)
372+
api_metadata = build_api_metadata(context=ctx)
373+
api_instances: dict[str, FastAPI] = {version: FastAPI(**api_metadata) for version in versions}
379374

380375
for router in VersionedAPIRouter.get_instances():
381376
router_version: str = cast("Any", router).version

tests/aignostics_foundry_core/api/core_test.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ def test_api_tag_constants() -> None:
5656

5757
@pytest.mark.unit
5858
def test_build_api_metadata_returns_dict_with_title() -> None:
59-
"""build_api_metadata returns a dict containing the title key."""
59+
"""build_api_metadata returns a dict containing the title derived from context."""
6060
from aignostics_foundry_core.api.core import build_api_metadata
6161

62-
result = build_api_metadata(title=TEST_TITLE, description="Test API", repository_url="https://example.com")
62+
result = build_api_metadata(context=make_context(name=TEST_TITLE))
6363

6464
assert isinstance(result, dict)
6565
assert result[TITLE_KEY] == TEST_TITLE
@@ -184,7 +184,7 @@ def test_build_api_metadata_includes_version_when_provided() -> None:
184184
"""build_api_metadata adds a 'version' key when version is supplied."""
185185
from aignostics_foundry_core.api.core import build_api_metadata
186186

187-
result = build_api_metadata(title=TEST_TITLE, version=TEST_VERSION_STR)
187+
result = build_api_metadata(version=TEST_VERSION_STR, context=make_context())
188188

189189
assert result["version"] == TEST_VERSION_STR
190190

@@ -194,13 +194,26 @@ def test_build_versioned_api_tags_returns_tag_for_version() -> None:
194194
"""build_versioned_api_tags returns a single-element list with the correct name."""
195195
from aignostics_foundry_core.api.core import build_versioned_api_tags
196196

197-
tags = build_versioned_api_tags("v2", repository_url=BASE_URL)
197+
tags = build_versioned_api_tags("v2", context=make_context(repository_url=BASE_URL))
198198

199199
assert len(tags) == 1
200200
assert tags[0]["name"] == "v2"
201201
assert BASE_URL in tags[0]["externalDocs"]["url"]
202202

203203

204+
@pytest.mark.unit
205+
def test_build_api_metadata_contact_uses_context_author() -> None:
206+
"""build_api_metadata populates contact from context's PackageMetadata author fields."""
207+
from aignostics_foundry_core.api.core import build_api_metadata
208+
from aignostics_foundry_core.foundry import PackageMetadata
209+
210+
ctx = make_context(metadata=PackageMetadata(author_name="Ada", author_email="ada@example.com"))
211+
result = build_api_metadata(context=ctx)
212+
213+
assert result["contact"]["name"] == "Ada"
214+
assert result["contact"]["email"] == "ada@example.com"
215+
216+
204217
@pytest.mark.unit
205218
def test_build_root_api_tags_one_entry_per_version() -> None:
206219
"""build_root_api_tags returns one tag dict per version with correct name and URL."""
@@ -217,7 +230,7 @@ def test_build_root_api_tags_one_entry_per_version() -> None:
217230

218231
@pytest.mark.unit
219232
def test_get_versioned_api_instances_returns_fastapi_per_version() -> None:
220-
"""get_versioned_api_instances returns a FastAPI instance for each requested version."""
233+
"""get_versioned_api_instances returns a FastAPI instance with the context name as title."""
221234
from fastapi import FastAPI
222235

223236
from aignostics_foundry_core.api.core import VersionedAPIRouter, get_versioned_api_instances
@@ -227,6 +240,7 @@ def test_get_versioned_api_instances_returns_fastapi_per_version() -> None:
227240

228241
assert VERSION_GVI in result
229242
assert isinstance(result[VERSION_GVI], FastAPI)
243+
assert result[VERSION_GVI].title == "aignostics_foundry_core"
230244

231245

232246
@pytest.mark.unit

0 commit comments

Comments
 (0)