diff --git a/ATTRIBUTIONS.md b/ATTRIBUTIONS.md index aa99fbd..3d3cf30 100644 --- a/ATTRIBUTIONS.md +++ b/ATTRIBUTIONS.md @@ -360,7 +360,7 @@ SOFTWARE. ``` -## aignostics-foundry-core (0.7.0) - MIT License +## aignostics-foundry-core (0.7.1) - MIT License 🏭 Foundational infrastructure for Foundry components. diff --git a/src/aignostics_foundry_core/AGENTS.md b/src/aignostics_foundry_core/AGENTS.md index 8e6e2ec..39cfe73 100644 --- a/src/aignostics_foundry_core/AGENTS.md +++ b/src/aignostics_foundry_core/AGENTS.md @@ -23,7 +23,7 @@ This file provides an overview of all modules in `aignostics_foundry_core`, thei | **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 | | **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(*, context=None)`, `BasePageBuilder`, `gui_register_pages(*, context=None)`, `gui_run(*, context=None, …)`; constants `WINDOW_SIZE`, `BROWSER_RECONNECT_TIMEOUT`, `RESPONSE_TIMEOUT` | | **console** | Themed terminal output | Module-level `console` object (Rich `Console`) with colour theme and `_get_console()` factory | -| **foundry** | Project context injection | `FoundryContext`, `FoundryContext.from_package()`, `set_context()`, `get_context()` — centralised project-specific values (name, version, `version_full`, `version_with_vcs_ref`, environment, env files, URLs, `python_version`, runtime mode flags `is_container`, `is_cli`, `is_test`, `is_library`, `database: DatabaseSettings \| None`) derived from package metadata and environment variables; `from_package()` populates `database` from `{env_prefix}DB_*` env vars when `{env_prefix}DB_URL` is present | +| **foundry** | Project context injection | `PackageMetadata`, `FoundryContext`, `FoundryContext.from_package()`, `set_context()`, `get_context()` — centralised project-specific values (name, version, `version_full`, `version_with_vcs_ref`, environment, env files, URLs, `python_version`, `metadata: PackageMetadata` (description, author_name, author_email), runtime mode flags `is_container`, `is_cli`, `is_test`, `is_library`, `database: DatabaseSettings \| None`) derived from package metadata and environment variables; `from_package()` populates `metadata` from `importlib.metadata` and `database` from `{env_prefix}DB_*` env vars when `{env_prefix}DB_URL` is present | | **di** | Dependency injection | `locate_subclasses(cls, *, context=None)`, `locate_implementations(cls, *, context=None)`, `load_modules(*, context=None)`, `discover_plugin_packages`, `clear_caches`, `PLUGIN_ENTRY_POINT_GROUP` for plugin and subclass discovery | | **health** | Service health checks | `Health` model and `HealthStatus` enum for tree-structured health status | | **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; `console`, `Panel`, and `Text` are imported lazily inside `load_settings` (error path only) | @@ -41,9 +41,17 @@ This file provides an overview of all modules in `aignostics_foundry_core`, thei application startup makes the context available everywhere in the library without threading values through call sites. Tests pass an explicit context override and never touch global state. - **Key Features**: + - `PackageMetadata(BaseModel)` — frozen; fields: `description: str = ""`, + `author_name: str | None = None`, `author_email: str | None = None`, + `repository_url: str = ""`, `documentation_url: str = ""`. + Constructor: `PackageMetadata.from_name(package_name)` — reads all five fields from + `importlib.metadata` (``Summary``, ``Author-email``, ``Project-URL``). + Defaults to empty values for direct construction (e.g. in tests). - `FoundryContext(BaseModel)` — frozen; fields: `name`, `version`, `version_full`, `version_with_vcs_ref`, `environment`, - `env_file: list[Path]`, `repository_url`, `documentation_url`, `python_version` (Python runtime - version string, e.g. `"3.11.9"`), plus four runtime mode bool flags: `is_container`, `is_cli`, + `env_file: list[Path]`, `env_prefix`, `python_version` (Python runtime version string, + e.g. `"3.11.9"`), `metadata: PackageMetadata` (all package-derived fields: description, + author, URLs; populated by `from_package()` via `PackageMetadata.from_name()`; defaults to + empty `PackageMetadata()`), plus four runtime mode bool flags: `is_container`, `is_cli`, `is_test`, `is_library` (all default `False`), and `database: DatabaseSettings | None` (populated by `from_package()` when `{env_prefix}DB_URL` is set; `None` otherwise). - `FoundryContext.from_package(package_name)` — classmethod that derives all values from @@ -51,6 +59,7 @@ This file provides an overview of all modules in `aignostics_foundry_core`, thei `BUILDER`, `BUILD_DATE`, `CI_RUN_ID`, `CI_RUN_NUMBER`, `{NAME}_ENV_FILE`, `{NAME}_RUNNING_IN_CONTAINER`, `PYTEST_RUNNING_{NAME}`). Environment fallback chain: `{NAME}_ENVIRONMENT` → `ENV` → `VERCEL_ENV` → `RAILWAY_ENVIRONMENT` → `"local"`. + Calls `PackageMetadata.from_name(package_name)` to populate `ctx.metadata`. Also checks `{NAME}_DB_URL`: when present, constructs `DatabaseSettings(_env_prefix="{NAME}_DB_")` and stores it in `ctx.database`; otherwise `ctx.database` is `None`. - `set_context(ctx)` — installs *ctx* as the process-level singleton. @@ -126,10 +135,10 @@ This file provides an overview of all modules in `aignostics_foundry_core`, thei - `API_TAG_PUBLIC`, `API_TAG_AUTHENTICATED`, `API_TAG_ADMIN`, `API_TAG_INTERNAL`, `API_TAG_INTERNAL_ADMIN` — string constants for OpenAPI tagging - `create_public_router(module_tag, *, version, prefix, …)` — public (unauthenticated) router - `create_authenticated_router`, `create_admin_router`, `create_internal_router`, `create_internal_admin_router` — router factories that inject the appropriate `require_*` dependency from `api.auth` - - `build_api_metadata(title, description, author_name, author_email, repository_url, documentation_url, version)` — returns a `dict` suitable for `FastAPI(**metadata)` - - `build_versioned_api_tags(version_name, repository_url)` — OpenAPI tags for a single versioned sub-app + - `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) + - `build_versioned_api_tags(version_name, *, context=None)` — OpenAPI tags for a single versioned sub-app; reads `repository_url` from *context* - `build_root_api_tags(base_url, versions)` — OpenAPI tags for the root app linking to each version's docs - - `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 + - `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 - `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 - **Location**: `aignostics_foundry_core/api/core.py` - **Dependencies**: `fastapi>=0.110,<1` (mandatory); `aignostics_foundry_core.di` (`load_modules`) diff --git a/src/aignostics_foundry_core/api/core.py b/src/aignostics_foundry_core/api/core.py index 54f18b6..291326e 100644 --- a/src/aignostics_foundry_core/api/core.py +++ b/src/aignostics_foundry_core/api/core.py @@ -262,41 +262,33 @@ def create_internal_admin_router( return cast("APIRouter", VersionedAPIRouter(version, prefix=actual_prefix, tags=tags, dependencies=dependencies)) -def build_api_metadata( # noqa: PLR0913, PLR0917 - title: str, - description: str = "", - author_name: str = "", - author_email: str = "", - repository_url: str = "", - documentation_url: str = "", - version: str | None = None, -) -> dict[str, Any]: +def build_api_metadata(version: str | None = None, *, context: FoundryContext | None = None) -> dict[str, Any]: """Build a metadata dictionary suitable for passing to a FastAPI instance. + All fields (title, description, author, URLs) are derived from *context*. + Args: - title: The API title (project name). - description: Human-readable description of the API. - author_name: Contact person or team name. - author_email: Contact email address. - repository_url: URL to the source repository. - documentation_url: URL to the documentation or terms of service. version: Optional API version string. + context: Project context supplying the title, description, author, and URLs. + When ``None``, the global context installed via + :func:`aignostics_foundry_core.foundry.set_context` is used. Returns: Dictionary containing FastAPI metadata keys. """ + ctx = context or get_context() metadata: dict[str, Any] = { - "title": title, - "description": description, + "title": ctx.name, + "description": ctx.metadata.description, "contact": { - "name": author_name or "Unknown", - "email": author_email, - "url": repository_url, + "name": ctx.metadata.author_name or "Unknown", + "email": ctx.metadata.author_email or "", + "url": ctx.metadata.repository_url, }, - "terms_of_service": documentation_url, + "terms_of_service": ctx.metadata.documentation_url, "license_info": { "name": "Aignostics Commercial License", - "url": f"{repository_url}/blob/main/LICENSE", + "url": f"{ctx.metadata.repository_url}/blob/main/LICENSE", }, } if version is not None: @@ -304,16 +296,19 @@ def build_api_metadata( # noqa: PLR0913, PLR0917 return metadata -def build_versioned_api_tags(version_name: str, repository_url: str = "") -> list[dict[str, Any]]: +def build_versioned_api_tags(version_name: str, *, context: FoundryContext | None = None) -> list[dict[str, Any]]: """Build ``openapi_tags`` for a versioned API instance. Args: version_name: The version name (e.g., "v1"). - repository_url: URL to the source repository (used for external docs link). + context: Project context supplying the repository URL for the external docs link. + When ``None``, the global context installed via + :func:`aignostics_foundry_core.foundry.set_context` is used. Returns: List of OpenAPI tag dictionaries for the versioned API. """ + repository_url = (context or get_context()).metadata.repository_url return [ { "name": version_name, @@ -351,7 +346,6 @@ def build_root_api_tags(base_url: str, versions: list[str]) -> list[dict[str, An def get_versioned_api_instances( versions: list[str], - build_metadata: dict[str, Any] | None = None, *, context: FoundryContext | None = None, ) -> dict[str, FastAPI]: @@ -364,18 +358,19 @@ def get_versioned_api_instances( Args: versions: Ordered list of API version names (e.g., ``["v1", "v2"]``). - build_metadata: Optional extra kwargs forwarded to each ``FastAPI()`` constructor. - context: Project context supplying the package name. When ``None``, - the global context installed via - :func:`aignostics_foundry_core.foundry.set_context` is used. + context: Project context supplying the package name, title, description, author, + and URLs for each ``FastAPI`` instance. When ``None``, the global context + installed via :func:`aignostics_foundry_core.foundry.set_context` is used. Returns: Mapping from version name to its configured ``FastAPI`` instance. """ from fastapi import FastAPI # noqa: PLC0415 - load_modules(context=context or get_context()) - api_instances: dict[str, FastAPI] = {version: FastAPI(**(build_metadata or {})) for version in versions} + ctx = context or get_context() + load_modules(context=ctx) + api_metadata = build_api_metadata(context=ctx) + api_instances: dict[str, FastAPI] = {version: FastAPI(**api_metadata) for version in versions} for router in VersionedAPIRouter.get_instances(): router_version: str = cast("Any", router).version diff --git a/src/aignostics_foundry_core/foundry.py b/src/aignostics_foundry_core/foundry.py index 4ccf5c8..3923b0b 100644 --- a/src/aignostics_foundry_core/foundry.py +++ b/src/aignostics_foundry_core/foundry.py @@ -37,6 +37,57 @@ def _empty_path_list() -> list[Path]: return [] +class PackageMetadata(BaseModel): + """All package-derived metadata: description, author, and project URLs. + + Populated via :meth:`from_name` from ``importlib.metadata``. + Defaults to empty/``None`` values so contexts constructed directly (e.g. in tests) + work without any extra setup. + """ + + model_config = {"frozen": True} + + description: str = "" + author_name: str | None = None + author_email: str | None = None + repository_url: str = "" + documentation_url: str = "" + + @classmethod + def from_name(cls, package_name: str) -> PackageMetadata: + """Return a :class:`PackageMetadata` populated from installed package metadata. + + Reads ``Summary``, ``Author-email``, and ``Project-URL`` entries from + ``importlib.metadata`` for *package_name*. + + Args: + package_name: The importable package name (e.g. ``"aignostics_foundry_core"``). + + Returns: + A fully populated, frozen :class:`PackageMetadata` instance. + """ + pkg_meta = metadata.metadata(package_name) + authors = pkg_meta.get_all("Author-email") or [] + author = authors[0] if authors else None + author_name = author.split("<")[0].strip() if author else None + author_email = author.split("<")[1].strip(" >") if author else None + description = pkg_meta.get("Summary") or "" + repository_url = "" + documentation_url = "" + for url_entry in pkg_meta.get_all("Project-URL") or []: + if url_entry.startswith("Source"): + repository_url = url_entry.split(", ", 1)[1] + elif url_entry.startswith("Documentation"): + documentation_url = url_entry.split(", ", 1)[1] + return cls( + description=description, + author_name=author_name, + author_email=author_email, + repository_url=repository_url, + documentation_url=documentation_url, + ) + + class FoundryContext(BaseModel): """Immutable project context carrying all project-specific values. @@ -71,14 +122,18 @@ class FoundryContext(BaseModel): environment: str env_file: list[Path] = Field(default_factory=_empty_path_list) env_prefix: str = "" - repository_url: str = "" - documentation_url: str = "" is_container: bool = False is_cli: bool = False is_test: bool = False is_library: bool = False python_version: str = "" project_path: Path | None = None + metadata: PackageMetadata = Field(default_factory=PackageMetadata) + """Package-derived author and description metadata. + + Populated by :meth:`from_package` from ``importlib.metadata``. + Defaults to empty values when the context is constructed directly (e.g. in tests). + """ database: DatabaseSettings | None = None """Database settings resolved from ``{env_prefix}DB_*`` environment variables. @@ -119,7 +174,6 @@ def from_package(cls, package_name: str) -> FoundryContext: name_upper = name.upper() version = metadata.version(package_name) environment = _detect_environment(name_upper) - repository_url, documentation_url = _extract_urls(package_name) project_path = _find_project_path(package_name) vcs_ref = os.environ.get("VCS_REF") or (project_path and _get_vcs_ref_from_git(project_path)) or "unknown" env_prefix = f"{name_upper}_" @@ -138,8 +192,7 @@ def from_package(cls, package_name: str) -> FoundryContext: environment=environment, env_file=env_files, env_prefix=env_prefix, - repository_url=repository_url, - documentation_url=documentation_url, + metadata=PackageMetadata.from_name(package_name), python_version=platform.python_version(), project_path=project_path, database=database, @@ -284,19 +337,6 @@ def _any_env_file_has(key: str, env_files: list[Path]) -> bool: return any(key in dotenv_values(f) for f in env_files if f.is_file()) -def _extract_urls(package_name: str) -> tuple[str, str]: - """Return ``(repository_url, documentation_url)`` from package metadata.""" - pkg_metadata = metadata.metadata(package_name) - repository_url = "" - documentation_url = "" - for url_entry in pkg_metadata.get_all("Project-URL") or []: - if url_entry.startswith("Source"): - repository_url = url_entry.split(", ", 1)[1] - elif url_entry.startswith("Documentation"): - documentation_url = url_entry.split(", ", 1)[1] - return repository_url, documentation_url - - def _build_runtime_flags(name: str, name_upper: str) -> dict[str, bool]: """Compute runtime mode flags from environment and process state. diff --git a/src/aignostics_foundry_core/sentry.py b/src/aignostics_foundry_core/sentry.py index eb134d2..a193c24 100644 --- a/src/aignostics_foundry_core/sentry.py +++ b/src/aignostics_foundry_core/sentry.py @@ -275,8 +275,8 @@ def sentry_initialize( "aignx/base", { "project_name": ctx.name, - "repository_url": ctx.repository_url, - "documentation_url": ctx.documentation_url, + "repository_url": ctx.metadata.repository_url, + "documentation_url": ctx.metadata.documentation_url, "version_full": ctx.version_full, "in_container": ctx.is_container, "test_mode": ctx.is_test, diff --git a/src/aignostics_foundry_core/user_agent.py b/src/aignostics_foundry_core/user_agent.py index dd7ad22..87904f2 100644 --- a/src/aignostics_foundry_core/user_agent.py +++ b/src/aignostics_foundry_core/user_agent.py @@ -42,6 +42,6 @@ def user_agent(*, context: "FoundryContext | None" = None) -> str: # Format: {project}-python-sdk/{version_full} ({platform}; +{repository_url}; {optional_parts}) # TODO(oliverm): Find a way to not hard code python-sdk here. This was taken as such from Bridge. base_info = f"{ctx.name}-python-sdk/{ctx.version_full}" - system_info = f"{platform.platform()}; +{ctx.repository_url}{optional_suffix}" + system_info = f"{platform.platform()}; +{ctx.metadata.repository_url}{optional_suffix}" return f"{base_info} ({system_info})" diff --git a/tests/aignostics_foundry_core/api/core_test.py b/tests/aignostics_foundry_core/api/core_test.py index bb4b477..a90955f 100644 --- a/tests/aignostics_foundry_core/api/core_test.py +++ b/tests/aignostics_foundry_core/api/core_test.py @@ -56,10 +56,10 @@ def test_api_tag_constants() -> None: @pytest.mark.unit def test_build_api_metadata_returns_dict_with_title() -> None: - """build_api_metadata returns a dict containing the title key.""" + """build_api_metadata returns a dict containing the title derived from context.""" from aignostics_foundry_core.api.core import build_api_metadata - result = build_api_metadata(title=TEST_TITLE, description="Test API", repository_url="https://example.com") + result = build_api_metadata(context=make_context(name=TEST_TITLE)) assert isinstance(result, dict) assert result[TITLE_KEY] == TEST_TITLE @@ -184,7 +184,7 @@ def test_build_api_metadata_includes_version_when_provided() -> None: """build_api_metadata adds a 'version' key when version is supplied.""" from aignostics_foundry_core.api.core import build_api_metadata - result = build_api_metadata(title=TEST_TITLE, version=TEST_VERSION_STR) + result = build_api_metadata(version=TEST_VERSION_STR, context=make_context()) assert result["version"] == TEST_VERSION_STR @@ -194,13 +194,26 @@ def test_build_versioned_api_tags_returns_tag_for_version() -> None: """build_versioned_api_tags returns a single-element list with the correct name.""" from aignostics_foundry_core.api.core import build_versioned_api_tags - tags = build_versioned_api_tags("v2", repository_url=BASE_URL) + tags = build_versioned_api_tags("v2", context=make_context(repository_url=BASE_URL)) assert len(tags) == 1 assert tags[0]["name"] == "v2" assert BASE_URL in tags[0]["externalDocs"]["url"] +@pytest.mark.unit +def test_build_api_metadata_contact_uses_context_author() -> None: + """build_api_metadata populates contact from context's PackageMetadata author fields.""" + from aignostics_foundry_core.api.core import build_api_metadata + from aignostics_foundry_core.foundry import PackageMetadata + + ctx = make_context(metadata=PackageMetadata(author_name="Ada", author_email="ada@example.com")) + result = build_api_metadata(context=ctx) + + assert result["contact"]["name"] == "Ada" + assert result["contact"]["email"] == "ada@example.com" + + @pytest.mark.unit def test_build_root_api_tags_one_entry_per_version() -> None: """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: @pytest.mark.unit def test_get_versioned_api_instances_returns_fastapi_per_version() -> None: - """get_versioned_api_instances returns a FastAPI instance for each requested version.""" + """get_versioned_api_instances returns a FastAPI instance with the context name as title.""" from fastapi import FastAPI 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: assert VERSION_GVI in result assert isinstance(result[VERSION_GVI], FastAPI) + assert result[VERSION_GVI].title == "aignostics_foundry_core" @pytest.mark.unit diff --git a/tests/aignostics_foundry_core/foundry_test.py b/tests/aignostics_foundry_core/foundry_test.py index b04c8b0..f2d3b1a 100644 --- a/tests/aignostics_foundry_core/foundry_test.py +++ b/tests/aignostics_foundry_core/foundry_test.py @@ -14,7 +14,7 @@ import pytest from pydantic import ValidationError -from aignostics_foundry_core.foundry import FoundryContext, get_context, reset_context, set_context +from aignostics_foundry_core.foundry import FoundryContext, PackageMetadata, get_context, reset_context, set_context from tests.conftest import make_context # Constants (SonarQube S1192) @@ -38,6 +38,77 @@ INIT_PY = "__init__.py" +# --------------------------------------------------------------------------- +# PackageMetadata — field defaults +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +def test_foundry_context_metadata_field_defaults() -> None: + """A directly-constructed FoundryContext has empty PackageMetadata defaults.""" + ctx = make_context() + assert not ctx.metadata.description + assert ctx.metadata.author_name is None + assert ctx.metadata.author_email is None + assert not ctx.metadata.repository_url + assert not ctx.metadata.documentation_url + + +# --------------------------------------------------------------------------- +# PackageMetadata.from_name() — detail assertions +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +def test_package_metadata_from_name_description() -> None: + """PackageMetadata.from_name() returns a non-empty description (Summary field).""" + pkg_meta = PackageMetadata.from_name(PACKAGE_NAME) + assert isinstance(pkg_meta.description, str) + assert pkg_meta.description + + +@pytest.mark.unit +def test_package_metadata_from_name_author_name() -> None: + """PackageMetadata.from_name() returns a non-None, non-empty author_name.""" + pkg_meta = PackageMetadata.from_name(PACKAGE_NAME) + assert pkg_meta.author_name is not None + assert pkg_meta.author_name + + +@pytest.mark.unit +def test_package_metadata_from_name_author_email() -> None: + """PackageMetadata.from_name() returns a non-None author_email containing '@'.""" + pkg_meta = PackageMetadata.from_name(PACKAGE_NAME) + assert pkg_meta.author_email is not None + assert "@" in pkg_meta.author_email + + +@pytest.mark.unit +def test_package_metadata_from_name_repository_url() -> None: + """PackageMetadata.from_name() returns a non-empty repository_url (Source URL).""" + pkg_meta = PackageMetadata.from_name(PACKAGE_NAME) + assert pkg_meta.repository_url + + +@pytest.mark.unit +def test_package_metadata_from_name_documentation_url() -> None: + """PackageMetadata.from_name() returns a non-empty documentation_url (Documentation URL).""" + pkg_meta = PackageMetadata.from_name(PACKAGE_NAME) + assert pkg_meta.documentation_url + + +# --------------------------------------------------------------------------- +# from_package() — wires metadata via PackageMetadata.from_name() +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +def test_from_package_metadata_is_package_metadata_instance() -> None: + """from_package() sets .metadata to a PackageMetadata populated via from_name().""" + ctx = FoundryContext.from_package(PACKAGE_NAME) + assert ctx.metadata == PackageMetadata.from_name(PACKAGE_NAME) + + @pytest.fixture(autouse=True) def _reset_context() -> Generator[None, None, None]: # pyright: ignore[reportUnusedFunction] """Reset global _context to None before and after every test. diff --git a/tests/aignostics_foundry_core/user_agent_test.py b/tests/aignostics_foundry_core/user_agent_test.py index c099873..8caa0fe 100644 --- a/tests/aignostics_foundry_core/user_agent_test.py +++ b/tests/aignostics_foundry_core/user_agent_test.py @@ -55,7 +55,6 @@ def test_user_agent_uses_version_full(self, monkeypatch: pytest.MonkeyPatch) -> version_full=CTX_VERSION_FULL, version_with_vcs_ref=CTX_VERSION, environment="test", - repository_url=CTX_REPOSITORY_URL, ) result = user_agent(context=ctx) diff --git a/tests/conftest.py b/tests/conftest.py index a1c702a..60232d3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,7 @@ import pytest from aignostics_foundry_core.database import DatabaseSettings -from aignostics_foundry_core.foundry import FoundryContext +from aignostics_foundry_core.foundry import FoundryContext, PackageMetadata __all__ = ["make_context"] @@ -71,6 +71,7 @@ def make_context( # noqa: PLR0913 repository_url: str = "", database: DatabaseSettings | None = None, env_file: list[Path] | None = None, + metadata: PackageMetadata | None = None, **kwargs: bool, ) -> FoundryContext: """Create a minimal FoundryContext for testing. @@ -81,13 +82,18 @@ def make_context( # noqa: PLR0913 version: The version string (defaults to ``"0.0.0"``). environment: The deployment environment (defaults to ``"test"``). project_path: Optional path to the project root. - repository_url: The project repository URL (defaults to ``""``). + repository_url: Shorthand to set ``metadata.repository_url`` when *metadata* is + not provided. Ignored when *metadata* is supplied explicitly. database: Optional :class:`~aignostics_foundry_core.database.DatabaseSettings` instance to attach to the context. env_file: Optional list of ``.env`` file paths to attach to the context. + metadata: Optional :class:`~aignostics_foundry_core.foundry.PackageMetadata` + to attach to the context. When ``None``, a ``PackageMetadata`` with + ``repository_url`` set from the *repository_url* argument is used. **kwargs: Optional boolean flags forwarded to :class:`FoundryContext` (``is_test``, ``is_cli``, ``is_container``, ``is_library``). """ + resolved_metadata = metadata if metadata is not None else PackageMetadata(repository_url=repository_url) return FoundryContext( name=name, version=version, @@ -96,8 +102,8 @@ def make_context( # noqa: PLR0913 environment=environment, env_prefix=env_prefix, project_path=project_path, - repository_url=repository_url, database=database, env_file=env_file or [], + metadata=resolved_metadata, **kwargs, # type: ignore[arg-type] )