diff --git a/.envrc b/.envrc index 9495d001..882679b7 100644 --- a/.envrc +++ b/.envrc @@ -32,7 +32,8 @@ PROJECT_SCRATCH_IDENTITY="${PROJECT_SCRATCH_IDENTITY%/}" PROJECT_SCRATCH="${HOME}/tmp/.flext-runtime${PROJECT_SCRATCH_IDENTITY}/scratch" mkdir -p "${PROJECT_SCRATCH}" if command -v chattr >/dev/null 2>&1; then - if [[ "$(stat -f -c %T "${PROJECT_SCRATCH}")" == "btrfs" ]]; then + project_scratch_fs="$(stat -f -c %T "${PROJECT_SCRATCH}" 2>/dev/null || true)" + if [[ "${project_scratch_fs}" == "btrfs" ]]; then chattr +C "${PROJECT_SCRATCH}" fi fi @@ -92,7 +93,7 @@ if [[ -v AGENTS_GAS_CITY_ROOT ]]; then # Source: typed BeadsWorkspaceEnvironmentSpec shared by both direnv profiles. # Environment sources are host-provided and optional: isolated CI must still # activate while a city-connected host keeps their exports. -source_env_if_exists "$HOME/.config/environment.d/projects/agent-tools.envrc" +source_env_if_exists "${HOME}/.config/environment.d/projects/agent-tools.envrc" # End SECTION: beads watched inputs # === SECTION: canonical roots (managed) === diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 815fcc0b..e23f1b0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -164,7 +164,7 @@ jobs: run: CI=Y make check # Why (aihub-v01jg): CI=Y runs ONE HALF of the gate set - # (lint pyright silent-failure deferred-self-reference security markdown markdown-format markdown-code loc-cap boundary runtime-census namespace tier-whitelist index-declarations smells codemod layout canonical-alias direnv duplication); the complement + # (lint pyright silent-failure deferred-self-reference security markdown loc-cap boundary runtime-census namespace tier-whitelist index-declarations smells codemod layout canonical-alias direnv duplication); the complement # (pyrefly mypy) is owned by # CI=N and, without this step, ran on developer # machines only. That split let real defects reach main twice: five diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 846038d5..9629e040 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -135,7 +135,7 @@ jobs: pages: write id-token: write environment: - name: ${{ 'github-pages' }} + name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy to GitHub Pages diff --git a/.gitignore b/.gitignore index 0ff5810b..fd23f07b 100644 --- a/.gitignore +++ b/.gitignore @@ -372,9 +372,6 @@ CLAUDE.local.md !.github/workflows/ci.yml !.github/workflows/docs.yml !.github/workflows/release.yml -!.github/workflows/_fragments/ -!.github/workflows/_fragments/cross_repo_dependency_credential -!.github/workflows/_fragments/testmon_cache !.qlty/ !.qlty/qlty.toml !.vscode/settings.json diff --git a/Makefile b/Makefile index e6687170..c24dd73f 100644 --- a/Makefile +++ b/Makefile @@ -1037,10 +1037,10 @@ _builtin-self-test: _builtin_require_environment _builtin-self-check: _builtin_require_environment @set -eu; \ - gates="lint,pyrefly,mypy,pyright,silent-failure,deferred-self-reference,security,markdown,markdown-format,markdown-code,loc-cap,boundary,runtime-census,namespace,tier-whitelist,index-declarations,smells,codemod,layout,canonical-alias,direnv,duplication"; \ + gates="lint,pyrefly,mypy,pyright,silent-failure,deferred-self-reference,security,markdown,loc-cap,boundary,runtime-census,namespace,tier-whitelist,index-declarations,smells,codemod,layout,canonical-alias,direnv,duplication"; \ if [ "$(strip $(CI))" = "Y" ]; then \ - gates="lint,pyright,silent-failure,deferred-self-reference,security,markdown,markdown-format,markdown-code,loc-cap,boundary,runtime-census,namespace,tier-whitelist,index-declarations,smells,codemod,layout,canonical-alias,direnv,duplication"; \ - printf 'INFO: CI=Y runs check gates: lint pyright silent-failure deferred-self-reference security markdown markdown-format markdown-code loc-cap boundary runtime-census namespace tier-whitelist index-declarations smells codemod layout canonical-alias direnv duplication\n'; \ + gates="lint,pyright,silent-failure,deferred-self-reference,security,markdown,loc-cap,boundary,runtime-census,namespace,tier-whitelist,index-declarations,smells,codemod,layout,canonical-alias,direnv,duplication"; \ + printf 'INFO: CI=Y runs check gates: lint pyright silent-failure deferred-self-reference security markdown loc-cap boundary runtime-census namespace tier-whitelist index-declarations smells codemod layout canonical-alias direnv duplication\n'; \ fi; \ if [ -z "$$gates" ]; then \ printf 'ERROR: no check gates remain after CI=Y filtering\n' >&2; \ @@ -1075,10 +1075,10 @@ _builtin_build_artifacts: # make.ci.local_check_gates. _builtin_check_all: _builtin_require_environment @set -eu; \ - gates="lint,pyrefly,mypy,pyright,silent-failure,deferred-self-reference,security,markdown,markdown-format,markdown-code,loc-cap,boundary,runtime-census,namespace,tier-whitelist,index-declarations,smells,codemod,layout,canonical-alias,direnv,duplication"; \ + gates="lint,pyrefly,mypy,pyright,silent-failure,deferred-self-reference,security,markdown,loc-cap,boundary,runtime-census,namespace,tier-whitelist,index-declarations,smells,codemod,layout,canonical-alias,direnv,duplication"; \ if [ "$(strip $(CI))" = "Y" ]; then \ - gates="lint,pyright,silent-failure,deferred-self-reference,security,markdown,markdown-format,markdown-code,loc-cap,boundary,runtime-census,namespace,tier-whitelist,index-declarations,smells,codemod,layout,canonical-alias,direnv,duplication"; \ - printf 'INFO: CI=Y runs check gates: lint pyright silent-failure deferred-self-reference security markdown markdown-format markdown-code loc-cap boundary runtime-census namespace tier-whitelist index-declarations smells codemod layout canonical-alias direnv duplication\n'; \ + gates="lint,pyright,silent-failure,deferred-self-reference,security,markdown,loc-cap,boundary,runtime-census,namespace,tier-whitelist,index-declarations,smells,codemod,layout,canonical-alias,direnv,duplication"; \ + printf 'INFO: CI=Y runs check gates: lint pyright silent-failure deferred-self-reference security markdown loc-cap boundary runtime-census namespace tier-whitelist index-declarations smells codemod layout canonical-alias direnv duplication\n'; \ fi; \ if [ -z "$$gates" ]; then \ printf 'ERROR: no check gates remain after CI=Y filtering\n' >&2; \ diff --git a/README.md b/README.md index 1e96351d..9e12c331 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,13 @@ FLEXT API - High-Performance REST API with FastAPI ## Module Map -::: flext_api options: members: false show_root_heading: false show_root_toc_entry: -false show_source: false +::: flext_api + + options: + members: false + show_root_heading: false + show_root_toc_entry: false + show_source: false ## Collection Rules @@ -57,9 +62,10 @@ verification). ## Quality Gates Canonical `make` verbs (`gen`, `check`, `test`, `fmt`, `docs`) execute their declared -operations directly — see -[`/flext/AGENTS.md`](https://github.com/flext-sh/flext/blob/0.12.0-dev/AGENTS.md) -`Build & Test` and `Required Python quality gates`. +operations directly. + +See [`/flext/AGENTS.md`](https://github.com/flext-sh/flext/blob/0.12.0-dev/AGENTS.md) +for the build, test, and Python quality gates. ## Governance Pointer diff --git a/docs/api-reference/generated/overview.md b/docs/api-reference/generated/overview.md index fe5b6125..83e9895d 100644 --- a/docs/api-reference/generated/overview.md +++ b/docs/api-reference/generated/overview.md @@ -12,10 +12,10 @@ - Version: `0.12.0` - Description: FLEXT API - High-Performance REST API with FastAPI - Doc summary: Flext Api package. -- Classifiers: `Development Status :: 3 - Alpha`, `Framework :: FastAPI`, - `Intended Audience :: Developers`, `Operating System :: OS Independent`, - `Programming Language :: Python :: 3 :: Only`, - `Programming Language :: Python :: 3.13` (+3 more) +- Classifiers: Development Status :: 3 - Alpha, Framework :: FastAPI, Intended Audience + :: Developers, Operating System :: OS Independent, Programming Language :: Python :: 3 + :: Only, Programming Language :: Python :: 3.13, Topic :: Internet :: WWW/HTTP :: HTTP + Servers, Topic :: Software Development :: Libraries :: Python Modules, Typing :: Typed - Project class: `domain` - Keywords: `enterprise`, `fastapi`, `flext`, `http`, `rest`, `typed` - Main facades: `FlextApi`, `FlextApiAsyncClient`, `FlextApiCli`, `FlextApiClient`, @@ -24,7 +24,7 @@ - Alias exports: `c`, `d`, `e`, `h`, `m`, `p`, `r`, `s`, `t`, `u`, `x` - Public symbol exports: `FlextApi`, `FlextApiAsyncClient`, `FlextApiCli`, `FlextApiClient`, `FlextApiClientBase`, `FlextApiConfig`, `FlextApiConstants`, - `FlextApiModels`, `FlextApiProtocols`, `FlextApiServiceBase` (+5 more) + `FlextApiModels`, `FlextApiProtocols`, `FlextApiServiceBase` (+12 more) - Exported module shortcuts: `api`, `services` - Generated module pages: `12` diff --git a/docs/api-reference/generated/public-api.md b/docs/api-reference/generated/public-api.md index cdbff39a..3025a431 100644 --- a/docs/api-reference/generated/public-api.md +++ b/docs/api-reference/generated/public-api.md @@ -8,5 +8,9 @@ -::: flext_api options: show_root_heading: true show_root_full_path: false show_source: -false +::: flext_api + + options: + show_root_heading: true + show_root_full_path: false + show_source: false diff --git a/docs/api/core.md b/docs/api/core.md index 5521060a..2c3ba91e 100644 --- a/docs/api/core.md +++ b/docs/api/core.md @@ -28,7 +28,7 @@ executes validated `m.Api.HttpRequest` instances through `request(...)`. It does expose `get/post/put/delete/patch` directly; those methods live on the `FlextApi` facade. -````python notest +```python notest from __future__ import annotations from flext_api import FlextApiClient, FlextApiSettings, c, m, p @@ -62,7 +62,8 @@ if result.success: print(response.body) else: print(f"Transport error: {result.error}") - ``` +``` + **Key Features:** - Type-safe HTTP operations via Pydantic models @@ -80,8 +81,8 @@ else: ### FlextApi - Unified Facade -`FlextApi` is the public entry point. It creates and owns a `FlextApiClient` -lazily and exposes convenience methods for each HTTP verb. +`FlextApi` is the public entry point. It creates and owns a `FlextApiClient` lazily and +exposes convenience methods for each HTTP verb. ```python from __future__ import annotations @@ -102,7 +103,8 @@ if result.success: print(f"Body: {response.body}") else: print(f"Error: {result.error}") - ``` +``` + ### HTTP Methods All methods return `p.Result[m.Api.HttpResponse]`. @@ -121,7 +123,7 @@ result: p.Result[m.Api.HttpResponse] = api.get("/users") result = api.get("/users", request_kwargs={"params": {"limit": 10, "offset": 0}}) result = api.get("/users", headers={"Accept": "application/json"}) -```` +``` **POST/PUT/PATCH/DELETE Requests:** @@ -268,7 +270,7 @@ print(timeout_result.unwrap()) ### Complete HTTP Client Example -````python notest +```python notest from __future__ import annotations from flext_api import FlextApi, FlextApiSettings, m, p, r @@ -367,8 +369,8 @@ if update_result.success: delete_result = client.delete_user(1) if delete_result.success: print(f"Deleted user, status: {delete_result.unwrap().status_code}") - ``` +``` + This core API provides the public HTTP surface for `flext-api`: typed settings, a validated request model, a monadic response model, and the `FlextApi` facade for convenient HTTP verbs. -```` diff --git a/docs/architecture/decisions/002-railway-pattern.md b/docs/architecture/decisions/002-railway-pattern.md index dde705b2..5e02d64d 100644 --- a/docs/architecture/decisions/002-railway-pattern.md +++ b/docs/architecture/decisions/002-railway-pattern.md @@ -64,7 +64,7 @@ Every public method returns `p.Result[T]`. Operations are composed using `flat_m ### Option 1: Traditional Exceptions -````python notest +```python notest from __future__ import annotations import httpx @@ -75,7 +75,8 @@ def get_user(user_id: int) -> dict: response = httpx.get(f"https://api.example.com/users/{user_id}") response.raise_for_status() return response.json() - ``` +``` + ### Option 2: Result Pattern (Custom Implementation) ```python @@ -89,7 +90,8 @@ class Result: self.success = success self.value = value self.error = error - ``` +``` + ### Option 3: Hybrid Approach - **Description**: Use railway pattern internally but expose traditional APIs @@ -145,11 +147,11 @@ api = FakeUserApi(runtime_settings=FlextApiSettings(base_url="https://example.co result = api.fetch_user(123) assert result.success assert result.unwrap().body["name"] == "Alice" -```` +``` ### Usage in Application Code -````python notest +```python notest from __future__ import annotations from flext_api import FlextApi, FlextApiSettings, m, p, r @@ -176,7 +178,8 @@ if result.success: print(f"Found profile: {profile['bio']}") else: print(f"Error: {result.error}") - ``` +``` + ### Testing Railway Code ```python @@ -216,7 +219,7 @@ def test_get_user_not_found(): test_get_user_success() test_get_user_not_found() -```` +``` ## Migration Strategy @@ -249,7 +252,3 @@ r[str].fail("JSON parsing failed: invalid response format") - [Railway-Oriented Programming](https://fsharpforfunandprofit.com/rop/) - GitHub Issue: #156 - Railway Pattern Implementation - -``` - -``` diff --git a/docs/index.md b/docs/index.md index 7797bc22..2876bede 100644 --- a/docs/index.md +++ b/docs/index.md @@ -29,8 +29,13 @@ docstrings. ## Public Surface Summary -::: flext_api options: members: false show_root_heading: false show_root_toc_entry: -false show_source: false +::: flext_api + + options: + members: false + show_root_heading: false + show_root_toc_entry: false + show_source: false ## Collection Rules @@ -42,9 +47,10 @@ verification). ## Quality Gates Canonical `make` verbs (`gen`, `check`, `test`, `fmt`, `docs`) execute their declared -operations directly — see -[`/flext/AGENTS.md`](https://github.com/flext-sh/flext/blob/0.12.0-dev/AGENTS.md) -`Build & Test` and `Required Python quality gates`. +operations directly. + +See [`/flext/AGENTS.md`](https://github.com/flext-sh/flext/blob/0.12.0-dev/AGENTS.md) +for the build, test, and Python quality gates. ## Governance Pointer diff --git a/mkdocs.yml b/mkdocs.yml index d918818c..afd2be6d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,6 +38,7 @@ theme: icon: material/weather-sunny name: Switch to light mode + plugins: - search - autorefs diff --git a/src/flext_api/__init__.py b/src/flext_api/__init__.py index cd15aaf5..f190bad6 100644 --- a/src/flext_api/__init__.py +++ b/src/flext_api/__init__.py @@ -30,7 +30,17 @@ from .cli import FlextApiCli from .constants import FlextApiConstants, FlextApiConstants as c from .models import FlextApiModels, FlextApiModels as m - from .protocols import FlextApiProtocols, FlextApiProtocols as p + from .protocols import ( + FlextApiProtocols, + FlextApiProtocols as p, + HttpxAsyncClient, + HttpxClient, + HttpxHTTPError, + HttpxHTTPStatusError, + HttpxRequestError, + HttpxResponse, + HttpxTimeoutException, + ) from .services.async_client import FlextApiAsyncClient from .services.base_client import FlextApiClientBase from .services.client import FlextApiClient @@ -50,6 +60,13 @@ "FlextApiSettings", "FlextApiTypes", "FlextApiUtilities", + "HttpxAsyncClient", + "HttpxClient", + "HttpxHTTPError", + "HttpxHTTPStatusError", + "HttpxRequestError", + "HttpxResponse", + "HttpxTimeoutException", "__author__", "__author_email__", "__description__", @@ -85,7 +102,17 @@ ".cli": ("FlextApiCli",), ".constants": ("FlextApiConstants", "c"), ".models": ("FlextApiModels", "m"), - ".protocols": ("FlextApiProtocols", "p"), + ".protocols": ( + "FlextApiProtocols", + "HttpxAsyncClient", + "HttpxClient", + "HttpxHTTPError", + "HttpxHTTPStatusError", + "HttpxRequestError", + "HttpxResponse", + "HttpxTimeoutException", + "p", + ), ".services": ("services",), ".services.async_client": ("FlextApiAsyncClient",), ".services.base_client": ("FlextApiClientBase",), diff --git a/src/flext_api/_protocols/transports.py b/src/flext_api/_protocols/transports.py index 1f90da13..26d5bc33 100644 --- a/src/flext_api/_protocols/transports.py +++ b/src/flext_api/_protocols/transports.py @@ -11,7 +11,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, override +from typing import TYPE_CHECKING, ClassVar, Protocol, override, runtime_checkable import httpx @@ -29,6 +29,18 @@ class FlextApiProtocolsTransports: """FLEXT API transport implementations.""" + @runtime_checkable + class Httpx(Protocol): + """Protocol namespace for owner-derived HTTP status contracts. + + Runtime classes and exceptions are published as module-level + ``Httpx*`` class-object re-exports. Keeping class identity out of this + protocol namespace preserves both construction/isinstance semantics and + the Protocol/namespace census contract. + """ + + CONFLICT: ClassVar[int] = int(httpx.codes.CONFLICT) + # Why: no member here carries @abstractmethod (TransportPlugin's Protocol # bodies are structural, not abstract), so an explicit ABC base added # nothing but tripped pyrefly's direct-abstract-base-instantiation check diff --git a/src/flext_api/_utilities/serializers.py b/src/flext_api/_utilities/serializers.py index 80ecc341..7d6b8562 100644 --- a/src/flext_api/_utilities/serializers.py +++ b/src/flext_api/_utilities/serializers.py @@ -45,7 +45,7 @@ def unpackb(data: bytes) -> p.Result[t.JsonValue]: result = msgpack.unpackb(data) if result is None: return r[t.JsonValue].fail( - "msgpack nil is forbidden because None cannot be a success payload" + "msgpack nil is forbidden because Result cannot carry None as success" ) normalized = t.Api.API_JSON_VALUE_ADAPTER.validate_python(result) return r[t.JsonValue].ok(normalized) diff --git a/src/flext_api/protocols.py b/src/flext_api/protocols.py index 6c712756..3681e9dd 100644 --- a/src/flext_api/protocols.py +++ b/src/flext_api/protocols.py @@ -11,6 +11,15 @@ from __future__ import annotations from flext_web import p +from httpx import ( + AsyncClient as HttpxAsyncClient, + Client as HttpxClient, + HTTPError as HttpxHTTPError, + HTTPStatusError as HttpxHTTPStatusError, + RequestError as HttpxRequestError, + Response as HttpxResponse, + TimeoutException as HttpxTimeoutException, +) from ._protocols import ( FlextApiProtocolPlugins, @@ -39,4 +48,18 @@ class Api( p = FlextApiProtocols -__all__: list[str] = ["FlextApiProtocols", "FlextApiProtocolsTransports", "p"] +# Module-level explicit class-object re-exports: consumers can construct and +# isinstance-narrow these names with both static and runtime class semantics. + +__all__: list[str] = [ + "FlextApiProtocols", + "FlextApiProtocolsTransports", + "HttpxAsyncClient", + "HttpxClient", + "HttpxHTTPError", + "HttpxHTTPStatusError", + "HttpxRequestError", + "HttpxResponse", + "HttpxTimeoutException", + "p", +] diff --git a/tests/constants.py b/tests/constants.py index 22fb86aa..e3d4a908 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -12,13 +12,7 @@ class TestsFlextApiConstants(c): """Test constants for flext-api — extends flext_api.c.""" - class _ApiConstants: - """API-specific test constants.""" - - class _WebConstants: - """Web-specific test constants.""" - - class TestsFlextApi(_ApiConstants, _WebConstants): + class Tests(c): """Test-specific constants.""" diff --git a/tests/models.py b/tests/models.py index bf753146..e25d2bec 100644 --- a/tests/models.py +++ b/tests/models.py @@ -12,13 +12,7 @@ class TestsFlextApiModels(m): """Test models for flext-api — extends flext_api.m.""" - class _RequestModels: - """Request-specific test models.""" - - class _ResponseModels: - """Response-specific test models.""" - - class TestsFlextApi(_RequestModels, _ResponseModels): + class Tests(m): """Test-specific models.""" diff --git a/tests/protocols.py b/tests/protocols.py index c95607a1..f44ad17e 100644 --- a/tests/protocols.py +++ b/tests/protocols.py @@ -12,13 +12,7 @@ class TestsFlextApiProtocols(p): """Test protocols for flext-api — extends flext_api.p.""" - class _ClientProtocols: - """Client-specific test protocols.""" - - class _TransportProtocols: - """Transport-specific test protocols.""" - - class TestsFlextApi(_ClientProtocols, _TransportProtocols): + class Tests(p): """Test-specific protocols.""" diff --git a/tests/typings.py b/tests/typings.py index 31cd7ed9..b2fac909 100644 --- a/tests/typings.py +++ b/tests/typings.py @@ -12,13 +12,7 @@ class TestsFlextApiTypes(t): """Test type aliases for flext-api — extends flext_api.t.""" - class _RequestTypes: - """Request-specific test type aliases.""" - - class _ResponseTypes: - """Response-specific test type aliases.""" - - class TestsFlextApi(_RequestTypes, _ResponseTypes): + class Tests(t): """Test-specific type aliases.""" diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index 4cc84787..85dd7d49 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -11,14 +11,16 @@ if TYPE_CHECKING: from flext_tests import c, d, e, h, m, p, r, s, t, td, tf, tk, tm, tv, u, x - from ._model_contract import TestsFlextApiModelContract + from .model_contract import TestsFlextApiModelContract from .test_async_client import TestsFlextApiAsyncClientSmoke from .test_response_wire_content import TestsFlextApiResponseWireContent from .test_serializers import TestsFlextApiSerializers from .test_smoke import TestsFlextApiSmoke + from .test_transports_facade_httpx import TestsFlextApiHttpxContracts from .test_utilities_transport import TestsFlextApiUtilitiesTransport __all__: tuple[str, ...] = ( "TestsFlextApiAsyncClientSmoke", + "TestsFlextApiHttpxContracts", "TestsFlextApiModelContract", "TestsFlextApiResponseWireContent", "TestsFlextApiSerializers", @@ -45,11 +47,12 @@ _LAZY_IMPORTS = MappingProxyType( build_lazy_import_map( MappingProxyType({ - "._model_contract": ("TestsFlextApiModelContract",), + ".model_contract": ("TestsFlextApiModelContract",), ".test_async_client": ("TestsFlextApiAsyncClientSmoke",), ".test_response_wire_content": ("TestsFlextApiResponseWireContent",), ".test_serializers": ("TestsFlextApiSerializers",), ".test_smoke": ("TestsFlextApiSmoke",), + ".test_transports_facade_httpx": ("TestsFlextApiHttpxContracts",), ".test_utilities_transport": ("TestsFlextApiUtilitiesTransport",), "flext_tests": ( "c", diff --git a/tests/unit/_model_contract.py b/tests/unit/model_contract.py similarity index 100% rename from tests/unit/_model_contract.py rename to tests/unit/model_contract.py diff --git a/tests/unit/test_async_client.py b/tests/unit/test_async_client.py index e6718421..3fe154c9 100644 --- a/tests/unit/test_async_client.py +++ b/tests/unit/test_async_client.py @@ -16,7 +16,7 @@ from flext_api import FlextApiAsyncClient, FlextApiSettings -from ._model_contract import TestsFlextApiModelContract +from .model_contract import TestsFlextApiModelContract class TestsFlextApiAsyncClientSmoke(TestsFlextApiModelContract): diff --git a/tests/unit/test_serializers.py b/tests/unit/test_serializers.py index f0f7583f..a52b2390 100644 --- a/tests/unit/test_serializers.py +++ b/tests/unit/test_serializers.py @@ -69,13 +69,6 @@ def test_unpackb_invalid_input_fails(self) -> None: tm.that(result.failure, eq=True) tm.that(result.error, is_=str) - def test_unpackb_rejects_msgpack_nil(self) -> None: - """Msgpack nil is rejected: None is not a valid success payload.""" - result = u.Api.unpackb(b"\xc0") - - tm.that(result.success, eq=False) - tm.that(result.error, has="nil is forbidden") - def test_packb_unpackb_roundtrip(self) -> None: """packb() followed by unpackb() yields the original value.""" original: t.JsonValue = {"key": "value", "list": [1, 2, 3]} @@ -118,13 +111,14 @@ def test_packb_unpackb_roundtrip_bool(self) -> None: tm.that(result.success, eq=True) tm.that(result.value, eq=original) - def test_packb_unpackb_roundtrip_none_is_rejected(self) -> None: - """None payloads fail loudly: nil cannot be a success payload.""" + def test_unpackb_rejects_nil_payload(self) -> None: + """An encoded msgpack nil fails because Result cannot succeed with None.""" packed = u.Api.packb(None) result = u.Api.unpackb(packed) tm.that(result.success, eq=False) - tm.that(result.error, has="nil is forbidden") + tm.that(result.failure, eq=True) + tm.that(str(result.error), has="Result cannot carry None") __all__: list[str] = ["TestsFlextApiSerializers"] diff --git a/tests/unit/test_smoke.py b/tests/unit/test_smoke.py index d956fc25..74c8e34d 100644 --- a/tests/unit/test_smoke.py +++ b/tests/unit/test_smoke.py @@ -14,7 +14,7 @@ from flext_api import FlextApi, FlextApiClient, FlextApiSettings, c -from ._model_contract import TestsFlextApiModelContract +from .model_contract import TestsFlextApiModelContract class TestsFlextApiSmoke(TestsFlextApiModelContract): diff --git a/tests/unit/test_transports_facade_httpx.py b/tests/unit/test_transports_facade_httpx.py new file mode 100644 index 00000000..d0e216df --- /dev/null +++ b/tests/unit/test_transports_facade_httpx.py @@ -0,0 +1,51 @@ +"""The public HTTP class contracts own httpx construction and exception identity.""" + +from __future__ import annotations + +import httpx + +from flext_api import ( + HttpxAsyncClient, + HttpxClient, + HttpxHTTPError, + HttpxHTTPStatusError, + HttpxRequestError, + HttpxResponse, + HttpxTimeoutException, + p, +) + + +class TestsFlextApiHttpxContracts: + """Observable construction and exception contracts of the HTTP owner.""" + + def test_client_primitives_are_the_owner_types(self) -> None: + """Client factories and response type are the httpx owner types.""" + assert HttpxClient is httpx.Client + assert HttpxAsyncClient is httpx.AsyncClient + assert HttpxResponse is httpx.Response + + def test_exception_primitives_are_the_owner_types(self) -> None: + """Exception types match the httpx owner for consumer except clauses.""" + assert HttpxHTTPError is httpx.HTTPError + assert HttpxHTTPStatusError is httpx.HTTPStatusError + assert HttpxRequestError is httpx.RequestError + assert HttpxTimeoutException is httpx.TimeoutException + + def test_conflict_status_is_derived_from_the_owner(self) -> None: + """The conflict constant stays int-typed and equals the httpx owner.""" + conflict = p.Api.Httpx.CONFLICT + assert isinstance(conflict, int) + assert conflict == int(httpx.codes.CONFLICT) + + def test_client_constructs_through_the_public_contract(self) -> None: + """A real client constructs and closes through the owner type.""" + client = HttpxClient(timeout=2.0) + try: + assert not client.is_closed + finally: + client.close() + assert client.is_closed + + +__all__: tuple[str, ...] = ("TestsFlextApiHttpxContracts",) diff --git a/tests/utilities.py b/tests/utilities.py index b344e5d9..a598b4b0 100644 --- a/tests/utilities.py +++ b/tests/utilities.py @@ -12,13 +12,7 @@ class TestsFlextApiUtilities(u): """Test utilities for flext-api — extends flext_api.u.""" - class _RequestUtilities: - """Request-specific test utilities.""" - - class _ResponseUtilities: - """Response-specific test utilities.""" - - class TestsFlextApi(_RequestUtilities, _ResponseUtilities): + class Tests(u): """Test-specific utilities."""