From b510df6e58e663f57cad838e48e14d37f1701a12 Mon Sep 17 00:00:00 2001 From: Adrian Dankiv Date: Sun, 25 Jan 2026 21:48:52 +0100 Subject: [PATCH 1/4] chore: remove unused files --- .../context_logging_20251128202816.py | 0 .../context_logging_20251128202818.py | 97 ------------------- .../context_logging_20251128203653.py | 96 ------------------ .../context_logging_20251128203946.py | 96 ------------------ 4 files changed, 289 deletions(-) delete mode 100644 .history/fastapi_request_context/adapters/context_logging_20251128202816.py delete mode 100644 .history/fastapi_request_context/adapters/context_logging_20251128202818.py delete mode 100644 .history/fastapi_request_context/adapters/context_logging_20251128203653.py delete mode 100644 .history/fastapi_request_context/adapters/context_logging_20251128203946.py diff --git a/.history/fastapi_request_context/adapters/context_logging_20251128202816.py b/.history/fastapi_request_context/adapters/context_logging_20251128202816.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/fastapi_request_context/adapters/context_logging_20251128202818.py b/.history/fastapi_request_context/adapters/context_logging_20251128202818.py deleted file mode 100644 index dd7c56a..0000000 --- a/.history/fastapi_request_context/adapters/context_logging_20251128202818.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Context adapter using context-logging library.""" - -from typing import Any - - -class ContextLoggingAdapter: - """Context adapter using the context-logging library. - - This adapter integrates with the context-logging library for automatic - context injection into log records. Requires the optional dependency: - - pip install fastapi-request-context[context-logging] - - Benefits over ContextVarsAdapter: - - Automatic injection into log records - - Thread-safe with copy-on-write semantics - - Built-in support for nested contexts - - Example: - >>> from fastapi_request_context import RequestContextMiddleware, RequestContextConfig - >>> from fastapi_request_context.adapters import ContextLoggingAdapter - >>> - >>> config = RequestContextConfig(context_adapter=ContextLoggingAdapter()) - >>> app = RequestContextMiddleware(app, config=config) - - Raises: - ImportError: If context-logging is not installed. - """ - - def __init__(self) -> None: - """Initialize the adapter. - - Raises: - ImportError: If context-logging is not installed. - """ - try: - import context_logging # noqa: F401 - except ImportError as e: - msg = ( - "context-logging is required for ContextLoggingAdapter. " - "Install with: pip install fastapi-request-context[context-logging]" - ) - raise ImportError(msg) from e - - def set_value(self, key: str, value: Any) -> None: - """Set a context value. - - Args: - key: The context key. - value: The value to store. - """ - import context_logging - - context_logging.set_logging_var(key, value) - - def get_value(self, key: str) -> Any: - """Get a context value. - - Args: - key: The context key to retrieve. - - Returns: - The stored value, or None if not set. - """ - import context_logging - - return context_logging.get_logging_var(key) - - def get_all(self) -> dict[str, Any]: - """Get all context values. - - Returns: - A copy of all stored context key-value pairs. - """ - import context_logging - - return dict(context_logging.get_logging_context()) - - def enter_context(self, initial_values: dict[str, Any]) -> None: - """Enter a new context scope with initial values. - - Args: - initial_values: Initial context values to set. - """ - import context_logging - - for key, value in initial_values.items(): - context_logging.set_logging_var(key, value) - - def exit_context(self) -> None: - """Exit the current context scope. - - Note: context-logging manages its own cleanup via context managers, - so this is a no-op. The context is automatically cleaned up when - the async context exits. - """ - # context-logging handles cleanup automatically diff --git a/.history/fastapi_request_context/adapters/context_logging_20251128203653.py b/.history/fastapi_request_context/adapters/context_logging_20251128203653.py deleted file mode 100644 index 6f04e7d..0000000 --- a/.history/fastapi_request_context/adapters/context_logging_20251128203653.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Context adapter using context-logging library.""" - -from typing import Any - - -class ContextLoggingAdapter: - """Context adapter using the context-logging library. - - This adapter integrates with the context-logging library for automatic - context injection into log records. Requires the optional dependency: - - pip install fastapi-request-context[context-logging] - - Benefits over ContextVarsAdapter: - - Automatic injection into log records - - Thread-safe with copy-on-write semantics - - Built-in support for nested contexts - - Example: - >>> from fastapi_request_context import RequestContextMiddleware, RequestContextConfig - >>> from fastapi_request_context.adapters import ContextLoggingAdapter - >>> - >>> config = RequestContextConfig(context_adapter=ContextLoggingAdapter()) - >>> app = RequestContextMiddleware(app, config=config) - - Raises: - ImportError: If context-logging is not installed. - """ - - def __init__(self) -> None: - """Initialize the adapter. - - Raises: - ImportError: If context-logging is not installed. - """ - try: - from context_logging import current_context # noqa: F401 - except ImportError as e: - msg = ( - "context-logging is required for ContextLoggingAdapter. " - "Install with: pip install fastapi-request-context[context-logging]" - ) - raise ImportError(msg) from e - - self._context_manager: Any = None - - def set_value(self, key: str, value: Any) -> None: - """Set a context value. - - Args: - key: The context key. - value: The value to store. - """ - from context_logging import current_context - - current_context[key] = value - - def get_value(self, key: str) -> Any: - """Get a context value. - - Args: - key: The context key to retrieve. - - Returns: - The stored value, or None if not set. - """ - from context_logging import current_context - - return current_context.get(key) - - def get_all(self) -> dict[str, Any]: - """Get all context values. - - Returns: - A copy of all stored context key-value pairs. - """ - from context_logging import current_context - - return dict(current_context) - - def enter_context(self, initial_values: dict[str, Any]) -> None: - """Enter a new context scope with initial values. - - Args: - initial_values: Initial context values to set. - """ - from context_logging import Context - - self._context_manager = Context(**initial_values) - self._context_manager.__enter__() - - def exit_context(self) -> None: - """Exit the current context scope.""" - if self._context_manager is not None: - self._context_manager.__exit__(None, None, None) - self._context_manager = None diff --git a/.history/fastapi_request_context/adapters/context_logging_20251128203946.py b/.history/fastapi_request_context/adapters/context_logging_20251128203946.py deleted file mode 100644 index c1dadc7..0000000 --- a/.history/fastapi_request_context/adapters/context_logging_20251128203946.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Context adapter using context-logging library.""" - -from typing import Any - - -class ContextLoggingAdapter: - """Context adapter using the context-logging library. - - This adapter integrates with the context-logging library for automatic - context injection into log records. Requires the optional dependency: - - pip install fastapi-request-context[context-logging] - - Benefits over ContextVarsAdapter: - - Automatic injection into log records - - Thread-safe with copy-on-write semantics - - Built-in support for nested contexts - - Example: - >>> from fastapi_request_context import RequestContextMiddleware, RequestContextConfig - >>> from fastapi_request_context.adapters import ContextLoggingAdapter - >>> - >>> config = RequestContextConfig(context_adapter=ContextLoggingAdapter()) - >>> app = RequestContextMiddleware(app, config=config) - - Raises: - ImportError: If context-logging is not installed. - """ - - def __init__(self) -> None: - """Initialize the adapter. - - Raises: - ImportError: If context-logging is not installed. - """ - try: - from context_logging import current_context # noqa: F401, PLC0415 - except ImportError as e: - msg = ( - "context-logging is required for ContextLoggingAdapter. " - "Install with: pip install fastapi-request-context[context-logging]" - ) - raise ImportError(msg) from e - - self._context_manager: Any = None - - def set_value(self, key: str, value: Any) -> None: # noqa: ANN401 - """Set a context value. - - Args: - key: The context key. - value: The value to store. - """ - from context_logging import current_context # noqa: PLC0415 - - current_context[key] = value - - def get_value(self, key: str) -> Any: # noqa: ANN401 - """Get a context value. - - Args: - key: The context key to retrieve. - - Returns: - The stored value, or None if not set. - """ - from context_logging import current_context # noqa: PLC0415 - - return current_context.get(key) - - def get_all(self) -> dict[str, Any]: - """Get all context values. - - Returns: - A copy of all stored context key-value pairs. - """ - from context_logging import current_context # noqa: PLC0415 - - return dict(current_context) - - def enter_context(self, initial_values: dict[str, Any]) -> None: - """Enter a new context scope with initial values. - - Args: - initial_values: Initial context values to set. - """ - from context_logging import Context # noqa: PLC0415 - - self._context_manager = Context(**initial_values) - self._context_manager.__enter__() - - def exit_context(self) -> None: - """Exit the current context scope.""" - if self._context_manager is not None: - self._context_manager.__exit__(None, None, None) - self._context_manager = None From ee0f9e1b295b2659da808fe479c22eb04b852d98 Mon Sep 17 00:00:00 2001 From: Adrian Dankiv Date: Sun, 25 Jan 2026 21:49:38 +0100 Subject: [PATCH 2/4] feat: add configuration options to ContextLoggingAdapter Expose context-logging's Context parameters (name, log_execution_time, fill_exception_context) to allow users to customize context behavior. --- .../adapters/context_logging.py | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/fastapi_request_context/adapters/context_logging.py b/fastapi_request_context/adapters/context_logging.py index 9adb91c..926f4ae 100644 --- a/fastapi_request_context/adapters/context_logging.py +++ b/fastapi_request_context/adapters/context_logging.py @@ -20,16 +20,32 @@ class ContextLoggingAdapter: >>> from fastapi_request_context import RequestContextMiddleware, RequestContextConfig >>> from fastapi_request_context.adapters import ContextLoggingAdapter >>> - >>> config = RequestContextConfig(context_adapter=ContextLoggingAdapter()) + >>> adapter = ContextLoggingAdapter( + ... name="request_context", + ... log_execution_time=True, + ... fill_exception_context=True + ... ) + >>> config = RequestContextConfig(context_adapter=adapter) >>> app = RequestContextMiddleware(app, config=config) Raises: ImportError: If context-logging is not installed. """ - def __init__(self) -> None: + def __init__( + self, + name: str | None = None, + *, + log_execution_time: bool | None = None, + fill_exception_context: bool | None = None, + ) -> None: """Initialize the adapter. + Args: + name: Optional name for the context. + log_execution_time: Whether to log execution time for the context. + fill_exception_context: Whether to fill exception context automatically. + Raises: ImportError: If context-logging is not installed. """ @@ -42,6 +58,9 @@ def __init__(self) -> None: ) raise ImportError(msg) from e + self._name = name + self._log_execution_time = log_execution_time + self._fill_exception_context = fill_exception_context self._context_manager: Any = None def set_value(self, key: str, value: Any) -> None: # noqa: ANN401 @@ -86,7 +105,11 @@ def __enter__(self) -> Self: """ from context_logging import Context # noqa: PLC0415 - self._context_manager = Context() + self._context_manager = Context( + self._name, + log_execution_time=self._log_execution_time, + fill_exception_context=self._fill_exception_context, + ) self._context_manager.__enter__() return self From f656b0f4c79b23cc8ae0f6493c484ec332c9b901 Mon Sep 17 00:00:00 2001 From: Adrian Dankiv Date: Sun, 25 Jan 2026 22:22:10 +0100 Subject: [PATCH 3/4] chore: remove unused files --- .history/.githooks/pre-commit_20251128213641 | 0 .history/.githooks/pre-commit_20251128213642 | 8 - .history/.githooks/pre-push_20251128213642 | 0 .history/.githooks/pre-push_20251128213643 | 5 - .../.github/workflows/ci_20251128203410.yaml | 0 .../.github/workflows/ci_20251128203411.yaml | 94 ----- .../.github/workflows/ci_20251128212404.yaml | 94 ----- .../.github/workflows/ci_20251128213730.yaml | 122 ------ .../workflows/release_20251128203416.yaml | 0 .../workflows/release_20251128203417.yaml | 57 --- .../workflows/release_20251128213749.yaml | 37 -- .history/.gitignore_20251128202717 | 0 .history/.gitignore_20251128202718 | 152 -------- .history/.python-version_20251128202709 | 0 .history/.python-version_20251128202710 | 1 - .history/CHANGELOG_20251128213855.md | 0 .history/CHANGELOG_20251128213856.md | 13 - .history/LICENSE_20251128202720 | 0 .history/LICENSE_20251128202721 | 21 - .history/Makefile_20251128202724 | 0 .history/Makefile_20251128202725 | 21 - .history/Makefile_20251128213658 | 26 -- .history/Makefile_20251128214628 | 26 -- .history/Makefile_20251128214634 | 26 -- .history/README_20251128203457.md | 0 .history/README_20251128203458.md | 286 -------------- .history/README_20251128211252.md | 291 -------------- .history/README_20251128211308.md | 305 --------------- .history/README_20251128211324.md | 304 --------------- .history/README_20251128211331.md | 304 --------------- .../examples/basic_usage_20251128203309.py | 0 .../examples/basic_usage_20251128203310.py | 46 --- .../examples/custom_adapter_20251128203330.py | 0 .../examples/custom_adapter_20251128203331.py | 81 ---- .../examples/custom_fields_20251128203320.py | 0 .../examples/custom_fields_20251128203321.py | 77 ---- .../examples/custom_fields_20251128203530.py | 85 ----- .../examples/custom_fields_20251128203539.py | 86 ----- .../examples/custom_fields_20251128204054.py | 86 ----- .../examples/custom_fields_20251128204140.py | 86 ----- .../examples/custom_fields_20251128211931.py | 79 ---- .../examples/custom_fields_20251128211939.py | 77 ---- .../logging_integration_20251128203342.py | 0 .../logging_integration_20251128203343.py | 99 ----- .../logging_integration_20251128211235.py | 99 ----- .../examples/validation_20251128203355.py | 0 .../examples/validation_20251128203356.py | 92 ----- .../__init___20251128203030.py | 0 .../__init___20251128203031.py | 60 --- .../__init___20251128204025.py | 56 --- .../__init___20251128211134.py | 52 --- .../adapters/__init___20251128202745.py | 0 .../adapters/__init___20251128202746.py | 11 - .../adapters/base_20251128202756.py | 0 .../adapters/base_20251128202757.py | 80 ---- .../adapters/base_20251128204014.py | 80 ---- .../adapters/contextvars_20251128202806.py | 0 .../adapters/contextvars_20251128202807.py | 77 ---- .../adapters/contextvars_20251128203719.py | 78 ---- .../adapters/contextvars_20251128203915.py | 78 ---- .../adapters/contextvars_20251128203930.py | 85 ----- .../config_20251128202827.py | 0 .../config_20251128202828.py | 65 ---- .../context_20251128202838.py | 0 .../context_20251128202839.py | 93 ----- .../context_20251128203948.py | 93 ----- .../fields_20251128202743.py | 0 .../fields_20251128202744.py | 25 -- .../fields_20251128203522.py | 35 -- .../fields_20251128211530.py | 33 -- .../fields_20251128211921.py | 25 -- .../formatters/__init___20251128202919.py | 0 .../formatters/__init___20251128202921.py | 9 - .../formatters/__init___20251128211213.py | 9 - .../formatters/json_20251128202933.py | 0 .../formatters/json_20251128202934.py | 99 ----- .../formatters/json_20251128203951.py | 99 ----- .../formatters/json_20251128204234.py | 101 ----- .../formatters/json_20251128211547.py | 101 ----- .../formatters/local_20251128202947.py | 0 .../formatters/local_20251128202948.py | 103 ----- .../formatters/local_20251128204231.py | 103 ----- .../formatters/local_20251128204402.py | 103 ----- .../formatters/local_20251128204418.py | 103 ----- .../formatters/simple_20251128204443.py | 103 ----- .../formatters/simple_20251128211203.py | 105 ----- .../middleware_20251128202910.py | 0 .../middleware_20251128202911.py | 185 --------- .../middleware_20251128211125.py | 162 -------- .../types_20251128202738.py | 0 .../types_20251128202739.py | 25 -- .../validation_20251128203015.py | 0 .../validation_20251128203016.py | 175 --------- .../validation_20251128203958.py | 176 --------- .../validation_20251128204121.py | 174 --------- .../validation_20251128204241.py | 175 --------- .../validation_20251128204255.py | 175 --------- .../validation_20251128211403.py | 175 --------- .../validation_20251128211747.py | 175 --------- .history/pyproject_20251128202707.toml | 0 .history/pyproject_20251128202708.toml | 265 ------------- .history/pyproject_20251128203841.toml | 268 ------------- .history/pyproject_20251128204045.toml | 268 ------------- .history/pyproject_20251128204103.toml | 271 ------------- .history/pyproject_20251128204132.toml | 271 ------------- .history/pyproject_20251128204338.toml | 273 ------------- .history/pyproject_20251128204455.toml | 274 ------------- .history/pyproject_20251128211609.toml | 275 ------------- .history/pyproject_20251128211628.toml | 288 -------------- .history/pyproject_20251128211655.toml | 294 -------------- .history/pyproject_20251128211859.toml | 298 --------------- .history/pyproject_20251128211907.toml | 298 --------------- .history/pyproject_20251128212355.toml | 297 -------------- .history/pyproject_20251128212422.toml | 297 -------------- .history/pyproject_20251128213813.toml | 304 --------------- .history/pyproject_20251128213826.toml | 305 --------------- .history/pyproject_20251128213847.toml | 281 -------------- .history/tests/__init___20251128203038.py | 0 .history/tests/__init___20251128204032.py | 1 - .../tests/adapters/__init___20251128212009.py | 0 .../tests/adapters/__init___20251128212010.py | 1 - .../test_context_logging_20251128212029.py | 0 .../test_context_logging_20251128212031.py | 88 ----- .../test_contextvars_20251128212021.py | 0 .../test_contextvars_20251128212022.py | 99 ----- .history/tests/conftest_20251128203042.py | 0 .history/tests/conftest_20251128203043.py | 27 -- .history/tests/conftest_20251128204315.py | 30 -- .../formatters/__init___20251128212042.py | 0 .../formatters/__init___20251128212044.py | 1 - .../test_integration_20251128212119.py | 0 .../test_integration_20251128212120.py | 46 --- .../formatters/test_json_20251128212057.py | 0 .../formatters/test_json_20251128212058.py | 137 ------- .../formatters/test_simple_20251128212115.py | 0 .../formatters/test_simple_20251128212116.py | 189 --------- .../tests/test_adapters_20251128203159.py | 0 .../tests/test_adapters_20251128203200.py | 166 -------- .../tests/test_adapters_20251128203757.py | 168 -------- .../tests/test_adapters_20251128203814.py | 172 --------- .../tests/test_adapters_20251128211715.py | 180 --------- .../tests/test_adapters_20251128211725.py | 186 --------- .history/tests/test_context_20251128203140.py | 0 .history/tests/test_context_20251128203141.py | 182 --------- .history/tests/test_context_20251128203532.py | 190 --------- .history/tests/test_context_20251128203541.py | 191 --------- .history/tests/test_context_20251128211433.py | 193 ---------- .history/tests/test_context_20251128211447.py | 201 ---------- .history/tests/test_context_20251128211949.py | 191 --------- .../tests/test_formatters_20251128203233.py | 0 .../tests/test_formatters_20251128203234.py | 327 ---------------- .../tests/test_formatters_20251128203848.py | 325 ---------------- .../tests/test_formatters_20251128204354.py | 328 ---------------- .../tests/test_formatters_20251128211220.py | 328 ---------------- .../tests/test_formatters_20251128211227.py | 328 ---------------- .../tests/test_formatters_20251128211505.py | 361 ------------------ .../tests/test_middleware_20251128203118.py | 0 .../tests/test_middleware_20251128203119.py | 295 -------------- .../tests/test_middleware_20251128211424.py | 322 ---------------- .../tests/test_middleware_20251128211814.py | 325 ---------------- .../tests/test_validation_20251128203254.py | 0 .../tests/test_validation_20251128203255.py | 199 ---------- .../tests/test_validation_20251128203733.py | 203 ---------- .../validation/__init___20251128212141.py | 0 .../validation/__init___20251128212142.py | 1 - .../test_dependencies_20251128212150.py | 0 .../test_dependencies_20251128212151.py | 42 -- .../test_is_async_20251128212146.py | 0 .../test_is_async_20251128212147.py | 46 --- .../validation/test_routes_20251128212203.py | 0 .../validation/test_routes_20251128212204.py | 122 ------ 171 files changed, 17865 deletions(-) delete mode 100644 .history/.githooks/pre-commit_20251128213641 delete mode 100644 .history/.githooks/pre-commit_20251128213642 delete mode 100644 .history/.githooks/pre-push_20251128213642 delete mode 100644 .history/.githooks/pre-push_20251128213643 delete mode 100644 .history/.github/workflows/ci_20251128203410.yaml delete mode 100644 .history/.github/workflows/ci_20251128203411.yaml delete mode 100644 .history/.github/workflows/ci_20251128212404.yaml delete mode 100644 .history/.github/workflows/ci_20251128213730.yaml delete mode 100644 .history/.github/workflows/release_20251128203416.yaml delete mode 100644 .history/.github/workflows/release_20251128203417.yaml delete mode 100644 .history/.github/workflows/release_20251128213749.yaml delete mode 100644 .history/.gitignore_20251128202717 delete mode 100644 .history/.gitignore_20251128202718 delete mode 100644 .history/.python-version_20251128202709 delete mode 100644 .history/.python-version_20251128202710 delete mode 100644 .history/CHANGELOG_20251128213855.md delete mode 100644 .history/CHANGELOG_20251128213856.md delete mode 100644 .history/LICENSE_20251128202720 delete mode 100644 .history/LICENSE_20251128202721 delete mode 100644 .history/Makefile_20251128202724 delete mode 100644 .history/Makefile_20251128202725 delete mode 100644 .history/Makefile_20251128213658 delete mode 100644 .history/Makefile_20251128214628 delete mode 100644 .history/Makefile_20251128214634 delete mode 100644 .history/README_20251128203457.md delete mode 100644 .history/README_20251128203458.md delete mode 100644 .history/README_20251128211252.md delete mode 100644 .history/README_20251128211308.md delete mode 100644 .history/README_20251128211324.md delete mode 100644 .history/README_20251128211331.md delete mode 100644 .history/examples/basic_usage_20251128203309.py delete mode 100644 .history/examples/basic_usage_20251128203310.py delete mode 100644 .history/examples/custom_adapter_20251128203330.py delete mode 100644 .history/examples/custom_adapter_20251128203331.py delete mode 100644 .history/examples/custom_fields_20251128203320.py delete mode 100644 .history/examples/custom_fields_20251128203321.py delete mode 100644 .history/examples/custom_fields_20251128203530.py delete mode 100644 .history/examples/custom_fields_20251128203539.py delete mode 100644 .history/examples/custom_fields_20251128204054.py delete mode 100644 .history/examples/custom_fields_20251128204140.py delete mode 100644 .history/examples/custom_fields_20251128211931.py delete mode 100644 .history/examples/custom_fields_20251128211939.py delete mode 100644 .history/examples/logging_integration_20251128203342.py delete mode 100644 .history/examples/logging_integration_20251128203343.py delete mode 100644 .history/examples/logging_integration_20251128211235.py delete mode 100644 .history/examples/validation_20251128203355.py delete mode 100644 .history/examples/validation_20251128203356.py delete mode 100644 .history/fastapi_request_context/__init___20251128203030.py delete mode 100644 .history/fastapi_request_context/__init___20251128203031.py delete mode 100644 .history/fastapi_request_context/__init___20251128204025.py delete mode 100644 .history/fastapi_request_context/__init___20251128211134.py delete mode 100644 .history/fastapi_request_context/adapters/__init___20251128202745.py delete mode 100644 .history/fastapi_request_context/adapters/__init___20251128202746.py delete mode 100644 .history/fastapi_request_context/adapters/base_20251128202756.py delete mode 100644 .history/fastapi_request_context/adapters/base_20251128202757.py delete mode 100644 .history/fastapi_request_context/adapters/base_20251128204014.py delete mode 100644 .history/fastapi_request_context/adapters/contextvars_20251128202806.py delete mode 100644 .history/fastapi_request_context/adapters/contextvars_20251128202807.py delete mode 100644 .history/fastapi_request_context/adapters/contextvars_20251128203719.py delete mode 100644 .history/fastapi_request_context/adapters/contextvars_20251128203915.py delete mode 100644 .history/fastapi_request_context/adapters/contextvars_20251128203930.py delete mode 100644 .history/fastapi_request_context/config_20251128202827.py delete mode 100644 .history/fastapi_request_context/config_20251128202828.py delete mode 100644 .history/fastapi_request_context/context_20251128202838.py delete mode 100644 .history/fastapi_request_context/context_20251128202839.py delete mode 100644 .history/fastapi_request_context/context_20251128203948.py delete mode 100644 .history/fastapi_request_context/fields_20251128202743.py delete mode 100644 .history/fastapi_request_context/fields_20251128202744.py delete mode 100644 .history/fastapi_request_context/fields_20251128203522.py delete mode 100644 .history/fastapi_request_context/fields_20251128211530.py delete mode 100644 .history/fastapi_request_context/fields_20251128211921.py delete mode 100644 .history/fastapi_request_context/formatters/__init___20251128202919.py delete mode 100644 .history/fastapi_request_context/formatters/__init___20251128202921.py delete mode 100644 .history/fastapi_request_context/formatters/__init___20251128211213.py delete mode 100644 .history/fastapi_request_context/formatters/json_20251128202933.py delete mode 100644 .history/fastapi_request_context/formatters/json_20251128202934.py delete mode 100644 .history/fastapi_request_context/formatters/json_20251128203951.py delete mode 100644 .history/fastapi_request_context/formatters/json_20251128204234.py delete mode 100644 .history/fastapi_request_context/formatters/json_20251128211547.py delete mode 100644 .history/fastapi_request_context/formatters/local_20251128202947.py delete mode 100644 .history/fastapi_request_context/formatters/local_20251128202948.py delete mode 100644 .history/fastapi_request_context/formatters/local_20251128204231.py delete mode 100644 .history/fastapi_request_context/formatters/local_20251128204402.py delete mode 100644 .history/fastapi_request_context/formatters/local_20251128204418.py delete mode 100644 .history/fastapi_request_context/formatters/simple_20251128204443.py delete mode 100644 .history/fastapi_request_context/formatters/simple_20251128211203.py delete mode 100644 .history/fastapi_request_context/middleware_20251128202910.py delete mode 100644 .history/fastapi_request_context/middleware_20251128202911.py delete mode 100644 .history/fastapi_request_context/middleware_20251128211125.py delete mode 100644 .history/fastapi_request_context/types_20251128202738.py delete mode 100644 .history/fastapi_request_context/types_20251128202739.py delete mode 100644 .history/fastapi_request_context/validation_20251128203015.py delete mode 100644 .history/fastapi_request_context/validation_20251128203016.py delete mode 100644 .history/fastapi_request_context/validation_20251128203958.py delete mode 100644 .history/fastapi_request_context/validation_20251128204121.py delete mode 100644 .history/fastapi_request_context/validation_20251128204241.py delete mode 100644 .history/fastapi_request_context/validation_20251128204255.py delete mode 100644 .history/fastapi_request_context/validation_20251128211403.py delete mode 100644 .history/fastapi_request_context/validation_20251128211747.py delete mode 100644 .history/pyproject_20251128202707.toml delete mode 100644 .history/pyproject_20251128202708.toml delete mode 100644 .history/pyproject_20251128203841.toml delete mode 100644 .history/pyproject_20251128204045.toml delete mode 100644 .history/pyproject_20251128204103.toml delete mode 100644 .history/pyproject_20251128204132.toml delete mode 100644 .history/pyproject_20251128204338.toml delete mode 100644 .history/pyproject_20251128204455.toml delete mode 100644 .history/pyproject_20251128211609.toml delete mode 100644 .history/pyproject_20251128211628.toml delete mode 100644 .history/pyproject_20251128211655.toml delete mode 100644 .history/pyproject_20251128211859.toml delete mode 100644 .history/pyproject_20251128211907.toml delete mode 100644 .history/pyproject_20251128212355.toml delete mode 100644 .history/pyproject_20251128212422.toml delete mode 100644 .history/pyproject_20251128213813.toml delete mode 100644 .history/pyproject_20251128213826.toml delete mode 100644 .history/pyproject_20251128213847.toml delete mode 100644 .history/tests/__init___20251128203038.py delete mode 100644 .history/tests/__init___20251128204032.py delete mode 100644 .history/tests/adapters/__init___20251128212009.py delete mode 100644 .history/tests/adapters/__init___20251128212010.py delete mode 100644 .history/tests/adapters/test_context_logging_20251128212029.py delete mode 100644 .history/tests/adapters/test_context_logging_20251128212031.py delete mode 100644 .history/tests/adapters/test_contextvars_20251128212021.py delete mode 100644 .history/tests/adapters/test_contextvars_20251128212022.py delete mode 100644 .history/tests/conftest_20251128203042.py delete mode 100644 .history/tests/conftest_20251128203043.py delete mode 100644 .history/tests/conftest_20251128204315.py delete mode 100644 .history/tests/formatters/__init___20251128212042.py delete mode 100644 .history/tests/formatters/__init___20251128212044.py delete mode 100644 .history/tests/formatters/test_integration_20251128212119.py delete mode 100644 .history/tests/formatters/test_integration_20251128212120.py delete mode 100644 .history/tests/formatters/test_json_20251128212057.py delete mode 100644 .history/tests/formatters/test_json_20251128212058.py delete mode 100644 .history/tests/formatters/test_simple_20251128212115.py delete mode 100644 .history/tests/formatters/test_simple_20251128212116.py delete mode 100644 .history/tests/test_adapters_20251128203159.py delete mode 100644 .history/tests/test_adapters_20251128203200.py delete mode 100644 .history/tests/test_adapters_20251128203757.py delete mode 100644 .history/tests/test_adapters_20251128203814.py delete mode 100644 .history/tests/test_adapters_20251128211715.py delete mode 100644 .history/tests/test_adapters_20251128211725.py delete mode 100644 .history/tests/test_context_20251128203140.py delete mode 100644 .history/tests/test_context_20251128203141.py delete mode 100644 .history/tests/test_context_20251128203532.py delete mode 100644 .history/tests/test_context_20251128203541.py delete mode 100644 .history/tests/test_context_20251128211433.py delete mode 100644 .history/tests/test_context_20251128211447.py delete mode 100644 .history/tests/test_context_20251128211949.py delete mode 100644 .history/tests/test_formatters_20251128203233.py delete mode 100644 .history/tests/test_formatters_20251128203234.py delete mode 100644 .history/tests/test_formatters_20251128203848.py delete mode 100644 .history/tests/test_formatters_20251128204354.py delete mode 100644 .history/tests/test_formatters_20251128211220.py delete mode 100644 .history/tests/test_formatters_20251128211227.py delete mode 100644 .history/tests/test_formatters_20251128211505.py delete mode 100644 .history/tests/test_middleware_20251128203118.py delete mode 100644 .history/tests/test_middleware_20251128203119.py delete mode 100644 .history/tests/test_middleware_20251128211424.py delete mode 100644 .history/tests/test_middleware_20251128211814.py delete mode 100644 .history/tests/test_validation_20251128203254.py delete mode 100644 .history/tests/test_validation_20251128203255.py delete mode 100644 .history/tests/test_validation_20251128203733.py delete mode 100644 .history/tests/validation/__init___20251128212141.py delete mode 100644 .history/tests/validation/__init___20251128212142.py delete mode 100644 .history/tests/validation/test_dependencies_20251128212150.py delete mode 100644 .history/tests/validation/test_dependencies_20251128212151.py delete mode 100644 .history/tests/validation/test_is_async_20251128212146.py delete mode 100644 .history/tests/validation/test_is_async_20251128212147.py delete mode 100644 .history/tests/validation/test_routes_20251128212203.py delete mode 100644 .history/tests/validation/test_routes_20251128212204.py diff --git a/.history/.githooks/pre-commit_20251128213641 b/.history/.githooks/pre-commit_20251128213641 deleted file mode 100644 index e69de29..0000000 diff --git a/.history/.githooks/pre-commit_20251128213642 b/.history/.githooks/pre-commit_20251128213642 deleted file mode 100644 index 3819438..0000000 --- a/.history/.githooks/pre-commit_20251128213642 +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -set -e - -# Only run lint if we're not in a merge state. -if [ ! -f "${GIT_DIR}/MERGE_HEAD" ]; then - make lint -fi diff --git a/.history/.githooks/pre-push_20251128213642 b/.history/.githooks/pre-push_20251128213642 deleted file mode 100644 index e69de29..0000000 diff --git a/.history/.githooks/pre-push_20251128213643 b/.history/.githooks/pre-push_20251128213643 deleted file mode 100644 index d16c520..0000000 --- a/.history/.githooks/pre-push_20251128213643 +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -set -e - -make lint test diff --git a/.history/.github/workflows/ci_20251128203410.yaml b/.history/.github/workflows/ci_20251128203410.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/.history/.github/workflows/ci_20251128203411.yaml b/.history/.github/workflows/ci_20251128203411.yaml deleted file mode 100644 index 988a9c2..0000000 --- a/.history/.github/workflows/ci_20251128203411.yaml +++ /dev/null @@ -1,94 +0,0 @@ -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - version: "latest" - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install dependencies - run: uv sync --all-extras - - - name: Run ruff check - run: uv run ruff check fastapi_request_context tests/ examples/ - - - name: Run ruff format check - run: uv run ruff format --check fastapi_request_context tests/ examples/ - - - name: Run mypy - run: uv run mypy fastapi_request_context tests/ - - test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.10", "3.11", "3.12"] - fastapi-version: ["0.100.0", "0.110.0", "0.115.0"] - - steps: - - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - version: "latest" - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - uv sync --all-extras - uv pip install "fastapi==${{ matrix.fastapi-version }}" - - - name: Run tests - run: uv run pytest tests/ -v - - coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - version: "latest" - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install dependencies - run: uv sync --all-extras - - - name: Run coverage - run: | - uv run coverage run -m pytest tests/ - uv run coverage xml -o coverage.xml - uv run coverage report --fail-under=90 - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - with: - file: coverage.xml - fail_ci_if_error: false diff --git a/.history/.github/workflows/ci_20251128212404.yaml b/.history/.github/workflows/ci_20251128212404.yaml deleted file mode 100644 index 85ceb55..0000000 --- a/.history/.github/workflows/ci_20251128212404.yaml +++ /dev/null @@ -1,94 +0,0 @@ -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - version: "latest" - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install dependencies - run: uv sync --all-extras - - - name: Run ruff check - run: uv run ruff check fastapi_request_context tests/ examples/ - - - name: Run ruff format check - run: uv run ruff format --check fastapi_request_context tests/ examples/ - - - name: Run mypy - run: uv run mypy fastapi_request_context tests/ - - test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.12", "3.13"] - fastapi-version: ["0.100.0", "0.110.0", "0.115.0"] - - steps: - - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - version: "latest" - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - uv sync --all-extras - uv pip install "fastapi==${{ matrix.fastapi-version }}" - - - name: Run tests - run: uv run pytest tests/ -v - - coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - version: "latest" - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install dependencies - run: uv sync --all-extras - - - name: Run coverage - run: | - uv run coverage run -m pytest tests/ - uv run coverage xml -o coverage.xml - uv run coverage report --fail-under=90 - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - with: - file: coverage.xml - fail_ci_if_error: false diff --git a/.history/.github/workflows/ci_20251128213730.yaml b/.history/.github/workflows/ci_20251128213730.yaml deleted file mode 100644 index 3e8b35a..0000000 --- a/.history/.github/workflows/ci_20251128213730.yaml +++ /dev/null @@ -1,122 +0,0 @@ -name: CI - -on: - push: - branches: - - main - pull_request: - branches: - - '*' - -jobs: - test: - runs-on: ubuntu-latest - strategy: - fail-fast: true - matrix: - py: - - "3.13" - - "3.12" - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Set up Python ${{ matrix.py }} - run: uv python install ${{ matrix.py }} - - - name: Install tox - run: uv tool install tox --with tox-uv - - - name: Setup test suite - run: tox -vv --notest - - - name: Run test suite - run: tox --skip-pkg-install - - lint: - name: lint (3.13) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Set up Python 3.13 - run: uv python install 3.13 - - - name: Install tox - run: uv tool install tox --with tox-uv - - - name: Setup test suite - run: tox -vv --notest - - - name: Run lint - run: tox -e lint - - coverage: - name: coverage (3.13) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Set up Python 3.13 - run: uv python install 3.13 - - - name: Install tox - run: uv tool install tox --with tox-uv - - - name: Setup test suite - run: tox -vv --notest - - - name: Run coverage - run: tox -e coverage - - - uses: actions/upload-artifact@v4 - with: - name: coverage_report - path: ./reports - - coverage-report: - name: coverage report - needs: - - coverage - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - uses: actions/download-artifact@v4 - with: - name: coverage_report - path: reports - - - name: Produce the coverage report - uses: insightsengineering/coverage-action@v2 - with: - path: ./reports/coverage.xml - threshold: 90 - fail: false - publish: true - diff: true - diff-branch: main - diff-storage: _xml_coverage_reports - coverage-reduction-failure: false - togglable-report: true diff --git a/.history/.github/workflows/release_20251128203416.yaml b/.history/.github/workflows/release_20251128203416.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/.history/.github/workflows/release_20251128203417.yaml b/.history/.github/workflows/release_20251128203417.yaml deleted file mode 100644 index 860f154..0000000 --- a/.history/.github/workflows/release_20251128203417.yaml +++ /dev/null @@ -1,57 +0,0 @@ -name: Release - -on: - push: - branches: [main] - -jobs: - release: - runs-on: ubuntu-latest - concurrency: release - permissions: - id-token: write - contents: write - - environment: - name: release - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - version: "latest" - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install dependencies - run: uv sync --all-extras - - - name: Python Semantic Release - id: release - uses: python-semantic-release/python-semantic-release@v9.15.1 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - - - name: Build package - if: steps.release.outputs.released == 'true' - run: uv build - - - name: Publish to PyPI - if: steps.release.outputs.released == 'true' - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_TOKEN }} - - - name: Publish to GitHub Releases - if: steps.release.outputs.released == 'true' - uses: python-semantic-release/upload-to-gh-release@main - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - tag: ${{ steps.release.outputs.tag }} diff --git a/.history/.github/workflows/release_20251128213749.yaml b/.history/.github/workflows/release_20251128213749.yaml deleted file mode 100644 index e089633..0000000 --- a/.history/.github/workflows/release_20251128213749.yaml +++ /dev/null @@ -1,37 +0,0 @@ -name: Semantic Release - -on: - workflow_run: - workflows: ["CI"] - branches: [main] - types: - - completed - -jobs: - release: - runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' }} - concurrency: release - environment: release - permissions: - id-token: write - contents: write - - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Setup | Force release branch to be at workflow sha - run: | - git reset --hard ${{ github.sha }} - - - name: Python Semantic Release - id: release - uses: python-semantic-release/python-semantic-release@v10.5.2 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - - - name: Publish to PyPI - if: steps.release.outputs.released == 'true' - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.history/.gitignore_20251128202717 b/.history/.gitignore_20251128202717 deleted file mode 100644 index e69de29..0000000 diff --git a/.history/.gitignore_20251128202718 b/.history/.gitignore_20251128202718 deleted file mode 100644 index 07cc173..0000000 --- a/.history/.gitignore_20251128202718 +++ /dev/null @@ -1,152 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -reports/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -Pipfile.lock - -# poetry -poetry.lock - -# pdm -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582 -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# IDE -.idea/ -.vscode/ -*.swp -*.swo -*~ - -# UV -uv.lock - -# OS -.DS_Store -Thumbs.db diff --git a/.history/.python-version_20251128202709 b/.history/.python-version_20251128202709 deleted file mode 100644 index e69de29..0000000 diff --git a/.history/.python-version_20251128202710 b/.history/.python-version_20251128202710 deleted file mode 100644 index c8cfe39..0000000 --- a/.history/.python-version_20251128202710 +++ /dev/null @@ -1 +0,0 @@ -3.10 diff --git a/.history/CHANGELOG_20251128213855.md b/.history/CHANGELOG_20251128213855.md deleted file mode 100644 index e69de29..0000000 diff --git a/.history/CHANGELOG_20251128213856.md b/.history/CHANGELOG_20251128213856.md deleted file mode 100644 index fdbbf60..0000000 --- a/.history/CHANGELOG_20251128213856.md +++ /dev/null @@ -1,13 +0,0 @@ -# CHANGELOG - -## v0.1.0 (Unreleased) - -### Features - -- Initial release -- Request ID tracking with automatic generation -- Correlation ID support for distributed tracing -- Pluggable context storage adapters (contextvars, context-logging) -- Logging formatters (JSON, Simple) with context integration -- Validation utilities for async routes and dependencies -- Full type hints and documentation diff --git a/.history/LICENSE_20251128202720 b/.history/LICENSE_20251128202720 deleted file mode 100644 index e69de29..0000000 diff --git a/.history/LICENSE_20251128202721 b/.history/LICENSE_20251128202721 deleted file mode 100644 index 6fcfa40..0000000 --- a/.history/LICENSE_20251128202721 +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 Adrian Dankiv - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/.history/Makefile_20251128202724 b/.history/Makefile_20251128202724 deleted file mode 100644 index e69de29..0000000 diff --git a/.history/Makefile_20251128202725 b/.history/Makefile_20251128202725 deleted file mode 100644 index 0713db4..0000000 --- a/.history/Makefile_20251128202725 +++ /dev/null @@ -1,21 +0,0 @@ -.PHONY: lint lint-fix test test-all coverage - -lint: - uv run ruff check fastapi_request_context tests/ examples/ - uv run ruff format --check fastapi_request_context tests/ examples/ - uv run mypy fastapi_request_context tests/ - -lint-fix: - uv run ruff format fastapi_request_context tests/ examples/ - uv run ruff check --fix fastapi_request_context tests/ examples/ - -test: - uv run pytest tests/ -v - -test-all: - uv run tox - -coverage: - uv run coverage run -m pytest tests/ - uv run coverage report - uv run coverage html diff --git a/.history/Makefile_20251128213658 b/.history/Makefile_20251128213658 deleted file mode 100644 index ef95685..0000000 --- a/.history/Makefile_20251128213658 +++ /dev/null @@ -1,26 +0,0 @@ -.PHONY: lint lint-fix test test-all coverage install-githooks - -install-githooks: - @cd .git/hooks; \ - ln -sf ../../.githooks/pre-push .; \ - ln -sf ../../.githooks/pre-commit .; - -lint: - uv run ruff check fastapi_request_context tests/ examples/ - uv run ruff format --check fastapi_request_context tests/ examples/ - uv run mypy fastapi_request_context tests/ - -lint-fix: - uv run ruff format fastapi_request_context tests/ examples/ - uv run ruff check --fix fastapi_request_context tests/ examples/ - -test: - uv run pytest tests/ -v - -test-all: - uv run tox - -coverage: - uv run coverage run -m pytest tests/ - uv run coverage report - uv run coverage html diff --git a/.history/Makefile_20251128214628 b/.history/Makefile_20251128214628 deleted file mode 100644 index 8d14c68..0000000 --- a/.history/Makefile_20251128214628 +++ /dev/null @@ -1,26 +0,0 @@ -.PHONY: lint lint-fix test test-cov test-all install-githooks - -install-githooks: - @cd .git/hooks; \ - ln -sf ../../.githooks/pre-push .; \ - ln -sf ../../.githooks/pre-commit .; - -lint: - uv run ruff check fastapi_request_context tests/ examples/ - uv run ruff format --check fastapi_request_context tests/ examples/ - uv run mypy fastapi_request_context tests/ - -lint-fix: - uv run ruff format fastapi_request_context tests/ examples/ - uv run ruff check --fix fastapi_request_context tests/ examples/ - -test: - uv run pytest tests/ -v - -test-all: - uv run tox - -coverage: - uv run coverage run -m pytest tests/ - uv run coverage report - uv run coverage html diff --git a/.history/Makefile_20251128214634 b/.history/Makefile_20251128214634 deleted file mode 100644 index 0ee605b..0000000 --- a/.history/Makefile_20251128214634 +++ /dev/null @@ -1,26 +0,0 @@ -.PHONY: lint lint-fix test test-cov test-all install-githooks - -install-githooks: - @cd .git/hooks; \ - ln -sf ../../.githooks/pre-push .; \ - ln -sf ../../.githooks/pre-commit .; - -lint: - uv run ruff check fastapi_request_context tests/ examples/ - uv run ruff format --check fastapi_request_context tests/ examples/ - uv run mypy fastapi_request_context tests/ - -lint-fix: - uv run ruff format fastapi_request_context tests/ examples/ - uv run ruff check --fix fastapi_request_context tests/ examples/ - -test: - uv run pytest tests/ -v - -test-all: - uv run tox - -test-cov: - uv run coverage run -m pytest tests/ - uv run coverage report - uv run coverage html diff --git a/.history/README_20251128203457.md b/.history/README_20251128203457.md deleted file mode 100644 index e69de29..0000000 diff --git a/.history/README_20251128203458.md b/.history/README_20251128203458.md deleted file mode 100644 index 470de10..0000000 --- a/.history/README_20251128203458.md +++ /dev/null @@ -1,286 +0,0 @@ -# fastapi-request-context - -[![PyPI version](https://badge.fury.io/py/fastapi-request-context.svg)](https://badge.fury.io/py/fastapi-request-context) -[![Python versions](https://img.shields.io/pypi/pyversions/fastapi-request-context.svg)](https://pypi.org/project/fastapi-request-context/) -[![CI](https://github.com/ADR-007/fastapi-request-context/actions/workflows/ci.yaml/badge.svg)](https://github.com/ADR-007/fastapi-request-context/actions/workflows/ci.yaml) -[![codecov](https://codecov.io/gh/ADR-007/fastapi-request-context/branch/main/graph/badge.svg)](https://codecov.io/gh/ADR-007/fastapi-request-context) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - -FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with first-class logging integration. - -## Features - -- **Automatic request ID generation** - Every request gets a unique ID -- **Correlation ID support** - Accept from header or generate for distributed tracing -- **Response header injection** - Automatically add `X-Request-Id` and `X-Correlation-Id` to responses -- **Pluggable context backends** - Use `contextvars` (default) or `context-logging` -- **Custom context fields** - Extend with your own fields via StrEnum -- **Logging integration** - JSON and human-readable formatters with automatic context injection -- **Validation utilities** - Check that routes and dependencies are async -- **Zero configuration** - Works out of the box with sensible defaults -- **Type-safe** - Full type hints and mypy strict mode - -## Installation - -```bash -# Basic installation -pip install fastapi-request-context - -# With context-logging support -pip install fastapi-request-context[context-logging] - -# With JSON formatter support -pip install fastapi-request-context[json-formatter] - -# All optional dependencies -pip install fastapi-request-context[all] -``` - -Using uv: -```bash -uv add fastapi-request-context -``` - -## Quick Start - -```python -from fastapi import FastAPI -from fastapi_request_context import RequestContextMiddleware - -app = FastAPI() - -# Wrap with middleware - that's it! -app = RequestContextMiddleware(app) -``` - -Every request now has: -- Unique `request_id` (always generated) -- `correlation_id` (from `X-Correlation-Id` header or generated) -- Both added to response headers - -## Usage - -### Access Context Values - -```python -from fastapi_request_context import get_context, get_full_context, StandardContextField - -@app.get("/") -async def root(): - request_id = get_context(StandardContextField.REQUEST_ID) - correlation_id = get_context(StandardContextField.CORRELATION_ID) - - # Or get everything - all_context = get_full_context() - - return {"request_id": request_id} -``` - -### Custom Context Fields - -```python -from enum import StrEnum -from fastapi_request_context import set_context, get_context - -class MyContextField(StrEnum): - USER_ID = "user_id" - ORG_ID = "org_id" - -async def get_current_user(token: str): - user_id = decode_token(token) - set_context(MyContextField.USER_ID, user_id) - return user_id - -@app.get("/me") -async def me(user_id: int = Depends(get_current_user)): - # Context is available throughout the request - return {"user_id": get_context(MyContextField.USER_ID)} -``` - -### Configuration - -```python -from fastapi_request_context import RequestContextMiddleware, RequestContextConfig -from uuid import uuid4 - -config = RequestContextConfig( - # Custom ID generators - request_id_generator=lambda: str(uuid4()), - correlation_id_generator=lambda: str(uuid4()), - - # Custom header names - request_id_header="X-My-Request-Id", - correlation_id_header="X-My-Correlation-Id", - - # Disable response headers - add_response_headers=False, - - # Use context-logging adapter - context_adapter="context_logging", - - # Only process HTTP (not WebSocket) - scope_types={"http"}, -) - -app = RequestContextMiddleware(app, config=config) -``` - -### Logging Integration - -#### JSON Formatter (Production) - -```python -import logging -from fastapi_request_context.formatters import JsonContextFormatter - -handler = logging.StreamHandler() -handler.setFormatter(JsonContextFormatter()) -logging.basicConfig(handlers=[handler], level=logging.INFO) - -# Logs automatically include context: -# {"message": "Processing", "level": "INFO", "request_id": "...", "user_id": 123} -``` - -#### Local Formatter (Development) - -```python -from fastapi_request_context import StandardContextField -from fastapi_request_context.formatters import LocalContextFormatter - -handler = logging.StreamHandler() -handler.setFormatter(LocalContextFormatter( - fmt="%(asctime)s %(levelname)s %(context)s %(message)s", - shorten_fields={StandardContextField.REQUEST_ID}, # Show first 8 chars - hidden_fields={StandardContextField.CORRELATION_ID}, # Hide completely -)) -logging.basicConfig(handlers=[handler], level=logging.INFO) - -# Output: 2025-01-15 10:30:00 INFO [request_id=3fa85f64 user_id=123] Processing -``` - -### Custom Context Adapter - -```python -from fastapi_request_context.adapters import ContextAdapter - -class RedisAdapter(ContextAdapter): - def set_value(self, key: str, value: Any) -> None: - redis.hset(self._request_key, key, value) - - def get_value(self, key: str) -> Any: - return redis.hget(self._request_key, key) - - def get_all(self) -> dict[str, Any]: - return redis.hgetall(self._request_key) - - def enter_context(self, initial_values: dict[str, Any]) -> None: - self._request_key = f"request:{uuid4()}" - redis.hmset(self._request_key, initial_values) - - def exit_context(self) -> None: - redis.delete(self._request_key) - -config = RequestContextConfig(context_adapter=RedisAdapter()) -app = RequestContextMiddleware(app, config=config) -``` - -### Validation Utilities - -Ensure all routes and dependencies are async (required for proper context propagation): - -```python -from fastapi_request_context.validation import check_routes_and_dependencies_are_async - -@app.on_event("startup") -async def validate(): - warnings = check_routes_and_dependencies_are_async(app) - # Logs warnings for any sync routes/dependencies - - # Or raise an error - check_routes_and_dependencies_are_async(app, raise_on_sync=True) -``` - -## API Reference - -### Middleware - -- `RequestContextMiddleware(app, config=None)` - Main middleware class -- `FastAPIWrapperMiddleware(app)` - Base class for custom middleware - -### Configuration - -- `RequestContextConfig` - Configuration dataclass with all options - -### Context Functions - -- `set_context(key, value)` - Set a context value -- `get_context(key)` - Get a context value (returns None if not set) -- `get_full_context()` - Get all context values as a dict - -### Fields - -- `StandardContextField` - Built-in fields (REQUEST_ID, CORRELATION_ID) - -### Adapters - -- `ContextAdapter` - Protocol for custom adapters -- `ContextVarsAdapter` - Default adapter using Python's contextvars -- `ContextLoggingAdapter` - Adapter using context-logging library - -### Formatters - -- `JsonContextFormatter` - JSON formatter for production -- `LocalContextFormatter` - Human-readable formatter for development - -### Validation - -- `is_async(func)` - Check if a function is async -- `check_dependencies_are_async(deps)` - Check dependencies -- `check_routes_and_dependencies_are_async(app)` - Check entire app - -## Why This Library? - -### vs. Manual Implementation - -| Feature | Manual | This Library | -|---------|--------|--------------| -| Request ID generation | DIY | ✅ Built-in | -| Correlation ID | DIY | ✅ Built-in | -| Response headers | DIY | ✅ Automatic | -| Context storage | DIY | ✅ Pluggable | -| Logging integration | DIY | ✅ Included | -| Type safety | Maybe | ✅ Full | -| Tests | Maybe | ✅ >90% coverage | - -### vs. Other Libraries - -- **Zero dependencies** beyond FastAPI (optional extras available) -- **Pluggable adapters** - not locked to one context library -- **Validation utilities** - catch sync code issues early -- **Production-ready formatters** - JSON and local dev support - -## Contributing - -Contributions are welcome! Please feel free to submit a Pull Request. - -```bash -# Clone the repo -git clone https://github.com/ADR-007/fastapi-request-context.git -cd fastapi-request-context - -# Install dependencies -uv sync --all-extras - -# Run tests -make test - -# Run linting -make lint - -# Fix linting issues -make lint-fix -``` - -## License - -MIT License - see [LICENSE](LICENSE) for details. diff --git a/.history/README_20251128211252.md b/.history/README_20251128211252.md deleted file mode 100644 index ed235dc..0000000 --- a/.history/README_20251128211252.md +++ /dev/null @@ -1,291 +0,0 @@ -# fastapi-request-context - -[![PyPI version](https://badge.fury.io/py/fastapi-request-context.svg)](https://badge.fury.io/py/fastapi-request-context) -[![Python versions](https://img.shields.io/pypi/pyversions/fastapi-request-context.svg)](https://pypi.org/project/fastapi-request-context/) -[![CI](https://github.com/ADR-007/fastapi-request-context/actions/workflows/ci.yaml/badge.svg)](https://github.com/ADR-007/fastapi-request-context/actions/workflows/ci.yaml) -[![codecov](https://codecov.io/gh/ADR-007/fastapi-request-context/branch/main/graph/badge.svg)](https://codecov.io/gh/ADR-007/fastapi-request-context) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - -FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with first-class logging integration. - -## Features - -- **Automatic request ID generation** - Every request gets a unique ID -- **Correlation ID support** - Accept from header or generate for distributed tracing -- **Response header injection** - Automatically add `X-Request-Id` and `X-Correlation-Id` to responses -- **Pluggable context backends** - Use `contextvars` (default) or `context-logging` -- **Custom context fields** - Extend with your own fields via StrEnum -- **Logging integration** - JSON and human-readable formatters with automatic context injection -- **Validation utilities** - Check that routes and dependencies are async -- **Zero configuration** - Works out of the box with sensible defaults -- **Type-safe** - Full type hints and mypy strict mode - -## Installation - -```bash -# Basic installation -pip install fastapi-request-context - -# With context-logging support -pip install fastapi-request-context[context-logging] - -# With JSON formatter support -pip install fastapi-request-context[json-formatter] - -# All optional dependencies -pip install fastapi-request-context[all] -``` - -Using uv: -```bash -uv add fastapi-request-context -``` - -## Quick Start - -```python -from fastapi import FastAPI -from fastapi_request_context import RequestContextMiddleware - -# Create your app -app = FastAPI() - -# Keep reference to raw app if needed (e.g., for TaskIQ, testing) -raw_app = app - -# Wrap with middleware -app = RequestContextMiddleware(app) -``` - -Every request now has: -- Unique `request_id` (always generated) -- `correlation_id` (from `X-Correlation-Id` header or generated) -- Both added to response headers -- Context available in all log records (including access logs!) - -## Usage - -### Access Context Values - -```python -from fastapi_request_context import get_context, get_full_context, StandardContextField - -@app.get("/") -async def root(): - request_id = get_context(StandardContextField.REQUEST_ID) - correlation_id = get_context(StandardContextField.CORRELATION_ID) - - # Or get everything - all_context = get_full_context() - - return {"request_id": request_id} -``` - -### Custom Context Fields - -```python -from enum import StrEnum -from fastapi_request_context import set_context, get_context - -class MyContextField(StrEnum): - USER_ID = "user_id" - ORG_ID = "org_id" - -async def get_current_user(token: str): - user_id = decode_token(token) - set_context(MyContextField.USER_ID, user_id) - return user_id - -@app.get("/me") -async def me(user_id: int = Depends(get_current_user)): - # Context is available throughout the request - return {"user_id": get_context(MyContextField.USER_ID)} -``` - -### Configuration - -```python -from fastapi_request_context import RequestContextMiddleware, RequestContextConfig -from uuid import uuid4 - -config = RequestContextConfig( - # Custom ID generators - request_id_generator=lambda: str(uuid4()), - correlation_id_generator=lambda: str(uuid4()), - - # Custom header names - request_id_header="X-My-Request-Id", - correlation_id_header="X-My-Correlation-Id", - - # Disable response headers - add_response_headers=False, - - # Use context-logging adapter - context_adapter="context_logging", - - # Only process HTTP (not WebSocket) - scope_types={"http"}, -) - -app = RequestContextMiddleware(app, config=config) -``` - -### Logging Integration - -#### JSON Formatter (Production) - -```python -import logging -from fastapi_request_context.formatters import JsonContextFormatter - -handler = logging.StreamHandler() -handler.setFormatter(JsonContextFormatter()) -logging.basicConfig(handlers=[handler], level=logging.INFO) - -# Logs automatically include context: -# {"message": "Processing", "level": "INFO", "request_id": "...", "user_id": 123} -``` - -#### Local Formatter (Development) - -```python -from fastapi_request_context import StandardContextField -from fastapi_request_context.formatters import LocalContextFormatter - -handler = logging.StreamHandler() -handler.setFormatter(LocalContextFormatter( - fmt="%(asctime)s %(levelname)s %(context)s %(message)s", - shorten_fields={StandardContextField.REQUEST_ID}, # Show first 8 chars - hidden_fields={StandardContextField.CORRELATION_ID}, # Hide completely -)) -logging.basicConfig(handlers=[handler], level=logging.INFO) - -# Output: 2025-01-15 10:30:00 INFO [request_id=3fa85f64 user_id=123] Processing -``` - -### Custom Context Adapter - -```python -from fastapi_request_context.adapters import ContextAdapter - -class RedisAdapter(ContextAdapter): - def set_value(self, key: str, value: Any) -> None: - redis.hset(self._request_key, key, value) - - def get_value(self, key: str) -> Any: - return redis.hget(self._request_key, key) - - def get_all(self) -> dict[str, Any]: - return redis.hgetall(self._request_key) - - def enter_context(self, initial_values: dict[str, Any]) -> None: - self._request_key = f"request:{uuid4()}" - redis.hmset(self._request_key, initial_values) - - def exit_context(self) -> None: - redis.delete(self._request_key) - -config = RequestContextConfig(context_adapter=RedisAdapter()) -app = RequestContextMiddleware(app, config=config) -``` - -### Validation Utilities - -Ensure all routes and dependencies are async (required for proper context propagation): - -```python -from fastapi_request_context.validation import check_routes_and_dependencies_are_async - -@app.on_event("startup") -async def validate(): - warnings = check_routes_and_dependencies_are_async(app) - # Logs warnings for any sync routes/dependencies - - # Or raise an error - check_routes_and_dependencies_are_async(app, raise_on_sync=True) -``` - -## API Reference - -### Middleware - -- `RequestContextMiddleware(app, config=None)` - Main middleware class -- `FastAPIWrapperMiddleware(app)` - Base class for custom middleware - -### Configuration - -- `RequestContextConfig` - Configuration dataclass with all options - -### Context Functions - -- `set_context(key, value)` - Set a context value -- `get_context(key)` - Get a context value (returns None if not set) -- `get_full_context()` - Get all context values as a dict - -### Fields - -- `StandardContextField` - Built-in fields (REQUEST_ID, CORRELATION_ID) - -### Adapters - -- `ContextAdapter` - Protocol for custom adapters -- `ContextVarsAdapter` - Default adapter using Python's contextvars -- `ContextLoggingAdapter` - Adapter using context-logging library - -### Formatters - -- `JsonContextFormatter` - JSON formatter for production -- `LocalContextFormatter` - Human-readable formatter for development - -### Validation - -- `is_async(func)` - Check if a function is async -- `check_dependencies_are_async(deps)` - Check dependencies -- `check_routes_and_dependencies_are_async(app)` - Check entire app - -## Why This Library? - -### vs. Manual Implementation - -| Feature | Manual | This Library | -|---------|--------|--------------| -| Request ID generation | DIY | ✅ Built-in | -| Correlation ID | DIY | ✅ Built-in | -| Response headers | DIY | ✅ Automatic | -| Context storage | DIY | ✅ Pluggable | -| Logging integration | DIY | ✅ Included | -| Type safety | Maybe | ✅ Full | -| Tests | Maybe | ✅ >90% coverage | - -### vs. Other Libraries - -- **Zero dependencies** beyond FastAPI (optional extras available) -- **Pluggable adapters** - not locked to one context library -- **Validation utilities** - catch sync code issues early -- **Production-ready formatters** - JSON and local dev support - -## Contributing - -Contributions are welcome! Please feel free to submit a Pull Request. - -```bash -# Clone the repo -git clone https://github.com/ADR-007/fastapi-request-context.git -cd fastapi-request-context - -# Install dependencies -uv sync --all-extras - -# Run tests -make test - -# Run linting -make lint - -# Fix linting issues -make lint-fix -``` - -## License - -MIT License - see [LICENSE](LICENSE) for details. diff --git a/.history/README_20251128211308.md b/.history/README_20251128211308.md deleted file mode 100644 index 729214c..0000000 --- a/.history/README_20251128211308.md +++ /dev/null @@ -1,305 +0,0 @@ -# fastapi-request-context - -[![PyPI version](https://badge.fury.io/py/fastapi-request-context.svg)](https://badge.fury.io/py/fastapi-request-context) -[![Python versions](https://img.shields.io/pypi/pyversions/fastapi-request-context.svg)](https://pypi.org/project/fastapi-request-context/) -[![CI](https://github.com/ADR-007/fastapi-request-context/actions/workflows/ci.yaml/badge.svg)](https://github.com/ADR-007/fastapi-request-context/actions/workflows/ci.yaml) -[![codecov](https://codecov.io/gh/ADR-007/fastapi-request-context/branch/main/graph/badge.svg)](https://codecov.io/gh/ADR-007/fastapi-request-context) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - -FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with first-class logging integration. - -## Features - -- **Automatic request ID generation** - Every request gets a unique ID -- **Correlation ID support** - Accept from header or generate for distributed tracing -- **Response header injection** - Automatically add `X-Request-Id` and `X-Correlation-Id` to responses -- **Pluggable context backends** - Use `contextvars` (default) or `context-logging` -- **Custom context fields** - Extend with your own fields via StrEnum -- **Logging integration** - JSON and human-readable formatters with automatic context injection -- **Validation utilities** - Check that routes and dependencies are async -- **Zero configuration** - Works out of the box with sensible defaults -- **Type-safe** - Full type hints and mypy strict mode - -## Installation - -```bash -# Basic installation -pip install fastapi-request-context - -# With context-logging support -pip install fastapi-request-context[context-logging] - -# With JSON formatter support -pip install fastapi-request-context[json-formatter] - -# All optional dependencies -pip install fastapi-request-context[all] -``` - -Using uv: -```bash -uv add fastapi-request-context -``` - -## Quick Start - -```python -from fastapi import FastAPI -from fastapi_request_context import RequestContextMiddleware - -# Create your app -app = FastAPI() - -# Keep reference to raw app if needed (e.g., for TaskIQ, testing) -raw_app = app - -# Wrap with middleware -app = RequestContextMiddleware(app) -``` - -Every request now has: -- Unique `request_id` (always generated) -- `correlation_id` (from `X-Correlation-Id` header or generated) -- Both added to response headers -- Context available in all log records (including access logs!) - -## Usage - -### Access Context Values - -```python -from fastapi_request_context import get_context, get_full_context, StandardContextField - -@app.get("/") -async def root(): - request_id = get_context(StandardContextField.REQUEST_ID) - correlation_id = get_context(StandardContextField.CORRELATION_ID) - - # Or get everything - all_context = get_full_context() - - return {"request_id": request_id} -``` - -### Custom Context Fields - -```python -from enum import StrEnum -from fastapi_request_context import set_context, get_context - -class MyContextField(StrEnum): - USER_ID = "user_id" - ORG_ID = "org_id" - -async def get_current_user(token: str): - user_id = decode_token(token) - set_context(MyContextField.USER_ID, user_id) - return user_id - -@app.get("/me") -async def me(user_id: int = Depends(get_current_user)): - # Context is available throughout the request - return {"user_id": get_context(MyContextField.USER_ID)} -``` - -### Configuration - -```python -from fastapi_request_context import RequestContextMiddleware, RequestContextConfig -from uuid import uuid4 - -config = RequestContextConfig( - # Custom ID generators - request_id_generator=lambda: str(uuid4()), - correlation_id_generator=lambda: str(uuid4()), - - # Custom header names - request_id_header="X-My-Request-Id", - correlation_id_header="X-My-Correlation-Id", - - # Disable response headers - add_response_headers=False, - - # Use context-logging adapter - context_adapter="context_logging", - - # Only process HTTP (not WebSocket) - scope_types={"http"}, -) - -app = RequestContextMiddleware(app, config=config) -``` - -### Logging Integration - -#### JSON Formatter (Production) - -```python -import logging -from fastapi_request_context.formatters import JsonContextFormatter - -handler = logging.StreamHandler() -handler.setFormatter(JsonContextFormatter()) -logging.basicConfig(handlers=[handler], level=logging.INFO) - -# Logs automatically include context: -# {"message": "Processing", "level": "INFO", "request_id": "...", "user_id": 123} -``` - -#### Simple Formatter (Human-Readable) - -```python -from fastapi_request_context import StandardContextField -from fastapi_request_context.formatters import SimpleContextFormatter - -handler = logging.StreamHandler() -handler.setFormatter(SimpleContextFormatter( - fmt="%(asctime)s %(levelname)s %(context)s %(message)s", - shorten_fields={StandardContextField.REQUEST_ID}, # Show first 8 chars - hidden_fields={StandardContextField.CORRELATION_ID}, # Hide completely -)) -logging.basicConfig(handlers=[handler], level=logging.INFO) - -# Output: 2025-01-15 10:30:00 INFO [request_id=3fa85f64 user_id=123] Processing -``` - -### Access Logs Integration - -Context is automatically available in **all log records**, including Uvicorn access logs when using `context-logging` adapter: - -```python -from fastapi_request_context import RequestContextMiddleware, RequestContextConfig - -config = RequestContextConfig(context_adapter="context_logging") -app = RequestContextMiddleware(app, config=config) - -# Now access logs will include request_id and correlation_id! -# Example: INFO 127.0.0.1:8000 - "GET / HTTP/1.1" 200 [request_id=abc123] -``` - -### Custom Context Adapter - -```python -from fastapi_request_context.adapters import ContextAdapter - -class RedisAdapter(ContextAdapter): - def set_value(self, key: str, value: Any) -> None: - redis.hset(self._request_key, key, value) - - def get_value(self, key: str) -> Any: - return redis.hget(self._request_key, key) - - def get_all(self) -> dict[str, Any]: - return redis.hgetall(self._request_key) - - def enter_context(self, initial_values: dict[str, Any]) -> None: - self._request_key = f"request:{uuid4()}" - redis.hmset(self._request_key, initial_values) - - def exit_context(self) -> None: - redis.delete(self._request_key) - -config = RequestContextConfig(context_adapter=RedisAdapter()) -app = RequestContextMiddleware(app, config=config) -``` - -### Validation Utilities - -Ensure all routes and dependencies are async (required for proper context propagation): - -```python -from fastapi_request_context.validation import check_routes_and_dependencies_are_async - -@app.on_event("startup") -async def validate(): - warnings = check_routes_and_dependencies_are_async(app) - # Logs warnings for any sync routes/dependencies - - # Or raise an error - check_routes_and_dependencies_are_async(app, raise_on_sync=True) -``` - -## API Reference - -### Middleware - -- `RequestContextMiddleware(app, config=None)` - Main middleware class -- `FastAPIWrapperMiddleware(app)` - Base class for custom middleware - -### Configuration - -- `RequestContextConfig` - Configuration dataclass with all options - -### Context Functions - -- `set_context(key, value)` - Set a context value -- `get_context(key)` - Get a context value (returns None if not set) -- `get_full_context()` - Get all context values as a dict - -### Fields - -- `StandardContextField` - Built-in fields (REQUEST_ID, CORRELATION_ID) - -### Adapters - -- `ContextAdapter` - Protocol for custom adapters -- `ContextVarsAdapter` - Default adapter using Python's contextvars -- `ContextLoggingAdapter` - Adapter using context-logging library - -### Formatters - -- `JsonContextFormatter` - JSON formatter for production -- `LocalContextFormatter` - Human-readable formatter for development - -### Validation - -- `is_async(func)` - Check if a function is async -- `check_dependencies_are_async(deps)` - Check dependencies -- `check_routes_and_dependencies_are_async(app)` - Check entire app - -## Why This Library? - -### vs. Manual Implementation - -| Feature | Manual | This Library | -|---------|--------|--------------| -| Request ID generation | DIY | ✅ Built-in | -| Correlation ID | DIY | ✅ Built-in | -| Response headers | DIY | ✅ Automatic | -| Context storage | DIY | ✅ Pluggable | -| Logging integration | DIY | ✅ Included | -| Type safety | Maybe | ✅ Full | -| Tests | Maybe | ✅ >90% coverage | - -### vs. Other Libraries - -- **Zero dependencies** beyond FastAPI (optional extras available) -- **Pluggable adapters** - not locked to one context library -- **Validation utilities** - catch sync code issues early -- **Production-ready formatters** - JSON and local dev support - -## Contributing - -Contributions are welcome! Please feel free to submit a Pull Request. - -```bash -# Clone the repo -git clone https://github.com/ADR-007/fastapi-request-context.git -cd fastapi-request-context - -# Install dependencies -uv sync --all-extras - -# Run tests -make test - -# Run linting -make lint - -# Fix linting issues -make lint-fix -``` - -## License - -MIT License - see [LICENSE](LICENSE) for details. diff --git a/.history/README_20251128211324.md b/.history/README_20251128211324.md deleted file mode 100644 index 9ef2157..0000000 --- a/.history/README_20251128211324.md +++ /dev/null @@ -1,304 +0,0 @@ -# fastapi-request-context - -[![PyPI version](https://badge.fury.io/py/fastapi-request-context.svg)](https://badge.fury.io/py/fastapi-request-context) -[![Python versions](https://img.shields.io/pypi/pyversions/fastapi-request-context.svg)](https://pypi.org/project/fastapi-request-context/) -[![CI](https://github.com/ADR-007/fastapi-request-context/actions/workflows/ci.yaml/badge.svg)](https://github.com/ADR-007/fastapi-request-context/actions/workflows/ci.yaml) -[![codecov](https://codecov.io/gh/ADR-007/fastapi-request-context/branch/main/graph/badge.svg)](https://codecov.io/gh/ADR-007/fastapi-request-context) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - -FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with first-class logging integration. - -## Features - -- **Automatic request ID generation** - Every request gets a unique ID -- **Correlation ID support** - Accept from header or generate for distributed tracing -- **Response header injection** - Automatically add `X-Request-Id` and `X-Correlation-Id` to responses -- **Pluggable context backends** - Use `contextvars` (default) or `context-logging` -- **Custom context fields** - Extend with your own fields via StrEnum -- **Logging integration** - JSON and human-readable formatters with automatic context injection -- **Validation utilities** - Check that routes and dependencies are async -- **Zero configuration** - Works out of the box with sensible defaults -- **Type-safe** - Full type hints and mypy strict mode - -## Installation - -```bash -# Basic installation -pip install fastapi-request-context - -# With context-logging support -pip install fastapi-request-context[context-logging] - -# With JSON formatter support -pip install fastapi-request-context[json-formatter] - -# All optional dependencies -pip install fastapi-request-context[all] -``` - -Using uv: -```bash -uv add fastapi-request-context -``` - -## Quick Start - -```python -from fastapi import FastAPI -from fastapi_request_context import RequestContextMiddleware - -# Create your app -app = FastAPI() - -# Keep reference to raw app if needed (e.g., for TaskIQ, testing) -raw_app = app - -# Wrap with middleware -app = RequestContextMiddleware(app) -``` - -Every request now has: -- Unique `request_id` (always generated) -- `correlation_id` (from `X-Correlation-Id` header or generated) -- Both added to response headers -- Context available in all log records (including access logs!) - -## Usage - -### Access Context Values - -```python -from fastapi_request_context import get_context, get_full_context, StandardContextField - -@app.get("/") -async def root(): - request_id = get_context(StandardContextField.REQUEST_ID) - correlation_id = get_context(StandardContextField.CORRELATION_ID) - - # Or get everything - all_context = get_full_context() - - return {"request_id": request_id} -``` - -### Custom Context Fields - -```python -from enum import StrEnum -from fastapi_request_context import set_context, get_context - -class MyContextField(StrEnum): - USER_ID = "user_id" - ORG_ID = "org_id" - -async def get_current_user(token: str): - user_id = decode_token(token) - set_context(MyContextField.USER_ID, user_id) - return user_id - -@app.get("/me") -async def me(user_id: int = Depends(get_current_user)): - # Context is available throughout the request - return {"user_id": get_context(MyContextField.USER_ID)} -``` - -### Configuration - -```python -from fastapi_request_context import RequestContextMiddleware, RequestContextConfig -from uuid import uuid4 - -config = RequestContextConfig( - # Custom ID generators - request_id_generator=lambda: str(uuid4()), - correlation_id_generator=lambda: str(uuid4()), - - # Custom header names - request_id_header="X-My-Request-Id", - correlation_id_header="X-My-Correlation-Id", - - # Disable response headers - add_response_headers=False, - - # Use context-logging adapter - context_adapter="context_logging", - - # Only process HTTP (not WebSocket) - scope_types={"http"}, -) - -app = RequestContextMiddleware(app, config=config) -``` - -### Logging Integration - -#### JSON Formatter (Production) - -```python -import logging -from fastapi_request_context.formatters import JsonContextFormatter - -handler = logging.StreamHandler() -handler.setFormatter(JsonContextFormatter()) -logging.basicConfig(handlers=[handler], level=logging.INFO) - -# Logs automatically include context: -# {"message": "Processing", "level": "INFO", "request_id": "...", "user_id": 123} -``` - -#### Simple Formatter (Human-Readable) - -```python -from fastapi_request_context import StandardContextField -from fastapi_request_context.formatters import SimpleContextFormatter - -handler = logging.StreamHandler() -handler.setFormatter(SimpleContextFormatter( - fmt="%(asctime)s %(levelname)s %(context)s %(message)s", - shorten_fields={StandardContextField.REQUEST_ID}, # Show first 8 chars - hidden_fields={StandardContextField.CORRELATION_ID}, # Hide completely -)) -logging.basicConfig(handlers=[handler], level=logging.INFO) - -# Output: 2025-01-15 10:30:00 INFO [request_id=3fa85f64 user_id=123] Processing -``` - -### Access Logs Integration - -Context is automatically available in **all log records**, including Uvicorn access logs when using `context-logging` adapter: - -```python -from fastapi_request_context import RequestContextMiddleware, RequestContextConfig - -config = RequestContextConfig(context_adapter="context_logging") -app = RequestContextMiddleware(app, config=config) - -# Now access logs will include request_id and correlation_id! -# Example: INFO 127.0.0.1:8000 - "GET / HTTP/1.1" 200 [request_id=abc123] -``` - -### Custom Context Adapter - -```python -from fastapi_request_context.adapters import ContextAdapter - -class RedisAdapter(ContextAdapter): - def set_value(self, key: str, value: Any) -> None: - redis.hset(self._request_key, key, value) - - def get_value(self, key: str) -> Any: - return redis.hget(self._request_key, key) - - def get_all(self) -> dict[str, Any]: - return redis.hgetall(self._request_key) - - def enter_context(self, initial_values: dict[str, Any]) -> None: - self._request_key = f"request:{uuid4()}" - redis.hmset(self._request_key, initial_values) - - def exit_context(self) -> None: - redis.delete(self._request_key) - -config = RequestContextConfig(context_adapter=RedisAdapter()) -app = RequestContextMiddleware(app, config=config) -``` - -### Validation Utilities - -Ensure all routes and dependencies are async (required for proper context propagation): - -```python -from fastapi_request_context.validation import check_routes_and_dependencies_are_async - -@app.on_event("startup") -async def validate(): - warnings = check_routes_and_dependencies_are_async(app) - # Logs warnings for any sync routes/dependencies - - # Or raise an error - check_routes_and_dependencies_are_async(app, raise_on_sync=True) -``` - -## API Reference - -### Middleware - -- `RequestContextMiddleware(app, config=None)` - Main middleware class - -### Configuration - -- `RequestContextConfig` - Configuration dataclass with all options - -### Context Functions - -- `set_context(key, value)` - Set a context value -- `get_context(key)` - Get a context value (returns None if not set) -- `get_full_context()` - Get all context values as a dict - -### Fields - -- `StandardContextField` - Built-in fields (REQUEST_ID, CORRELATION_ID) - -### Adapters - -- `ContextAdapter` - Protocol for custom adapters -- `ContextVarsAdapter` - Default adapter using Python's contextvars -- `ContextLoggingAdapter` - Adapter using context-logging library (enables access log integration) - -### Formatters - -- `JsonContextFormatter` - JSON formatter for structured logging -- `SimpleContextFormatter` - Human-readable formatter with inline context - -### Validation - -- `is_async(func)` - Check if a function is async -- `check_dependencies_are_async(deps)` - Check dependencies -- `check_routes_and_dependencies_are_async(app)` - Check entire app - -## Why This Library? - -### vs. Manual Implementation - -| Feature | Manual | This Library | -|---------|--------|--------------| -| Request ID generation | DIY | ✅ Built-in | -| Correlation ID | DIY | ✅ Built-in | -| Response headers | DIY | ✅ Automatic | -| Context storage | DIY | ✅ Pluggable | -| Logging integration | DIY | ✅ Included | -| Type safety | Maybe | ✅ Full | -| Tests | Maybe | ✅ >90% coverage | - -### vs. Other Libraries - -- **Zero dependencies** beyond FastAPI (optional extras available) -- **Pluggable adapters** - not locked to one context library -- **Validation utilities** - catch sync code issues early -- **Production-ready formatters** - JSON and local dev support - -## Contributing - -Contributions are welcome! Please feel free to submit a Pull Request. - -```bash -# Clone the repo -git clone https://github.com/ADR-007/fastapi-request-context.git -cd fastapi-request-context - -# Install dependencies -uv sync --all-extras - -# Run tests -make test - -# Run linting -make lint - -# Fix linting issues -make lint-fix -``` - -## License - -MIT License - see [LICENSE](LICENSE) for details. diff --git a/.history/README_20251128211331.md b/.history/README_20251128211331.md deleted file mode 100644 index e75eabc..0000000 --- a/.history/README_20251128211331.md +++ /dev/null @@ -1,304 +0,0 @@ -# fastapi-request-context - -[![PyPI version](https://badge.fury.io/py/fastapi-request-context.svg)](https://badge.fury.io/py/fastapi-request-context) -[![Python versions](https://img.shields.io/pypi/pyversions/fastapi-request-context.svg)](https://pypi.org/project/fastapi-request-context/) -[![CI](https://github.com/ADR-007/fastapi-request-context/actions/workflows/ci.yaml/badge.svg)](https://github.com/ADR-007/fastapi-request-context/actions/workflows/ci.yaml) -[![codecov](https://codecov.io/gh/ADR-007/fastapi-request-context/branch/main/graph/badge.svg)](https://codecov.io/gh/ADR-007/fastapi-request-context) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - -FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with first-class logging integration. - -## Features - -- **Automatic request ID generation** - Every request gets a unique ID -- **Correlation ID support** - Accept from header or generate for distributed tracing -- **Response header injection** - Automatically add `X-Request-Id` and `X-Correlation-Id` to responses -- **Pluggable context backends** - Use `contextvars` (default) or `context-logging` -- **Custom context fields** - Extend with your own fields via StrEnum -- **Logging integration** - JSON and human-readable formatters with automatic context injection -- **Validation utilities** - Check that routes and dependencies are async -- **Zero configuration** - Works out of the box with sensible defaults -- **Type-safe** - Full type hints and mypy strict mode - -## Installation - -```bash -# Basic installation -pip install fastapi-request-context - -# With context-logging support -pip install fastapi-request-context[context-logging] - -# With JSON formatter support -pip install fastapi-request-context[json-formatter] - -# All optional dependencies -pip install fastapi-request-context[all] -``` - -Using uv: -```bash -uv add fastapi-request-context -``` - -## Quick Start - -```python -from fastapi import FastAPI -from fastapi_request_context import RequestContextMiddleware - -# Create your app -app = FastAPI() - -# Keep reference to raw app if needed (e.g., for TaskIQ, testing) -raw_app = app - -# Wrap with middleware -app = RequestContextMiddleware(app) -``` - -Every request now has: -- Unique `request_id` (always generated) -- `correlation_id` (from `X-Correlation-Id` header or generated) -- Both added to response headers -- Context available in all log records (including access logs!) - -## Usage - -### Access Context Values - -```python -from fastapi_request_context import get_context, get_full_context, StandardContextField - -@app.get("/") -async def root(): - request_id = get_context(StandardContextField.REQUEST_ID) - correlation_id = get_context(StandardContextField.CORRELATION_ID) - - # Or get everything - all_context = get_full_context() - - return {"request_id": request_id} -``` - -### Custom Context Fields - -```python -from enum import StrEnum -from fastapi_request_context import set_context, get_context - -class MyContextField(StrEnum): - USER_ID = "user_id" - ORG_ID = "org_id" - -async def get_current_user(token: str): - user_id = decode_token(token) - set_context(MyContextField.USER_ID, user_id) - return user_id - -@app.get("/me") -async def me(user_id: int = Depends(get_current_user)): - # Context is available throughout the request - return {"user_id": get_context(MyContextField.USER_ID)} -``` - -### Configuration - -```python -from fastapi_request_context import RequestContextMiddleware, RequestContextConfig -from uuid import uuid4 - -config = RequestContextConfig( - # Custom ID generators - request_id_generator=lambda: str(uuid4()), - correlation_id_generator=lambda: str(uuid4()), - - # Custom header names - request_id_header="X-My-Request-Id", - correlation_id_header="X-My-Correlation-Id", - - # Disable response headers - add_response_headers=False, - - # Use context-logging adapter - context_adapter="context_logging", - - # Only process HTTP (not WebSocket) - scope_types={"http"}, -) - -app = RequestContextMiddleware(app, config=config) -``` - -### Logging Integration - -#### JSON Formatter (Production) - -```python -import logging -from fastapi_request_context.formatters import JsonContextFormatter - -handler = logging.StreamHandler() -handler.setFormatter(JsonContextFormatter()) -logging.basicConfig(handlers=[handler], level=logging.INFO) - -# Logs automatically include context: -# {"message": "Processing", "level": "INFO", "request_id": "...", "user_id": 123} -``` - -#### Simple Formatter (Human-Readable) - -```python -from fastapi_request_context import StandardContextField -from fastapi_request_context.formatters import SimpleContextFormatter - -handler = logging.StreamHandler() -handler.setFormatter(SimpleContextFormatter( - fmt="%(asctime)s %(levelname)s %(context)s %(message)s", - shorten_fields={StandardContextField.REQUEST_ID}, # Show first 8 chars - hidden_fields={StandardContextField.CORRELATION_ID}, # Hide completely -)) -logging.basicConfig(handlers=[handler], level=logging.INFO) - -# Output: 2025-01-15 10:30:00 INFO [request_id=3fa85f64 user_id=123] Processing -``` - -### Access Logs Integration - -Context is automatically available in **all log records**, including Uvicorn access logs when using `context-logging` adapter: - -```python -from fastapi_request_context import RequestContextMiddleware, RequestContextConfig - -config = RequestContextConfig(context_adapter="context_logging") -app = RequestContextMiddleware(app, config=config) - -# Now access logs will include request_id and correlation_id! -# Example: INFO 127.0.0.1:8000 - "GET / HTTP/1.1" 200 [request_id=abc123] -``` - -### Custom Context Adapter - -```python -from fastapi_request_context.adapters import ContextAdapter - -class RedisAdapter(ContextAdapter): - def set_value(self, key: str, value: Any) -> None: - redis.hset(self._request_key, key, value) - - def get_value(self, key: str) -> Any: - return redis.hget(self._request_key, key) - - def get_all(self) -> dict[str, Any]: - return redis.hgetall(self._request_key) - - def enter_context(self, initial_values: dict[str, Any]) -> None: - self._request_key = f"request:{uuid4()}" - redis.hmset(self._request_key, initial_values) - - def exit_context(self) -> None: - redis.delete(self._request_key) - -config = RequestContextConfig(context_adapter=RedisAdapter()) -app = RequestContextMiddleware(app, config=config) -``` - -### Validation Utilities - -Ensure all routes and dependencies are async (required for proper context propagation): - -```python -from fastapi_request_context.validation import check_routes_and_dependencies_are_async - -@app.on_event("startup") -async def validate(): - warnings = check_routes_and_dependencies_are_async(app) - # Logs warnings for any sync routes/dependencies - - # Or raise an error - check_routes_and_dependencies_are_async(app, raise_on_sync=True) -``` - -## API Reference - -### Middleware - -- `RequestContextMiddleware(app, config=None)` - Main middleware class - -### Configuration - -- `RequestContextConfig` - Configuration dataclass with all options - -### Context Functions - -- `set_context(key, value)` - Set a context value -- `get_context(key)` - Get a context value (returns None if not set) -- `get_full_context()` - Get all context values as a dict - -### Fields - -- `StandardContextField` - Built-in fields (REQUEST_ID, CORRELATION_ID) - -### Adapters - -- `ContextAdapter` - Protocol for custom adapters -- `ContextVarsAdapter` - Default adapter using Python's contextvars -- `ContextLoggingAdapter` - Adapter using context-logging library (enables access log integration) - -### Formatters - -- `JsonContextFormatter` - JSON formatter for structured logging -- `SimpleContextFormatter` - Human-readable formatter with inline context - -### Validation - -- `is_async(func)` - Check if a function is async -- `check_dependencies_are_async(deps)` - Check dependencies -- `check_routes_and_dependencies_are_async(app)` - Check entire app - -## Why This Library? - -### vs. Manual Implementation - -| Feature | Manual | This Library | -|---------|--------|--------------| -| Request ID generation | DIY | ✅ Built-in | -| Correlation ID | DIY | ✅ Built-in | -| Response headers | DIY | ✅ Automatic | -| Context storage | DIY | ✅ Pluggable | -| Logging integration | DIY | ✅ Included | -| Type safety | Maybe | ✅ Full | -| Tests | Maybe | ✅ 100% coverage | - -### vs. Other Libraries - -- **Zero dependencies** beyond FastAPI (optional extras available) -- **Pluggable adapters** - not locked to one context library -- **Validation utilities** - catch sync code issues early -- **Production-ready formatters** - JSON and local dev support - -## Contributing - -Contributions are welcome! Please feel free to submit a Pull Request. - -```bash -# Clone the repo -git clone https://github.com/ADR-007/fastapi-request-context.git -cd fastapi-request-context - -# Install dependencies -uv sync --all-extras - -# Run tests -make test - -# Run linting -make lint - -# Fix linting issues -make lint-fix -``` - -## License - -MIT License - see [LICENSE](LICENSE) for details. diff --git a/.history/examples/basic_usage_20251128203309.py b/.history/examples/basic_usage_20251128203309.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/examples/basic_usage_20251128203310.py b/.history/examples/basic_usage_20251128203310.py deleted file mode 100644 index 371b2ac..0000000 --- a/.history/examples/basic_usage_20251128203310.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Basic usage example - zero configuration. - -This example shows the simplest way to use fastapi-request-context. -Every request automatically gets: -- Unique request_id (always generated) -- Correlation_id (from header or generated) -- Both added to response headers -""" - -from fastapi import FastAPI - -from fastapi_request_context import ( - RequestContextMiddleware, - StandardContextField, - get_context, - get_full_context, -) - -app = FastAPI() - - -@app.get("/") -async def root() -> dict[str, str]: - """Simple endpoint that returns request IDs.""" - return { - "request_id": get_context(StandardContextField.REQUEST_ID), - "correlation_id": get_context(StandardContextField.CORRELATION_ID), - } - - -@app.get("/full-context") -async def full_context() -> dict[str, str]: - """Return all context values.""" - return get_full_context() - - -# Wrap with middleware - that's it! -app = RequestContextMiddleware(app) # type: ignore[assignment] - -if __name__ == "__main__": - import uvicorn - - print("Starting server at http://localhost:8000") - print("Try: curl -v http://localhost:8000/") - print("Try: curl -H 'X-Correlation-Id: my-trace-123' http://localhost:8000/") - uvicorn.run(app, host="0.0.0.0", port=8000) # noqa: S104 diff --git a/.history/examples/custom_adapter_20251128203330.py b/.history/examples/custom_adapter_20251128203330.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/examples/custom_adapter_20251128203331.py b/.history/examples/custom_adapter_20251128203331.py deleted file mode 100644 index 49ed828..0000000 --- a/.history/examples/custom_adapter_20251128203331.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Custom context adapter example. - -This example shows how to create a custom context adapter -for specialized storage needs (Redis, database, etc.). -""" - -from typing import Any - -from fastapi import FastAPI - -from fastapi_request_context import ( - RequestContextConfig, - RequestContextMiddleware, - get_context, -) -from fastapi_request_context.adapters.base import ContextAdapter - - -class InMemoryAdapter(ContextAdapter): - """Simple in-memory adapter for demonstration. - - In a real application, you might use Redis, a database, - or another storage backend. - """ - - def __init__(self) -> None: - """Initialize the adapter.""" - self._storage: dict[str, Any] = {} - - def set_value(self, key: str, value: Any) -> None: - """Store a value.""" - self._storage[key] = value - print(f" [InMemoryAdapter] Set {key}={value}") - - def get_value(self, key: str) -> Any: - """Retrieve a value.""" - value = self._storage.get(key) - print(f" [InMemoryAdapter] Get {key}={value}") - return value - - def get_all(self) -> dict[str, Any]: - """Get all stored values.""" - return dict(self._storage) - - def enter_context(self, initial_values: dict[str, Any]) -> None: - """Initialize context with values.""" - self._storage = dict(initial_values) - print(f" [InMemoryAdapter] Enter context: {initial_values}") - - def exit_context(self) -> None: - """Clean up context.""" - print(f" [InMemoryAdapter] Exit context: {self._storage}") - self._storage.clear() - - -app = FastAPI() - - -@app.get("/") -async def root() -> dict[str, Any]: - """Show current context values.""" - return { - "request_id": get_context("request_id"), - "correlation_id": get_context("correlation_id"), - } - - -# Create custom adapter -custom_adapter = InMemoryAdapter() - -# Configure middleware with custom adapter -config = RequestContextConfig(context_adapter=custom_adapter) -app = RequestContextMiddleware(app, config=config) # type: ignore[assignment] - -if __name__ == "__main__": - import uvicorn - - print("Starting server with custom adapter at http://localhost:8000") - print("Watch the console to see adapter calls") - print("Try: curl http://localhost:8000/") - uvicorn.run(app, host="0.0.0.0", port=8000) # noqa: S104 diff --git a/.history/examples/custom_fields_20251128203320.py b/.history/examples/custom_fields_20251128203320.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/examples/custom_fields_20251128203321.py b/.history/examples/custom_fields_20251128203321.py deleted file mode 100644 index 174abd0..0000000 --- a/.history/examples/custom_fields_20251128203321.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Using custom context fields. - -This example shows how to define and use custom context fields -for your application. Use StrEnum for type-safe field access. -""" - -from enum import StrEnum -from typing import Annotated - -from fastapi import Depends, FastAPI, Header - -from fastapi_request_context import ( - RequestContextMiddleware, - StandardContextField, - get_context, - get_full_context, - set_context, -) - - -class MyContextField(StrEnum): - """Custom context fields for this application.""" - - USER_ID = "user_id" - ORGANIZATION_ID = "organization_id" - TENANT_ID = "tenant_id" - - -app = FastAPI() - - -async def get_current_user( - authorization: Annotated[str | None, Header()] = None, -) -> int | None: - """Dependency that extracts user from auth header and stores in context.""" - if not authorization: - return None - - # In real app, decode JWT token here - user_id = 12345 - - # Store in context - will be available throughout request - set_context(MyContextField.USER_ID, user_id) - set_context(MyContextField.ORGANIZATION_ID, "org-abc") - - return user_id - - -@app.get("/me") -async def me(user_id: int | None = Depends(get_current_user)) -> dict[str, str | int | None]: - """Endpoint that uses custom context fields.""" - return { - "user_id": user_id, - "org_id": get_context(MyContextField.ORGANIZATION_ID), - "request_id": get_context(StandardContextField.REQUEST_ID), - } - - -@app.get("/debug-context") -async def debug_context( - user_id: int | None = Depends(get_current_user), -) -> dict[str, str | int | None]: - """Show all context values for debugging.""" - return get_full_context() - - -# Apply middleware -app = RequestContextMiddleware(app) # type: ignore[assignment] - -if __name__ == "__main__": - import uvicorn - - print("Starting server at http://localhost:8000") - print("Try: curl http://localhost:8000/me") - print("Try: curl -H 'Authorization: Bearer token' http://localhost:8000/me") - print("Try: curl -H 'Authorization: Bearer token' http://localhost:8000/debug-context") - uvicorn.run(app, host="0.0.0.0", port=8000) # noqa: S104 diff --git a/.history/examples/custom_fields_20251128203530.py b/.history/examples/custom_fields_20251128203530.py deleted file mode 100644 index 0d1b2ae..0000000 --- a/.history/examples/custom_fields_20251128203530.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Using custom context fields. - -This example shows how to define and use custom context fields -for your application. Use StrEnum for type-safe field access. -""" - -import sys - -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from enum import Enum - - class StrEnum(str, Enum): - pass -from typing import Annotated - -from fastapi import Depends, FastAPI, Header - -from fastapi_request_context import ( - RequestContextMiddleware, - StandardContextField, - get_context, - get_full_context, - set_context, -) - - -class MyContextField(StrEnum): - """Custom context fields for this application.""" - - USER_ID = "user_id" - ORGANIZATION_ID = "organization_id" - TENANT_ID = "tenant_id" - - -app = FastAPI() - - -async def get_current_user( - authorization: Annotated[str | None, Header()] = None, -) -> int | None: - """Dependency that extracts user from auth header and stores in context.""" - if not authorization: - return None - - # In real app, decode JWT token here - user_id = 12345 - - # Store in context - will be available throughout request - set_context(MyContextField.USER_ID, user_id) - set_context(MyContextField.ORGANIZATION_ID, "org-abc") - - return user_id - - -@app.get("/me") -async def me(user_id: int | None = Depends(get_current_user)) -> dict[str, str | int | None]: - """Endpoint that uses custom context fields.""" - return { - "user_id": user_id, - "org_id": get_context(MyContextField.ORGANIZATION_ID), - "request_id": get_context(StandardContextField.REQUEST_ID), - } - - -@app.get("/debug-context") -async def debug_context( - user_id: int | None = Depends(get_current_user), -) -> dict[str, str | int | None]: - """Show all context values for debugging.""" - return get_full_context() - - -# Apply middleware -app = RequestContextMiddleware(app) # type: ignore[assignment] - -if __name__ == "__main__": - import uvicorn - - print("Starting server at http://localhost:8000") - print("Try: curl http://localhost:8000/me") - print("Try: curl -H 'Authorization: Bearer token' http://localhost:8000/me") - print("Try: curl -H 'Authorization: Bearer token' http://localhost:8000/debug-context") - uvicorn.run(app, host="0.0.0.0", port=8000) # noqa: S104 diff --git a/.history/examples/custom_fields_20251128203539.py b/.history/examples/custom_fields_20251128203539.py deleted file mode 100644 index 13c486a..0000000 --- a/.history/examples/custom_fields_20251128203539.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Using custom context fields. - -This example shows how to define and use custom context fields -for your application. Use StrEnum for type-safe field access. -""" - -import sys - -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from enum import Enum - - class StrEnum(str, Enum): - pass - -from typing import Annotated - -from fastapi import Depends, FastAPI, Header - -from fastapi_request_context import ( - RequestContextMiddleware, - StandardContextField, - get_context, - get_full_context, - set_context, -) - - -class MyContextField(StrEnum): - """Custom context fields for this application.""" - - USER_ID = "user_id" - ORGANIZATION_ID = "organization_id" - TENANT_ID = "tenant_id" - - -app = FastAPI() - - -async def get_current_user( - authorization: Annotated[str | None, Header()] = None, -) -> int | None: - """Dependency that extracts user from auth header and stores in context.""" - if not authorization: - return None - - # In real app, decode JWT token here - user_id = 12345 - - # Store in context - will be available throughout request - set_context(MyContextField.USER_ID, user_id) - set_context(MyContextField.ORGANIZATION_ID, "org-abc") - - return user_id - - -@app.get("/me") -async def me(user_id: int | None = Depends(get_current_user)) -> dict[str, str | int | None]: - """Endpoint that uses custom context fields.""" - return { - "user_id": user_id, - "org_id": get_context(MyContextField.ORGANIZATION_ID), - "request_id": get_context(StandardContextField.REQUEST_ID), - } - - -@app.get("/debug-context") -async def debug_context( - user_id: int | None = Depends(get_current_user), -) -> dict[str, str | int | None]: - """Show all context values for debugging.""" - return get_full_context() - - -# Apply middleware -app = RequestContextMiddleware(app) # type: ignore[assignment] - -if __name__ == "__main__": - import uvicorn - - print("Starting server at http://localhost:8000") - print("Try: curl http://localhost:8000/me") - print("Try: curl -H 'Authorization: Bearer token' http://localhost:8000/me") - print("Try: curl -H 'Authorization: Bearer token' http://localhost:8000/debug-context") - uvicorn.run(app, host="0.0.0.0", port=8000) # noqa: S104 diff --git a/.history/examples/custom_fields_20251128204054.py b/.history/examples/custom_fields_20251128204054.py deleted file mode 100644 index 88277d4..0000000 --- a/.history/examples/custom_fields_20251128204054.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Using custom context fields. - -This example shows how to define and use custom context fields -for your application. Use StrEnum for type-safe field access. -""" - -import sys - -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from enum import Enum - - class StrEnum(str, Enum): # noqa: N818 - """Backport of StrEnum for Python 3.10.""" - -from typing import Annotated - -from fastapi import Depends, FastAPI, Header - -from fastapi_request_context import ( - RequestContextMiddleware, - StandardContextField, - get_context, - get_full_context, - set_context, -) - - -class MyContextField(StrEnum): - """Custom context fields for this application.""" - - USER_ID = "user_id" - ORGANIZATION_ID = "organization_id" - TENANT_ID = "tenant_id" - - -app = FastAPI() - - -async def get_current_user( - authorization: Annotated[str | None, Header()] = None, -) -> int | None: - """Dependency that extracts user from auth header and stores in context.""" - if not authorization: - return None - - # In real app, decode JWT token here - user_id = 12345 - - # Store in context - will be available throughout request - set_context(MyContextField.USER_ID, user_id) - set_context(MyContextField.ORGANIZATION_ID, "org-abc") - - return user_id - - -@app.get("/me") -async def me(user_id: int | None = Depends(get_current_user)) -> dict[str, str | int | None]: - """Endpoint that uses custom context fields.""" - return { - "user_id": user_id, - "org_id": get_context(MyContextField.ORGANIZATION_ID), - "request_id": get_context(StandardContextField.REQUEST_ID), - } - - -@app.get("/debug-context") -async def debug_context( - user_id: int | None = Depends(get_current_user), -) -> dict[str, str | int | None]: - """Show all context values for debugging.""" - return get_full_context() - - -# Apply middleware -app = RequestContextMiddleware(app) # type: ignore[assignment] - -if __name__ == "__main__": - import uvicorn - - print("Starting server at http://localhost:8000") - print("Try: curl http://localhost:8000/me") - print("Try: curl -H 'Authorization: Bearer token' http://localhost:8000/me") - print("Try: curl -H 'Authorization: Bearer token' http://localhost:8000/debug-context") - uvicorn.run(app, host="0.0.0.0", port=8000) # noqa: S104 diff --git a/.history/examples/custom_fields_20251128204140.py b/.history/examples/custom_fields_20251128204140.py deleted file mode 100644 index 3555fd4..0000000 --- a/.history/examples/custom_fields_20251128204140.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Using custom context fields. - -This example shows how to define and use custom context fields -for your application. Use StrEnum for type-safe field access. -""" - -import sys - -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from enum import Enum - - class StrEnum(str, Enum): - """Backport of StrEnum for Python 3.10.""" - -from typing import Annotated - -from fastapi import Depends, FastAPI, Header - -from fastapi_request_context import ( - RequestContextMiddleware, - StandardContextField, - get_context, - get_full_context, - set_context, -) - - -class MyContextField(StrEnum): - """Custom context fields for this application.""" - - USER_ID = "user_id" - ORGANIZATION_ID = "organization_id" - TENANT_ID = "tenant_id" - - -app = FastAPI() - - -async def get_current_user( - authorization: Annotated[str | None, Header()] = None, -) -> int | None: - """Dependency that extracts user from auth header and stores in context.""" - if not authorization: - return None - - # In real app, decode JWT token here - user_id = 12345 - - # Store in context - will be available throughout request - set_context(MyContextField.USER_ID, user_id) - set_context(MyContextField.ORGANIZATION_ID, "org-abc") - - return user_id - - -@app.get("/me") -async def me(user_id: int | None = Depends(get_current_user)) -> dict[str, str | int | None]: - """Endpoint that uses custom context fields.""" - return { - "user_id": user_id, - "org_id": get_context(MyContextField.ORGANIZATION_ID), - "request_id": get_context(StandardContextField.REQUEST_ID), - } - - -@app.get("/debug-context") -async def debug_context( - user_id: int | None = Depends(get_current_user), -) -> dict[str, str | int | None]: - """Show all context values for debugging.""" - return get_full_context() - - -# Apply middleware -app = RequestContextMiddleware(app) # type: ignore[assignment] - -if __name__ == "__main__": - import uvicorn - - print("Starting server at http://localhost:8000") - print("Try: curl http://localhost:8000/me") - print("Try: curl -H 'Authorization: Bearer token' http://localhost:8000/me") - print("Try: curl -H 'Authorization: Bearer token' http://localhost:8000/debug-context") - uvicorn.run(app, host="0.0.0.0", port=8000) # noqa: S104 diff --git a/.history/examples/custom_fields_20251128211931.py b/.history/examples/custom_fields_20251128211931.py deleted file mode 100644 index 25607f1..0000000 --- a/.history/examples/custom_fields_20251128211931.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Using custom context fields. - -This example shows how to define and use custom context fields -for your application. Use StrEnum for type-safe field access. -""" - -import sys - -from enum import StrEnum -from typing import Annotated - -from fastapi import Depends, FastAPI, Header - -from fastapi_request_context import ( - RequestContextMiddleware, - StandardContextField, - get_context, - get_full_context, - set_context, -) - - -class MyContextField(StrEnum): - """Custom context fields for this application.""" - - USER_ID = "user_id" - ORGANIZATION_ID = "organization_id" - TENANT_ID = "tenant_id" - - -app = FastAPI() - - -async def get_current_user( - authorization: Annotated[str | None, Header()] = None, -) -> int | None: - """Dependency that extracts user from auth header and stores in context.""" - if not authorization: - return None - - # In real app, decode JWT token here - user_id = 12345 - - # Store in context - will be available throughout request - set_context(MyContextField.USER_ID, user_id) - set_context(MyContextField.ORGANIZATION_ID, "org-abc") - - return user_id - - -@app.get("/me") -async def me(user_id: int | None = Depends(get_current_user)) -> dict[str, str | int | None]: - """Endpoint that uses custom context fields.""" - return { - "user_id": user_id, - "org_id": get_context(MyContextField.ORGANIZATION_ID), - "request_id": get_context(StandardContextField.REQUEST_ID), - } - - -@app.get("/debug-context") -async def debug_context( - user_id: int | None = Depends(get_current_user), -) -> dict[str, str | int | None]: - """Show all context values for debugging.""" - return get_full_context() - - -# Apply middleware -app = RequestContextMiddleware(app) # type: ignore[assignment] - -if __name__ == "__main__": - import uvicorn - - print("Starting server at http://localhost:8000") - print("Try: curl http://localhost:8000/me") - print("Try: curl -H 'Authorization: Bearer token' http://localhost:8000/me") - print("Try: curl -H 'Authorization: Bearer token' http://localhost:8000/debug-context") - uvicorn.run(app, host="0.0.0.0", port=8000) # noqa: S104 diff --git a/.history/examples/custom_fields_20251128211939.py b/.history/examples/custom_fields_20251128211939.py deleted file mode 100644 index 174abd0..0000000 --- a/.history/examples/custom_fields_20251128211939.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Using custom context fields. - -This example shows how to define and use custom context fields -for your application. Use StrEnum for type-safe field access. -""" - -from enum import StrEnum -from typing import Annotated - -from fastapi import Depends, FastAPI, Header - -from fastapi_request_context import ( - RequestContextMiddleware, - StandardContextField, - get_context, - get_full_context, - set_context, -) - - -class MyContextField(StrEnum): - """Custom context fields for this application.""" - - USER_ID = "user_id" - ORGANIZATION_ID = "organization_id" - TENANT_ID = "tenant_id" - - -app = FastAPI() - - -async def get_current_user( - authorization: Annotated[str | None, Header()] = None, -) -> int | None: - """Dependency that extracts user from auth header and stores in context.""" - if not authorization: - return None - - # In real app, decode JWT token here - user_id = 12345 - - # Store in context - will be available throughout request - set_context(MyContextField.USER_ID, user_id) - set_context(MyContextField.ORGANIZATION_ID, "org-abc") - - return user_id - - -@app.get("/me") -async def me(user_id: int | None = Depends(get_current_user)) -> dict[str, str | int | None]: - """Endpoint that uses custom context fields.""" - return { - "user_id": user_id, - "org_id": get_context(MyContextField.ORGANIZATION_ID), - "request_id": get_context(StandardContextField.REQUEST_ID), - } - - -@app.get("/debug-context") -async def debug_context( - user_id: int | None = Depends(get_current_user), -) -> dict[str, str | int | None]: - """Show all context values for debugging.""" - return get_full_context() - - -# Apply middleware -app = RequestContextMiddleware(app) # type: ignore[assignment] - -if __name__ == "__main__": - import uvicorn - - print("Starting server at http://localhost:8000") - print("Try: curl http://localhost:8000/me") - print("Try: curl -H 'Authorization: Bearer token' http://localhost:8000/me") - print("Try: curl -H 'Authorization: Bearer token' http://localhost:8000/debug-context") - uvicorn.run(app, host="0.0.0.0", port=8000) # noqa: S104 diff --git a/.history/examples/logging_integration_20251128203342.py b/.history/examples/logging_integration_20251128203342.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/examples/logging_integration_20251128203343.py b/.history/examples/logging_integration_20251128203343.py deleted file mode 100644 index 49c006e..0000000 --- a/.history/examples/logging_integration_20251128203343.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Logging integration example. - -This example shows how to set up logging formatters -that automatically include request context in log messages. -""" - -import logging -import sys - -from fastapi import FastAPI - -from fastapi_request_context import ( - RequestContextMiddleware, - StandardContextField, - set_context, -) -from fastapi_request_context.formatters import JsonContextFormatter, LocalContextFormatter - - -def setup_json_logging() -> None: - """Set up JSON logging for production environments.""" - handler = logging.StreamHandler(sys.stdout) - handler.setFormatter(JsonContextFormatter()) - logging.basicConfig(handlers=[handler], level=logging.INFO, force=True) - - -def setup_local_logging() -> None: - """Set up human-readable logging for local development.""" - handler = logging.StreamHandler(sys.stdout) - handler.setFormatter( - LocalContextFormatter( - fmt="%(asctime)s %(levelname)s %(context)s %(message)s", - # Shorten UUIDs for readability - shorten_fields={ - StandardContextField.REQUEST_ID, - StandardContextField.CORRELATION_ID, - }, - shorten_length=8, - ) - ) - logging.basicConfig(handlers=[handler], level=logging.INFO, force=True) - - -# Choose based on environment -USE_JSON = False # Set to True for production-style output -if USE_JSON: - setup_json_logging() -else: - setup_local_logging() - -logger = logging.getLogger(__name__) - -app = FastAPI() - - -@app.get("/") -async def root() -> dict[str, str]: - """Endpoint that logs with context.""" - logger.info("Processing request") - return {"status": "ok"} - - -@app.get("/process/{item_id}") -async def process_item(item_id: int) -> dict[str, str]: - """Endpoint that does more processing with logging.""" - logger.info("Starting to process item") - - # Add custom context - set_context("item_id", item_id) - - logger.info("Fetching item from database") - # ... database operation ... - - logger.info("Processing item") - # ... processing ... - - logger.info("Completed processing") - return {"status": "processed", "item_id": str(item_id)} - - -@app.get("/error") -async def trigger_error() -> dict[str, str]: - """Endpoint that logs an error.""" - logger.error("Something went wrong!") - return {"status": "error"} - - -# Apply middleware -app = RequestContextMiddleware(app) # type: ignore[assignment] - -if __name__ == "__main__": - import uvicorn - - print("Starting server at http://localhost:8000") - print(f"Logging mode: {'JSON' if USE_JSON else 'Local'}") - print("Try: curl http://localhost:8000/") - print("Try: curl http://localhost:8000/process/123") - print("Try: curl http://localhost:8000/error") - uvicorn.run(app, host="0.0.0.0", port=8000) # noqa: S104 diff --git a/.history/examples/logging_integration_20251128211235.py b/.history/examples/logging_integration_20251128211235.py deleted file mode 100644 index a04fbee..0000000 --- a/.history/examples/logging_integration_20251128211235.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Logging integration example. - -This example shows how to set up logging formatters -that automatically include request context in log messages. -""" - -import logging -import sys - -from fastapi import FastAPI - -from fastapi_request_context import ( - RequestContextMiddleware, - StandardContextField, - set_context, -) -from fastapi_request_context.formatters import JsonContextFormatter, SimpleContextFormatter - - -def setup_json_logging() -> None: - """Set up JSON logging for production environments.""" - handler = logging.StreamHandler(sys.stdout) - handler.setFormatter(JsonContextFormatter()) - logging.basicConfig(handlers=[handler], level=logging.INFO, force=True) - - -def setup_local_logging() -> None: - """Set up human-readable logging for local development.""" - handler = logging.StreamHandler(sys.stdout) - handler.setFormatter( - SimpleContextFormatter( - fmt="%(asctime)s %(levelname)s %(context)s %(message)s", - # Shorten UUIDs for readability - shorten_fields={ - StandardContextField.REQUEST_ID, - StandardContextField.CORRELATION_ID, - }, - shorten_length=8, - ), - ) - logging.basicConfig(handlers=[handler], level=logging.INFO, force=True) - - -# Choose based on environment -USE_JSON = False # Set to True for production-style output -if USE_JSON: - setup_json_logging() -else: - setup_local_logging() - -logger = logging.getLogger(__name__) - -app = FastAPI() - - -@app.get("/") -async def root() -> dict[str, str]: - """Endpoint that logs with context.""" - logger.info("Processing request") - return {"status": "ok"} - - -@app.get("/process/{item_id}") -async def process_item(item_id: int) -> dict[str, str]: - """Endpoint that does more processing with logging.""" - logger.info("Starting to process item") - - # Add custom context - set_context("item_id", item_id) - - logger.info("Fetching item from database") - # ... database operation ... - - logger.info("Processing item") - # ... processing ... - - logger.info("Completed processing") - return {"status": "processed", "item_id": str(item_id)} - - -@app.get("/error") -async def trigger_error() -> dict[str, str]: - """Endpoint that logs an error.""" - logger.error("Something went wrong!") - return {"status": "error"} - - -# Apply middleware -app = RequestContextMiddleware(app) # type: ignore[assignment] - -if __name__ == "__main__": - import uvicorn - - print("Starting server at http://localhost:8000") - print(f"Logging mode: {'JSON' if USE_JSON else 'Local'}") - print("Try: curl http://localhost:8000/") - print("Try: curl http://localhost:8000/process/123") - print("Try: curl http://localhost:8000/error") - uvicorn.run(app, host="0.0.0.0", port=8000) # noqa: S104 diff --git a/.history/examples/validation_20251128203355.py b/.history/examples/validation_20251128203355.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/examples/validation_20251128203356.py b/.history/examples/validation_20251128203356.py deleted file mode 100644 index 523ef24..0000000 --- a/.history/examples/validation_20251128203356.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Validation utilities example. - -This example shows how to use validation utilities to check -that all routes and dependencies are async. - -Context variables only work correctly with async code. Sync routes -running in thread pools won't have access to request context. -""" - -from contextlib import asynccontextmanager -from typing import Any, AsyncIterator - -from fastapi import Depends, FastAPI - -from fastapi_request_context import RequestContextMiddleware, get_context -from fastapi_request_context.validation import check_routes_and_dependencies_are_async - - -# Example: Async dependency (correct) -async def async_dependency() -> int: - """This dependency is async - context will work correctly.""" - return 42 - - -# Example: Sync dependency (will generate warning) -def sync_dependency() -> str: - """This dependency is sync - may have context issues.""" - return "sync value" - - -@asynccontextmanager -async def lifespan(app: FastAPI) -> AsyncIterator[None]: - """Lifespan context that validates routes on startup.""" - # Check all routes and dependencies are async - warnings = check_routes_and_dependencies_are_async(app) - - if warnings: - print("\n⚠️ Sync routes/dependencies detected:") - for warning in warnings: - print(f" - {warning}") - print("\nContext propagation may not work correctly for these.") - print("Consider making them async.\n") - else: - print("\n✅ All routes and dependencies are async!\n") - - yield - - -app = FastAPI(lifespan=lifespan) - - -# Async route (correct) -@app.get("/async-route") -async def async_route(value: int = Depends(async_dependency)) -> dict[str, Any]: - """This route is async - context works correctly.""" - return { - "status": "ok", - "value": value, - "request_id": get_context("request_id"), - } - - -# Sync route (will generate warning) -@app.get("/sync-route") -def sync_route() -> dict[str, Any]: - """This route is sync - context may not work.""" - # WARNING: get_context() may return None in sync routes! - return { - "status": "ok", - "request_id": get_context("request_id"), # May be None! - } - - -# Route with sync dependency (will generate warning) -@app.get("/sync-dep") -async def route_with_sync_dep(value: str = Depends(sync_dependency)) -> dict[str, Any]: - """This route has a sync dependency - may have issues.""" - return { - "status": "ok", - "value": value, - } - - -# Apply middleware -app = RequestContextMiddleware(app) # type: ignore[assignment] - -if __name__ == "__main__": - import uvicorn - - print("Starting server at http://localhost:8000") - print("Watch startup output for validation warnings") - uvicorn.run(app, host="0.0.0.0", port=8000) # noqa: S104 diff --git a/.history/fastapi_request_context/__init___20251128203030.py b/.history/fastapi_request_context/__init___20251128203030.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/fastapi_request_context/__init___20251128203031.py b/.history/fastapi_request_context/__init___20251128203031.py deleted file mode 100644 index 7d8d247..0000000 --- a/.history/fastapi_request_context/__init___20251128203031.py +++ /dev/null @@ -1,60 +0,0 @@ -"""FastAPI middleware for request ID tracking, correlation IDs, and extensible request context. - -This library provides: -- Automatic request ID generation for every request -- Correlation ID support for distributed tracing -- Response header injection (X-Request-Id, X-Correlation-Id) -- Pluggable context storage (contextvars, context-logging) -- Extensible context fields via StrEnum -- Logging formatters with automatic context injection -- Validation utilities for async routes and dependencies - -Basic Usage: - >>> from fastapi import FastAPI - >>> from fastapi_request_context import RequestContextMiddleware - >>> - >>> app = FastAPI() - >>> app = RequestContextMiddleware(app) - -Custom Fields: - >>> from enum import StrEnum - >>> from fastapi_request_context import set_context, get_context - >>> - >>> class MyField(StrEnum): - ... USER_ID = "user_id" - >>> - >>> set_context(MyField.USER_ID, 123) - >>> user_id = get_context(MyField.USER_ID) -""" - -from fastapi_request_context.config import RequestContextConfig -from fastapi_request_context.context import ( - get_adapter, - get_context, - get_full_context, - set_adapter, - set_context, -) -from fastapi_request_context.fields import StandardContextField -from fastapi_request_context.middleware import ( - FastAPIWrapperMiddleware, - RequestContextMiddleware, -) - -__all__ = [ - # Middleware - "RequestContextMiddleware", - "FastAPIWrapperMiddleware", - # Configuration - "RequestContextConfig", - # Fields - "StandardContextField", - # Context functions - "set_context", - "get_context", - "get_full_context", - "set_adapter", - "get_adapter", -] - -__version__ = "0.1.0" diff --git a/.history/fastapi_request_context/__init___20251128204025.py b/.history/fastapi_request_context/__init___20251128204025.py deleted file mode 100644 index ef2d929..0000000 --- a/.history/fastapi_request_context/__init___20251128204025.py +++ /dev/null @@ -1,56 +0,0 @@ -"""FastAPI middleware for request ID tracking, correlation IDs, and extensible request context. - -This library provides: -- Automatic request ID generation for every request -- Correlation ID support for distributed tracing -- Response header injection (X-Request-Id, X-Correlation-Id) -- Pluggable context storage (contextvars, context-logging) -- Extensible context fields via StrEnum -- Logging formatters with automatic context injection -- Validation utilities for async routes and dependencies - -Basic Usage: - >>> from fastapi import FastAPI - >>> from fastapi_request_context import RequestContextMiddleware - >>> - >>> app = FastAPI() - >>> app = RequestContextMiddleware(app) - -Custom Fields: - >>> from enum import StrEnum - >>> from fastapi_request_context import set_context, get_context - >>> - >>> class MyField(StrEnum): - ... USER_ID = "user_id" - >>> - >>> set_context(MyField.USER_ID, 123) - >>> user_id = get_context(MyField.USER_ID) -""" - -from fastapi_request_context.config import RequestContextConfig -from fastapi_request_context.context import ( - get_adapter, - get_context, - get_full_context, - set_adapter, - set_context, -) -from fastapi_request_context.fields import StandardContextField -from fastapi_request_context.middleware import ( - FastAPIWrapperMiddleware, - RequestContextMiddleware, -) - -__all__ = [ - "FastAPIWrapperMiddleware", - "RequestContextConfig", - "RequestContextMiddleware", - "StandardContextField", - "get_adapter", - "get_context", - "get_full_context", - "set_adapter", - "set_context", -] - -__version__ = "0.1.0" diff --git a/.history/fastapi_request_context/__init___20251128211134.py b/.history/fastapi_request_context/__init___20251128211134.py deleted file mode 100644 index 253d500..0000000 --- a/.history/fastapi_request_context/__init___20251128211134.py +++ /dev/null @@ -1,52 +0,0 @@ -"""FastAPI middleware for request ID tracking, correlation IDs, and extensible request context. - -This library provides: -- Automatic request ID generation for every request -- Correlation ID support for distributed tracing -- Response header injection (X-Request-Id, X-Correlation-Id) -- Pluggable context storage (contextvars, context-logging) -- Extensible context fields via StrEnum -- Logging formatters with automatic context injection -- Validation utilities for async routes and dependencies - -Basic Usage: - >>> from fastapi import FastAPI - >>> from fastapi_request_context import RequestContextMiddleware - >>> - >>> app = FastAPI() - >>> app = RequestContextMiddleware(app) - -Custom Fields: - >>> from enum import StrEnum - >>> from fastapi_request_context import set_context, get_context - >>> - >>> class MyField(StrEnum): - ... USER_ID = "user_id" - >>> - >>> set_context(MyField.USER_ID, 123) - >>> user_id = get_context(MyField.USER_ID) -""" - -from fastapi_request_context.config import RequestContextConfig -from fastapi_request_context.context import ( - get_adapter, - get_context, - get_full_context, - set_adapter, - set_context, -) -from fastapi_request_context.fields import StandardContextField -from fastapi_request_context.middleware import RequestContextMiddleware - -__all__ = [ - "RequestContextConfig", - "RequestContextMiddleware", - "StandardContextField", - "get_adapter", - "get_context", - "get_full_context", - "set_adapter", - "set_context", -] - -__version__ = "0.1.0" diff --git a/.history/fastapi_request_context/adapters/__init___20251128202745.py b/.history/fastapi_request_context/adapters/__init___20251128202745.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/fastapi_request_context/adapters/__init___20251128202746.py b/.history/fastapi_request_context/adapters/__init___20251128202746.py deleted file mode 100644 index cb8ee21..0000000 --- a/.history/fastapi_request_context/adapters/__init___20251128202746.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Context storage adapters.""" - -from fastapi_request_context.adapters.base import ContextAdapter -from fastapi_request_context.adapters.context_logging import ContextLoggingAdapter -from fastapi_request_context.adapters.contextvars import ContextVarsAdapter - -__all__ = [ - "ContextAdapter", - "ContextLoggingAdapter", - "ContextVarsAdapter", -] diff --git a/.history/fastapi_request_context/adapters/base_20251128202756.py b/.history/fastapi_request_context/adapters/base_20251128202756.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/fastapi_request_context/adapters/base_20251128202757.py b/.history/fastapi_request_context/adapters/base_20251128202757.py deleted file mode 100644 index 65d4085..0000000 --- a/.history/fastapi_request_context/adapters/base_20251128202757.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Base protocol for context adapters.""" - -from typing import Any, Protocol, runtime_checkable - - -@runtime_checkable -class ContextAdapter(Protocol): - """Protocol for context storage adapters. - - Adapters provide the underlying storage mechanism for request context. - The library provides two built-in adapters: - - - `ContextVarsAdapter`: Uses Python's built-in contextvars (default, no deps) - - `ContextLoggingAdapter`: Uses context-logging library (optional dependency) - - Custom adapters can be created by implementing this protocol. - - Example: - >>> class RedisAdapter(ContextAdapter): - ... def set_value(self, key: str, value: Any) -> None: - ... redis.hset(self._request_key, key, value) - ... - ... def get_value(self, key: str) -> Any: - ... return redis.hget(self._request_key, key) - ... - ... def get_all(self) -> dict[str, Any]: - ... return redis.hgetall(self._request_key) - ... - ... def enter_context(self, initial_values: dict[str, Any]) -> None: - ... self._request_key = f"request:{uuid4()}" - ... redis.hmset(self._request_key, initial_values) - ... - ... def exit_context(self) -> None: - ... redis.delete(self._request_key) - """ - - def set_value(self, key: str, value: Any) -> None: - """Set a context value. - - Args: - key: The context key (e.g., "request_id", "user_id"). - value: The value to store. - """ - ... - - def get_value(self, key: str) -> Any: - """Get a context value. - - Args: - key: The context key to retrieve. - - Returns: - The stored value, or None if not set. - """ - ... - - def get_all(self) -> dict[str, Any]: - """Get all context values. - - Returns: - A copy of all stored context key-value pairs. - """ - ... - - def enter_context(self, initial_values: dict[str, Any]) -> None: - """Enter a new context scope with initial values. - - Called at the start of each request to initialize context storage. - - Args: - initial_values: Initial context values to set (e.g., request_id). - """ - ... - - def exit_context(self) -> None: - """Exit the current context scope. - - Called at the end of each request to clean up context storage. - """ - ... diff --git a/.history/fastapi_request_context/adapters/base_20251128204014.py b/.history/fastapi_request_context/adapters/base_20251128204014.py deleted file mode 100644 index 6cf77bf..0000000 --- a/.history/fastapi_request_context/adapters/base_20251128204014.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Base protocol for context adapters.""" - -from typing import Any, Protocol, runtime_checkable - - -@runtime_checkable -class ContextAdapter(Protocol): - """Protocol for context storage adapters. - - Adapters provide the underlying storage mechanism for request context. - The library provides two built-in adapters: - - - `ContextVarsAdapter`: Uses Python's built-in contextvars (default, no deps) - - `ContextLoggingAdapter`: Uses context-logging library (optional dependency) - - Custom adapters can be created by implementing this protocol. - - Example: - >>> class RedisAdapter(ContextAdapter): - ... def set_value(self, key: str, value: Any) -> None: - ... redis.hset(self._request_key, key, value) - ... - ... def get_value(self, key: str) -> Any: - ... return redis.hget(self._request_key, key) - ... - ... def get_all(self) -> dict[str, Any]: - ... return redis.hgetall(self._request_key) - ... - ... def enter_context(self, initial_values: dict[str, Any]) -> None: - ... self._request_key = f"request:{uuid4()}" - ... redis.hmset(self._request_key, initial_values) - ... - ... def exit_context(self) -> None: - ... redis.delete(self._request_key) - """ - - def set_value(self, key: str, value: Any) -> None: # noqa: ANN401 - """Set a context value. - - Args: - key: The context key (e.g., "request_id", "user_id"). - value: The value to store. - """ - ... - - def get_value(self, key: str) -> Any: # noqa: ANN401 - """Get a context value. - - Args: - key: The context key to retrieve. - - Returns: - The stored value, or None if not set. - """ - ... - - def get_all(self) -> dict[str, Any]: - """Get all context values. - - Returns: - A copy of all stored context key-value pairs. - """ - ... - - def enter_context(self, initial_values: dict[str, Any]) -> None: - """Enter a new context scope with initial values. - - Called at the start of each request to initialize context storage. - - Args: - initial_values: Initial context values to set (e.g., request_id). - """ - ... - - def exit_context(self) -> None: - """Exit the current context scope. - - Called at the end of each request to clean up context storage. - """ - ... diff --git a/.history/fastapi_request_context/adapters/contextvars_20251128202806.py b/.history/fastapi_request_context/adapters/contextvars_20251128202806.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/fastapi_request_context/adapters/contextvars_20251128202807.py b/.history/fastapi_request_context/adapters/contextvars_20251128202807.py deleted file mode 100644 index 8baa5ba..0000000 --- a/.history/fastapi_request_context/adapters/contextvars_20251128202807.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Context adapter using Python's built-in contextvars.""" - -from contextvars import ContextVar, Token -from typing import Any - -from fastapi_request_context.types import ContextDict - - -class ContextVarsAdapter: - """Context adapter using Python's built-in contextvars. - - This is the default adapter as it has no external dependencies. - It stores all context values in a single ContextVar containing a dict. - - Note: - This adapter works correctly with async code. However, sync code - running in thread pools may not see context values. Use the - validation utilities to ensure all routes and dependencies are async. - - Example: - >>> from fastapi_request_context import RequestContextMiddleware, RequestContextConfig - >>> from fastapi_request_context.adapters import ContextVarsAdapter - >>> - >>> config = RequestContextConfig(context_adapter=ContextVarsAdapter()) - >>> app = RequestContextMiddleware(app, config=config) - """ - - def __init__(self) -> None: - """Initialize the adapter with a ContextVar.""" - self._context_var: ContextVar[ContextDict] = ContextVar( - "request_context", - default={}, - ) - self._token: Token[ContextDict] | None = None - - def set_value(self, key: str, value: Any) -> None: - """Set a context value. - - Args: - key: The context key. - value: The value to store. - """ - context = self._context_var.get() - context[key] = value - - def get_value(self, key: str) -> Any: - """Get a context value. - - Args: - key: The context key to retrieve. - - Returns: - The stored value, or None if not set. - """ - return self._context_var.get().get(key) - - def get_all(self) -> dict[str, Any]: - """Get all context values. - - Returns: - A copy of all stored context key-value pairs. - """ - return dict(self._context_var.get()) - - def enter_context(self, initial_values: dict[str, Any]) -> None: - """Enter a new context scope with initial values. - - Args: - initial_values: Initial context values to set. - """ - self._token = self._context_var.set(dict(initial_values)) - - def exit_context(self) -> None: - """Exit the current context scope and reset to previous state.""" - if self._token is not None: - self._context_var.reset(self._token) - self._token = None diff --git a/.history/fastapi_request_context/adapters/contextvars_20251128203719.py b/.history/fastapi_request_context/adapters/contextvars_20251128203719.py deleted file mode 100644 index d0fdd65..0000000 --- a/.history/fastapi_request_context/adapters/contextvars_20251128203719.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Context adapter using Python's built-in contextvars.""" - -from contextvars import ContextVar -from typing import Any - -from fastapi_request_context.types import ContextDict - -# Module-level ContextVar to ensure proper async isolation -_context_var: ContextVar[ContextDict] = ContextVar( - "fastapi_request_context", - default={}, -) - - -class ContextVarsAdapter: - """Context adapter using Python's built-in contextvars. - - This is the default adapter as it has no external dependencies. - It stores all context values in a module-level ContextVar containing a dict. - - Note: - This adapter works correctly with async code. However, sync code - running in thread pools may not see context values. Use the - validation utilities to ensure all routes and dependencies are async. - - Example: - >>> from fastapi_request_context import RequestContextMiddleware, RequestContextConfig - >>> from fastapi_request_context.adapters import ContextVarsAdapter - >>> - >>> config = RequestContextConfig(context_adapter=ContextVarsAdapter()) - >>> app = RequestContextMiddleware(app, config=config) - """ - - def set_value(self, key: str, value: Any) -> None: - """Set a context value. - - Args: - key: The context key. - value: The value to store. - """ - context = _context_var.get() - context[key] = value - - def get_value(self, key: str) -> Any: - """Get a context value. - - Args: - key: The context key to retrieve. - - Returns: - The stored value, or None if not set. - """ - return _context_var.get().get(key) - - def get_all(self) -> dict[str, Any]: - """Get all context values. - - Returns: - A copy of all stored context key-value pairs. - """ - return dict(_context_var.get()) - - def enter_context(self, initial_values: dict[str, Any]) -> None: - """Enter a new context scope with initial values. - - Args: - initial_values: Initial context values to set. - """ - # Set a new dict for this context - contextvars handles isolation - _context_var.set(dict(initial_values)) - - def exit_context(self) -> None: - """Exit the current context scope. - - Clears the context dict. Each async task has its own copy due to - contextvars copy-on-write semantics. - """ - _context_var.set({}) diff --git a/.history/fastapi_request_context/adapters/contextvars_20251128203915.py b/.history/fastapi_request_context/adapters/contextvars_20251128203915.py deleted file mode 100644 index a107757..0000000 --- a/.history/fastapi_request_context/adapters/contextvars_20251128203915.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Context adapter using Python's built-in contextvars.""" - -from contextvars import ContextVar -from typing import Any - -from fastapi_request_context.types import ContextDict - -# Module-level ContextVar to ensure proper async isolation -_context_var: ContextVar[ContextDict | None] = ContextVar( - "fastapi_request_context", - default=None, -) - - -class ContextVarsAdapter: - """Context adapter using Python's built-in contextvars. - - This is the default adapter as it has no external dependencies. - It stores all context values in a module-level ContextVar containing a dict. - - Note: - This adapter works correctly with async code. However, sync code - running in thread pools may not see context values. Use the - validation utilities to ensure all routes and dependencies are async. - - Example: - >>> from fastapi_request_context import RequestContextMiddleware, RequestContextConfig - >>> from fastapi_request_context.adapters import ContextVarsAdapter - >>> - >>> config = RequestContextConfig(context_adapter=ContextVarsAdapter()) - >>> app = RequestContextMiddleware(app, config=config) - """ - - def set_value(self, key: str, value: Any) -> None: - """Set a context value. - - Args: - key: The context key. - value: The value to store. - """ - context = _context_var.get() - context[key] = value - - def get_value(self, key: str) -> Any: - """Get a context value. - - Args: - key: The context key to retrieve. - - Returns: - The stored value, or None if not set. - """ - return _context_var.get().get(key) - - def get_all(self) -> dict[str, Any]: - """Get all context values. - - Returns: - A copy of all stored context key-value pairs. - """ - return dict(_context_var.get()) - - def enter_context(self, initial_values: dict[str, Any]) -> None: - """Enter a new context scope with initial values. - - Args: - initial_values: Initial context values to set. - """ - # Set a new dict for this context - contextvars handles isolation - _context_var.set(dict(initial_values)) - - def exit_context(self) -> None: - """Exit the current context scope. - - Clears the context dict. Each async task has its own copy due to - contextvars copy-on-write semantics. - """ - _context_var.set({}) diff --git a/.history/fastapi_request_context/adapters/contextvars_20251128203930.py b/.history/fastapi_request_context/adapters/contextvars_20251128203930.py deleted file mode 100644 index a4ea48b..0000000 --- a/.history/fastapi_request_context/adapters/contextvars_20251128203930.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Context adapter using Python's built-in contextvars.""" - -from contextvars import ContextVar -from typing import Any - -from fastapi_request_context.types import ContextDict - -# Module-level ContextVar to ensure proper async isolation -_context_var: ContextVar[ContextDict | None] = ContextVar( - "fastapi_request_context", - default=None, -) - - -class ContextVarsAdapter: - """Context adapter using Python's built-in contextvars. - - This is the default adapter as it has no external dependencies. - It stores all context values in a module-level ContextVar containing a dict. - - Note: - This adapter works correctly with async code. However, sync code - running in thread pools may not see context values. Use the - validation utilities to ensure all routes and dependencies are async. - - Example: - >>> from fastapi_request_context import RequestContextMiddleware, RequestContextConfig - >>> from fastapi_request_context.adapters import ContextVarsAdapter - >>> - >>> config = RequestContextConfig(context_adapter=ContextVarsAdapter()) - >>> app = RequestContextMiddleware(app, config=config) - """ - - def set_value(self, key: str, value: Any) -> None: # noqa: ANN401 - """Set a context value. - - Args: - key: The context key. - value: The value to store. - """ - context = _context_var.get() - if context is not None: - context[key] = value - - def get_value(self, key: str) -> Any: # noqa: ANN401 - """Get a context value. - - Args: - key: The context key to retrieve. - - Returns: - The stored value, or None if not set. - """ - context = _context_var.get() - if context is None: - return None - return context.get(key) - - def get_all(self) -> dict[str, Any]: - """Get all context values. - - Returns: - A copy of all stored context key-value pairs. - """ - context = _context_var.get() - if context is None: - return {} - return dict(context) - - def enter_context(self, initial_values: dict[str, Any]) -> None: - """Enter a new context scope with initial values. - - Args: - initial_values: Initial context values to set. - """ - # Set a new dict for this context - contextvars handles isolation - _context_var.set(dict(initial_values)) - - def exit_context(self) -> None: - """Exit the current context scope. - - Clears the context dict. Each async task has its own copy due to - contextvars copy-on-write semantics. - """ - _context_var.set(None) diff --git a/.history/fastapi_request_context/config_20251128202827.py b/.history/fastapi_request_context/config_20251128202827.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/fastapi_request_context/config_20251128202828.py b/.history/fastapi_request_context/config_20251128202828.py deleted file mode 100644 index 1633afd..0000000 --- a/.history/fastapi_request_context/config_20251128202828.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Configuration for RequestContextMiddleware.""" - -from dataclasses import dataclass, field -from typing import Union -from uuid import uuid4 - -from fastapi_request_context.adapters.base import ContextAdapter -from fastapi_request_context.adapters.contextvars import ContextVarsAdapter -from fastapi_request_context.types import IdGeneratorFunc - - -def _default_id_generator() -> str: - """Generate a UUID4 string.""" - return str(uuid4()) - - -@dataclass -class RequestContextConfig: - """Configuration for RequestContextMiddleware. - - All settings have sensible defaults, so you can use the middleware - with zero configuration. - - Attributes: - request_id_generator: Function to generate request IDs. Default: UUID4. - correlation_id_generator: Function to generate correlation IDs. Default: UUID4. - request_id_header: Header name for request ID. Default: "X-Request-Id". - correlation_id_header: Header name for correlation ID. Default: "X-Correlation-Id". - add_response_headers: Whether to add headers to response. Default: True. - context_adapter: Context storage adapter. Default: ContextVarsAdapter. - scope_types: ASGI scope types to process. Default: {"http", "websocket"}. - - Example: - >>> from fastapi_request_context import RequestContextMiddleware, RequestContextConfig - >>> - >>> # Custom configuration - >>> config = RequestContextConfig( - ... request_id_header="X-My-Request-Id", - ... add_response_headers=False, - ... ) - >>> app = RequestContextMiddleware(app, config=config) - """ - - request_id_generator: IdGeneratorFunc = field(default=_default_id_generator) - """Function to generate unique request IDs.""" - - correlation_id_generator: IdGeneratorFunc = field(default=_default_id_generator) - """Function to generate correlation IDs when not provided in header.""" - - request_id_header: str = "X-Request-Id" - """Header name for request ID in responses.""" - - correlation_id_header: str = "X-Correlation-Id" - """Header name for correlation ID in requests and responses.""" - - add_response_headers: bool = True - """Whether to add request_id and correlation_id to response headers.""" - - context_adapter: Union[ContextAdapter, str] = field( # noqa: UP007 - default_factory=ContextVarsAdapter - ) - """Context storage adapter. Can be an adapter instance or "contextvars"/"context_logging".""" - - scope_types: set[str] = field(default_factory=lambda: {"http", "websocket"}) - """ASGI scope types to process. Other types pass through unchanged.""" diff --git a/.history/fastapi_request_context/context_20251128202838.py b/.history/fastapi_request_context/context_20251128202838.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/fastapi_request_context/context_20251128202839.py b/.history/fastapi_request_context/context_20251128202839.py deleted file mode 100644 index 6fbe3c0..0000000 --- a/.history/fastapi_request_context/context_20251128202839.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Context management functions.""" - -from enum import Enum -from typing import Any - -from fastapi_request_context.adapters.base import ContextAdapter -from fastapi_request_context.adapters.contextvars import ContextVarsAdapter -from fastapi_request_context.types import ContextDict - -# Global adapter instance, set by middleware -_adapter: ContextAdapter = ContextVarsAdapter() - - -def set_adapter(adapter: ContextAdapter) -> None: - """Set the global context adapter. - - This is called by the middleware during initialization. - You typically don't need to call this directly. - - Args: - adapter: The context adapter to use. - """ - global _adapter # noqa: PLW0603 - _adapter = adapter - - -def get_adapter() -> ContextAdapter: - """Get the current context adapter. - - Returns: - The currently configured context adapter. - """ - return _adapter - - -def set_context(key: str | Enum, value: Any) -> None: - """Set a context value. - - The value will be available throughout the request lifecycle and - automatically included in log records (when using appropriate adapters - and formatters). - - Args: - key: The context key. Can be a string or an Enum (uses .value). - value: The value to store. - - Example: - >>> from enum import StrEnum - >>> from fastapi_request_context import set_context - >>> - >>> class MyField(StrEnum): - ... USER_ID = "user_id" - >>> - >>> set_context(MyField.USER_ID, 123) - >>> set_context("custom_field", "custom_value") - """ - field_key = key.value if isinstance(key, Enum) else key - _adapter.set_value(field_key, value) - - -def get_context(key: str | Enum) -> Any: - """Get a context value. - - Args: - key: The context key. Can be a string or an Enum (uses .value). - - Returns: - The stored value, or None if not set. - - Example: - >>> from fastapi_request_context import get_context, StandardContextField - >>> - >>> request_id = get_context(StandardContextField.REQUEST_ID) - >>> user_id = get_context("user_id") - """ - field_key = key.value if isinstance(key, Enum) else key - return _adapter.get_value(field_key) - - -def get_full_context() -> ContextDict: - """Get all context values. - - Returns: - A copy of all stored context key-value pairs. - - Example: - >>> from fastapi_request_context import get_full_context - >>> - >>> context = get_full_context() - >>> print(context) - {'request_id': '...', 'correlation_id': '...', 'user_id': 123} - """ - return _adapter.get_all() diff --git a/.history/fastapi_request_context/context_20251128203948.py b/.history/fastapi_request_context/context_20251128203948.py deleted file mode 100644 index 11bae91..0000000 --- a/.history/fastapi_request_context/context_20251128203948.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Context management functions.""" - -from enum import Enum -from typing import Any - -from fastapi_request_context.adapters.base import ContextAdapter -from fastapi_request_context.adapters.contextvars import ContextVarsAdapter -from fastapi_request_context.types import ContextDict - -# Global adapter instance, set by middleware -_adapter: ContextAdapter = ContextVarsAdapter() - - -def set_adapter(adapter: ContextAdapter) -> None: - """Set the global context adapter. - - This is called by the middleware during initialization. - You typically don't need to call this directly. - - Args: - adapter: The context adapter to use. - """ - global _adapter # noqa: PLW0603 - _adapter = adapter - - -def get_adapter() -> ContextAdapter: - """Get the current context adapter. - - Returns: - The currently configured context adapter. - """ - return _adapter - - -def set_context(key: str | Enum, value: Any) -> None: # noqa: ANN401 - """Set a context value. - - The value will be available throughout the request lifecycle and - automatically included in log records (when using appropriate adapters - and formatters). - - Args: - key: The context key. Can be a string or an Enum (uses .value). - value: The value to store. - - Example: - >>> from enum import StrEnum - >>> from fastapi_request_context import set_context - >>> - >>> class MyField(StrEnum): - ... USER_ID = "user_id" - >>> - >>> set_context(MyField.USER_ID, 123) - >>> set_context("custom_field", "custom_value") - """ - field_key = key.value if isinstance(key, Enum) else key - _adapter.set_value(field_key, value) - - -def get_context(key: str | Enum) -> Any: # noqa: ANN401 - """Get a context value. - - Args: - key: The context key. Can be a string or an Enum (uses .value). - - Returns: - The stored value, or None if not set. - - Example: - >>> from fastapi_request_context import get_context, StandardContextField - >>> - >>> request_id = get_context(StandardContextField.REQUEST_ID) - >>> user_id = get_context("user_id") - """ - field_key = key.value if isinstance(key, Enum) else key - return _adapter.get_value(field_key) - - -def get_full_context() -> ContextDict: - """Get all context values. - - Returns: - A copy of all stored context key-value pairs. - - Example: - >>> from fastapi_request_context import get_full_context - >>> - >>> context = get_full_context() - >>> print(context) - {'request_id': '...', 'correlation_id': '...', 'user_id': 123} - """ - return _adapter.get_all() diff --git a/.history/fastapi_request_context/fields_20251128202743.py b/.history/fastapi_request_context/fields_20251128202743.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/fastapi_request_context/fields_20251128202744.py b/.history/fastapi_request_context/fields_20251128202744.py deleted file mode 100644 index 137c865..0000000 --- a/.history/fastapi_request_context/fields_20251128202744.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Standard context field definitions.""" - -from enum import StrEnum - - -class StandardContextField(StrEnum): - """Standard context fields provided by the library. - - These are the built-in fields that the middleware automatically sets. - Applications can define their own StrEnum for custom fields. - - Example: - >>> from enum import StrEnum - >>> class MyAppField(StrEnum): - ... USER_ID = "user_id" - ... ORG_ID = "org_id" - >>> - >>> set_context(MyAppField.USER_ID, 123) - """ - - REQUEST_ID = "request_id" - """Unique identifier for this request. Always generated, never from header.""" - - CORRELATION_ID = "correlation_id" - """Correlation ID for distributed tracing. May be from header or generated.""" diff --git a/.history/fastapi_request_context/fields_20251128203522.py b/.history/fastapi_request_context/fields_20251128203522.py deleted file mode 100644 index 5d5d7c1..0000000 --- a/.history/fastapi_request_context/fields_20251128203522.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Standard context field definitions.""" - -import sys - -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from enum import Enum - - class StrEnum(str, Enum): - """Backport of StrEnum for Python 3.10.""" - - pass - - -class StandardContextField(StrEnum): - """Standard context fields provided by the library. - - These are the built-in fields that the middleware automatically sets. - Applications can define their own StrEnum for custom fields. - - Example: - >>> from enum import StrEnum - >>> class MyAppField(StrEnum): - ... USER_ID = "user_id" - ... ORG_ID = "org_id" - >>> - >>> set_context(MyAppField.USER_ID, 123) - """ - - REQUEST_ID = "request_id" - """Unique identifier for this request. Always generated, never from header.""" - - CORRELATION_ID = "correlation_id" - """Correlation ID for distributed tracing. May be from header or generated.""" diff --git a/.history/fastapi_request_context/fields_20251128211530.py b/.history/fastapi_request_context/fields_20251128211530.py deleted file mode 100644 index 1057e92..0000000 --- a/.history/fastapi_request_context/fields_20251128211530.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Standard context field definitions.""" - -import sys - -if sys.version_info >= (3, 11): - from enum import StrEnum # pragma: no cover -else: - from enum import Enum - - class StrEnum(str, Enum): - """Backport of StrEnum for Python 3.10.""" - - -class StandardContextField(StrEnum): - """Standard context fields provided by the library. - - These are the built-in fields that the middleware automatically sets. - Applications can define their own StrEnum for custom fields. - - Example: - >>> from enum import StrEnum - >>> class MyAppField(StrEnum): - ... USER_ID = "user_id" - ... ORG_ID = "org_id" - >>> - >>> set_context(MyAppField.USER_ID, 123) - """ - - REQUEST_ID = "request_id" - """Unique identifier for this request. Always generated, never from header.""" - - CORRELATION_ID = "correlation_id" - """Correlation ID for distributed tracing. May be from header or generated.""" diff --git a/.history/fastapi_request_context/fields_20251128211921.py b/.history/fastapi_request_context/fields_20251128211921.py deleted file mode 100644 index 137c865..0000000 --- a/.history/fastapi_request_context/fields_20251128211921.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Standard context field definitions.""" - -from enum import StrEnum - - -class StandardContextField(StrEnum): - """Standard context fields provided by the library. - - These are the built-in fields that the middleware automatically sets. - Applications can define their own StrEnum for custom fields. - - Example: - >>> from enum import StrEnum - >>> class MyAppField(StrEnum): - ... USER_ID = "user_id" - ... ORG_ID = "org_id" - >>> - >>> set_context(MyAppField.USER_ID, 123) - """ - - REQUEST_ID = "request_id" - """Unique identifier for this request. Always generated, never from header.""" - - CORRELATION_ID = "correlation_id" - """Correlation ID for distributed tracing. May be from header or generated.""" diff --git a/.history/fastapi_request_context/formatters/__init___20251128202919.py b/.history/fastapi_request_context/formatters/__init___20251128202919.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/fastapi_request_context/formatters/__init___20251128202921.py b/.history/fastapi_request_context/formatters/__init___20251128202921.py deleted file mode 100644 index b399779..0000000 --- a/.history/fastapi_request_context/formatters/__init___20251128202921.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Logging formatters with context injection.""" - -from fastapi_request_context.formatters.json import JsonContextFormatter -from fastapi_request_context.formatters.local import LocalContextFormatter - -__all__ = [ - "JsonContextFormatter", - "LocalContextFormatter", -] diff --git a/.history/fastapi_request_context/formatters/__init___20251128211213.py b/.history/fastapi_request_context/formatters/__init___20251128211213.py deleted file mode 100644 index adbc310..0000000 --- a/.history/fastapi_request_context/formatters/__init___20251128211213.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Logging formatters for request context.""" - -from fastapi_request_context.formatters.json import JsonContextFormatter -from fastapi_request_context.formatters.simple import SimpleContextFormatter - -__all__ = [ - "JsonContextFormatter", - "SimpleContextFormatter", -] diff --git a/.history/fastapi_request_context/formatters/json_20251128202933.py b/.history/fastapi_request_context/formatters/json_20251128202933.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/fastapi_request_context/formatters/json_20251128202934.py b/.history/fastapi_request_context/formatters/json_20251128202934.py deleted file mode 100644 index 505ad6b..0000000 --- a/.history/fastapi_request_context/formatters/json_20251128202934.py +++ /dev/null @@ -1,99 +0,0 @@ -"""JSON formatter for production logging.""" - -import logging -from typing import Any - -from fastapi_request_context.context import get_full_context - - -class JsonContextFormatter(logging.Formatter): - """JSON formatter that includes request context. - - Outputs log records as JSON with automatic context injection. - Ideal for production environments with log aggregation (ELK, CloudWatch, etc.). - - If python-json-logger is installed, uses it for better JSON formatting. - Otherwise, falls back to a simple JSON implementation. - - Example: - >>> import logging - >>> from fastapi_request_context.formatters import JsonContextFormatter - >>> - >>> handler = logging.StreamHandler() - >>> handler.setFormatter(JsonContextFormatter()) - >>> logging.basicConfig(handlers=[handler], level=logging.INFO) - >>> - >>> logging.info("Request processed") - # Output: {"message": "Request processed", "level": "INFO", - # "request_id": "...", "correlation_id": "..."} - - Attributes: - context_key: Key name for nested context in output (None for flat). - include_standard_fields: Include level, name, timestamp in output. - """ - - def __init__( - self, - fmt: str | None = None, - datefmt: str | None = None, - context_key: str | None = None, - include_standard_fields: bool = True, - ) -> None: - """Initialize the formatter. - - Args: - fmt: Format string (unused, kept for compatibility). - datefmt: Date format string. - context_key: If set, nest context under this key. - If None, merge context at top level. - include_standard_fields: Include level, logger name, timestamp. - """ - super().__init__(fmt=fmt, datefmt=datefmt) - self.context_key = context_key - self.include_standard_fields = include_standard_fields - self._json_formatter: logging.Formatter | None = None - - # Try to use python-json-logger if available - try: - from pythonjsonlogger import jsonlogger - - self._json_formatter = jsonlogger.JsonFormatter(datefmt=datefmt) - except ImportError: - pass - - def format(self, record: logging.LogRecord) -> str: - """Format the log record as JSON. - - Args: - record: The log record to format. - - Returns: - JSON-formatted string. - """ - import json - - # Build base log data - log_data: dict[str, Any] = { - "message": record.getMessage(), - } - - if self.include_standard_fields: - log_data.update({ - "level": record.levelname, - "logger": record.name, - "timestamp": self.formatTime(record, self.datefmt), - }) - - # Add exception info if present - if record.exc_info: - log_data["exception"] = self.formatException(record.exc_info) - - # Add context - context = get_full_context() - if context: - if self.context_key: - log_data[self.context_key] = context - else: - log_data.update(context) - - return json.dumps(log_data, default=str) diff --git a/.history/fastapi_request_context/formatters/json_20251128203951.py b/.history/fastapi_request_context/formatters/json_20251128203951.py deleted file mode 100644 index 3133c51..0000000 --- a/.history/fastapi_request_context/formatters/json_20251128203951.py +++ /dev/null @@ -1,99 +0,0 @@ -"""JSON formatter for production logging.""" - -import logging -from typing import Any - -from fastapi_request_context.context import get_full_context - - -class JsonContextFormatter(logging.Formatter): - """JSON formatter that includes request context. - - Outputs log records as JSON with automatic context injection. - Ideal for production environments with log aggregation (ELK, CloudWatch, etc.). - - If python-json-logger is installed, uses it for better JSON formatting. - Otherwise, falls back to a simple JSON implementation. - - Example: - >>> import logging - >>> from fastapi_request_context.formatters import JsonContextFormatter - >>> - >>> handler = logging.StreamHandler() - >>> handler.setFormatter(JsonContextFormatter()) - >>> logging.basicConfig(handlers=[handler], level=logging.INFO) - >>> - >>> logging.info("Request processed") - # Output: {"message": "Request processed", "level": "INFO", - # "request_id": "...", "correlation_id": "..."} - - Attributes: - context_key: Key name for nested context in output (None for flat). - include_standard_fields: Include level, name, timestamp in output. - """ - - def __init__( - self, - fmt: str | None = None, - datefmt: str | None = None, - context_key: str | None = None, - include_standard_fields: bool = True, - ) -> None: - """Initialize the formatter. - - Args: - fmt: Format string (unused, kept for compatibility). - datefmt: Date format string. - context_key: If set, nest context under this key. - If None, merge context at top level. - include_standard_fields: Include level, logger name, timestamp. - """ - super().__init__(fmt=fmt, datefmt=datefmt) - self.context_key = context_key - self.include_standard_fields = include_standard_fields - self._json_formatter: logging.Formatter | None = None - - # Try to use python-json-logger if available - try: - from pythonjsonlogger import jsonlogger # noqa: PLC0415 - - self._json_formatter = jsonlogger.JsonFormatter(datefmt=datefmt) - except ImportError: - pass - - def format(self, record: logging.LogRecord) -> str: - """Format the log record as JSON. - - Args: - record: The log record to format. - - Returns: - JSON-formatted string. - """ - import json # noqa: PLC0415 - - # Build base log data - log_data: dict[str, Any] = { - "message": record.getMessage(), - } - - if self.include_standard_fields: - log_data.update({ - "level": record.levelname, - "logger": record.name, - "timestamp": self.formatTime(record, self.datefmt), - }) - - # Add exception info if present - if record.exc_info: - log_data["exception"] = self.formatException(record.exc_info) - - # Add context - context = get_full_context() - if context: - if self.context_key: - log_data[self.context_key] = context - else: - log_data.update(context) - - return json.dumps(log_data, default=str) diff --git a/.history/fastapi_request_context/formatters/json_20251128204234.py b/.history/fastapi_request_context/formatters/json_20251128204234.py deleted file mode 100644 index e620398..0000000 --- a/.history/fastapi_request_context/formatters/json_20251128204234.py +++ /dev/null @@ -1,101 +0,0 @@ -"""JSON formatter for production logging.""" - -import logging -from typing import Any - -from fastapi_request_context.context import get_full_context - - -class JsonContextFormatter(logging.Formatter): - """JSON formatter that includes request context. - - Outputs log records as JSON with automatic context injection. - Ideal for production environments with log aggregation (ELK, CloudWatch, etc.). - - If python-json-logger is installed, uses it for better JSON formatting. - Otherwise, falls back to a simple JSON implementation. - - Example: - >>> import logging - >>> from fastapi_request_context.formatters import JsonContextFormatter - >>> - >>> handler = logging.StreamHandler() - >>> handler.setFormatter(JsonContextFormatter()) - >>> logging.basicConfig(handlers=[handler], level=logging.INFO) - >>> - >>> logging.info("Request processed") - # Output: {"message": "Request processed", "level": "INFO", - # "request_id": "...", "correlation_id": "..."} - - Attributes: - context_key: Key name for nested context in output (None for flat). - include_standard_fields: Include level, name, timestamp in output. - """ - - def __init__( - self, - fmt: str | None = None, - datefmt: str | None = None, - context_key: str | None = None, - include_standard_fields: bool = True, - ) -> None: - """Initialize the formatter. - - Args: - fmt: Format string (unused, kept for compatibility). - datefmt: Date format string. - context_key: If set, nest context under this key. - If None, merge context at top level. - include_standard_fields: Include level, logger name, timestamp. - """ - super().__init__(fmt=fmt, datefmt=datefmt) - self.context_key = context_key - self.include_standard_fields = include_standard_fields - self._json_formatter: logging.Formatter | None = None - - # Try to use python-json-logger if available - try: - from pythonjsonlogger.json import JsonFormatter # noqa: PLC0415 - - self._json_formatter = JsonFormatter(datefmt=datefmt) - except ImportError: - pass - - def format(self, record: logging.LogRecord) -> str: - """Format the log record as JSON. - - Args: - record: The log record to format. - - Returns: - JSON-formatted string. - """ - import json # noqa: PLC0415 - - # Build base log data - log_data: dict[str, Any] = { - "message": record.getMessage(), - } - - if self.include_standard_fields: - log_data.update( - { - "level": record.levelname, - "logger": record.name, - "timestamp": self.formatTime(record, self.datefmt), - } - ) - - # Add exception info if present - if record.exc_info: - log_data["exception"] = self.formatException(record.exc_info) - - # Add context - context = get_full_context() - if context: - if self.context_key: - log_data[self.context_key] = context - else: - log_data.update(context) - - return json.dumps(log_data, default=str) diff --git a/.history/fastapi_request_context/formatters/json_20251128211547.py b/.history/fastapi_request_context/formatters/json_20251128211547.py deleted file mode 100644 index e1517ae..0000000 --- a/.history/fastapi_request_context/formatters/json_20251128211547.py +++ /dev/null @@ -1,101 +0,0 @@ -"""JSON formatter for production logging.""" - -import logging -from typing import Any - -from fastapi_request_context.context import get_full_context - - -class JsonContextFormatter(logging.Formatter): - """JSON formatter that includes request context. - - Outputs log records as JSON with automatic context injection. - Ideal for production environments with log aggregation (ELK, CloudWatch, etc.). - - If python-json-logger is installed, uses it for better JSON formatting. - Otherwise, falls back to a simple JSON implementation. - - Example: - >>> import logging - >>> from fastapi_request_context.formatters import JsonContextFormatter - >>> - >>> handler = logging.StreamHandler() - >>> handler.setFormatter(JsonContextFormatter()) - >>> logging.basicConfig(handlers=[handler], level=logging.INFO) - >>> - >>> logging.info("Request processed") - # Output: {"message": "Request processed", "level": "INFO", - # "request_id": "...", "correlation_id": "..."} - - Attributes: - context_key: Key name for nested context in output (None for flat). - include_standard_fields: Include level, name, timestamp in output. - """ - - def __init__( - self, - fmt: str | None = None, - datefmt: str | None = None, - context_key: str | None = None, - include_standard_fields: bool = True, - ) -> None: - """Initialize the formatter. - - Args: - fmt: Format string (unused, kept for compatibility). - datefmt: Date format string. - context_key: If set, nest context under this key. - If None, merge context at top level. - include_standard_fields: Include level, logger name, timestamp. - """ - super().__init__(fmt=fmt, datefmt=datefmt) - self.context_key = context_key - self.include_standard_fields = include_standard_fields - self._json_formatter: logging.Formatter | None = None - - # Try to use python-json-logger if available - try: - from pythonjsonlogger.json import JsonFormatter # noqa: PLC0415 - - self._json_formatter = JsonFormatter(datefmt=datefmt) - except ImportError: # pragma: no cover - pass # Falls back to built-in JSON formatting - - def format(self, record: logging.LogRecord) -> str: - """Format the log record as JSON. - - Args: - record: The log record to format. - - Returns: - JSON-formatted string. - """ - import json # noqa: PLC0415 - - # Build base log data - log_data: dict[str, Any] = { - "message": record.getMessage(), - } - - if self.include_standard_fields: - log_data.update( - { - "level": record.levelname, - "logger": record.name, - "timestamp": self.formatTime(record, self.datefmt), - }, - ) - - # Add exception info if present - if record.exc_info: - log_data["exception"] = self.formatException(record.exc_info) - - # Add context - context = get_full_context() - if context: - if self.context_key: - log_data[self.context_key] = context - else: - log_data.update(context) - - return json.dumps(log_data, default=str) diff --git a/.history/fastapi_request_context/formatters/local_20251128202947.py b/.history/fastapi_request_context/formatters/local_20251128202947.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/fastapi_request_context/formatters/local_20251128202948.py b/.history/fastapi_request_context/formatters/local_20251128202948.py deleted file mode 100644 index cc2371e..0000000 --- a/.history/fastapi_request_context/formatters/local_20251128202948.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Local development formatter with human-readable output.""" - -import logging -from collections.abc import Set as AbstractSet - -from fastapi_request_context.context import get_full_context - - -class LocalContextFormatter(logging.Formatter): - """Human-readable formatter for local development. - - Outputs log records in a readable format with context inline. - Supports shortening UUIDs and hiding fields for cleaner output. - - Example: - >>> import logging - >>> from fastapi_request_context import StandardContextField - >>> from fastapi_request_context.formatters import LocalContextFormatter - >>> - >>> formatter = LocalContextFormatter( - ... fmt="%(asctime)s %(levelname)s %(context)s %(message)s", - ... shorten_fields={StandardContextField.REQUEST_ID}, - ... hidden_fields={StandardContextField.CORRELATION_ID}, - ... ) - >>> handler = logging.StreamHandler() - >>> handler.setFormatter(formatter) - >>> logging.basicConfig(handlers=[handler], level=logging.INFO) - >>> - >>> logging.info("Request processed") - # Output: 2025-01-15 10:30:00 INFO [request_id=3fa85f64] Request processed - - Attributes: - shorten_fields: Fields to truncate to 8 characters (e.g., UUIDs). - hidden_fields: Fields to completely hide from output. - shorten_length: Number of characters for shortened fields. - """ - - def __init__( - self, - fmt: str | None = None, - datefmt: str | None = None, - shorten_fields: AbstractSet[str] | None = None, - hidden_fields: AbstractSet[str] | None = None, - shorten_length: int = 8, - ) -> None: - """Initialize the formatter. - - Args: - fmt: Format string. Use %(context)s for context placeholder. - datefmt: Date format string. - shorten_fields: Set of field names to truncate. - hidden_fields: Set of field names to hide. - shorten_length: Length for shortened field values. - """ - # Default format if not provided - if fmt is None: - fmt = "%(asctime)s %(levelname)s %(context)s %(message)s" - - super().__init__(fmt=fmt, datefmt=datefmt) - self.shorten_fields = shorten_fields or set() - self.hidden_fields = hidden_fields or set() - self.shorten_length = shorten_length - - def _format_context(self) -> str: - """Format context values for display. - - Returns: - Formatted context string like "[key1=val1 key2=val2]". - """ - context = get_full_context() - if not context: - return "" - - parts: list[str] = [] - for key, value in context.items(): - # Skip hidden fields - if key in self.hidden_fields: - continue - - # Shorten if configured - str_value = str(value) - if key in self.shorten_fields and len(str_value) > self.shorten_length: - str_value = str_value[: self.shorten_length] - - parts.append(f"{key}={str_value}") - - if not parts: - return "" - - return f"[{' '.join(parts)}]" - - def format(self, record: logging.LogRecord) -> str: - """Format the log record with context. - - Args: - record: The log record to format. - - Returns: - Formatted string with context. - """ - # Add context to record for format string - record.context = self._format_context() # type: ignore[attr-defined] - return super().format(record) diff --git a/.history/fastapi_request_context/formatters/local_20251128204231.py b/.history/fastapi_request_context/formatters/local_20251128204231.py deleted file mode 100644 index d1debf8..0000000 --- a/.history/fastapi_request_context/formatters/local_20251128204231.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Local development formatter with human-readable output.""" - -import logging -from collections.abc import Set as AbstractSet - -from fastapi_request_context.context import get_full_context - - -class LocalContextFormatter(logging.Formatter): - """Human-readable formatter for local development. - - Outputs log records in a readable format with context inline. - Supports shortening UUIDs and hiding fields for cleaner output. - - Example: - >>> import logging - >>> from fastapi_request_context import StandardContextField - >>> from fastapi_request_context.formatters import LocalContextFormatter - >>> - >>> formatter = LocalContextFormatter( - ... fmt="%(asctime)s %(levelname)s %(context)s %(message)s", - ... shorten_fields={StandardContextField.REQUEST_ID}, - ... hidden_fields={StandardContextField.CORRELATION_ID}, - ... ) - >>> handler = logging.StreamHandler() - >>> handler.setFormatter(formatter) - >>> logging.basicConfig(handlers=[handler], level=logging.INFO) - >>> - >>> logging.info("Request processed") - # Output: 2025-01-15 10:30:00 INFO [request_id=3fa85f64] Request processed - - Attributes: - shorten_fields: Fields to truncate to 8 characters (e.g., UUIDs). - hidden_fields: Fields to completely hide from output. - shorten_length: Number of characters for shortened fields. - """ - - def __init__( - self, - fmt: str | None = None, - datefmt: str | None = None, - shorten_fields: AbstractSet[str] | None = None, - hidden_fields: AbstractSet[str] | None = None, - shorten_length: int = 8, - ) -> None: - """Initialize the formatter. - - Args: - fmt: Format string. Use %(context)s for context placeholder. - datefmt: Date format string. - shorten_fields: Set of field names to truncate. - hidden_fields: Set of field names to hide. - shorten_length: Length for shortened field values. - """ - # Default format if not provided - if fmt is None: - fmt = "%(asctime)s %(levelname)s %(context)s %(message)s" - - super().__init__(fmt=fmt, datefmt=datefmt) - self.shorten_fields = shorten_fields or set() - self.hidden_fields = hidden_fields or set() - self.shorten_length = shorten_length - - def _format_context(self) -> str: - """Format context values for display. - - Returns: - Formatted context string like "[key1=val1 key2=val2]". - """ - context = get_full_context() - if not context: - return "" - - parts: list[str] = [] - for key, value in context.items(): - # Skip hidden fields - if key in self.hidden_fields: - continue - - # Shorten if configured - str_value = str(value) - if key in self.shorten_fields and len(str_value) > self.shorten_length: - str_value = str_value[: self.shorten_length] - - parts.append(f"{key}={str_value}") - - if not parts: - return "" - - return f"[{' '.join(parts)}]" - - def format(self, record: logging.LogRecord) -> str: - """Format the log record with context. - - Args: - record: The log record to format. - - Returns: - Formatted string with context. - """ - # Add context to record for format string - record.context = self._format_context() # type: ignore[attr-defined, unused-ignore] - return super().format(record) diff --git a/.history/fastapi_request_context/formatters/local_20251128204402.py b/.history/fastapi_request_context/formatters/local_20251128204402.py deleted file mode 100644 index cc2371e..0000000 --- a/.history/fastapi_request_context/formatters/local_20251128204402.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Local development formatter with human-readable output.""" - -import logging -from collections.abc import Set as AbstractSet - -from fastapi_request_context.context import get_full_context - - -class LocalContextFormatter(logging.Formatter): - """Human-readable formatter for local development. - - Outputs log records in a readable format with context inline. - Supports shortening UUIDs and hiding fields for cleaner output. - - Example: - >>> import logging - >>> from fastapi_request_context import StandardContextField - >>> from fastapi_request_context.formatters import LocalContextFormatter - >>> - >>> formatter = LocalContextFormatter( - ... fmt="%(asctime)s %(levelname)s %(context)s %(message)s", - ... shorten_fields={StandardContextField.REQUEST_ID}, - ... hidden_fields={StandardContextField.CORRELATION_ID}, - ... ) - >>> handler = logging.StreamHandler() - >>> handler.setFormatter(formatter) - >>> logging.basicConfig(handlers=[handler], level=logging.INFO) - >>> - >>> logging.info("Request processed") - # Output: 2025-01-15 10:30:00 INFO [request_id=3fa85f64] Request processed - - Attributes: - shorten_fields: Fields to truncate to 8 characters (e.g., UUIDs). - hidden_fields: Fields to completely hide from output. - shorten_length: Number of characters for shortened fields. - """ - - def __init__( - self, - fmt: str | None = None, - datefmt: str | None = None, - shorten_fields: AbstractSet[str] | None = None, - hidden_fields: AbstractSet[str] | None = None, - shorten_length: int = 8, - ) -> None: - """Initialize the formatter. - - Args: - fmt: Format string. Use %(context)s for context placeholder. - datefmt: Date format string. - shorten_fields: Set of field names to truncate. - hidden_fields: Set of field names to hide. - shorten_length: Length for shortened field values. - """ - # Default format if not provided - if fmt is None: - fmt = "%(asctime)s %(levelname)s %(context)s %(message)s" - - super().__init__(fmt=fmt, datefmt=datefmt) - self.shorten_fields = shorten_fields or set() - self.hidden_fields = hidden_fields or set() - self.shorten_length = shorten_length - - def _format_context(self) -> str: - """Format context values for display. - - Returns: - Formatted context string like "[key1=val1 key2=val2]". - """ - context = get_full_context() - if not context: - return "" - - parts: list[str] = [] - for key, value in context.items(): - # Skip hidden fields - if key in self.hidden_fields: - continue - - # Shorten if configured - str_value = str(value) - if key in self.shorten_fields and len(str_value) > self.shorten_length: - str_value = str_value[: self.shorten_length] - - parts.append(f"{key}={str_value}") - - if not parts: - return "" - - return f"[{' '.join(parts)}]" - - def format(self, record: logging.LogRecord) -> str: - """Format the log record with context. - - Args: - record: The log record to format. - - Returns: - Formatted string with context. - """ - # Add context to record for format string - record.context = self._format_context() # type: ignore[attr-defined] - return super().format(record) diff --git a/.history/fastapi_request_context/formatters/local_20251128204418.py b/.history/fastapi_request_context/formatters/local_20251128204418.py deleted file mode 100644 index a1c54c4..0000000 --- a/.history/fastapi_request_context/formatters/local_20251128204418.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Local development formatter with human-readable output.""" - -import logging -from collections.abc import Set as AbstractSet - -from fastapi_request_context.context import get_full_context - - -class LocalContextFormatter(logging.Formatter): - """Human-readable formatter for local development. - - Outputs log records in a readable format with context inline. - Supports shortening UUIDs and hiding fields for cleaner output. - - Example: - >>> import logging - >>> from fastapi_request_context import StandardContextField - >>> from fastapi_request_context.formatters import LocalContextFormatter - >>> - >>> formatter = LocalContextFormatter( - ... fmt="%(asctime)s %(levelname)s %(context)s %(message)s", - ... shorten_fields={StandardContextField.REQUEST_ID}, - ... hidden_fields={StandardContextField.CORRELATION_ID}, - ... ) - >>> handler = logging.StreamHandler() - >>> handler.setFormatter(formatter) - >>> logging.basicConfig(handlers=[handler], level=logging.INFO) - >>> - >>> logging.info("Request processed") - # Output: 2025-01-15 10:30:00 INFO [request_id=3fa85f64] Request processed - - Attributes: - shorten_fields: Fields to truncate to 8 characters (e.g., UUIDs). - hidden_fields: Fields to completely hide from output. - shorten_length: Number of characters for shortened fields. - """ - - def __init__( - self, - fmt: str | None = None, - datefmt: str | None = None, - shorten_fields: AbstractSet[str] | None = None, - hidden_fields: AbstractSet[str] | None = None, - shorten_length: int = 8, - ) -> None: - """Initialize the formatter. - - Args: - fmt: Format string. Use %(context)s for context placeholder. - datefmt: Date format string. - shorten_fields: Set of field names to truncate. - hidden_fields: Set of field names to hide. - shorten_length: Length for shortened field values. - """ - # Default format if not provided - if fmt is None: - fmt = "%(asctime)s %(levelname)s %(context)s %(message)s" - - super().__init__(fmt=fmt, datefmt=datefmt) - self.shorten_fields = shorten_fields or set() - self.hidden_fields = hidden_fields or set() - self.shorten_length = shorten_length - - def _format_context(self) -> str: - """Format context values for display. - - Returns: - Formatted context string like "[key1=val1 key2=val2]". - """ - context = get_full_context() - if not context: - return "" - - parts: list[str] = [] - for key, value in context.items(): - # Skip hidden fields - if key in self.hidden_fields: - continue - - # Shorten if configured - str_value = str(value) - if key in self.shorten_fields and len(str_value) > self.shorten_length: - str_value = str_value[: self.shorten_length] - - parts.append(f"{key}={str_value}") - - if not parts: - return "" - - return f"[{' '.join(parts)}]" - - def format(self, record: logging.LogRecord) -> str: - """Format the log record with context. - - Args: - record: The log record to format. - - Returns: - Formatted string with context. - """ - # Add context to record for format string - setattr(record, "context", self._format_context()) - return super().format(record) diff --git a/.history/fastapi_request_context/formatters/simple_20251128204443.py b/.history/fastapi_request_context/formatters/simple_20251128204443.py deleted file mode 100644 index a7076d8..0000000 --- a/.history/fastapi_request_context/formatters/simple_20251128204443.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Local development formatter with human-readable output.""" - -import logging -from collections.abc import Set as AbstractSet - -from fastapi_request_context.context import get_full_context - - -class LocalContextFormatter(logging.Formatter): - """Human-readable formatter for local development. - - Outputs log records in a readable format with context inline. - Supports shortening UUIDs and hiding fields for cleaner output. - - Example: - >>> import logging - >>> from fastapi_request_context import StandardContextField - >>> from fastapi_request_context.formatters import LocalContextFormatter - >>> - >>> formatter = LocalContextFormatter( - ... fmt="%(asctime)s %(levelname)s %(context)s %(message)s", - ... shorten_fields={StandardContextField.REQUEST_ID}, - ... hidden_fields={StandardContextField.CORRELATION_ID}, - ... ) - >>> handler = logging.StreamHandler() - >>> handler.setFormatter(formatter) - >>> logging.basicConfig(handlers=[handler], level=logging.INFO) - >>> - >>> logging.info("Request processed") - # Output: 2025-01-15 10:30:00 INFO [request_id=3fa85f64] Request processed - - Attributes: - shorten_fields: Fields to truncate to 8 characters (e.g., UUIDs). - hidden_fields: Fields to completely hide from output. - shorten_length: Number of characters for shortened fields. - """ - - def __init__( - self, - fmt: str | None = None, - datefmt: str | None = None, - shorten_fields: AbstractSet[str] | None = None, - hidden_fields: AbstractSet[str] | None = None, - shorten_length: int = 8, - ) -> None: - """Initialize the formatter. - - Args: - fmt: Format string. Use %(context)s for context placeholder. - datefmt: Date format string. - shorten_fields: Set of field names to truncate. - hidden_fields: Set of field names to hide. - shorten_length: Length for shortened field values. - """ - # Default format if not provided - if fmt is None: - fmt = "%(asctime)s %(levelname)s %(context)s %(message)s" - - super().__init__(fmt=fmt, datefmt=datefmt) - self.shorten_fields = shorten_fields or set() - self.hidden_fields = hidden_fields or set() - self.shorten_length = shorten_length - - def _format_context(self) -> str: - """Format context values for display. - - Returns: - Formatted context string like "[key1=val1 key2=val2]". - """ - context = get_full_context() - if not context: - return "" - - parts: list[str] = [] - for key, value in context.items(): - # Skip hidden fields - if key in self.hidden_fields: - continue - - # Shorten if configured - str_value = str(value) - if key in self.shorten_fields and len(str_value) > self.shorten_length: - str_value = str_value[: self.shorten_length] - - parts.append(f"{key}={str_value}") - - if not parts: - return "" - - return f"[{' '.join(parts)}]" - - def format(self, record: logging.LogRecord) -> str: - """Format the log record with context. - - Args: - record: The log record to format. - - Returns: - Formatted string with context. - """ - # Add context to record for format string - record.context = self._format_context() - return super().format(record) diff --git a/.history/fastapi_request_context/formatters/simple_20251128211203.py b/.history/fastapi_request_context/formatters/simple_20251128211203.py deleted file mode 100644 index 66194b2..0000000 --- a/.history/fastapi_request_context/formatters/simple_20251128211203.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Simple human-readable formatter with context support.""" - -import logging -from collections.abc import Set as AbstractSet - -from fastapi_request_context.context import get_full_context - - -class SimpleContextFormatter(logging.Formatter): - """Human-readable formatter with inline context. - - Outputs log records in a readable format with context inline. - Supports shortening UUIDs and hiding fields for cleaner output. - Suitable for both development and production environments where - human-readable logs are preferred over JSON. - - Example: - >>> import logging - >>> from fastapi_request_context import StandardContextField - >>> from fastapi_request_context.formatters import SimpleContextFormatter - >>> - >>> formatter = SimpleContextFormatter( - ... fmt="%(asctime)s %(levelname)s %(context)s %(message)s", - ... shorten_fields={StandardContextField.REQUEST_ID}, - ... hidden_fields={StandardContextField.CORRELATION_ID}, - ... ) - >>> handler = logging.StreamHandler() - >>> handler.setFormatter(formatter) - >>> logging.basicConfig(handlers=[handler], level=logging.INFO) - >>> - >>> logging.info("Request processed") - # Output: 2025-01-15 10:30:00 INFO [request_id=3fa85f64] Request processed - - Attributes: - shorten_fields: Fields to truncate to 8 characters (e.g., UUIDs). - hidden_fields: Fields to completely hide from output. - shorten_length: Number of characters for shortened fields. - """ - - def __init__( - self, - fmt: str | None = None, - datefmt: str | None = None, - shorten_fields: AbstractSet[str] | None = None, - hidden_fields: AbstractSet[str] | None = None, - shorten_length: int = 8, - ) -> None: - """Initialize the formatter. - - Args: - fmt: Format string. Use %(context)s for context placeholder. - datefmt: Date format string. - shorten_fields: Set of field names to truncate. - hidden_fields: Set of field names to hide. - shorten_length: Length for shortened field values. - """ - # Default format if not provided - if fmt is None: - fmt = "%(asctime)s %(levelname)s %(context)s %(message)s" - - super().__init__(fmt=fmt, datefmt=datefmt) - self.shorten_fields = shorten_fields or set() - self.hidden_fields = hidden_fields or set() - self.shorten_length = shorten_length - - def _format_context(self) -> str: - """Format context values for display. - - Returns: - Formatted context string like "[key1=val1 key2=val2]". - """ - context = get_full_context() - if not context: - return "" - - parts: list[str] = [] - for key, value in context.items(): - # Skip hidden fields - if key in self.hidden_fields: - continue - - # Shorten if configured - str_value = str(value) - if key in self.shorten_fields and len(str_value) > self.shorten_length: - str_value = str_value[: self.shorten_length] - - parts.append(f"{key}={str_value}") - - if not parts: - return "" - - return f"[{' '.join(parts)}]" - - def format(self, record: logging.LogRecord) -> str: - """Format the log record with context. - - Args: - record: The log record to format. - - Returns: - Formatted string with context. - """ - # Add context to record for format string - record.context = self._format_context() - return super().format(record) diff --git a/.history/fastapi_request_context/middleware_20251128202910.py b/.history/fastapi_request_context/middleware_20251128202910.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/fastapi_request_context/middleware_20251128202911.py b/.history/fastapi_request_context/middleware_20251128202911.py deleted file mode 100644 index e031751..0000000 --- a/.history/fastapi_request_context/middleware_20251128202911.py +++ /dev/null @@ -1,185 +0,0 @@ -"""Request context middleware for FastAPI.""" - -from typing import Any - -from starlette.types import ASGIApp, Message, Receive, Scope, Send - -from fastapi_request_context.adapters.base import ContextAdapter -from fastapi_request_context.adapters.context_logging import ContextLoggingAdapter -from fastapi_request_context.adapters.contextvars import ContextVarsAdapter -from fastapi_request_context.config import RequestContextConfig -from fastapi_request_context.context import set_adapter -from fastapi_request_context.fields import StandardContextField - - -def _get_adapter(adapter_config: ContextAdapter | str) -> ContextAdapter: - """Get adapter instance from config value. - - Args: - adapter_config: Either an adapter instance or a string identifier. - - Returns: - A ContextAdapter instance. - - Raises: - ValueError: If string identifier is not recognized. - """ - if isinstance(adapter_config, str): - if adapter_config == "contextvars": - return ContextVarsAdapter() - if adapter_config == "context_logging": - return ContextLoggingAdapter() - msg = f"Unknown adapter: {adapter_config}. Use 'contextvars' or 'context_logging'." - raise ValueError(msg) - return adapter_config - - -def _get_header_value(scope: Scope, header_name: str) -> str | None: - """Extract header value from ASGI scope (case-insensitive). - - Args: - scope: ASGI scope. - header_name: Header name to find. - - Returns: - Header value or None if not found. - """ - headers: list[tuple[bytes, bytes]] = scope.get("headers", []) - header_name_lower = header_name.lower().encode() - for name, value in headers: - if name.lower() == header_name_lower: - return value.decode() - return None - - -class RequestContextMiddleware: - """ASGI middleware for request context management. - - This middleware: - 1. Generates a unique request_id for each request - 2. Accepts or generates a correlation_id for distributed tracing - 3. Stores both in context (accessible via get_context()) - 4. Adds both to response headers - - The middleware can wrap a FastAPI application or any ASGI app. - - Example: - >>> from fastapi import FastAPI - >>> from fastapi_request_context import RequestContextMiddleware - >>> - >>> app = FastAPI() - >>> app = RequestContextMiddleware(app) - - With configuration: - >>> from fastapi_request_context import RequestContextMiddleware, RequestContextConfig - >>> - >>> config = RequestContextConfig( - ... request_id_header="X-My-Request-Id", - ... add_response_headers=False, - ... ) - >>> app = RequestContextMiddleware(app, config=config) - """ - - def __init__( - self, - app: ASGIApp, - config: RequestContextConfig | None = None, - ) -> None: - """Initialize the middleware. - - Args: - app: The ASGI application to wrap. - config: Optional configuration. Uses defaults if not provided. - """ - self.app = app - self.config = config or RequestContextConfig() - self._adapter = _get_adapter(self.config.context_adapter) - # Set global adapter for context functions - set_adapter(self._adapter) - - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - """Process ASGI request. - - Args: - scope: ASGI scope. - receive: ASGI receive function. - send: ASGI send function. - """ - # Only process configured scope types - if scope["type"] not in self.config.scope_types: - await self.app(scope, receive, send) - return - - # Generate request_id (always new for security) - request_id = self.config.request_id_generator() - - # Get or generate correlation_id - correlation_id = _get_header_value( - scope, self.config.correlation_id_header - ) or self.config.correlation_id_generator() - - # Set up context - initial_context: dict[str, Any] = { - StandardContextField.REQUEST_ID.value: request_id, - StandardContextField.CORRELATION_ID.value: correlation_id, - } - - self._adapter.enter_context(initial_context) - - try: - if self.config.add_response_headers: - # Wrap send to inject headers - async def send_with_headers(message: Message) -> None: - if message["type"] == "http.response.start": - headers: list[tuple[bytes, bytes]] = list( - message.get("headers", []) - ) - headers.append(( - self.config.request_id_header.lower().encode(), - request_id.encode(), - )) - headers.append(( - self.config.correlation_id_header.lower().encode(), - correlation_id.encode(), - )) - message = {**message, "headers": headers} - await send(message) - - await self.app(scope, receive, send_with_headers) - else: - await self.app(scope, receive, send) - finally: - self._adapter.exit_context() - - -class FastAPIWrapperMiddleware: - """Base class for ASGI middleware that wraps FastAPI applications. - - This provides a simpler interface for creating custom middleware - while maintaining proper ASGI compatibility. - - Example: - >>> class MyMiddleware(FastAPIWrapperMiddleware): - ... async def __call__(self, scope, receive, send): - ... # Custom logic before - ... await super().__call__(scope, receive, send) - ... # Custom logic after - """ - - def __init__(self, app: ASGIApp) -> None: - """Initialize the middleware. - - Args: - app: The ASGI application to wrap. - """ - self.app = app - - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - """Process ASGI request. - - Args: - scope: ASGI scope. - receive: ASGI receive function. - send: ASGI send function. - """ - await self.app(scope, receive, send) diff --git a/.history/fastapi_request_context/middleware_20251128211125.py b/.history/fastapi_request_context/middleware_20251128211125.py deleted file mode 100644 index 7d69dd0..0000000 --- a/.history/fastapi_request_context/middleware_20251128211125.py +++ /dev/null @@ -1,162 +0,0 @@ -"""Request context middleware for FastAPI.""" - -from typing import Any - -from starlette.types import ASGIApp, Message, Receive, Scope, Send - -from fastapi_request_context.adapters.base import ContextAdapter -from fastapi_request_context.adapters.context_logging import ContextLoggingAdapter -from fastapi_request_context.adapters.contextvars import ContextVarsAdapter -from fastapi_request_context.config import RequestContextConfig -from fastapi_request_context.context import set_adapter -from fastapi_request_context.fields import StandardContextField - - -def _get_adapter(adapter_config: ContextAdapter | str) -> ContextAdapter: - """Get adapter instance from config value. - - Args: - adapter_config: Either an adapter instance or a string identifier. - - Returns: - A ContextAdapter instance. - - Raises: - ValueError: If string identifier is not recognized. - """ - if isinstance(adapter_config, str): - if adapter_config == "contextvars": - return ContextVarsAdapter() - if adapter_config == "context_logging": - return ContextLoggingAdapter() - msg = f"Unknown adapter: {adapter_config}. Use 'contextvars' or 'context_logging'." - raise ValueError(msg) - return adapter_config - - -def _get_header_value(scope: Scope, header_name: str) -> str | None: - """Extract header value from ASGI scope (case-insensitive). - - Args: - scope: ASGI scope. - header_name: Header name to find. - - Returns: - Header value or None if not found. - """ - headers: list[tuple[bytes, bytes]] = scope.get("headers", []) - header_name_lower = header_name.lower().encode() - for name, value in headers: - if name.lower() == header_name_lower: - return value.decode() - return None - - -class RequestContextMiddleware: - """ASGI middleware for request context management. - - This middleware: - 1. Generates a unique request_id for each request - 2. Accepts or generates a correlation_id for distributed tracing - 3. Stores both in context (accessible via get_context()) - 4. Adds both to response headers - - The middleware can wrap a FastAPI application or any ASGI app. - - Example: - >>> from fastapi import FastAPI - >>> from fastapi_request_context import RequestContextMiddleware - >>> - >>> app = FastAPI() - >>> app = RequestContextMiddleware(app) - - With configuration: - >>> from fastapi_request_context import RequestContextMiddleware, RequestContextConfig - >>> - >>> config = RequestContextConfig( - ... request_id_header="X-My-Request-Id", - ... add_response_headers=False, - ... ) - >>> app = RequestContextMiddleware(app, config=config) - """ - - def __init__( - self, - app: ASGIApp, - config: RequestContextConfig | None = None, - ) -> None: - """Initialize the middleware. - - Args: - app: The ASGI application to wrap. - config: Optional configuration. Uses defaults if not provided. - """ - self.app = app - self.config = config or RequestContextConfig() - self._adapter = _get_adapter(self.config.context_adapter) - # Set global adapter for context functions - set_adapter(self._adapter) - - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - """Process ASGI request. - - Args: - scope: ASGI scope. - receive: ASGI receive function. - send: ASGI send function. - """ - # Only process configured scope types - if scope["type"] not in self.config.scope_types: - await self.app(scope, receive, send) - return - - # Generate request_id (always new for security) - request_id = self.config.request_id_generator() - - # Get or generate correlation_id - correlation_id = ( - _get_header_value( - scope, - self.config.correlation_id_header, - ) - or self.config.correlation_id_generator() - ) - - # Set up context - initial_context: dict[str, Any] = { - StandardContextField.REQUEST_ID.value: request_id, - StandardContextField.CORRELATION_ID.value: correlation_id, - } - - self._adapter.enter_context(initial_context) - - try: - if self.config.add_response_headers: - # Wrap send to inject headers - async def send_with_headers(message: Message) -> None: - if message["type"] == "http.response.start": - headers: list[tuple[bytes, bytes]] = list( - message.get("headers", []), - ) - headers.append( - ( - self.config.request_id_header.lower().encode(), - request_id.encode(), - ), - ) - headers.append( - ( - self.config.correlation_id_header.lower().encode(), - correlation_id.encode(), - ), - ) - message = {**message, "headers": headers} - await send(message) - - await self.app(scope, receive, send_with_headers) - else: - await self.app(scope, receive, send) - finally: - self._adapter.exit_context() - - diff --git a/.history/fastapi_request_context/types_20251128202738.py b/.history/fastapi_request_context/types_20251128202738.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/fastapi_request_context/types_20251128202739.py b/.history/fastapi_request_context/types_20251128202739.py deleted file mode 100644 index 4e0d8ad..0000000 --- a/.history/fastapi_request_context/types_20251128202739.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Type aliases and protocols for fastapi-request-context.""" - -from collections.abc import Callable -from typing import Any, Protocol - - -class IdGenerator(Protocol): - """Protocol for ID generator functions.""" - - def __call__(self) -> str: - """Generate a unique ID string.""" - ... - - -ContextKey = str -"""Type alias for context keys.""" - -ContextValue = Any -"""Type alias for context values.""" - -ContextDict = dict[ContextKey, ContextValue] -"""Type alias for context dictionary.""" - -IdGeneratorFunc = Callable[[], str] -"""Type alias for ID generator functions.""" diff --git a/.history/fastapi_request_context/validation_20251128203015.py b/.history/fastapi_request_context/validation_20251128203015.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/fastapi_request_context/validation_20251128203016.py b/.history/fastapi_request_context/validation_20251128203016.py deleted file mode 100644 index ad09f36..0000000 --- a/.history/fastapi_request_context/validation_20251128203016.py +++ /dev/null @@ -1,175 +0,0 @@ -"""Validation utilities for async routes and dependencies. - -Context variables (and thus request context) require async code to work correctly. -Sync routes running in thread pools won't have access to the context set in the -async middleware. This module provides utilities to check that all routes and -dependencies are async. -""" - -import asyncio -import inspect -import logging -from collections.abc import Callable -from typing import Any - -from fastapi import FastAPI -from fastapi.routing import APIRoute - -logger = logging.getLogger(__name__) - - -def is_async(func: Callable[..., Any]) -> bool: - """Check if a function is async. - - Handles regular functions, coroutine functions, and classes with __call__. - - Args: - func: Function to check. - - Returns: - True if the function is async, False otherwise. - """ - if asyncio.iscoroutinefunction(func): - return True - - # Check for callable classes - if hasattr(func, "__call__"): - return asyncio.iscoroutinefunction(func.__call__) # type: ignore[operator] - - return False - - -def _get_dependency_functions(depends: Any) -> list[Callable[..., Any]]: - """Extract dependency functions from a Depends object. - - Args: - depends: A Depends object or similar. - - Returns: - List of dependency callable functions. - """ - functions: list[Callable[..., Any]] = [] - - # Handle Depends object - if hasattr(depends, "dependency") and depends.dependency is not None: - functions.append(depends.dependency) - - return functions - - -def _get_route_dependencies(route: APIRoute) -> list[Callable[..., Any]]: - """Get all dependency functions from a route. - - Args: - route: FastAPI route to inspect. - - Returns: - List of dependency callable functions. - """ - dependencies: list[Callable[..., Any]] = [] - - # Route-level dependencies - for depends in route.dependencies or []: - dependencies.extend(_get_dependency_functions(depends)) - - # Endpoint parameter dependencies - if route.endpoint: - sig = inspect.signature(route.endpoint) - for param in sig.parameters.values(): - if param.default is not inspect.Parameter.empty: - dependencies.extend(_get_dependency_functions(param.default)) - - return dependencies - - -def check_dependencies_are_async( - dependencies: list[Callable[..., Any]], - *, - raise_on_sync: bool = False, -) -> list[str]: - """Check that all dependencies are async. - - Args: - dependencies: List of dependency functions to check. - raise_on_sync: If True, raise an error for sync dependencies. - - Returns: - List of warning messages for sync dependencies. - - Raises: - ValueError: If raise_on_sync is True and sync dependencies found. - """ - warnings: list[str] = [] - - for dep in dependencies: - if not is_async(dep): - name = getattr(dep, "__name__", str(dep)) - msg = f"Sync dependency: {name}" - warnings.append(msg) - logger.warning(msg) - - if raise_on_sync and warnings: - error_msg = "Sync dependencies found:\n" + "\n".join(f" - {w}" for w in warnings) - raise ValueError(error_msg) - - return warnings - - -def check_routes_and_dependencies_are_async( - app: FastAPI, - *, - raise_on_sync: bool = False, -) -> list[str]: - """Check that all routes and dependencies in a FastAPI app are async. - - This is important because context variables only work correctly in async code. - Sync routes running in thread pools won't have access to request context. - - Call this at application startup to catch issues early. - - Args: - app: FastAPI application to check. - raise_on_sync: If True, raise an error for sync routes/dependencies. - - Returns: - List of warning messages for sync routes/dependencies. - - Raises: - ValueError: If raise_on_sync is True and sync items found. - - Example: - >>> from fastapi import FastAPI - >>> from fastapi_request_context.validation import ( - ... check_routes_and_dependencies_are_async - ... ) - >>> - >>> app = FastAPI() - >>> - >>> @app.on_event("startup") - ... async def validate(): - ... check_routes_and_dependencies_are_async(app) - """ - warnings: list[str] = [] - - for route in app.routes: - if not isinstance(route, APIRoute): - continue - - # Check endpoint - if route.endpoint and not is_async(route.endpoint): - name = getattr(route.endpoint, "__name__", str(route.endpoint)) - msg = f"Sync route: {route.methods} {route.path} ({name})" - warnings.append(msg) - logger.warning(msg) - - # Check dependencies - dependencies = _get_route_dependencies(route) - dep_warnings = check_dependencies_are_async(dependencies) - for dep_warning in dep_warnings: - warnings.append(f" in {route.path}: {dep_warning}") - - if raise_on_sync and warnings: - error_msg = "Sync routes/dependencies found:\n" + "\n".join(f" - {w}" for w in warnings) - raise ValueError(error_msg) - - return warnings diff --git a/.history/fastapi_request_context/validation_20251128203958.py b/.history/fastapi_request_context/validation_20251128203958.py deleted file mode 100644 index dea44f1..0000000 --- a/.history/fastapi_request_context/validation_20251128203958.py +++ /dev/null @@ -1,176 +0,0 @@ -"""Validation utilities for async routes and dependencies. - -Context variables (and thus request context) require async code to work correctly. -Sync routes running in thread pools won't have access to the context set in the -async middleware. This module provides utilities to check that all routes and -dependencies are async. -""" - -import asyncio -import inspect -import logging -from collections.abc import Callable -from typing import Any - -from fastapi import FastAPI -from fastapi.routing import APIRoute - -logger = logging.getLogger(__name__) - - -def is_async(func: Callable[..., Any]) -> bool: - """Check if a function is async. - - Handles regular functions, coroutine functions, and classes with __call__. - - Args: - func: Function to check. - - Returns: - True if the function is async, False otherwise. - """ - if asyncio.iscoroutinefunction(func): - return True - - # Check for callable classes - if callable(func): - call_method = getattr(func, "__call__", None) - if call_method is not None: - return asyncio.iscoroutinefunction(call_method) - - return False - - -def _get_dependency_functions(depends: Any) -> list[Callable[..., Any]]: # noqa: ANN401 - """Extract dependency functions from a Depends object. - - Args: - depends: A Depends object or similar. - - Returns: - List of dependency callable functions. - """ - functions: list[Callable[..., Any]] = [] - - # Handle Depends object - if hasattr(depends, "dependency") and depends.dependency is not None: - functions.append(depends.dependency) - - return functions - - -def _get_route_dependencies(route: APIRoute) -> list[Callable[..., Any]]: - """Get all dependency functions from a route. - - Args: - route: FastAPI route to inspect. - - Returns: - List of dependency callable functions. - """ - dependencies: list[Callable[..., Any]] = [] - - # Route-level dependencies - for depends in route.dependencies or []: - dependencies.extend(_get_dependency_functions(depends)) - - # Endpoint parameter dependencies - if route.endpoint: - sig = inspect.signature(route.endpoint) - for param in sig.parameters.values(): - if param.default is not inspect.Parameter.empty: - dependencies.extend(_get_dependency_functions(param.default)) - - return dependencies - - -def check_dependencies_are_async( - dependencies: list[Callable[..., Any]], - *, - raise_on_sync: bool = False, -) -> list[str]: - """Check that all dependencies are async. - - Args: - dependencies: List of dependency functions to check. - raise_on_sync: If True, raise an error for sync dependencies. - - Returns: - List of warning messages for sync dependencies. - - Raises: - ValueError: If raise_on_sync is True and sync dependencies found. - """ - warnings: list[str] = [] - - for dep in dependencies: - if not is_async(dep): - name = getattr(dep, "__name__", str(dep)) - msg = f"Sync dependency: {name}" - warnings.append(msg) - logger.warning(msg) - - if raise_on_sync and warnings: - error_msg = "Sync dependencies found:\n" + "\n".join(f" - {w}" for w in warnings) - raise ValueError(error_msg) - - return warnings - - -def check_routes_and_dependencies_are_async( - app: FastAPI, - *, - raise_on_sync: bool = False, -) -> list[str]: - """Check that all routes and dependencies in a FastAPI app are async. - - This is important because context variables only work correctly in async code. - Sync routes running in thread pools won't have access to request context. - - Call this at application startup to catch issues early. - - Args: - app: FastAPI application to check. - raise_on_sync: If True, raise an error for sync routes/dependencies. - - Returns: - List of warning messages for sync routes/dependencies. - - Raises: - ValueError: If raise_on_sync is True and sync items found. - - Example: - >>> from fastapi import FastAPI - >>> from fastapi_request_context.validation import ( - ... check_routes_and_dependencies_are_async - ... ) - >>> - >>> app = FastAPI() - >>> - >>> @app.on_event("startup") - ... async def validate(): - ... check_routes_and_dependencies_are_async(app) - """ - warnings: list[str] = [] - - for route in app.routes: - if not isinstance(route, APIRoute): - continue - - # Check endpoint - if route.endpoint and not is_async(route.endpoint): - name = getattr(route.endpoint, "__name__", str(route.endpoint)) - msg = f"Sync route: {route.methods} {route.path} ({name})" - warnings.append(msg) - logger.warning(msg) - - # Check dependencies - dependencies = _get_route_dependencies(route) - dep_warnings = check_dependencies_are_async(dependencies) - warnings.extend(f" in {route.path}: {dep_warning}" for dep_warning in dep_warnings) - - if raise_on_sync and warnings: - error_msg = "Sync routes/dependencies found:\n" + "\n".join(f" - {w}" for w in warnings) - raise ValueError(error_msg) - - return warnings diff --git a/.history/fastapi_request_context/validation_20251128204121.py b/.history/fastapi_request_context/validation_20251128204121.py deleted file mode 100644 index 63902b9..0000000 --- a/.history/fastapi_request_context/validation_20251128204121.py +++ /dev/null @@ -1,174 +0,0 @@ -"""Validation utilities for async routes and dependencies. - -Context variables (and thus request context) require async code to work correctly. -Sync routes running in thread pools won't have access to the context set in the -async middleware. This module provides utilities to check that all routes and -dependencies are async. -""" - -import asyncio -import inspect -import logging -from collections.abc import Callable -from typing import Any - -from fastapi import FastAPI -from fastapi.routing import APIRoute - -logger = logging.getLogger(__name__) - - -def is_async(func: Callable[..., Any]) -> bool: - """Check if a function is async. - - Handles regular functions, coroutine functions, and classes with __call__. - - Args: - func: Function to check. - - Returns: - True if the function is async, False otherwise. - """ - if asyncio.iscoroutinefunction(func): - return True - - # Check for callable classes with async __call__ - if callable(func) and hasattr(func, "__call__"): # noqa: B004 - return asyncio.iscoroutinefunction(func.__call__) # type: ignore[operator] - - return False - - -def _get_dependency_functions(depends: Any) -> list[Callable[..., Any]]: # noqa: ANN401 - """Extract dependency functions from a Depends object. - - Args: - depends: A Depends object or similar. - - Returns: - List of dependency callable functions. - """ - functions: list[Callable[..., Any]] = [] - - # Handle Depends object - if hasattr(depends, "dependency") and depends.dependency is not None: - functions.append(depends.dependency) - - return functions - - -def _get_route_dependencies(route: APIRoute) -> list[Callable[..., Any]]: - """Get all dependency functions from a route. - - Args: - route: FastAPI route to inspect. - - Returns: - List of dependency callable functions. - """ - dependencies: list[Callable[..., Any]] = [] - - # Route-level dependencies - for depends in route.dependencies or []: - dependencies.extend(_get_dependency_functions(depends)) - - # Endpoint parameter dependencies - if route.endpoint: - sig = inspect.signature(route.endpoint) - for param in sig.parameters.values(): - if param.default is not inspect.Parameter.empty: - dependencies.extend(_get_dependency_functions(param.default)) - - return dependencies - - -def check_dependencies_are_async( - dependencies: list[Callable[..., Any]], - *, - raise_on_sync: bool = False, -) -> list[str]: - """Check that all dependencies are async. - - Args: - dependencies: List of dependency functions to check. - raise_on_sync: If True, raise an error for sync dependencies. - - Returns: - List of warning messages for sync dependencies. - - Raises: - ValueError: If raise_on_sync is True and sync dependencies found. - """ - warnings: list[str] = [] - - for dep in dependencies: - if not is_async(dep): - name = getattr(dep, "__name__", str(dep)) - msg = f"Sync dependency: {name}" - warnings.append(msg) - logger.warning(msg) - - if raise_on_sync and warnings: - error_msg = "Sync dependencies found:\n" + "\n".join(f" - {w}" for w in warnings) - raise ValueError(error_msg) - - return warnings - - -def check_routes_and_dependencies_are_async( - app: FastAPI, - *, - raise_on_sync: bool = False, -) -> list[str]: - """Check that all routes and dependencies in a FastAPI app are async. - - This is important because context variables only work correctly in async code. - Sync routes running in thread pools won't have access to request context. - - Call this at application startup to catch issues early. - - Args: - app: FastAPI application to check. - raise_on_sync: If True, raise an error for sync routes/dependencies. - - Returns: - List of warning messages for sync routes/dependencies. - - Raises: - ValueError: If raise_on_sync is True and sync items found. - - Example: - >>> from fastapi import FastAPI - >>> from fastapi_request_context.validation import ( - ... check_routes_and_dependencies_are_async - ... ) - >>> - >>> app = FastAPI() - >>> - >>> @app.on_event("startup") - ... async def validate(): - ... check_routes_and_dependencies_are_async(app) - """ - warnings: list[str] = [] - - for route in app.routes: - if not isinstance(route, APIRoute): - continue - - # Check endpoint - if route.endpoint and not is_async(route.endpoint): - name = getattr(route.endpoint, "__name__", str(route.endpoint)) - msg = f"Sync route: {route.methods} {route.path} ({name})" - warnings.append(msg) - logger.warning(msg) - - # Check dependencies - dependencies = _get_route_dependencies(route) - dep_warnings = check_dependencies_are_async(dependencies) - warnings.extend(f" in {route.path}: {dep_warning}" for dep_warning in dep_warnings) - - if raise_on_sync and warnings: - error_msg = "Sync routes/dependencies found:\n" + "\n".join(f" - {w}" for w in warnings) - raise ValueError(error_msg) - - return warnings diff --git a/.history/fastapi_request_context/validation_20251128204241.py b/.history/fastapi_request_context/validation_20251128204241.py deleted file mode 100644 index 429393c..0000000 --- a/.history/fastapi_request_context/validation_20251128204241.py +++ /dev/null @@ -1,175 +0,0 @@ -"""Validation utilities for async routes and dependencies. - -Context variables (and thus request context) require async code to work correctly. -Sync routes running in thread pools won't have access to the context set in the -async middleware. This module provides utilities to check that all routes and -dependencies are async. -""" - -import asyncio -import inspect -import logging -from collections.abc import Callable -from typing import Any - -from fastapi import FastAPI -from fastapi.routing import APIRoute - -logger = logging.getLogger(__name__) - - -def is_async(func: Callable[..., Any]) -> bool: - """Check if a function is async. - - Handles regular functions, coroutine functions, and classes with __call__. - - Args: - func: Function to check. - - Returns: - True if the function is async, False otherwise. - """ - if asyncio.iscoroutinefunction(func): - return True - - # Check for callable classes with async __call__ - if callable(func) and hasattr(func, "__call__"): # noqa: B004 - return asyncio.iscoroutinefunction(func.__call__) - - return False - - -def _get_dependency_functions(depends: Any) -> list[Callable[..., Any]]: # noqa: ANN401 - """Extract dependency functions from a Depends object. - - Args: - depends: A Depends object or similar. - - Returns: - List of dependency callable functions. - """ - functions: list[Callable[..., Any]] = [] - - # Handle Depends object - if hasattr(depends, "dependency") and depends.dependency is not None: - functions.append(depends.dependency) - - return functions - - -def _get_route_dependencies(route: APIRoute) -> list[Callable[..., Any]]: - """Get all dependency functions from a route. - - Args: - route: FastAPI route to inspect. - - Returns: - List of dependency callable functions. - """ - dependencies: list[Callable[..., Any]] = [] - - # Route-level dependencies - if route.dependencies: - for depends in route.dependencies: - dependencies.extend(_get_dependency_functions(depends)) - - # Endpoint parameter dependencies - if route.endpoint: - sig = inspect.signature(route.endpoint) - for param in sig.parameters.values(): - if param.default is not inspect.Parameter.empty: - dependencies.extend(_get_dependency_functions(param.default)) - - return dependencies - - -def check_dependencies_are_async( - dependencies: list[Callable[..., Any]], - *, - raise_on_sync: bool = False, -) -> list[str]: - """Check that all dependencies are async. - - Args: - dependencies: List of dependency functions to check. - raise_on_sync: If True, raise an error for sync dependencies. - - Returns: - List of warning messages for sync dependencies. - - Raises: - ValueError: If raise_on_sync is True and sync dependencies found. - """ - warnings: list[str] = [] - - for dep in dependencies: - if not is_async(dep): - name = getattr(dep, "__name__", str(dep)) - msg = f"Sync dependency: {name}" - warnings.append(msg) - logger.warning(msg) - - if raise_on_sync and warnings: - error_msg = "Sync dependencies found:\n" + "\n".join(f" - {w}" for w in warnings) - raise ValueError(error_msg) - - return warnings - - -def check_routes_and_dependencies_are_async( - app: FastAPI, - *, - raise_on_sync: bool = False, -) -> list[str]: - """Check that all routes and dependencies in a FastAPI app are async. - - This is important because context variables only work correctly in async code. - Sync routes running in thread pools won't have access to request context. - - Call this at application startup to catch issues early. - - Args: - app: FastAPI application to check. - raise_on_sync: If True, raise an error for sync routes/dependencies. - - Returns: - List of warning messages for sync routes/dependencies. - - Raises: - ValueError: If raise_on_sync is True and sync items found. - - Example: - >>> from fastapi import FastAPI - >>> from fastapi_request_context.validation import ( - ... check_routes_and_dependencies_are_async - ... ) - >>> - >>> app = FastAPI() - >>> - >>> @app.on_event("startup") - ... async def validate(): - ... check_routes_and_dependencies_are_async(app) - """ - warnings: list[str] = [] - - for route in app.routes: - if not isinstance(route, APIRoute): - continue - - # Check endpoint - if route.endpoint is not None and not is_async(route.endpoint): - name = getattr(route.endpoint, "__name__", str(route.endpoint)) - msg = f"Sync route: {route.methods} {route.path} ({name})" - warnings.append(msg) - logger.warning(msg) - - # Check dependencies - dependencies = _get_route_dependencies(route) - dep_warnings = check_dependencies_are_async(dependencies) - warnings.extend(f" in {route.path}: {dep_warning}" for dep_warning in dep_warnings) - - if raise_on_sync and warnings: - error_msg = "Sync routes/dependencies found:\n" + "\n".join(f" - {w}" for w in warnings) - raise ValueError(error_msg) - - return warnings diff --git a/.history/fastapi_request_context/validation_20251128204255.py b/.history/fastapi_request_context/validation_20251128204255.py deleted file mode 100644 index d175ad6..0000000 --- a/.history/fastapi_request_context/validation_20251128204255.py +++ /dev/null @@ -1,175 +0,0 @@ -"""Validation utilities for async routes and dependencies. - -Context variables (and thus request context) require async code to work correctly. -Sync routes running in thread pools won't have access to the context set in the -async middleware. This module provides utilities to check that all routes and -dependencies are async. -""" - -import asyncio -import inspect -import logging -from collections.abc import Callable -from typing import Any - -from fastapi import FastAPI -from fastapi.routing import APIRoute - -logger = logging.getLogger(__name__) - - -def is_async(func: Callable[..., Any]) -> bool: - """Check if a function is async. - - Handles regular functions, coroutine functions, and classes with __call__. - - Args: - func: Function to check. - - Returns: - True if the function is async, False otherwise. - """ - if asyncio.iscoroutinefunction(func): - return True - - # Check for callable classes with async __call__ - if callable(func) and hasattr(func, "__call__"): # noqa: B004 - return asyncio.iscoroutinefunction(func.__call__) - - return False - - -def _get_dependency_functions(depends: Any) -> list[Callable[..., Any]]: # noqa: ANN401 - """Extract dependency functions from a Depends object. - - Args: - depends: A Depends object or similar. - - Returns: - List of dependency callable functions. - """ - functions: list[Callable[..., Any]] = [] - - # Handle Depends object - if hasattr(depends, "dependency") and depends.dependency is not None: - functions.append(depends.dependency) - - return functions - - -def _get_route_dependencies(route: APIRoute) -> list[Callable[..., Any]]: - """Get all dependency functions from a route. - - Args: - route: FastAPI route to inspect. - - Returns: - List of dependency callable functions. - """ - dependencies: list[Callable[..., Any]] = [] - - # Route-level dependencies - if route.dependencies: - for depends in route.dependencies: - dependencies.extend(_get_dependency_functions(depends)) - - # Endpoint parameter dependencies - if route.endpoint is not None: - sig = inspect.signature(route.endpoint) - for param in sig.parameters.values(): - if param.default is not inspect.Parameter.empty: - dependencies.extend(_get_dependency_functions(param.default)) - - return dependencies - - -def check_dependencies_are_async( - dependencies: list[Callable[..., Any]], - *, - raise_on_sync: bool = False, -) -> list[str]: - """Check that all dependencies are async. - - Args: - dependencies: List of dependency functions to check. - raise_on_sync: If True, raise an error for sync dependencies. - - Returns: - List of warning messages for sync dependencies. - - Raises: - ValueError: If raise_on_sync is True and sync dependencies found. - """ - warnings: list[str] = [] - - for dep in dependencies: - if not is_async(dep): - name = getattr(dep, "__name__", str(dep)) - msg = f"Sync dependency: {name}" - warnings.append(msg) - logger.warning(msg) - - if raise_on_sync and warnings: - error_msg = "Sync dependencies found:\n" + "\n".join(f" - {w}" for w in warnings) - raise ValueError(error_msg) - - return warnings - - -def check_routes_and_dependencies_are_async( - app: FastAPI, - *, - raise_on_sync: bool = False, -) -> list[str]: - """Check that all routes and dependencies in a FastAPI app are async. - - This is important because context variables only work correctly in async code. - Sync routes running in thread pools won't have access to request context. - - Call this at application startup to catch issues early. - - Args: - app: FastAPI application to check. - raise_on_sync: If True, raise an error for sync routes/dependencies. - - Returns: - List of warning messages for sync routes/dependencies. - - Raises: - ValueError: If raise_on_sync is True and sync items found. - - Example: - >>> from fastapi import FastAPI - >>> from fastapi_request_context.validation import ( - ... check_routes_and_dependencies_are_async - ... ) - >>> - >>> app = FastAPI() - >>> - >>> @app.on_event("startup") - ... async def validate(): - ... check_routes_and_dependencies_are_async(app) - """ - warnings: list[str] = [] - - for route in app.routes: - if not isinstance(route, APIRoute): - continue - - # Check endpoint - if route.endpoint is not None and not is_async(route.endpoint): - name = getattr(route.endpoint, "__name__", str(route.endpoint)) - msg = f"Sync route: {route.methods} {route.path} ({name})" - warnings.append(msg) - logger.warning(msg) - - # Check dependencies - dependencies = _get_route_dependencies(route) - dep_warnings = check_dependencies_are_async(dependencies) - warnings.extend(f" in {route.path}: {dep_warning}" for dep_warning in dep_warnings) - - if raise_on_sync and warnings: - error_msg = "Sync routes/dependencies found:\n" + "\n".join(f" - {w}" for w in warnings) - raise ValueError(error_msg) - - return warnings diff --git a/.history/fastapi_request_context/validation_20251128211403.py b/.history/fastapi_request_context/validation_20251128211403.py deleted file mode 100644 index a93fec3..0000000 --- a/.history/fastapi_request_context/validation_20251128211403.py +++ /dev/null @@ -1,175 +0,0 @@ -"""Validation utilities for async routes and dependencies. - -Context variables (and thus request context) require async code to work correctly. -Sync routes running in thread pools won't have access to the context set in the -async middleware. This module provides utilities to check that all routes and -dependencies are async. -""" - -import asyncio -import inspect -import logging -from collections.abc import Callable -from typing import Any - -from fastapi import FastAPI -from fastapi.routing import APIRoute - -logger = logging.getLogger(__name__) - - -def is_async(func: Callable[..., Any]) -> bool: - """Check if a function is async. - - Handles regular functions, coroutine functions, and classes with __call__. - - Args: - func: Function to check. - - Returns: - True if the function is async, False otherwise. - """ - if asyncio.iscoroutinefunction(func): - return True - - # Check for callable classes with async __call__ - if hasattr(func, "__call__"): - return asyncio.iscoroutinefunction(func.__call__) - - return False # pragma: no cover - - -def _get_dependency_functions(depends: Any) -> list[Callable[..., Any]]: # noqa: ANN401 - """Extract dependency functions from a Depends object. - - Args: - depends: A Depends object or similar. - - Returns: - List of dependency callable functions. - """ - functions: list[Callable[..., Any]] = [] - - # Handle Depends object - if hasattr(depends, "dependency") and depends.dependency is not None: - functions.append(depends.dependency) - - return functions - - -def _get_route_dependencies(route: APIRoute) -> list[Callable[..., Any]]: - """Get all dependency functions from a route. - - Args: - route: FastAPI route to inspect. - - Returns: - List of dependency callable functions. - """ - dependencies: list[Callable[..., Any]] = [] - - # Route-level dependencies - if route.dependencies: - for depends in route.dependencies: - dependencies.extend(_get_dependency_functions(depends)) - - # Endpoint parameter dependencies - if route.endpoint is not None: - sig = inspect.signature(route.endpoint) - for param in sig.parameters.values(): - if param.default is not inspect.Parameter.empty: - dependencies.extend(_get_dependency_functions(param.default)) - - return dependencies - - -def check_dependencies_are_async( - dependencies: list[Callable[..., Any]], - *, - raise_on_sync: bool = False, -) -> list[str]: - """Check that all dependencies are async. - - Args: - dependencies: List of dependency functions to check. - raise_on_sync: If True, raise an error for sync dependencies. - - Returns: - List of warning messages for sync dependencies. - - Raises: - ValueError: If raise_on_sync is True and sync dependencies found. - """ - warnings: list[str] = [] - - for dep in dependencies: - if not is_async(dep): - name = getattr(dep, "__name__", str(dep)) - msg = f"Sync dependency: {name}" - warnings.append(msg) - logger.warning(msg) - - if raise_on_sync and warnings: - error_msg = "Sync dependencies found:\n" + "\n".join(f" - {w}" for w in warnings) - raise ValueError(error_msg) - - return warnings - - -def check_routes_and_dependencies_are_async( - app: FastAPI, - *, - raise_on_sync: bool = False, -) -> list[str]: - """Check that all routes and dependencies in a FastAPI app are async. - - This is important because context variables only work correctly in async code. - Sync routes running in thread pools won't have access to request context. - - Call this at application startup to catch issues early. - - Args: - app: FastAPI application to check. - raise_on_sync: If True, raise an error for sync routes/dependencies. - - Returns: - List of warning messages for sync routes/dependencies. - - Raises: - ValueError: If raise_on_sync is True and sync items found. - - Example: - >>> from fastapi import FastAPI - >>> from fastapi_request_context.validation import ( - ... check_routes_and_dependencies_are_async - ... ) - >>> - >>> app = FastAPI() - >>> - >>> @app.on_event("startup") - ... async def validate(): - ... check_routes_and_dependencies_are_async(app) - """ - warnings: list[str] = [] - - for route in app.routes: - if not isinstance(route, APIRoute): - continue - - # Check endpoint - if route.endpoint is not None and not is_async(route.endpoint): - name = getattr(route.endpoint, "__name__", str(route.endpoint)) - msg = f"Sync route: {route.methods} {route.path} ({name})" - warnings.append(msg) - logger.warning(msg) - - # Check dependencies - dependencies = _get_route_dependencies(route) - dep_warnings = check_dependencies_are_async(dependencies) - warnings.extend(f" in {route.path}: {dep_warning}" for dep_warning in dep_warnings) - - if raise_on_sync and warnings: - error_msg = "Sync routes/dependencies found:\n" + "\n".join(f" - {w}" for w in warnings) - raise ValueError(error_msg) - - return warnings diff --git a/.history/fastapi_request_context/validation_20251128211747.py b/.history/fastapi_request_context/validation_20251128211747.py deleted file mode 100644 index 8017e2b..0000000 --- a/.history/fastapi_request_context/validation_20251128211747.py +++ /dev/null @@ -1,175 +0,0 @@ -"""Validation utilities for async routes and dependencies. - -Context variables (and thus request context) require async code to work correctly. -Sync routes running in thread pools won't have access to the context set in the -async middleware. This module provides utilities to check that all routes and -dependencies are async. -""" - -import asyncio -import inspect -import logging -from collections.abc import Callable -from typing import Any - -from fastapi import FastAPI -from fastapi.routing import APIRoute - -logger = logging.getLogger(__name__) - - -def is_async(func: Callable[..., Any]) -> bool: - """Check if a function is async. - - Handles regular functions, coroutine functions, and classes with __call__. - - Args: - func: Function to check. - - Returns: - True if the function is async, False otherwise. - """ - if asyncio.iscoroutinefunction(func): - return True - - # Check for callable classes with async __call__ - if hasattr(func, "__call__"): # noqa: B004 - return asyncio.iscoroutinefunction(func.__call__) - - return False # pragma: no cover - - -def _get_dependency_functions(depends: Any) -> list[Callable[..., Any]]: # noqa: ANN401 - """Extract dependency functions from a Depends object. - - Args: - depends: A Depends object or similar. - - Returns: - List of dependency callable functions. - """ - functions: list[Callable[..., Any]] = [] - - # Handle Depends object - if hasattr(depends, "dependency") and depends.dependency is not None: - functions.append(depends.dependency) - - return functions - - -def _get_route_dependencies(route: APIRoute) -> list[Callable[..., Any]]: - """Get all dependency functions from a route. - - Args: - route: FastAPI route to inspect. - - Returns: - List of dependency callable functions. - """ - dependencies: list[Callable[..., Any]] = [] - - # Route-level dependencies - if route.dependencies: - for depends in route.dependencies: - dependencies.extend(_get_dependency_functions(depends)) - - # Endpoint parameter dependencies - if route.endpoint is not None: - sig = inspect.signature(route.endpoint) - for param in sig.parameters.values(): - if param.default is not inspect.Parameter.empty: - dependencies.extend(_get_dependency_functions(param.default)) - - return dependencies - - -def check_dependencies_are_async( - dependencies: list[Callable[..., Any]], - *, - raise_on_sync: bool = False, -) -> list[str]: - """Check that all dependencies are async. - - Args: - dependencies: List of dependency functions to check. - raise_on_sync: If True, raise an error for sync dependencies. - - Returns: - List of warning messages for sync dependencies. - - Raises: - ValueError: If raise_on_sync is True and sync dependencies found. - """ - warnings: list[str] = [] - - for dep in dependencies: - if not is_async(dep): - name = getattr(dep, "__name__", str(dep)) - msg = f"Sync dependency: {name}" - warnings.append(msg) - logger.warning(msg) - - if raise_on_sync and warnings: - error_msg = "Sync dependencies found:\n" + "\n".join(f" - {w}" for w in warnings) - raise ValueError(error_msg) - - return warnings - - -def check_routes_and_dependencies_are_async( - app: FastAPI, - *, - raise_on_sync: bool = False, -) -> list[str]: - """Check that all routes and dependencies in a FastAPI app are async. - - This is important because context variables only work correctly in async code. - Sync routes running in thread pools won't have access to request context. - - Call this at application startup to catch issues early. - - Args: - app: FastAPI application to check. - raise_on_sync: If True, raise an error for sync routes/dependencies. - - Returns: - List of warning messages for sync routes/dependencies. - - Raises: - ValueError: If raise_on_sync is True and sync items found. - - Example: - >>> from fastapi import FastAPI - >>> from fastapi_request_context.validation import ( - ... check_routes_and_dependencies_are_async - ... ) - >>> - >>> app = FastAPI() - >>> - >>> @app.on_event("startup") - ... async def validate(): - ... check_routes_and_dependencies_are_async(app) - """ - warnings: list[str] = [] - - for route in app.routes: - if not isinstance(route, APIRoute): - continue - - # Check endpoint - if route.endpoint is not None and not is_async(route.endpoint): - name = getattr(route.endpoint, "__name__", str(route.endpoint)) - msg = f"Sync route: {route.methods} {route.path} ({name})" - warnings.append(msg) - logger.warning(msg) - - # Check dependencies - dependencies = _get_route_dependencies(route) - dep_warnings = check_dependencies_are_async(dependencies) - warnings.extend(f" in {route.path}: {dep_warning}" for dep_warning in dep_warnings) - - if raise_on_sync and warnings: - error_msg = "Sync routes/dependencies found:\n" + "\n".join(f" - {w}" for w in warnings) - raise ValueError(error_msg) - - return warnings diff --git a/.history/pyproject_20251128202707.toml b/.history/pyproject_20251128202707.toml deleted file mode 100644 index e69de29..0000000 diff --git a/.history/pyproject_20251128202708.toml b/.history/pyproject_20251128202708.toml deleted file mode 100644 index a5d6c11..0000000 --- a/.history/pyproject_20251128202708.toml +++ /dev/null @@ -1,265 +0,0 @@ -[project] -name = "fastapi-request-context" -version = "0.1.0" -description = "FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with logging integration" -authors = [ - {name = "Adrian Dankiv", email = "adr-007@ukr.net"} -] -license = {text = "MIT"} -readme = "README.md" -keywords = ["fastapi", "request-id", "correlation-id", "request-context", "middleware", "logging", "tracing"] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Framework :: FastAPI", -] -requires-python = ">=3.10" -dependencies = [ - "fastapi>=0.100.0", -] - -[project.optional-dependencies] -context-logging = ["context-logging>=0.7.0"] -json-formatter = ["python-json-logger>=2.0.0"] -all = [ - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", -] - -[project.urls] -Homepage = "https://github.com/ADR-007/fastapi-request-context" -Repository = "https://github.com/ADR-007/fastapi-request-context" -Issues = "https://github.com/ADR-007/fastapi-request-context/issues" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.uv] -dev-dependencies = [ - "ruff>=0.3.5", - "mypy>=1.9.0", - "pytest>=8.1.1", - "pytest-asyncio>=0.23.0", - "coverage>=7.4.4", - "tox>=4.14.2", - "tox-uv>=1.0.0", - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", - "httpx>=0.27.0", -] - -[tool.tox] -legacy_tox_ini = """ -[tox] -min_version = 4.0 -env_list = - lint - type - py{310,311,312}-fastapi{100,110,115} - coverage - -[gh-actions] -python = - 3.12: lint, type, py312-fastapi{100,110,115} - 3.11: py311-fastapi115 - 3.10: py310-fastapi115 - -[testenv] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - context-logging - python-json-logger - httpx - fastapi100: fastapi>=0.100.0,<0.110.0 - fastapi110: fastapi>=0.110.0,<0.115.0 - fastapi115: fastapi>=0.115.0 -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 - -[testenv:lint] -runner = uv-venv-lock-runner -deps = - ruff - mypy - pytest - context-logging - python-json-logger -commands = - ruff check fastapi_request_context tests/ examples/ - ruff format --check fastapi_request_context tests/ examples/ - mypy fastapi_request_context tests/ - -[testenv:coverage] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - fastapi - context-logging - python-json-logger - httpx -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 -""" - -[tool.semantic_release] -version_toml = [ - "pyproject.toml:project.version", -] -assets = [] -commit_message = "{version}\n\nAutomatically generated by python-semantic-release" -commit_parser = "angular" -logging_use_named_masks = false -major_on_zero = true -tag_format = "v{version}" - -[tool.semantic_release.branches.main] -match = "(main|master)" -prerelease_token = "rc" -prerelease = false - -[tool.semantic_release.changelog] -template_dir = "templates" -changelog_file = "CHANGELOG.md" -exclude_commit_patterns = [] - -[tool.semantic_release.changelog.environment] -block_start_string = "{%" -block_end_string = "%}" -variable_start_string = "{{" -variable_end_string = "}}" -comment_start_string = "{#" -comment_end_string = "#}" -trim_blocks = false -lstrip_blocks = false -newline_sequence = "\n" -keep_trailing_newline = false -extensions = [] -autoescape = true - -[tool.semantic_release.commit_author] -env = "GIT_COMMIT_AUTHOR" -default = "semantic-release " - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf", "build", "docs"] - -[tool.semantic_release.remote] -name = "origin" -type = "github" -ignore_token_for_push = false - -[tool.semantic_release.publish] -dist_glob_patterns = ["dist/*"] -upload_to_vcs_release = true - -[tool.mypy] -python_version = "3.10" -strict = true -warn_return_any = true -warn_unused_configs = true - -[[tool.mypy.overrides]] -module = "tests.*" -disable_error_code = [ - "call-arg", -] - -[[tool.mypy.overrides]] -module = "context_logging.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "pythonjsonlogger.*" -ignore_missing_imports = true - -[tool.ruff] -target-version = "py310" -line-length = 100 - -[tool.ruff.lint] -select = [ - "F", # Pyflakes - "E", "W", # pycodestyle - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "ASYNC", # flake8-async - "S", # flake8-bandit - "BLE", # flake8-blind-except - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PYI", # flake8-pyi - "PT", # flake8-pytest-style - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "ERA", # eradicate - "PGH", # pygrep-hooks - "PL", # Pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # Perflint - "RUF", # Ruff-specific rules -] - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # Allow assert in tests - "S103", # Allow os.chmod - "D100", "D101", "D102", "D103", # Allow missing docstrings in tests - "ANN102", # Missing type annotation for cls - "TRY003", # Allow long exception messages - "PLR2004", # Allow magic values -] -"examples/**/*.py" = [ - "T201", # Allow print in examples - "D100", "D103", # Allow missing docstrings in examples - "INP001", # Allow missing __init__.py in examples -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" -testpaths = ["tests"] diff --git a/.history/pyproject_20251128203841.toml b/.history/pyproject_20251128203841.toml deleted file mode 100644 index 6a8cfa3..0000000 --- a/.history/pyproject_20251128203841.toml +++ /dev/null @@ -1,268 +0,0 @@ -[project] -name = "fastapi-request-context" -version = "0.1.0" -description = "FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with logging integration" -authors = [ - {name = "Adrian Dankiv", email = "adr-007@ukr.net"} -] -license = {text = "MIT"} -readme = "README.md" -keywords = ["fastapi", "request-id", "correlation-id", "request-context", "middleware", "logging", "tracing"] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Framework :: FastAPI", -] -requires-python = ">=3.10" -dependencies = [ - "fastapi>=0.100.0", -] - -[project.optional-dependencies] -context-logging = ["context-logging>=0.7.0"] -json-formatter = ["python-json-logger>=2.0.0"] -all = [ - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", -] - -[project.urls] -Homepage = "https://github.com/ADR-007/fastapi-request-context" -Repository = "https://github.com/ADR-007/fastapi-request-context" -Issues = "https://github.com/ADR-007/fastapi-request-context/issues" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.uv] -dev-dependencies = [ - "ruff>=0.3.5", - "mypy>=1.9.0", - "pytest>=8.1.1", - "pytest-asyncio>=0.23.0", - "coverage>=7.4.4", - "tox>=4.14.2", - "tox-uv>=1.0.0", - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", - "httpx>=0.27.0", -] - -[tool.tox] -legacy_tox_ini = """ -[tox] -min_version = 4.0 -env_list = - lint - type - py{310,311,312}-fastapi{100,110,115} - coverage - -[gh-actions] -python = - 3.12: lint, type, py312-fastapi{100,110,115} - 3.11: py311-fastapi115 - 3.10: py310-fastapi115 - -[testenv] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - context-logging - python-json-logger - httpx - fastapi100: fastapi>=0.100.0,<0.110.0 - fastapi110: fastapi>=0.110.0,<0.115.0 - fastapi115: fastapi>=0.115.0 -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 - -[testenv:lint] -runner = uv-venv-lock-runner -deps = - ruff - mypy - pytest - context-logging - python-json-logger -commands = - ruff check fastapi_request_context tests/ examples/ - ruff format --check fastapi_request_context tests/ examples/ - mypy fastapi_request_context tests/ - -[testenv:coverage] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - fastapi - context-logging - python-json-logger - httpx -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 -""" - -[tool.semantic_release] -version_toml = [ - "pyproject.toml:project.version", -] -assets = [] -commit_message = "{version}\n\nAutomatically generated by python-semantic-release" -commit_parser = "angular" -logging_use_named_masks = false -major_on_zero = true -tag_format = "v{version}" - -[tool.semantic_release.branches.main] -match = "(main|master)" -prerelease_token = "rc" -prerelease = false - -[tool.semantic_release.changelog] -template_dir = "templates" -changelog_file = "CHANGELOG.md" -exclude_commit_patterns = [] - -[tool.semantic_release.changelog.environment] -block_start_string = "{%" -block_end_string = "%}" -variable_start_string = "{{" -variable_end_string = "}}" -comment_start_string = "{#" -comment_end_string = "#}" -trim_blocks = false -lstrip_blocks = false -newline_sequence = "\n" -keep_trailing_newline = false -extensions = [] -autoescape = true - -[tool.semantic_release.commit_author] -env = "GIT_COMMIT_AUTHOR" -default = "semantic-release " - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf", "build", "docs"] - -[tool.semantic_release.remote] -name = "origin" -type = "github" -ignore_token_for_push = false - -[tool.semantic_release.publish] -dist_glob_patterns = ["dist/*"] -upload_to_vcs_release = true - -[tool.mypy] -python_version = "3.10" -strict = true -warn_return_any = true -warn_unused_configs = true - -[[tool.mypy.overrides]] -module = "tests.*" -disable_error_code = [ - "call-arg", -] - -[[tool.mypy.overrides]] -module = "context_logging.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "pythonjsonlogger.*" -ignore_missing_imports = true - -[tool.ruff] -target-version = "py310" -line-length = 100 - -[tool.ruff.lint] -select = [ - "F", # Pyflakes - "E", "W", # pycodestyle - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "ASYNC", # flake8-async - "S", # flake8-bandit - "BLE", # flake8-blind-except - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PYI", # flake8-pyi - "PT", # flake8-pytest-style - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "ERA", # eradicate - "PGH", # pygrep-hooks - "PL", # Pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # Perflint - "RUF", # Ruff-specific rules -] - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # Allow assert in tests - "S103", # Allow os.chmod - "D100", "D101", "D102", "D103", # Allow missing docstrings in tests - "ANN102", # Missing type annotation for cls - "TRY003", # Allow long exception messages - "PLR2004", # Allow magic values - "PLC0415", # Allow imports not at top level (for test isolation) - "ARG001", # Allow unused function arguments (fixtures) - "SLF001", # Allow private member access -] -"examples/**/*.py" = [ - "T201", # Allow print in examples - "D100", "D103", # Allow missing docstrings in examples - "INP001", # Allow missing __init__.py in examples -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" -testpaths = ["tests"] diff --git a/.history/pyproject_20251128204045.toml b/.history/pyproject_20251128204045.toml deleted file mode 100644 index 2b79f66..0000000 --- a/.history/pyproject_20251128204045.toml +++ /dev/null @@ -1,268 +0,0 @@ -[project] -name = "fastapi-request-context" -version = "0.1.0" -description = "FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with logging integration" -authors = [ - {name = "Adrian Dankiv", email = "adr-007@ukr.net"} -] -license = {text = "MIT"} -readme = "README.md" -keywords = ["fastapi", "request-id", "correlation-id", "request-context", "middleware", "logging", "tracing"] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Framework :: FastAPI", -] -requires-python = ">=3.10" -dependencies = [ - "fastapi>=0.100.0", -] - -[project.optional-dependencies] -context-logging = ["context-logging>=0.7.0"] -json-formatter = ["python-json-logger>=2.0.0"] -all = [ - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", -] - -[project.urls] -Homepage = "https://github.com/ADR-007/fastapi-request-context" -Repository = "https://github.com/ADR-007/fastapi-request-context" -Issues = "https://github.com/ADR-007/fastapi-request-context/issues" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.uv] -dev-dependencies = [ - "ruff>=0.3.5", - "mypy>=1.9.0", - "pytest>=8.1.1", - "pytest-asyncio>=0.23.0", - "coverage>=7.4.4", - "tox>=4.14.2", - "tox-uv>=1.0.0", - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", - "httpx>=0.27.0", -] - -[tool.tox] -legacy_tox_ini = """ -[tox] -min_version = 4.0 -env_list = - lint - type - py{310,311,312}-fastapi{100,110,115} - coverage - -[gh-actions] -python = - 3.12: lint, type, py312-fastapi{100,110,115} - 3.11: py311-fastapi115 - 3.10: py310-fastapi115 - -[testenv] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - context-logging - python-json-logger - httpx - fastapi100: fastapi>=0.100.0,<0.110.0 - fastapi110: fastapi>=0.110.0,<0.115.0 - fastapi115: fastapi>=0.115.0 -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 - -[testenv:lint] -runner = uv-venv-lock-runner -deps = - ruff - mypy - pytest - context-logging - python-json-logger -commands = - ruff check fastapi_request_context tests/ examples/ - ruff format --check fastapi_request_context tests/ examples/ - mypy fastapi_request_context tests/ - -[testenv:coverage] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - fastapi - context-logging - python-json-logger - httpx -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 -""" - -[tool.semantic_release] -version_toml = [ - "pyproject.toml:project.version", -] -assets = [] -commit_message = "{version}\n\nAutomatically generated by python-semantic-release" -commit_parser = "angular" -logging_use_named_masks = false -major_on_zero = true -tag_format = "v{version}" - -[tool.semantic_release.branches.main] -match = "(main|master)" -prerelease_token = "rc" -prerelease = false - -[tool.semantic_release.changelog] -template_dir = "templates" -changelog_file = "CHANGELOG.md" -exclude_commit_patterns = [] - -[tool.semantic_release.changelog.environment] -block_start_string = "{%" -block_end_string = "%}" -variable_start_string = "{{" -variable_end_string = "}}" -comment_start_string = "{#" -comment_end_string = "#}" -trim_blocks = false -lstrip_blocks = false -newline_sequence = "\n" -keep_trailing_newline = false -extensions = [] -autoescape = true - -[tool.semantic_release.commit_author] -env = "GIT_COMMIT_AUTHOR" -default = "semantic-release " - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf", "build", "docs"] - -[tool.semantic_release.remote] -name = "origin" -type = "github" -ignore_token_for_push = false - -[tool.semantic_release.publish] -dist_glob_patterns = ["dist/*"] -upload_to_vcs_release = true - -[tool.mypy] -python_version = "3.10" -strict = true -warn_return_any = true -warn_unused_configs = true - -[[tool.mypy.overrides]] -module = "tests.*" -disable_error_code = [ - "call-arg", -] - -[[tool.mypy.overrides]] -module = "context_logging.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "pythonjsonlogger.*" -ignore_missing_imports = true - -[tool.ruff] -target-version = "py310" -line-length = 100 - -[tool.ruff.lint] -select = [ - "F", # Pyflakes - "E", "W", # pycodestyle - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "ASYNC", # flake8-async - "S", # flake8-bandit - "BLE", # flake8-blind-except - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PYI", # flake8-pyi - "PT", # flake8-pytest-style - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "ERA", # eradicate - "PGH", # pygrep-hooks - "PL", # Pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # Perflint - "RUF", # Ruff-specific rules -] - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # Allow assert in tests - "S103", # Allow os.chmod - "D100", "D101", "D102", "D103", # Allow missing docstrings in tests - "ANN102", # Missing type annotation for cls - "TRY003", # Allow long exception messages - "PLR2004", # Allow magic values - "PLC0415", # Allow imports not at top level (for test isolation) - "ARG001", "ARG002", # Allow unused function arguments (fixtures) - "SLF001", # Allow private member access -] -"examples/**/*.py" = [ - "T201", # Allow print in examples - "D100", "D103", # Allow missing docstrings in examples - "INP001", # Allow missing __init__.py in examples -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" -testpaths = ["tests"] diff --git a/.history/pyproject_20251128204103.toml b/.history/pyproject_20251128204103.toml deleted file mode 100644 index b20611b..0000000 --- a/.history/pyproject_20251128204103.toml +++ /dev/null @@ -1,271 +0,0 @@ -[project] -name = "fastapi-request-context" -version = "0.1.0" -description = "FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with logging integration" -authors = [ - {name = "Adrian Dankiv", email = "adr-007@ukr.net"} -] -license = {text = "MIT"} -readme = "README.md" -keywords = ["fastapi", "request-id", "correlation-id", "request-context", "middleware", "logging", "tracing"] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Framework :: FastAPI", -] -requires-python = ">=3.10" -dependencies = [ - "fastapi>=0.100.0", -] - -[project.optional-dependencies] -context-logging = ["context-logging>=0.7.0"] -json-formatter = ["python-json-logger>=2.0.0"] -all = [ - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", -] - -[project.urls] -Homepage = "https://github.com/ADR-007/fastapi-request-context" -Repository = "https://github.com/ADR-007/fastapi-request-context" -Issues = "https://github.com/ADR-007/fastapi-request-context/issues" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.uv] -dev-dependencies = [ - "ruff>=0.3.5", - "mypy>=1.9.0", - "pytest>=8.1.1", - "pytest-asyncio>=0.23.0", - "coverage>=7.4.4", - "tox>=4.14.2", - "tox-uv>=1.0.0", - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", - "httpx>=0.27.0", -] - -[tool.tox] -legacy_tox_ini = """ -[tox] -min_version = 4.0 -env_list = - lint - type - py{310,311,312}-fastapi{100,110,115} - coverage - -[gh-actions] -python = - 3.12: lint, type, py312-fastapi{100,110,115} - 3.11: py311-fastapi115 - 3.10: py310-fastapi115 - -[testenv] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - context-logging - python-json-logger - httpx - fastapi100: fastapi>=0.100.0,<0.110.0 - fastapi110: fastapi>=0.110.0,<0.115.0 - fastapi115: fastapi>=0.115.0 -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 - -[testenv:lint] -runner = uv-venv-lock-runner -deps = - ruff - mypy - pytest - context-logging - python-json-logger -commands = - ruff check fastapi_request_context tests/ examples/ - ruff format --check fastapi_request_context tests/ examples/ - mypy fastapi_request_context tests/ - -[testenv:coverage] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - fastapi - context-logging - python-json-logger - httpx -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 -""" - -[tool.semantic_release] -version_toml = [ - "pyproject.toml:project.version", -] -assets = [] -commit_message = "{version}\n\nAutomatically generated by python-semantic-release" -commit_parser = "angular" -logging_use_named_masks = false -major_on_zero = true -tag_format = "v{version}" - -[tool.semantic_release.branches.main] -match = "(main|master)" -prerelease_token = "rc" -prerelease = false - -[tool.semantic_release.changelog] -template_dir = "templates" -changelog_file = "CHANGELOG.md" -exclude_commit_patterns = [] - -[tool.semantic_release.changelog.environment] -block_start_string = "{%" -block_end_string = "%}" -variable_start_string = "{{" -variable_end_string = "}}" -comment_start_string = "{#" -comment_end_string = "#}" -trim_blocks = false -lstrip_blocks = false -newline_sequence = "\n" -keep_trailing_newline = false -extensions = [] -autoescape = true - -[tool.semantic_release.commit_author] -env = "GIT_COMMIT_AUTHOR" -default = "semantic-release " - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf", "build", "docs"] - -[tool.semantic_release.remote] -name = "origin" -type = "github" -ignore_token_for_push = false - -[tool.semantic_release.publish] -dist_glob_patterns = ["dist/*"] -upload_to_vcs_release = true - -[tool.mypy] -python_version = "3.10" -strict = true -warn_return_any = true -warn_unused_configs = true - -[[tool.mypy.overrides]] -module = "tests.*" -disable_error_code = [ - "call-arg", -] - -[[tool.mypy.overrides]] -module = "context_logging.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "pythonjsonlogger.*" -ignore_missing_imports = true - -[tool.ruff] -target-version = "py310" -line-length = 100 - -[tool.ruff.lint] -select = [ - "F", # Pyflakes - "E", "W", # pycodestyle - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "ASYNC", # flake8-async - "S", # flake8-bandit - "BLE", # flake8-blind-except - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PYI", # flake8-pyi - "PT", # flake8-pytest-style - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "ERA", # eradicate - "PGH", # pygrep-hooks - "PL", # Pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # Perflint - "RUF", # Ruff-specific rules -] - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # Allow assert in tests - "S103", # Allow os.chmod - "D100", "D101", "D102", "D103", # Allow missing docstrings in tests - "ANN102", # Missing type annotation for cls - "TRY003", # Allow long exception messages - "PLR2004", # Allow magic values - "PLC0415", # Allow imports not at top level (for test isolation) - "ARG001", "ARG002", # Allow unused function arguments (fixtures) - "SLF001", # Allow private member access -] -"examples/**/*.py" = [ - "T201", # Allow print in examples - "D100", "D103", # Allow missing docstrings in examples - "INP001", # Allow missing __init__.py in examples - "ARG001", # Allow unused function arguments - "N818", # Allow exception naming in backport - "PLC0415", # Allow imports not at top level -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" -testpaths = ["tests"] diff --git a/.history/pyproject_20251128204132.toml b/.history/pyproject_20251128204132.toml deleted file mode 100644 index f729a1d..0000000 --- a/.history/pyproject_20251128204132.toml +++ /dev/null @@ -1,271 +0,0 @@ -[project] -name = "fastapi-request-context" -version = "0.1.0" -description = "FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with logging integration" -authors = [ - {name = "Adrian Dankiv", email = "adr-007@ukr.net"} -] -license = {text = "MIT"} -readme = "README.md" -keywords = ["fastapi", "request-id", "correlation-id", "request-context", "middleware", "logging", "tracing"] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Framework :: FastAPI", -] -requires-python = ">=3.10" -dependencies = [ - "fastapi>=0.100.0", -] - -[project.optional-dependencies] -context-logging = ["context-logging>=0.7.0"] -json-formatter = ["python-json-logger>=2.0.0"] -all = [ - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", -] - -[project.urls] -Homepage = "https://github.com/ADR-007/fastapi-request-context" -Repository = "https://github.com/ADR-007/fastapi-request-context" -Issues = "https://github.com/ADR-007/fastapi-request-context/issues" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.uv] -dev-dependencies = [ - "ruff>=0.3.5", - "mypy>=1.9.0", - "pytest>=8.1.1", - "pytest-asyncio>=0.23.0", - "coverage>=7.4.4", - "tox>=4.14.2", - "tox-uv>=1.0.0", - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", - "httpx>=0.27.0", -] - -[tool.tox] -legacy_tox_ini = """ -[tox] -min_version = 4.0 -env_list = - lint - type - py{310,311,312}-fastapi{100,110,115} - coverage - -[gh-actions] -python = - 3.12: lint, type, py312-fastapi{100,110,115} - 3.11: py311-fastapi115 - 3.10: py310-fastapi115 - -[testenv] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - context-logging - python-json-logger - httpx - fastapi100: fastapi>=0.100.0,<0.110.0 - fastapi110: fastapi>=0.110.0,<0.115.0 - fastapi115: fastapi>=0.115.0 -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 - -[testenv:lint] -runner = uv-venv-lock-runner -deps = - ruff - mypy - pytest - context-logging - python-json-logger -commands = - ruff check fastapi_request_context tests/ examples/ - ruff format --check fastapi_request_context tests/ examples/ - mypy fastapi_request_context tests/ - -[testenv:coverage] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - fastapi - context-logging - python-json-logger - httpx -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 -""" - -[tool.semantic_release] -version_toml = [ - "pyproject.toml:project.version", -] -assets = [] -commit_message = "{version}\n\nAutomatically generated by python-semantic-release" -commit_parser = "angular" -logging_use_named_masks = false -major_on_zero = true -tag_format = "v{version}" - -[tool.semantic_release.branches.main] -match = "(main|master)" -prerelease_token = "rc" -prerelease = false - -[tool.semantic_release.changelog] -template_dir = "templates" -changelog_file = "CHANGELOG.md" -exclude_commit_patterns = [] - -[tool.semantic_release.changelog.environment] -block_start_string = "{%" -block_end_string = "%}" -variable_start_string = "{{" -variable_end_string = "}}" -comment_start_string = "{#" -comment_end_string = "#}" -trim_blocks = false -lstrip_blocks = false -newline_sequence = "\n" -keep_trailing_newline = false -extensions = [] -autoescape = true - -[tool.semantic_release.commit_author] -env = "GIT_COMMIT_AUTHOR" -default = "semantic-release " - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf", "build", "docs"] - -[tool.semantic_release.remote] -name = "origin" -type = "github" -ignore_token_for_push = false - -[tool.semantic_release.publish] -dist_glob_patterns = ["dist/*"] -upload_to_vcs_release = true - -[tool.mypy] -python_version = "3.10" -strict = true -warn_return_any = true -warn_unused_configs = true - -[[tool.mypy.overrides]] -module = "tests.*" -disable_error_code = [ - "call-arg", -] - -[[tool.mypy.overrides]] -module = "context_logging.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "pythonjsonlogger.*" -ignore_missing_imports = true - -[tool.ruff] -target-version = "py310" -line-length = 100 - -[tool.ruff.lint] -select = [ - "F", # Pyflakes - "E", "W", # pycodestyle - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "ASYNC", # flake8-async - "S", # flake8-bandit - "BLE", # flake8-blind-except - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PYI", # flake8-pyi - "PT", # flake8-pytest-style - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "ERA", # eradicate - "PGH", # pygrep-hooks - "PL", # Pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # Perflint - "RUF", # Ruff-specific rules -] - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # Allow assert in tests - "S103", # Allow os.chmod - "D100", "D101", "D102", "D103", # Allow missing docstrings in tests - "ANN102", # Missing type annotation for cls - "TRY003", # Allow long exception messages - "PLR2004", # Allow magic values - "PLC0415", # Allow imports not at top level (for test isolation) - "ARG001", "ARG002", # Allow unused function arguments (fixtures) - "SLF001", # Allow private member access -] -"examples/**/*.py" = [ - "T201", # Allow print in examples - "D100", "D103", # Allow missing docstrings in examples - "INP001", # Allow missing __init__.py in examples - "ARG001", # Allow unused function arguments - "PLC0415", # Allow imports not at top level - "ANN401", # Allow Any type -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" -testpaths = ["tests"] diff --git a/.history/pyproject_20251128204338.toml b/.history/pyproject_20251128204338.toml deleted file mode 100644 index a997c42..0000000 --- a/.history/pyproject_20251128204338.toml +++ /dev/null @@ -1,273 +0,0 @@ -[project] -name = "fastapi-request-context" -version = "0.1.0" -description = "FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with logging integration" -authors = [ - {name = "Adrian Dankiv", email = "adr-007@ukr.net"} -] -license = {text = "MIT"} -readme = "README.md" -keywords = ["fastapi", "request-id", "correlation-id", "request-context", "middleware", "logging", "tracing"] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Framework :: FastAPI", -] -requires-python = ">=3.10" -dependencies = [ - "fastapi>=0.100.0", -] - -[project.optional-dependencies] -context-logging = ["context-logging>=0.7.0"] -json-formatter = ["python-json-logger>=2.0.0"] -all = [ - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", -] - -[project.urls] -Homepage = "https://github.com/ADR-007/fastapi-request-context" -Repository = "https://github.com/ADR-007/fastapi-request-context" -Issues = "https://github.com/ADR-007/fastapi-request-context/issues" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.uv] -dev-dependencies = [ - "ruff>=0.3.5", - "mypy>=1.9.0", - "pytest>=8.1.1", - "pytest-asyncio>=0.23.0", - "coverage>=7.4.4", - "tox>=4.14.2", - "tox-uv>=1.0.0", - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", - "httpx>=0.27.0", -] - -[tool.tox] -legacy_tox_ini = """ -[tox] -min_version = 4.0 -env_list = - lint - type - py{310,311,312}-fastapi{100,110,115} - coverage - -[gh-actions] -python = - 3.12: lint, type, py312-fastapi{100,110,115} - 3.11: py311-fastapi115 - 3.10: py310-fastapi115 - -[testenv] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - context-logging - python-json-logger - httpx - fastapi100: fastapi>=0.100.0,<0.110.0 - fastapi110: fastapi>=0.110.0,<0.115.0 - fastapi115: fastapi>=0.115.0 -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 - -[testenv:lint] -runner = uv-venv-lock-runner -deps = - ruff - mypy - pytest - context-logging - python-json-logger -commands = - ruff check fastapi_request_context tests/ examples/ - ruff format --check fastapi_request_context tests/ examples/ - mypy fastapi_request_context tests/ - -[testenv:coverage] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - fastapi - context-logging - python-json-logger - httpx -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 -""" - -[tool.semantic_release] -version_toml = [ - "pyproject.toml:project.version", -] -assets = [] -commit_message = "{version}\n\nAutomatically generated by python-semantic-release" -commit_parser = "angular" -logging_use_named_masks = false -major_on_zero = true -tag_format = "v{version}" - -[tool.semantic_release.branches.main] -match = "(main|master)" -prerelease_token = "rc" -prerelease = false - -[tool.semantic_release.changelog] -template_dir = "templates" -changelog_file = "CHANGELOG.md" -exclude_commit_patterns = [] - -[tool.semantic_release.changelog.environment] -block_start_string = "{%" -block_end_string = "%}" -variable_start_string = "{{" -variable_end_string = "}}" -comment_start_string = "{#" -comment_end_string = "#}" -trim_blocks = false -lstrip_blocks = false -newline_sequence = "\n" -keep_trailing_newline = false -extensions = [] -autoescape = true - -[tool.semantic_release.commit_author] -env = "GIT_COMMIT_AUTHOR" -default = "semantic-release " - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf", "build", "docs"] - -[tool.semantic_release.remote] -name = "origin" -type = "github" -ignore_token_for_push = false - -[tool.semantic_release.publish] -dist_glob_patterns = ["dist/*"] -upload_to_vcs_release = true - -[tool.mypy] -python_version = "3.10" -strict = true -warn_return_any = true -warn_unused_configs = true - -[[tool.mypy.overrides]] -module = "tests.*" -disable_error_code = [ - "call-arg", - "unused-ignore", - "misc", -] - -[[tool.mypy.overrides]] -module = "context_logging.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "pythonjsonlogger.*" -ignore_missing_imports = true - -[tool.ruff] -target-version = "py310" -line-length = 100 - -[tool.ruff.lint] -select = [ - "F", # Pyflakes - "E", "W", # pycodestyle - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "ASYNC", # flake8-async - "S", # flake8-bandit - "BLE", # flake8-blind-except - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PYI", # flake8-pyi - "PT", # flake8-pytest-style - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "ERA", # eradicate - "PGH", # pygrep-hooks - "PL", # Pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # Perflint - "RUF", # Ruff-specific rules -] - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # Allow assert in tests - "S103", # Allow os.chmod - "D100", "D101", "D102", "D103", # Allow missing docstrings in tests - "ANN102", # Missing type annotation for cls - "TRY003", # Allow long exception messages - "PLR2004", # Allow magic values - "PLC0415", # Allow imports not at top level (for test isolation) - "ARG001", "ARG002", # Allow unused function arguments (fixtures) - "SLF001", # Allow private member access -] -"examples/**/*.py" = [ - "T201", # Allow print in examples - "D100", "D103", # Allow missing docstrings in examples - "INP001", # Allow missing __init__.py in examples - "ARG001", # Allow unused function arguments - "PLC0415", # Allow imports not at top level - "ANN401", # Allow Any type -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" -testpaths = ["tests"] diff --git a/.history/pyproject_20251128204455.toml b/.history/pyproject_20251128204455.toml deleted file mode 100644 index b7992be..0000000 --- a/.history/pyproject_20251128204455.toml +++ /dev/null @@ -1,274 +0,0 @@ -[project] -name = "fastapi-request-context" -version = "0.1.0" -description = "FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with logging integration" -authors = [ - {name = "Adrian Dankiv", email = "adr-007@ukr.net"} -] -license = {text = "MIT"} -readme = "README.md" -keywords = ["fastapi", "request-id", "correlation-id", "request-context", "middleware", "logging", "tracing"] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Framework :: FastAPI", -] -requires-python = ">=3.10" -dependencies = [ - "fastapi>=0.100.0", -] - -[project.optional-dependencies] -context-logging = ["context-logging>=0.7.0"] -json-formatter = ["python-json-logger>=2.0.0"] -all = [ - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", -] - -[project.urls] -Homepage = "https://github.com/ADR-007/fastapi-request-context" -Repository = "https://github.com/ADR-007/fastapi-request-context" -Issues = "https://github.com/ADR-007/fastapi-request-context/issues" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.uv] -dev-dependencies = [ - "ruff>=0.3.5", - "mypy>=1.9.0", - "pytest>=8.1.1", - "pytest-asyncio>=0.23.0", - "coverage>=7.4.4", - "tox>=4.14.2", - "tox-uv>=1.0.0", - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", - "httpx>=0.27.0", -] - -[tool.tox] -legacy_tox_ini = """ -[tox] -min_version = 4.0 -env_list = - lint - type - py{310,311,312}-fastapi{100,110,115} - coverage - -[gh-actions] -python = - 3.12: lint, type, py312-fastapi{100,110,115} - 3.11: py311-fastapi115 - 3.10: py310-fastapi115 - -[testenv] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - context-logging - python-json-logger - httpx - fastapi100: fastapi>=0.100.0,<0.110.0 - fastapi110: fastapi>=0.110.0,<0.115.0 - fastapi115: fastapi>=0.115.0 -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 - -[testenv:lint] -runner = uv-venv-lock-runner -deps = - ruff - mypy - pytest - context-logging - python-json-logger -commands = - ruff check fastapi_request_context tests/ examples/ - ruff format --check fastapi_request_context tests/ examples/ - mypy fastapi_request_context tests/ - -[testenv:coverage] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - fastapi - context-logging - python-json-logger - httpx -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 -""" - -[tool.semantic_release] -version_toml = [ - "pyproject.toml:project.version", -] -assets = [] -commit_message = "{version}\n\nAutomatically generated by python-semantic-release" -commit_parser = "angular" -logging_use_named_masks = false -major_on_zero = true -tag_format = "v{version}" - -[tool.semantic_release.branches.main] -match = "(main|master)" -prerelease_token = "rc" -prerelease = false - -[tool.semantic_release.changelog] -template_dir = "templates" -changelog_file = "CHANGELOG.md" -exclude_commit_patterns = [] - -[tool.semantic_release.changelog.environment] -block_start_string = "{%" -block_end_string = "%}" -variable_start_string = "{{" -variable_end_string = "}}" -comment_start_string = "{#" -comment_end_string = "#}" -trim_blocks = false -lstrip_blocks = false -newline_sequence = "\n" -keep_trailing_newline = false -extensions = [] -autoescape = true - -[tool.semantic_release.commit_author] -env = "GIT_COMMIT_AUTHOR" -default = "semantic-release " - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf", "build", "docs"] - -[tool.semantic_release.remote] -name = "origin" -type = "github" -ignore_token_for_push = false - -[tool.semantic_release.publish] -dist_glob_patterns = ["dist/*"] -upload_to_vcs_release = true - -[tool.mypy] -python_version = "3.10" -strict = true -warn_return_any = true -warn_unused_configs = true - -[[tool.mypy.overrides]] -module = "tests.*" -disable_error_code = [ - "call-arg", - "unused-ignore", - "misc", -] - -[[tool.mypy.overrides]] -module = "context_logging.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "pythonjsonlogger.*" -ignore_missing_imports = true - -[tool.ruff] -target-version = "py310" -line-length = 100 - -[tool.ruff.lint] -select = [ - "F", # Pyflakes - "E", "W", # pycodestyle - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "ASYNC", # flake8-async - "S", # flake8-bandit - "BLE", # flake8-blind-except - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PYI", # flake8-pyi - "PT", # flake8-pytest-style - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "ERA", # eradicate - "PGH", # pygrep-hooks - "PL", # Pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # Perflint - "RUF", # Ruff-specific rules -] - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # Allow assert in tests - "S103", # Allow os.chmod - "D100", "D101", "D102", "D103", # Allow missing docstrings in tests - "ANN102", # Missing type annotation for cls - "ANN401", # Allow Any type in fixtures - "TRY003", # Allow long exception messages - "PLR2004", # Allow magic values - "PLC0415", # Allow imports not at top level (for test isolation) - "ARG001", "ARG002", # Allow unused function arguments (fixtures) - "SLF001", # Allow private member access -] -"examples/**/*.py" = [ - "T201", # Allow print in examples - "D100", "D103", # Allow missing docstrings in examples - "INP001", # Allow missing __init__.py in examples - "ARG001", # Allow unused function arguments - "PLC0415", # Allow imports not at top level - "ANN401", # Allow Any type -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" -testpaths = ["tests"] diff --git a/.history/pyproject_20251128211609.toml b/.history/pyproject_20251128211609.toml deleted file mode 100644 index 3435025..0000000 --- a/.history/pyproject_20251128211609.toml +++ /dev/null @@ -1,275 +0,0 @@ -[project] -name = "fastapi-request-context" -version = "0.1.0" -description = "FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with logging integration" -authors = [ - {name = "Adrian Dankiv", email = "adr-007@ukr.net"} -] -license = {text = "MIT"} -readme = "README.md" -keywords = ["fastapi", "request-id", "correlation-id", "request-context", "middleware", "logging", "tracing"] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Framework :: FastAPI", -] -requires-python = ">=3.10" -dependencies = [ - "fastapi>=0.100.0", -] - -[project.optional-dependencies] -context-logging = ["context-logging>=0.7.0"] -json-formatter = ["python-json-logger>=2.0.0"] -all = [ - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", -] - -[project.urls] -Homepage = "https://github.com/ADR-007/fastapi-request-context" -Repository = "https://github.com/ADR-007/fastapi-request-context" -Issues = "https://github.com/ADR-007/fastapi-request-context/issues" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.uv] -dev-dependencies = [ - "ruff>=0.3.5", - "mypy>=1.9.0", - "pytest>=8.1.1", - "pytest-asyncio>=0.23.0", - "coverage>=7.4.4", - "tox>=4.14.2", - "tox-uv>=1.0.0", - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", - "httpx>=0.27.0", -] - -[tool.tox] -legacy_tox_ini = """ -[tox] -min_version = 4.0 -env_list = - lint - type - py{310,311,312}-fastapi{100,110,115} - coverage - -[gh-actions] -python = - 3.12: lint, type, py312-fastapi{100,110,115} - 3.11: py311-fastapi115 - 3.10: py310-fastapi115 - -[testenv] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - context-logging - python-json-logger - httpx - fastapi100: fastapi>=0.100.0,<0.110.0 - fastapi110: fastapi>=0.110.0,<0.115.0 - fastapi115: fastapi>=0.115.0 -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 - -[testenv:lint] -runner = uv-venv-lock-runner -deps = - ruff - mypy - pytest - context-logging - python-json-logger -commands = - ruff check fastapi_request_context tests/ examples/ - ruff format --check fastapi_request_context tests/ examples/ - mypy fastapi_request_context tests/ - -[testenv:coverage] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - fastapi - context-logging - python-json-logger - httpx -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 100 - coverage report --skip-empty --fail-under 90 -""" - -[tool.semantic_release] -version_toml = [ - "pyproject.toml:project.version", -] -assets = [] -commit_message = "{version}\n\nAutomatically generated by python-semantic-release" -commit_parser = "angular" -logging_use_named_masks = false -major_on_zero = true -tag_format = "v{version}" - -[tool.semantic_release.branches.main] -match = "(main|master)" -prerelease_token = "rc" -prerelease = false - -[tool.semantic_release.changelog] -template_dir = "templates" -changelog_file = "CHANGELOG.md" -exclude_commit_patterns = [] - -[tool.semantic_release.changelog.environment] -block_start_string = "{%" -block_end_string = "%}" -variable_start_string = "{{" -variable_end_string = "}}" -comment_start_string = "{#" -comment_end_string = "#}" -trim_blocks = false -lstrip_blocks = false -newline_sequence = "\n" -keep_trailing_newline = false -extensions = [] -autoescape = true - -[tool.semantic_release.commit_author] -env = "GIT_COMMIT_AUTHOR" -default = "semantic-release " - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf", "build", "docs"] - -[tool.semantic_release.remote] -name = "origin" -type = "github" -ignore_token_for_push = false - -[tool.semantic_release.publish] -dist_glob_patterns = ["dist/*"] -upload_to_vcs_release = true - -[tool.mypy] -python_version = "3.10" -strict = true -warn_return_any = true -warn_unused_configs = true - -[[tool.mypy.overrides]] -module = "tests.*" -disable_error_code = [ - "call-arg", - "unused-ignore", - "misc", -] - -[[tool.mypy.overrides]] -module = "context_logging.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "pythonjsonlogger.*" -ignore_missing_imports = true - -[tool.ruff] -target-version = "py310" -line-length = 100 - -[tool.ruff.lint] -select = [ - "F", # Pyflakes - "E", "W", # pycodestyle - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "ASYNC", # flake8-async - "S", # flake8-bandit - "BLE", # flake8-blind-except - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PYI", # flake8-pyi - "PT", # flake8-pytest-style - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "ERA", # eradicate - "PGH", # pygrep-hooks - "PL", # Pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # Perflint - "RUF", # Ruff-specific rules -] - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # Allow assert in tests - "S103", # Allow os.chmod - "D100", "D101", "D102", "D103", # Allow missing docstrings in tests - "ANN102", # Missing type annotation for cls - "ANN401", # Allow Any type in fixtures - "TRY003", # Allow long exception messages - "PLR2004", # Allow magic values - "PLC0415", # Allow imports not at top level (for test isolation) - "ARG001", "ARG002", # Allow unused function arguments (fixtures) - "SLF001", # Allow private member access -] -"examples/**/*.py" = [ - "T201", # Allow print in examples - "D100", "D103", # Allow missing docstrings in examples - "INP001", # Allow missing __init__.py in examples - "ARG001", # Allow unused function arguments - "PLC0415", # Allow imports not at top level - "ANN401", # Allow Any type -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" -testpaths = ["tests"] diff --git a/.history/pyproject_20251128211628.toml b/.history/pyproject_20251128211628.toml deleted file mode 100644 index 0eb37d4..0000000 --- a/.history/pyproject_20251128211628.toml +++ /dev/null @@ -1,288 +0,0 @@ -[project] -name = "fastapi-request-context" -version = "0.1.0" -description = "FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with logging integration" -authors = [ - {name = "Adrian Dankiv", email = "adr-007@ukr.net"} -] -license = {text = "MIT"} -readme = "README.md" -keywords = ["fastapi", "request-id", "correlation-id", "request-context", "middleware", "logging", "tracing"] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Framework :: FastAPI", -] -requires-python = ">=3.10" -dependencies = [ - "fastapi>=0.100.0", -] - -[project.optional-dependencies] -context-logging = ["context-logging>=0.7.0"] -json-formatter = ["python-json-logger>=2.0.0"] -all = [ - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", -] - -[project.urls] -Homepage = "https://github.com/ADR-007/fastapi-request-context" -Repository = "https://github.com/ADR-007/fastapi-request-context" -Issues = "https://github.com/ADR-007/fastapi-request-context/issues" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.uv] -dev-dependencies = [ - "ruff>=0.3.5", - "mypy>=1.9.0", - "pytest>=8.1.1", - "pytest-asyncio>=0.23.0", - "coverage>=7.4.4", - "tox>=4.14.2", - "tox-uv>=1.0.0", - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", - "httpx>=0.27.0", -] - -[tool.tox] -legacy_tox_ini = """ -[tox] -min_version = 4.0 -env_list = - lint - type - py{310,311,312}-fastapi{100,110,115} - coverage - -[gh-actions] -python = - 3.12: lint, type, py312-fastapi{100,110,115} - 3.11: py311-fastapi115 - 3.10: py310-fastapi115 - -[testenv] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - context-logging - python-json-logger - httpx - fastapi100: fastapi>=0.100.0,<0.110.0 - fastapi110: fastapi>=0.110.0,<0.115.0 - fastapi115: fastapi>=0.115.0 -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 - -[testenv:lint] -runner = uv-venv-lock-runner -deps = - ruff - mypy - pytest - context-logging - python-json-logger -commands = - ruff check fastapi_request_context tests/ examples/ - ruff format --check fastapi_request_context tests/ examples/ - mypy fastapi_request_context tests/ - -[testenv:coverage] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - fastapi - context-logging - python-json-logger - httpx -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 100 - coverage report --skip-empty --fail-under 90 -""" - -[tool.semantic_release] -version_toml = [ - "pyproject.toml:project.version", -] -assets = [] -commit_message = "{version}\n\nAutomatically generated by python-semantic-release" -commit_parser = "angular" -logging_use_named_masks = false -major_on_zero = true -tag_format = "v{version}" - -[tool.semantic_release.branches.main] -match = "(main|master)" -prerelease_token = "rc" -prerelease = false - -[tool.semantic_release.changelog] -template_dir = "templates" -changelog_file = "CHANGELOG.md" -exclude_commit_patterns = [] - -[tool.semantic_release.changelog.environment] -block_start_string = "{%" -block_end_string = "%}" -variable_start_string = "{{" -variable_end_string = "}}" -comment_start_string = "{#" -comment_end_string = "#}" -trim_blocks = false -lstrip_blocks = false -newline_sequence = "\n" -keep_trailing_newline = false -extensions = [] -autoescape = true - -[tool.semantic_release.commit_author] -env = "GIT_COMMIT_AUTHOR" -default = "semantic-release " - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf", "build", "docs"] - -[tool.semantic_release.remote] -name = "origin" -type = "github" -ignore_token_for_push = false - -[tool.semantic_release.publish] -dist_glob_patterns = ["dist/*"] -upload_to_vcs_release = true - -[tool.mypy] -python_version = "3.10" -strict = true -warn_return_any = true -warn_unused_configs = true - -[[tool.mypy.overrides]] -module = "tests.*" -disable_error_code = [ - "call-arg", - "unused-ignore", - "misc", -] - -[[tool.mypy.overrides]] -module = "context_logging.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "pythonjsonlogger.*" -ignore_missing_imports = true - -[tool.ruff] -target-version = "py310" -line-length = 100 - -[tool.ruff.lint] -select = [ - "F", # Pyflakes - "E", "W", # pycodestyle - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "ASYNC", # flake8-async - "S", # flake8-bandit - "BLE", # flake8-blind-except - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PYI", # flake8-pyi - "PT", # flake8-pytest-style - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "ERA", # eradicate - "PGH", # pygrep-hooks - "PL", # Pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # Perflint - "RUF", # Ruff-specific rules -] - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # Allow assert in tests - "S103", # Allow os.chmod - "D100", "D101", "D102", "D103", # Allow missing docstrings in tests - "ANN102", # Missing type annotation for cls - "ANN401", # Allow Any type in fixtures - "TRY003", # Allow long exception messages - "PLR2004", # Allow magic values - "PLC0415", # Allow imports not at top level (for test isolation) - "ARG001", "ARG002", # Allow unused function arguments (fixtures) - "SLF001", # Allow private member access -] -"examples/**/*.py" = [ - "T201", # Allow print in examples - "D100", "D103", # Allow missing docstrings in examples - "INP001", # Allow missing __init__.py in examples - "ARG001", # Allow unused function arguments - "PLC0415", # Allow imports not at top level - "ANN401", # Allow Any type -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" -testpaths = ["tests"] - -[tool.coverage.run] -source = ["fastapi_request_context"] -branch = true - -[tool.coverage.report] -fail_under = 100 -exclude_lines = [ - "pragma: no cover", - "if TYPE_CHECKING:", - "@abstractmethod", - "raise NotImplementedError", -] diff --git a/.history/pyproject_20251128211655.toml b/.history/pyproject_20251128211655.toml deleted file mode 100644 index 4e84146..0000000 --- a/.history/pyproject_20251128211655.toml +++ /dev/null @@ -1,294 +0,0 @@ -[project] -name = "fastapi-request-context" -version = "0.1.0" -description = "FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with logging integration" -authors = [ - {name = "Adrian Dankiv", email = "adr-007@ukr.net"} -] -license = {text = "MIT"} -readme = "README.md" -keywords = ["fastapi", "request-id", "correlation-id", "request-context", "middleware", "logging", "tracing"] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Framework :: FastAPI", -] -requires-python = ">=3.10" -dependencies = [ - "fastapi>=0.100.0", -] - -[project.optional-dependencies] -context-logging = ["context-logging>=0.7.0"] -json-formatter = ["python-json-logger>=2.0.0"] -all = [ - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", -] - -[project.urls] -Homepage = "https://github.com/ADR-007/fastapi-request-context" -Repository = "https://github.com/ADR-007/fastapi-request-context" -Issues = "https://github.com/ADR-007/fastapi-request-context/issues" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.uv] -dev-dependencies = [ - "ruff>=0.3.5", - "mypy>=1.9.0", - "pytest>=8.1.1", - "pytest-asyncio>=0.23.0", - "coverage>=7.4.4", - "tox>=4.14.2", - "tox-uv>=1.0.0", - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", - "httpx>=0.27.0", -] - -[tool.tox] -legacy_tox_ini = """ -[tox] -min_version = 4.0 -env_list = - lint - type - py{310,311,312}-fastapi{100,110,115} - coverage - -[gh-actions] -python = - 3.12: lint, type, py312-fastapi{100,110,115} - 3.11: py311-fastapi115 - 3.10: py310-fastapi115 - -[testenv] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - context-logging - python-json-logger - httpx - fastapi100: fastapi>=0.100.0,<0.110.0 - fastapi110: fastapi>=0.110.0,<0.115.0 - fastapi115: fastapi>=0.115.0 -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 - -[testenv:lint] -runner = uv-venv-lock-runner -deps = - ruff - mypy - pytest - context-logging - python-json-logger -commands = - ruff check fastapi_request_context tests/ examples/ - ruff format --check fastapi_request_context tests/ examples/ - mypy fastapi_request_context tests/ - -[testenv:coverage] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - fastapi - context-logging - python-json-logger - httpx -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 100 - coverage report --skip-empty --fail-under 90 -""" - -[tool.semantic_release] -version_toml = [ - "pyproject.toml:project.version", -] -assets = [] -commit_message = "{version}\n\nAutomatically generated by python-semantic-release" -commit_parser = "angular" -logging_use_named_masks = false -major_on_zero = true -tag_format = "v{version}" - -[tool.semantic_release.branches.main] -match = "(main|master)" -prerelease_token = "rc" -prerelease = false - -[tool.semantic_release.changelog] -template_dir = "templates" -changelog_file = "CHANGELOG.md" -exclude_commit_patterns = [] - -[tool.semantic_release.changelog.environment] -block_start_string = "{%" -block_end_string = "%}" -variable_start_string = "{{" -variable_end_string = "}}" -comment_start_string = "{#" -comment_end_string = "#}" -trim_blocks = false -lstrip_blocks = false -newline_sequence = "\n" -keep_trailing_newline = false -extensions = [] -autoescape = true - -[tool.semantic_release.commit_author] -env = "GIT_COMMIT_AUTHOR" -default = "semantic-release " - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf", "build", "docs"] - -[tool.semantic_release.remote] -name = "origin" -type = "github" -ignore_token_for_push = false - -[tool.semantic_release.publish] -dist_glob_patterns = ["dist/*"] -upload_to_vcs_release = true - -[tool.mypy] -python_version = "3.10" -strict = true -warn_return_any = true -warn_unused_configs = true - -[[tool.mypy.overrides]] -module = "tests.*" -disable_error_code = [ - "call-arg", - "unused-ignore", - "misc", -] - -[[tool.mypy.overrides]] -module = "context_logging.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "pythonjsonlogger.*" -ignore_missing_imports = true - -[tool.ruff] -target-version = "py310" -line-length = 100 - -[tool.ruff.lint] -select = [ - "F", # Pyflakes - "E", "W", # pycodestyle - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "ASYNC", # flake8-async - "S", # flake8-bandit - "BLE", # flake8-blind-except - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PYI", # flake8-pyi - "PT", # flake8-pytest-style - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "ERA", # eradicate - "PGH", # pygrep-hooks - "PL", # Pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # Perflint - "RUF", # Ruff-specific rules -] - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # Allow assert in tests - "S103", # Allow os.chmod - "D100", "D101", "D102", "D103", # Allow missing docstrings in tests - "ANN102", # Missing type annotation for cls - "ANN401", # Allow Any type in fixtures - "TRY003", # Allow long exception messages - "PLR2004", # Allow magic values - "PLC0415", # Allow imports not at top level (for test isolation) - "ARG001", "ARG002", # Allow unused function arguments (fixtures) - "SLF001", # Allow private member access -] -"examples/**/*.py" = [ - "T201", # Allow print in examples - "D100", "D103", # Allow missing docstrings in examples - "INP001", # Allow missing __init__.py in examples - "ARG001", # Allow unused function arguments - "PLC0415", # Allow imports not at top level - "ANN401", # Allow Any type -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" -testpaths = ["tests"] - -[tool.coverage.run] -source = ["fastapi_request_context"] -branch = true - -[tool.coverage.report] -fail_under = 100 -exclude_lines = [ - "pragma: no cover", - "if TYPE_CHECKING:", - "@abstractmethod", - "raise NotImplementedError", - "\\.\\.\\.", # Protocol method stubs - "class.*\\(Protocol\\):", -] -exclude_also = [ - "def __repr__", - "@(abc\\.)?abstractmethod", -] diff --git a/.history/pyproject_20251128211859.toml b/.history/pyproject_20251128211859.toml deleted file mode 100644 index 088b5e8..0000000 --- a/.history/pyproject_20251128211859.toml +++ /dev/null @@ -1,298 +0,0 @@ -[project] -name = "fastapi-request-context" -version = "0.1.0" -description = "FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with logging integration" -authors = [ - {name = "Adrian Dankiv", email = "adr-007@ukr.net"} -] -license = {text = "MIT"} -readme = "README.md" -keywords = ["fastapi", "request-id", "correlation-id", "request-context", "middleware", "logging", "tracing"] -classifiers = [ - "Development Status :: 4 - Beta", - "Framework :: FastAPI", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", - "Topic :: Software Development :: Libraries :: Python Modules", - "Typing :: Typed", -] -requires-python = ">=3.12" -dependencies = [ - "fastapi>=0.100.0", -] - -[project.optional-dependencies] -context-logging = ["context-logging>=0.7.0"] -json-formatter = ["python-json-logger>=2.0.0"] -all = [ - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", -] - -[project.urls] -Homepage = "https://github.com/ADR-007/fastapi-request-context" -Repository = "https://github.com/ADR-007/fastapi-request-context" -Issues = "https://github.com/ADR-007/fastapi-request-context/issues" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.uv] -dev-dependencies = [ - "ruff>=0.3.5", - "mypy>=1.9.0", - "pytest>=8.1.1", - "pytest-asyncio>=0.23.0", - "coverage>=7.4.4", - "tox>=4.14.2", - "tox-uv>=1.0.0", - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", - "httpx>=0.27.0", -] - -[tool.tox] -legacy_tox_ini = """ -[tox] -min_version = 4.0 -env_list = - lint - type - py{310,311,312}-fastapi{100,110,115} - coverage - -[gh-actions] -python = - 3.12: lint, type, py312-fastapi{100,110,115} - 3.11: py311-fastapi115 - 3.10: py310-fastapi115 - -[testenv] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - context-logging - python-json-logger - httpx - fastapi100: fastapi>=0.100.0,<0.110.0 - fastapi110: fastapi>=0.110.0,<0.115.0 - fastapi115: fastapi>=0.115.0 -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 - -[testenv:lint] -runner = uv-venv-lock-runner -deps = - ruff - mypy - pytest - context-logging - python-json-logger -commands = - ruff check fastapi_request_context tests/ examples/ - ruff format --check fastapi_request_context tests/ examples/ - mypy fastapi_request_context tests/ - -[testenv:coverage] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - fastapi - context-logging - python-json-logger - httpx -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 100 - coverage report --skip-empty --fail-under 90 -""" - -[tool.semantic_release] -version_toml = [ - "pyproject.toml:project.version", -] -assets = [] -commit_message = "{version}\n\nAutomatically generated by python-semantic-release" -commit_parser = "angular" -logging_use_named_masks = false -major_on_zero = true -tag_format = "v{version}" - -[tool.semantic_release.branches.main] -match = "(main|master)" -prerelease_token = "rc" -prerelease = false - -[tool.semantic_release.changelog] -template_dir = "templates" -changelog_file = "CHANGELOG.md" -exclude_commit_patterns = [] - -[tool.semantic_release.changelog.environment] -block_start_string = "{%" -block_end_string = "%}" -variable_start_string = "{{" -variable_end_string = "}}" -comment_start_string = "{#" -comment_end_string = "#}" -trim_blocks = false -lstrip_blocks = false -newline_sequence = "\n" -keep_trailing_newline = false -extensions = [] -autoescape = true - -[tool.semantic_release.commit_author] -env = "GIT_COMMIT_AUTHOR" -default = "semantic-release " - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf", "build", "docs"] - -[tool.semantic_release.remote] -name = "origin" -type = "github" -ignore_token_for_push = false - -[tool.semantic_release.publish] -dist_glob_patterns = ["dist/*"] -upload_to_vcs_release = true - -[tool.mypy] -python_version = "3.10" -strict = true -warn_return_any = true -warn_unused_configs = true - -[[tool.mypy.overrides]] -module = "tests.*" -disable_error_code = [ - "call-arg", - "unused-ignore", - "misc", -] - -[[tool.mypy.overrides]] -module = "context_logging.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "pythonjsonlogger.*" -ignore_missing_imports = true - -[tool.ruff] -target-version = "py310" -line-length = 100 - -[tool.ruff.lint] -select = [ - "F", # Pyflakes - "E", "W", # pycodestyle - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "ASYNC", # flake8-async - "S", # flake8-bandit - "BLE", # flake8-blind-except - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PYI", # flake8-pyi - "PT", # flake8-pytest-style - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "ERA", # eradicate - "PGH", # pygrep-hooks - "PL", # Pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # Perflint - "RUF", # Ruff-specific rules -] - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # Allow assert in tests - "S103", # Allow os.chmod - "D100", "D101", "D102", "D103", # Allow missing docstrings in tests - "ANN102", # Missing type annotation for cls - "ANN401", # Allow Any type in fixtures - "TRY003", # Allow long exception messages - "PLR2004", # Allow magic values - "PLC0415", # Allow imports not at top level (for test isolation) - "ARG001", "ARG002", # Allow unused function arguments (fixtures) - "SLF001", # Allow private member access -] -"examples/**/*.py" = [ - "T201", # Allow print in examples - "D100", "D103", # Allow missing docstrings in examples - "INP001", # Allow missing __init__.py in examples - "ARG001", # Allow unused function arguments - "PLC0415", # Allow imports not at top level - "ANN401", # Allow Any type -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" -testpaths = ["tests"] - -[tool.coverage.run] -source = ["fastapi_request_context"] -branch = true - -[tool.coverage.report] -fail_under = 100 -exclude_lines = [ - "pragma: no cover", - "if TYPE_CHECKING:", - "@abstractmethod", - "raise NotImplementedError", - "\\.\\.\\.", # Protocol method stubs - "class.*\\(Protocol\\):", -] -exclude_also = [ - "def __repr__", - "@(abc\\.)?abstractmethod", -] diff --git a/.history/pyproject_20251128211907.toml b/.history/pyproject_20251128211907.toml deleted file mode 100644 index 136a03f..0000000 --- a/.history/pyproject_20251128211907.toml +++ /dev/null @@ -1,298 +0,0 @@ -[project] -name = "fastapi-request-context" -version = "0.1.0" -description = "FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with logging integration" -authors = [ - {name = "Adrian Dankiv", email = "adr-007@ukr.net"} -] -license = {text = "MIT"} -readme = "README.md" -keywords = ["fastapi", "request-id", "correlation-id", "request-context", "middleware", "logging", "tracing"] -classifiers = [ - "Development Status :: 4 - Beta", - "Framework :: FastAPI", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", - "Topic :: Software Development :: Libraries :: Python Modules", - "Typing :: Typed", -] -requires-python = ">=3.12" -dependencies = [ - "fastapi>=0.100.0", -] - -[project.optional-dependencies] -context-logging = ["context-logging>=0.7.0"] -json-formatter = ["python-json-logger>=2.0.0"] -all = [ - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", -] - -[project.urls] -Homepage = "https://github.com/ADR-007/fastapi-request-context" -Repository = "https://github.com/ADR-007/fastapi-request-context" -Issues = "https://github.com/ADR-007/fastapi-request-context/issues" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.uv] -dev-dependencies = [ - "ruff>=0.3.5", - "mypy>=1.9.0", - "pytest>=8.1.1", - "pytest-asyncio>=0.23.0", - "coverage>=7.4.4", - "tox>=4.14.2", - "tox-uv>=1.0.0", - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", - "httpx>=0.27.0", -] - -[tool.tox] -legacy_tox_ini = """ -[tox] -min_version = 4.0 -env_list = - lint - type - py{310,311,312}-fastapi{100,110,115} - coverage - -[gh-actions] -python = - 3.12: lint, type, py312-fastapi{100,110,115} - 3.11: py311-fastapi115 - 3.10: py310-fastapi115 - -[testenv] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - context-logging - python-json-logger - httpx - fastapi100: fastapi>=0.100.0,<0.110.0 - fastapi110: fastapi>=0.110.0,<0.115.0 - fastapi115: fastapi>=0.115.0 -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 - -[testenv:lint] -runner = uv-venv-lock-runner -deps = - ruff - mypy - pytest - context-logging - python-json-logger -commands = - ruff check fastapi_request_context tests/ examples/ - ruff format --check fastapi_request_context tests/ examples/ - mypy fastapi_request_context tests/ - -[testenv:coverage] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - fastapi - context-logging - python-json-logger - httpx -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 100 - coverage report --skip-empty --fail-under 90 -""" - -[tool.semantic_release] -version_toml = [ - "pyproject.toml:project.version", -] -assets = [] -commit_message = "{version}\n\nAutomatically generated by python-semantic-release" -commit_parser = "angular" -logging_use_named_masks = false -major_on_zero = true -tag_format = "v{version}" - -[tool.semantic_release.branches.main] -match = "(main|master)" -prerelease_token = "rc" -prerelease = false - -[tool.semantic_release.changelog] -template_dir = "templates" -changelog_file = "CHANGELOG.md" -exclude_commit_patterns = [] - -[tool.semantic_release.changelog.environment] -block_start_string = "{%" -block_end_string = "%}" -variable_start_string = "{{" -variable_end_string = "}}" -comment_start_string = "{#" -comment_end_string = "#}" -trim_blocks = false -lstrip_blocks = false -newline_sequence = "\n" -keep_trailing_newline = false -extensions = [] -autoescape = true - -[tool.semantic_release.commit_author] -env = "GIT_COMMIT_AUTHOR" -default = "semantic-release " - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf", "build", "docs"] - -[tool.semantic_release.remote] -name = "origin" -type = "github" -ignore_token_for_push = false - -[tool.semantic_release.publish] -dist_glob_patterns = ["dist/*"] -upload_to_vcs_release = true - -[tool.mypy] -python_version = "3.12" -strict = true -warn_return_any = true -warn_unused_configs = true - -[[tool.mypy.overrides]] -module = "tests.*" -disable_error_code = [ - "call-arg", - "unused-ignore", - "misc", -] - -[[tool.mypy.overrides]] -module = "context_logging.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "pythonjsonlogger.*" -ignore_missing_imports = true - -[tool.ruff] -target-version = "py310" -line-length = 100 - -[tool.ruff.lint] -select = [ - "F", # Pyflakes - "E", "W", # pycodestyle - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "ASYNC", # flake8-async - "S", # flake8-bandit - "BLE", # flake8-blind-except - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PYI", # flake8-pyi - "PT", # flake8-pytest-style - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "ERA", # eradicate - "PGH", # pygrep-hooks - "PL", # Pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # Perflint - "RUF", # Ruff-specific rules -] - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # Allow assert in tests - "S103", # Allow os.chmod - "D100", "D101", "D102", "D103", # Allow missing docstrings in tests - "ANN102", # Missing type annotation for cls - "ANN401", # Allow Any type in fixtures - "TRY003", # Allow long exception messages - "PLR2004", # Allow magic values - "PLC0415", # Allow imports not at top level (for test isolation) - "ARG001", "ARG002", # Allow unused function arguments (fixtures) - "SLF001", # Allow private member access -] -"examples/**/*.py" = [ - "T201", # Allow print in examples - "D100", "D103", # Allow missing docstrings in examples - "INP001", # Allow missing __init__.py in examples - "ARG001", # Allow unused function arguments - "PLC0415", # Allow imports not at top level - "ANN401", # Allow Any type -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" -testpaths = ["tests"] - -[tool.coverage.run] -source = ["fastapi_request_context"] -branch = true - -[tool.coverage.report] -fail_under = 100 -exclude_lines = [ - "pragma: no cover", - "if TYPE_CHECKING:", - "@abstractmethod", - "raise NotImplementedError", - "\\.\\.\\.", # Protocol method stubs - "class.*\\(Protocol\\):", -] -exclude_also = [ - "def __repr__", - "@(abc\\.)?abstractmethod", -] diff --git a/.history/pyproject_20251128212355.toml b/.history/pyproject_20251128212355.toml deleted file mode 100644 index b683a0c..0000000 --- a/.history/pyproject_20251128212355.toml +++ /dev/null @@ -1,297 +0,0 @@ -[project] -name = "fastapi-request-context" -version = "0.1.0" -description = "FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with logging integration" -authors = [ - {name = "Adrian Dankiv", email = "adr-007@ukr.net"} -] -license = {text = "MIT"} -readme = "README.md" -keywords = ["fastapi", "request-id", "correlation-id", "request-context", "middleware", "logging", "tracing"] -classifiers = [ - "Development Status :: 4 - Beta", - "Framework :: FastAPI", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", - "Topic :: Software Development :: Libraries :: Python Modules", - "Typing :: Typed", -] -requires-python = ">=3.12" -dependencies = [ - "fastapi>=0.100.0", -] - -[project.optional-dependencies] -context-logging = ["context-logging>=0.7.0"] -json-formatter = ["python-json-logger>=2.0.0"] -all = [ - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", -] - -[project.urls] -Homepage = "https://github.com/ADR-007/fastapi-request-context" -Repository = "https://github.com/ADR-007/fastapi-request-context" -Issues = "https://github.com/ADR-007/fastapi-request-context/issues" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.uv] -dev-dependencies = [ - "ruff>=0.3.5", - "mypy>=1.9.0", - "pytest>=8.1.1", - "pytest-asyncio>=0.23.0", - "coverage>=7.4.4", - "tox>=4.14.2", - "tox-uv>=1.0.0", - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", - "httpx>=0.27.0", -] - -[tool.tox] -legacy_tox_ini = """ -[tox] -min_version = 4.0 -env_list = - lint - type - py{312,313}-fastapi{100,110,115} - coverage - -[gh-actions] -python = - 3.12: lint, type, py312-fastapi{100,110,115} - 3.13: py313-fastapi115 - -[testenv] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - context-logging - python-json-logger - httpx - fastapi100: fastapi>=0.100.0,<0.110.0 - fastapi110: fastapi>=0.110.0,<0.115.0 - fastapi115: fastapi>=0.115.0 -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 - -[testenv:lint] -runner = uv-venv-lock-runner -deps = - ruff - mypy - pytest - context-logging - python-json-logger -commands = - ruff check fastapi_request_context tests/ examples/ - ruff format --check fastapi_request_context tests/ examples/ - mypy fastapi_request_context tests/ - -[testenv:coverage] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - fastapi - context-logging - python-json-logger - httpx -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 100 - coverage report --skip-empty --fail-under 90 -""" - -[tool.semantic_release] -version_toml = [ - "pyproject.toml:project.version", -] -assets = [] -commit_message = "{version}\n\nAutomatically generated by python-semantic-release" -commit_parser = "angular" -logging_use_named_masks = false -major_on_zero = true -tag_format = "v{version}" - -[tool.semantic_release.branches.main] -match = "(main|master)" -prerelease_token = "rc" -prerelease = false - -[tool.semantic_release.changelog] -template_dir = "templates" -changelog_file = "CHANGELOG.md" -exclude_commit_patterns = [] - -[tool.semantic_release.changelog.environment] -block_start_string = "{%" -block_end_string = "%}" -variable_start_string = "{{" -variable_end_string = "}}" -comment_start_string = "{#" -comment_end_string = "#}" -trim_blocks = false -lstrip_blocks = false -newline_sequence = "\n" -keep_trailing_newline = false -extensions = [] -autoescape = true - -[tool.semantic_release.commit_author] -env = "GIT_COMMIT_AUTHOR" -default = "semantic-release " - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf", "build", "docs"] - -[tool.semantic_release.remote] -name = "origin" -type = "github" -ignore_token_for_push = false - -[tool.semantic_release.publish] -dist_glob_patterns = ["dist/*"] -upload_to_vcs_release = true - -[tool.mypy] -python_version = "3.12" -strict = true -warn_return_any = true -warn_unused_configs = true - -[[tool.mypy.overrides]] -module = "tests.*" -disable_error_code = [ - "call-arg", - "unused-ignore", - "misc", -] - -[[tool.mypy.overrides]] -module = "context_logging.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "pythonjsonlogger.*" -ignore_missing_imports = true - -[tool.ruff] -target-version = "py310" -line-length = 100 - -[tool.ruff.lint] -select = [ - "F", # Pyflakes - "E", "W", # pycodestyle - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "ASYNC", # flake8-async - "S", # flake8-bandit - "BLE", # flake8-blind-except - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PYI", # flake8-pyi - "PT", # flake8-pytest-style - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "ERA", # eradicate - "PGH", # pygrep-hooks - "PL", # Pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # Perflint - "RUF", # Ruff-specific rules -] - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # Allow assert in tests - "S103", # Allow os.chmod - "D100", "D101", "D102", "D103", # Allow missing docstrings in tests - "ANN102", # Missing type annotation for cls - "ANN401", # Allow Any type in fixtures - "TRY003", # Allow long exception messages - "PLR2004", # Allow magic values - "PLC0415", # Allow imports not at top level (for test isolation) - "ARG001", "ARG002", # Allow unused function arguments (fixtures) - "SLF001", # Allow private member access -] -"examples/**/*.py" = [ - "T201", # Allow print in examples - "D100", "D103", # Allow missing docstrings in examples - "INP001", # Allow missing __init__.py in examples - "ARG001", # Allow unused function arguments - "PLC0415", # Allow imports not at top level - "ANN401", # Allow Any type -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" -testpaths = ["tests"] - -[tool.coverage.run] -source = ["fastapi_request_context"] -branch = true - -[tool.coverage.report] -fail_under = 100 -exclude_lines = [ - "pragma: no cover", - "if TYPE_CHECKING:", - "@abstractmethod", - "raise NotImplementedError", - "\\.\\.\\.", # Protocol method stubs - "class.*\\(Protocol\\):", -] -exclude_also = [ - "def __repr__", - "@(abc\\.)?abstractmethod", -] diff --git a/.history/pyproject_20251128212422.toml b/.history/pyproject_20251128212422.toml deleted file mode 100644 index 4b89fd9..0000000 --- a/.history/pyproject_20251128212422.toml +++ /dev/null @@ -1,297 +0,0 @@ -[project] -name = "fastapi-request-context" -version = "0.1.0" -description = "FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with logging integration" -authors = [ - {name = "Adrian Dankiv", email = "adr-007@ukr.net"} -] -license = {text = "MIT"} -readme = "README.md" -keywords = ["fastapi", "request-id", "correlation-id", "request-context", "middleware", "logging", "tracing"] -classifiers = [ - "Development Status :: 4 - Beta", - "Framework :: FastAPI", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", - "Topic :: Software Development :: Libraries :: Python Modules", - "Typing :: Typed", -] -requires-python = ">=3.12" -dependencies = [ - "fastapi>=0.100.0", -] - -[project.optional-dependencies] -context-logging = ["context-logging>=0.7.0"] -json-formatter = ["python-json-logger>=2.0.0"] -all = [ - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", -] - -[project.urls] -Homepage = "https://github.com/ADR-007/fastapi-request-context" -Repository = "https://github.com/ADR-007/fastapi-request-context" -Issues = "https://github.com/ADR-007/fastapi-request-context/issues" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[dependency-groups] -dev = [ - "ruff>=0.3.5", - "mypy>=1.9.0", - "pytest>=8.1.1", - "pytest-asyncio>=0.23.0", - "coverage>=7.4.4", - "tox>=4.14.2", - "tox-uv>=1.0.0", - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", - "httpx>=0.27.0", -] - -[tool.tox] -legacy_tox_ini = """ -[tox] -min_version = 4.0 -env_list = - lint - type - py{312,313}-fastapi{100,110,115} - coverage - -[gh-actions] -python = - 3.12: lint, type, py312-fastapi{100,110,115} - 3.13: py313-fastapi115 - -[testenv] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - context-logging - python-json-logger - httpx - fastapi100: fastapi>=0.100.0,<0.110.0 - fastapi110: fastapi>=0.110.0,<0.115.0 - fastapi115: fastapi>=0.115.0 -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 - -[testenv:lint] -runner = uv-venv-lock-runner -deps = - ruff - mypy - pytest - context-logging - python-json-logger -commands = - ruff check fastapi_request_context tests/ examples/ - ruff format --check fastapi_request_context tests/ examples/ - mypy fastapi_request_context tests/ - -[testenv:coverage] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - fastapi - context-logging - python-json-logger - httpx -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 100 - coverage report --skip-empty --fail-under 90 -""" - -[tool.semantic_release] -version_toml = [ - "pyproject.toml:project.version", -] -assets = [] -commit_message = "{version}\n\nAutomatically generated by python-semantic-release" -commit_parser = "angular" -logging_use_named_masks = false -major_on_zero = true -tag_format = "v{version}" - -[tool.semantic_release.branches.main] -match = "(main|master)" -prerelease_token = "rc" -prerelease = false - -[tool.semantic_release.changelog] -template_dir = "templates" -changelog_file = "CHANGELOG.md" -exclude_commit_patterns = [] - -[tool.semantic_release.changelog.environment] -block_start_string = "{%" -block_end_string = "%}" -variable_start_string = "{{" -variable_end_string = "}}" -comment_start_string = "{#" -comment_end_string = "#}" -trim_blocks = false -lstrip_blocks = false -newline_sequence = "\n" -keep_trailing_newline = false -extensions = [] -autoescape = true - -[tool.semantic_release.commit_author] -env = "GIT_COMMIT_AUTHOR" -default = "semantic-release " - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf", "build", "docs"] - -[tool.semantic_release.remote] -name = "origin" -type = "github" -ignore_token_for_push = false - -[tool.semantic_release.publish] -dist_glob_patterns = ["dist/*"] -upload_to_vcs_release = true - -[tool.mypy] -python_version = "3.12" -strict = true -warn_return_any = true -warn_unused_configs = true - -[[tool.mypy.overrides]] -module = "tests.*" -disable_error_code = [ - "call-arg", - "unused-ignore", - "misc", -] - -[[tool.mypy.overrides]] -module = "context_logging.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "pythonjsonlogger.*" -ignore_missing_imports = true - -[tool.ruff] -target-version = "py310" -line-length = 100 - -[tool.ruff.lint] -select = [ - "F", # Pyflakes - "E", "W", # pycodestyle - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "ASYNC", # flake8-async - "S", # flake8-bandit - "BLE", # flake8-blind-except - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PYI", # flake8-pyi - "PT", # flake8-pytest-style - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "ERA", # eradicate - "PGH", # pygrep-hooks - "PL", # Pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # Perflint - "RUF", # Ruff-specific rules -] - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # Allow assert in tests - "S103", # Allow os.chmod - "D100", "D101", "D102", "D103", # Allow missing docstrings in tests - "ANN102", # Missing type annotation for cls - "ANN401", # Allow Any type in fixtures - "TRY003", # Allow long exception messages - "PLR2004", # Allow magic values - "PLC0415", # Allow imports not at top level (for test isolation) - "ARG001", "ARG002", # Allow unused function arguments (fixtures) - "SLF001", # Allow private member access -] -"examples/**/*.py" = [ - "T201", # Allow print in examples - "D100", "D103", # Allow missing docstrings in examples - "INP001", # Allow missing __init__.py in examples - "ARG001", # Allow unused function arguments - "PLC0415", # Allow imports not at top level - "ANN401", # Allow Any type -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" -testpaths = ["tests"] - -[tool.coverage.run] -source = ["fastapi_request_context"] -branch = true - -[tool.coverage.report] -fail_under = 100 -exclude_lines = [ - "pragma: no cover", - "if TYPE_CHECKING:", - "@abstractmethod", - "raise NotImplementedError", - "\\.\\.\\.", # Protocol method stubs - "class.*\\(Protocol\\):", -] -exclude_also = [ - "def __repr__", - "@(abc\\.)?abstractmethod", -] diff --git a/.history/pyproject_20251128213813.toml b/.history/pyproject_20251128213813.toml deleted file mode 100644 index 9762819..0000000 --- a/.history/pyproject_20251128213813.toml +++ /dev/null @@ -1,304 +0,0 @@ -[project] -name = "fastapi-request-context" -version = "0.1.0" -description = "FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with logging integration" -authors = [ - {name = "Adrian Dankiv", email = "adr-007@ukr.net"} -] -license = {text = "MIT"} -readme = "README.md" -keywords = ["fastapi", "request-id", "correlation-id", "request-context", "middleware", "logging", "tracing"] -classifiers = [ - "Development Status :: 4 - Beta", - "Framework :: FastAPI", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", - "Topic :: Software Development :: Libraries :: Python Modules", - "Typing :: Typed", -] -requires-python = ">=3.12" -dependencies = [ - "fastapi>=0.100.0", -] - -[project.optional-dependencies] -context-logging = ["context-logging>=0.7.0"] -json-formatter = ["python-json-logger>=2.0.0"] -all = [ - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", -] - -[project.urls] -Homepage = "https://github.com/ADR-007/fastapi-request-context" -Repository = "https://github.com/ADR-007/fastapi-request-context" -Issues = "https://github.com/ADR-007/fastapi-request-context/issues" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[dependency-groups] -dev = [ - "ruff>=0.3.5", - "mypy>=1.9.0", - "pytest>=8.1.1", - "pytest-asyncio>=0.23.0", - "coverage>=7.4.4", - "tox>=4.14.2", - "tox-uv>=1.0.0", - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", - "httpx>=0.27.0", -] - -[tool.tox] -legacy_tox_ini = """ -[tox] -min_version = 4.0 -env_list = - lint - type - py{312,313}-fastapi{100,110,115} - coverage - -[gh-actions] -python = - 3.12: lint, type, py312-fastapi{100,110,115} - 3.13: py313-fastapi115 - -[testenv] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - context-logging - python-json-logger - httpx - fastapi100: fastapi>=0.100.0,<0.110.0 - fastapi110: fastapi>=0.110.0,<0.115.0 - fastapi115: fastapi>=0.115.0 -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 - -[testenv:lint] -runner = uv-venv-lock-runner -deps = - ruff - mypy - pytest - context-logging - python-json-logger -commands = - ruff check fastapi_request_context tests/ examples/ - ruff format --check fastapi_request_context tests/ examples/ - mypy fastapi_request_context tests/ - -[testenv:coverage] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - fastapi - context-logging - python-json-logger - httpx -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 100 - coverage report --skip-empty --fail-under 90 -""" - -[tool.semantic_release] -version_toml = [ - "pyproject.toml:project.version", -] -assets = [] -build_command = """ - python -m pip install -e '.[build]' - uv lock --upgrade-package "$PACKAGE_NAME" - git add uv.lock - uv build -""" -commit_message = "{version}\n\nAutomatically generated by python-semantic-release" -commit_parser = "conventional" -logging_use_named_masks = false -major_on_zero = true -tag_format = "v{version}" - -[tool.semantic_release.branches.main] -match = "(main|master)" -prerelease_token = "rc" -prerelease = false - -[tool.semantic_release.changelog] -exclude_commit_patterns = [] - -[tool.semantic_release.changelog.default_templates] -changelog_file = "CHANGELOG.md" - -[tool.semantic_release.changelog.environment] -block_start_string = "{%" -block_end_string = "%}" -variable_start_string = "{{" -variable_end_string = "}}" -comment_start_string = "{#" -comment_end_string = "#}" -trim_blocks = false -lstrip_blocks = false -newline_sequence = "\n" -keep_trailing_newline = false -extensions = [] -autoescape = true - -[tool.semantic_release.commit_author] -env = "GIT_COMMIT_AUTHOR" -default = "semantic-release " - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf", "build", "docs"] - -[tool.semantic_release.remote] -name = "origin" -type = "github" -ignore_token_for_push = false - -[tool.semantic_release.publish] -dist_glob_patterns = ["dist/*"] -upload_to_vcs_release = true - -[tool.mypy] -python_version = "3.12" -strict = true -warn_return_any = true -warn_unused_configs = true - -[[tool.mypy.overrides]] -module = "tests.*" -disable_error_code = [ - "call-arg", - "unused-ignore", - "misc", -] - -[[tool.mypy.overrides]] -module = "context_logging.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "pythonjsonlogger.*" -ignore_missing_imports = true - -[tool.ruff] -target-version = "py310" -line-length = 100 - -[tool.ruff.lint] -select = [ - "F", # Pyflakes - "E", "W", # pycodestyle - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "ASYNC", # flake8-async - "S", # flake8-bandit - "BLE", # flake8-blind-except - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PYI", # flake8-pyi - "PT", # flake8-pytest-style - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "ERA", # eradicate - "PGH", # pygrep-hooks - "PL", # Pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # Perflint - "RUF", # Ruff-specific rules -] - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # Allow assert in tests - "S103", # Allow os.chmod - "D100", "D101", "D102", "D103", # Allow missing docstrings in tests - "ANN102", # Missing type annotation for cls - "ANN401", # Allow Any type in fixtures - "TRY003", # Allow long exception messages - "PLR2004", # Allow magic values - "PLC0415", # Allow imports not at top level (for test isolation) - "ARG001", "ARG002", # Allow unused function arguments (fixtures) - "SLF001", # Allow private member access -] -"examples/**/*.py" = [ - "T201", # Allow print in examples - "D100", "D103", # Allow missing docstrings in examples - "INP001", # Allow missing __init__.py in examples - "ARG001", # Allow unused function arguments - "PLC0415", # Allow imports not at top level - "ANN401", # Allow Any type -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" -testpaths = ["tests"] - -[tool.coverage.run] -source = ["fastapi_request_context"] -branch = true - -[tool.coverage.report] -fail_under = 100 -exclude_lines = [ - "pragma: no cover", - "if TYPE_CHECKING:", - "@abstractmethod", - "raise NotImplementedError", - "\\.\\.\\.", # Protocol method stubs - "class.*\\(Protocol\\):", -] -exclude_also = [ - "def __repr__", - "@(abc\\.)?abstractmethod", -] diff --git a/.history/pyproject_20251128213826.toml b/.history/pyproject_20251128213826.toml deleted file mode 100644 index 5efa112..0000000 --- a/.history/pyproject_20251128213826.toml +++ /dev/null @@ -1,305 +0,0 @@ -[project] -name = "fastapi-request-context" -version = "0.1.0" -description = "FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with logging integration" -authors = [ - {name = "Adrian Dankiv", email = "adr-007@ukr.net"} -] -license = {text = "MIT"} -readme = "README.md" -keywords = ["fastapi", "request-id", "correlation-id", "request-context", "middleware", "logging", "tracing"] -classifiers = [ - "Development Status :: 4 - Beta", - "Framework :: FastAPI", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", - "Topic :: Software Development :: Libraries :: Python Modules", - "Typing :: Typed", -] -requires-python = ">=3.12" -dependencies = [ - "fastapi>=0.100.0", -] - -[project.optional-dependencies] -context-logging = ["context-logging>=0.7.0"] -json-formatter = ["python-json-logger>=2.0.0"] -build = ["uv>=0.9.0"] -all = [ - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", -] - -[project.urls] -Homepage = "https://github.com/ADR-007/fastapi-request-context" -Repository = "https://github.com/ADR-007/fastapi-request-context" -Issues = "https://github.com/ADR-007/fastapi-request-context/issues" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[dependency-groups] -dev = [ - "ruff>=0.3.5", - "mypy>=1.9.0", - "pytest>=8.1.1", - "pytest-asyncio>=0.23.0", - "coverage>=7.4.4", - "tox>=4.14.2", - "tox-uv>=1.0.0", - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", - "httpx>=0.27.0", -] - -[tool.tox] -legacy_tox_ini = """ -[tox] -min_version = 4.0 -env_list = - lint - type - py{312,313}-fastapi{100,110,115} - coverage - -[gh-actions] -python = - 3.12: lint, type, py312-fastapi{100,110,115} - 3.13: py313-fastapi115 - -[testenv] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - context-logging - python-json-logger - httpx - fastapi100: fastapi>=0.100.0,<0.110.0 - fastapi110: fastapi>=0.110.0,<0.115.0 - fastapi115: fastapi>=0.115.0 -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 - -[testenv:lint] -runner = uv-venv-lock-runner -deps = - ruff - mypy - pytest - context-logging - python-json-logger -commands = - ruff check fastapi_request_context tests/ examples/ - ruff format --check fastapi_request_context tests/ examples/ - mypy fastapi_request_context tests/ - -[testenv:coverage] -runner = uv-venv-lock-runner -deps = - pytest - pytest-asyncio - coverage - fastapi - context-logging - python-json-logger - httpx -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 100 - coverage report --skip-empty --fail-under 90 -""" - -[tool.semantic_release] -version_toml = [ - "pyproject.toml:project.version", -] -assets = [] -build_command = """ - python -m pip install -e '.[build]' - uv lock --upgrade-package "$PACKAGE_NAME" - git add uv.lock - uv build -""" -commit_message = "{version}\n\nAutomatically generated by python-semantic-release" -commit_parser = "conventional" -logging_use_named_masks = false -major_on_zero = true -tag_format = "v{version}" - -[tool.semantic_release.branches.main] -match = "(main|master)" -prerelease_token = "rc" -prerelease = false - -[tool.semantic_release.changelog] -exclude_commit_patterns = [] - -[tool.semantic_release.changelog.default_templates] -changelog_file = "CHANGELOG.md" - -[tool.semantic_release.changelog.environment] -block_start_string = "{%" -block_end_string = "%}" -variable_start_string = "{{" -variable_end_string = "}}" -comment_start_string = "{#" -comment_end_string = "#}" -trim_blocks = false -lstrip_blocks = false -newline_sequence = "\n" -keep_trailing_newline = false -extensions = [] -autoescape = true - -[tool.semantic_release.commit_author] -env = "GIT_COMMIT_AUTHOR" -default = "semantic-release " - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf", "build", "docs"] - -[tool.semantic_release.remote] -name = "origin" -type = "github" -ignore_token_for_push = false - -[tool.semantic_release.publish] -dist_glob_patterns = ["dist/*"] -upload_to_vcs_release = true - -[tool.mypy] -python_version = "3.12" -strict = true -warn_return_any = true -warn_unused_configs = true - -[[tool.mypy.overrides]] -module = "tests.*" -disable_error_code = [ - "call-arg", - "unused-ignore", - "misc", -] - -[[tool.mypy.overrides]] -module = "context_logging.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "pythonjsonlogger.*" -ignore_missing_imports = true - -[tool.ruff] -target-version = "py310" -line-length = 100 - -[tool.ruff.lint] -select = [ - "F", # Pyflakes - "E", "W", # pycodestyle - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "ASYNC", # flake8-async - "S", # flake8-bandit - "BLE", # flake8-blind-except - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PYI", # flake8-pyi - "PT", # flake8-pytest-style - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "ERA", # eradicate - "PGH", # pygrep-hooks - "PL", # Pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # Perflint - "RUF", # Ruff-specific rules -] - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # Allow assert in tests - "S103", # Allow os.chmod - "D100", "D101", "D102", "D103", # Allow missing docstrings in tests - "ANN102", # Missing type annotation for cls - "ANN401", # Allow Any type in fixtures - "TRY003", # Allow long exception messages - "PLR2004", # Allow magic values - "PLC0415", # Allow imports not at top level (for test isolation) - "ARG001", "ARG002", # Allow unused function arguments (fixtures) - "SLF001", # Allow private member access -] -"examples/**/*.py" = [ - "T201", # Allow print in examples - "D100", "D103", # Allow missing docstrings in examples - "INP001", # Allow missing __init__.py in examples - "ARG001", # Allow unused function arguments - "PLC0415", # Allow imports not at top level - "ANN401", # Allow Any type -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" -testpaths = ["tests"] - -[tool.coverage.run] -source = ["fastapi_request_context"] -branch = true - -[tool.coverage.report] -fail_under = 100 -exclude_lines = [ - "pragma: no cover", - "if TYPE_CHECKING:", - "@abstractmethod", - "raise NotImplementedError", - "\\.\\.\\.", # Protocol method stubs - "class.*\\(Protocol\\):", -] -exclude_also = [ - "def __repr__", - "@(abc\\.)?abstractmethod", -] diff --git a/.history/pyproject_20251128213847.toml b/.history/pyproject_20251128213847.toml deleted file mode 100644 index c3e4986..0000000 --- a/.history/pyproject_20251128213847.toml +++ /dev/null @@ -1,281 +0,0 @@ -[project] -name = "fastapi-request-context" -version = "0.1.0" -description = "FastAPI middleware for request ID tracking, correlation IDs, and extensible request context with logging integration" -authors = [ - {name = "Adrian Dankiv", email = "adr-007@ukr.net"} -] -license = {text = "MIT"} -readme = "README.md" -keywords = ["fastapi", "request-id", "correlation-id", "request-context", "middleware", "logging", "tracing"] -classifiers = [ - "Development Status :: 4 - Beta", - "Framework :: FastAPI", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", - "Topic :: Software Development :: Libraries :: Python Modules", - "Typing :: Typed", -] -requires-python = ">=3.12" -dependencies = [ - "fastapi>=0.100.0", -] - -[project.optional-dependencies] -context-logging = ["context-logging>=0.7.0"] -json-formatter = ["python-json-logger>=2.0.0"] -build = ["uv>=0.9.0"] -all = [ - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", -] - -[project.urls] -Homepage = "https://github.com/ADR-007/fastapi-request-context" -Repository = "https://github.com/ADR-007/fastapi-request-context" -Issues = "https://github.com/ADR-007/fastapi-request-context/issues" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[dependency-groups] -dev = [ - "ruff>=0.3.5", - "mypy>=1.9.0", - "pytest>=8.1.1", - "pytest-asyncio>=0.23.0", - "coverage>=7.4.4", - "tox>=4.14.2", - "tox-uv>=1.0.0", - "context-logging>=0.7.0", - "python-json-logger>=2.0.0", - "httpx>=0.27.0", -] - -[tool.tox] -legacy_tox_ini = """ -[tox] -min_version = 4.0 -env_list = - lint - py{312,313} - coverage - -[gh-actions] -python = - 3.13: lint, py313, coverage - 3.12: py312 - -[testenv] -runner = uv-venv-lock-runner -extras = all -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 90 - -[testenv:lint] -runner = uv-venv-lock-runner -commands = - ruff check fastapi_request_context tests/ examples/ - ruff format --check fastapi_request_context tests/ examples/ - mypy fastapi_request_context tests/ - -[testenv:coverage] -runner = uv-venv-lock-runner -extras = all -commands = - coverage run -m pytest tests/ - coverage html -d ./reports/htmlcov --omit="tests/*" - coverage xml -o ./reports/coverage.xml --omit="tests/*" - coverage report --skip-empty --fail-under 100 -""" - -[tool.semantic_release] -version_toml = [ - "pyproject.toml:project.version", -] -assets = [] -build_command = """ - python -m pip install -e '.[build]' - uv lock --upgrade-package "$PACKAGE_NAME" - git add uv.lock - uv build -""" -commit_message = "{version}\n\nAutomatically generated by python-semantic-release" -commit_parser = "conventional" -logging_use_named_masks = false -major_on_zero = true -tag_format = "v{version}" - -[tool.semantic_release.branches.main] -match = "(main|master)" -prerelease_token = "rc" -prerelease = false - -[tool.semantic_release.changelog] -exclude_commit_patterns = [] - -[tool.semantic_release.changelog.default_templates] -changelog_file = "CHANGELOG.md" - -[tool.semantic_release.changelog.environment] -block_start_string = "{%" -block_end_string = "%}" -variable_start_string = "{{" -variable_end_string = "}}" -comment_start_string = "{#" -comment_end_string = "#}" -trim_blocks = false -lstrip_blocks = false -newline_sequence = "\n" -keep_trailing_newline = false -extensions = [] -autoescape = true - -[tool.semantic_release.commit_author] -env = "GIT_COMMIT_AUTHOR" -default = "semantic-release " - -[tool.semantic_release.commit_parser_options] -allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] -minor_tags = ["feat"] -patch_tags = ["fix", "perf", "build", "docs"] - -[tool.semantic_release.remote] -name = "origin" -type = "github" -ignore_token_for_push = false - -[tool.semantic_release.publish] -dist_glob_patterns = ["dist/*"] -upload_to_vcs_release = true - -[tool.mypy] -python_version = "3.12" -strict = true -warn_return_any = true -warn_unused_configs = true - -[[tool.mypy.overrides]] -module = "tests.*" -disable_error_code = [ - "call-arg", - "unused-ignore", - "misc", -] - -[[tool.mypy.overrides]] -module = "context_logging.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "pythonjsonlogger.*" -ignore_missing_imports = true - -[tool.ruff] -target-version = "py310" -line-length = 100 - -[tool.ruff.lint] -select = [ - "F", # Pyflakes - "E", "W", # pycodestyle - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "ASYNC", # flake8-async - "S", # flake8-bandit - "BLE", # flake8-blind-except - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PYI", # flake8-pyi - "PT", # flake8-pytest-style - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "ERA", # eradicate - "PGH", # pygrep-hooks - "PL", # Pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # Perflint - "RUF", # Ruff-specific rules -] - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S101", # Allow assert in tests - "S103", # Allow os.chmod - "D100", "D101", "D102", "D103", # Allow missing docstrings in tests - "ANN102", # Missing type annotation for cls - "ANN401", # Allow Any type in fixtures - "TRY003", # Allow long exception messages - "PLR2004", # Allow magic values - "PLC0415", # Allow imports not at top level (for test isolation) - "ARG001", "ARG002", # Allow unused function arguments (fixtures) - "SLF001", # Allow private member access -] -"examples/**/*.py" = [ - "T201", # Allow print in examples - "D100", "D103", # Allow missing docstrings in examples - "INP001", # Allow missing __init__.py in examples - "ARG001", # Allow unused function arguments - "PLC0415", # Allow imports not at top level - "ANN401", # Allow Any type -] - -[tool.pytest.ini_options] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" -testpaths = ["tests"] - -[tool.coverage.run] -source = ["fastapi_request_context"] -branch = true - -[tool.coverage.report] -fail_under = 100 -exclude_lines = [ - "pragma: no cover", - "if TYPE_CHECKING:", - "@abstractmethod", - "raise NotImplementedError", - "\\.\\.\\.", # Protocol method stubs - "class.*\\(Protocol\\):", -] -exclude_also = [ - "def __repr__", - "@(abc\\.)?abstractmethod", -] diff --git a/.history/tests/__init___20251128203038.py b/.history/tests/__init___20251128203038.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/tests/__init___20251128204032.py b/.history/tests/__init___20251128204032.py deleted file mode 100644 index 0d53c69..0000000 --- a/.history/tests/__init___20251128204032.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for fastapi-request-context.""" diff --git a/.history/tests/adapters/__init___20251128212009.py b/.history/tests/adapters/__init___20251128212009.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/tests/adapters/__init___20251128212010.py b/.history/tests/adapters/__init___20251128212010.py deleted file mode 100644 index aa3d161..0000000 --- a/.history/tests/adapters/__init___20251128212010.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for context adapters.""" diff --git a/.history/tests/adapters/test_context_logging_20251128212029.py b/.history/tests/adapters/test_context_logging_20251128212029.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/tests/adapters/test_context_logging_20251128212031.py b/.history/tests/adapters/test_context_logging_20251128212031.py deleted file mode 100644 index f8a5fc9..0000000 --- a/.history/tests/adapters/test_context_logging_20251128212031.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Tests for ContextLoggingAdapter.""" - -import sys - -import pytest - -from fastapi_request_context.adapters import ContextAdapter, ContextLoggingAdapter - - -def test_set_and_get_value() -> None: - """Test basic set and get operations.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({}) - - try: - adapter.set_value("key1", "value1") - assert adapter.get_value("key1") == "value1" - finally: - adapter.exit_context() - - -def test_get_nonexistent_value() -> None: - """Test getting a nonexistent key returns None.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({}) - - try: - assert adapter.get_value("nonexistent") is None - finally: - adapter.exit_context() - - -def test_get_all() -> None: - """Test getting all values.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({"initial": "value"}) - - try: - adapter.set_value("key1", "value1") - - all_values = adapter.get_all() - assert "initial" in all_values - assert "key1" in all_values - finally: - adapter.exit_context() - - -def test_enter_context_with_initial_values() -> None: - """Test entering context with initial values.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({"request_id": "123"}) - - try: - assert adapter.get_value("request_id") == "123" - finally: - adapter.exit_context() - - -def test_implements_protocol() -> None: - """Test that adapter implements ContextAdapter protocol.""" - adapter = ContextLoggingAdapter() - assert isinstance(adapter, ContextAdapter) - - -def test_exit_context_without_enter() -> None: - """Test that exit_context does nothing when not entered.""" - adapter = ContextLoggingAdapter() - # Should not raise, just do nothing - adapter.exit_context() - - -def test_import_error_message(monkeypatch: pytest.MonkeyPatch) -> None: - """Test that helpful error message is shown when context-logging missing.""" - import importlib - - from fastapi_request_context.adapters import context_logging - - # Remove context_logging from modules to simulate it not being installed - original_modules = dict(sys.modules) - sys.modules["context_logging"] = None # type: ignore[assignment] - - try: - importlib.reload(context_logging) - - with pytest.raises(ImportError, match="context-logging is required"): - context_logging.ContextLoggingAdapter() - finally: - sys.modules.update(original_modules) diff --git a/.history/tests/adapters/test_contextvars_20251128212021.py b/.history/tests/adapters/test_contextvars_20251128212021.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/tests/adapters/test_contextvars_20251128212022.py b/.history/tests/adapters/test_contextvars_20251128212022.py deleted file mode 100644 index 309fac1..0000000 --- a/.history/tests/adapters/test_contextvars_20251128212022.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Tests for ContextVarsAdapter.""" - -from fastapi_request_context.adapters import ContextAdapter, ContextVarsAdapter - - -def test_set_and_get_value() -> None: - """Test basic set and get operations.""" - adapter = ContextVarsAdapter() - adapter.enter_context({}) - - try: - adapter.set_value("key1", "value1") - assert adapter.get_value("key1") == "value1" - finally: - adapter.exit_context() - - -def test_get_nonexistent_value() -> None: - """Test getting a nonexistent key returns None.""" - adapter = ContextVarsAdapter() - adapter.enter_context({}) - - try: - assert adapter.get_value("nonexistent") is None - finally: - adapter.exit_context() - - -def test_get_all() -> None: - """Test getting all values.""" - adapter = ContextVarsAdapter() - adapter.enter_context({"initial": "value"}) - - try: - adapter.set_value("key1", "value1") - adapter.set_value("key2", "value2") - - all_values = adapter.get_all() - assert all_values == { - "initial": "value", - "key1": "value1", - "key2": "value2", - } - finally: - adapter.exit_context() - - -def test_enter_context_with_initial_values() -> None: - """Test entering context with initial values.""" - adapter = ContextVarsAdapter() - adapter.enter_context({"request_id": "123", "correlation_id": "456"}) - - try: - assert adapter.get_value("request_id") == "123" - assert adapter.get_value("correlation_id") == "456" - finally: - adapter.exit_context() - - -def test_exit_context_clears_values() -> None: - """Test that exiting context clears values.""" - adapter = ContextVarsAdapter() - - adapter.enter_context({"key": "value"}) - assert adapter.get_value("key") == "value" - adapter.exit_context() - - # After exit, context is empty - assert adapter.get_value("key") is None - - -def test_get_all_returns_copy() -> None: - """Test that get_all returns a copy, not the original dict.""" - adapter = ContextVarsAdapter() - adapter.enter_context({"key": "value"}) - - try: - all_values = adapter.get_all() - all_values["new_key"] = "new_value" - - # Original should be unchanged - assert adapter.get_value("new_key") is None - finally: - adapter.exit_context() - - -def test_implements_protocol() -> None: - """Test that adapter implements ContextAdapter protocol.""" - adapter = ContextVarsAdapter() - assert isinstance(adapter, ContextAdapter) - - -def test_set_value_without_enter_context() -> None: - """Test that set_value does nothing when context not entered.""" - adapter = ContextVarsAdapter() - # Should not raise, just do nothing - adapter.set_value("key", "value") - # Value should be None since context was never entered - assert adapter.get_value("key") is None diff --git a/.history/tests/conftest_20251128203042.py b/.history/tests/conftest_20251128203042.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/tests/conftest_20251128203043.py b/.history/tests/conftest_20251128203043.py deleted file mode 100644 index 92c36a5..0000000 --- a/.history/tests/conftest_20251128203043.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Pytest configuration and fixtures.""" - -import pytest -from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient - -from fastapi_request_context import RequestContextMiddleware - - -@pytest.fixture -def app() -> FastAPI: - """Create a basic FastAPI app.""" - return FastAPI() - - -@pytest.fixture -def app_with_middleware(app: FastAPI) -> FastAPI: - """Create a FastAPI app wrapped with RequestContextMiddleware.""" - return RequestContextMiddleware(app) # type: ignore[return-value] - - -@pytest.fixture -async def client(app_with_middleware: FastAPI) -> AsyncClient: - """Create an async test client.""" - transport = ASGITransport(app=app_with_middleware) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - yield client diff --git a/.history/tests/conftest_20251128204315.py b/.history/tests/conftest_20251128204315.py deleted file mode 100644 index 33cd24f..0000000 --- a/.history/tests/conftest_20251128204315.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Pytest configuration and fixtures.""" - -from collections.abc import AsyncGenerator -from typing import Any - -import pytest -from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient - -from fastapi_request_context import RequestContextMiddleware - - -@pytest.fixture -def app() -> FastAPI: - """Create a basic FastAPI app.""" - return FastAPI() - - -@pytest.fixture -def app_with_middleware(app: FastAPI) -> Any: - """Create a FastAPI app wrapped with RequestContextMiddleware.""" - return RequestContextMiddleware(app) - - -@pytest.fixture -async def client(app_with_middleware: Any) -> AsyncGenerator[AsyncClient, None]: - """Create an async test client.""" - transport = ASGITransport(app=app_with_middleware) - async with AsyncClient(transport=transport, base_url="http://test") as client: - yield client diff --git a/.history/tests/formatters/__init___20251128212042.py b/.history/tests/formatters/__init___20251128212042.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/tests/formatters/__init___20251128212044.py b/.history/tests/formatters/__init___20251128212044.py deleted file mode 100644 index ea9587f..0000000 --- a/.history/tests/formatters/__init___20251128212044.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for logging formatters.""" diff --git a/.history/tests/formatters/test_integration_20251128212119.py b/.history/tests/formatters/test_integration_20251128212119.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/tests/formatters/test_integration_20251128212120.py b/.history/tests/formatters/test_integration_20251128212120.py deleted file mode 100644 index 5967aa6..0000000 --- a/.history/tests/formatters/test_integration_20251128212120.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Integration tests for formatters with middleware.""" - -import json -import logging -from typing import Any - -from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient - -from fastapi_request_context import RequestContextMiddleware, set_context -from fastapi_request_context.formatters import JsonContextFormatter - - -async def test_formatter_integration_with_middleware() -> None: - """Test formatters work correctly with middleware.""" - app = FastAPI() - log_records: list[str] = [] - - class CaptureHandler(logging.Handler): - def emit(self, record: logging.LogRecord) -> None: - log_records.append(self.format(record)) - - handler = CaptureHandler() - handler.setFormatter(JsonContextFormatter()) - logger = logging.getLogger("test_integration") - logger.addHandler(handler) - logger.setLevel(logging.INFO) - - @app.get("/") - async def root() -> dict[str, Any]: - set_context("user_id", 123) - logger.info("Processing request") - return {"status": "ok"} - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - await client.get("/") - - assert len(log_records) == 1 - data = json.loads(log_records[0]) - assert data["message"] == "Processing request" - assert data["user_id"] == 123 - assert "request_id" in data - - logger.removeHandler(handler) diff --git a/.history/tests/formatters/test_json_20251128212057.py b/.history/tests/formatters/test_json_20251128212057.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/tests/formatters/test_json_20251128212058.py b/.history/tests/formatters/test_json_20251128212058.py deleted file mode 100644 index f426e8a..0000000 --- a/.history/tests/formatters/test_json_20251128212058.py +++ /dev/null @@ -1,137 +0,0 @@ -"""Tests for JsonContextFormatter.""" - -import json -import logging -import sys - -from fastapi_request_context.formatters import JsonContextFormatter - - -def test_basic_formatting() -> None: - """Test basic JSON log formatting.""" - formatter = JsonContextFormatter() - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert data["message"] == "Test message" - assert data["level"] == "INFO" - assert data["logger"] == "test" - assert "timestamp" in data - - -def test_includes_context() -> None: - """Test that context is included in JSON output.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123", "user_id": 456}) - - try: - formatter = JsonContextFormatter() - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert data["request_id"] == "test-123" - assert data["user_id"] == 456 - finally: - adapter.exit_context() - - -def test_context_key_nesting() -> None: - """Test nesting context under a specific key.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123"}) - - try: - formatter = JsonContextFormatter(context_key="context") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "context" in data - assert data["context"]["request_id"] == "test-123" - finally: - adapter.exit_context() - - -def test_exclude_standard_fields() -> None: - """Test excluding standard fields from output.""" - formatter = JsonContextFormatter(include_standard_fields=False) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "message" in data - assert "level" not in data - assert "logger" not in data - assert "timestamp" not in data - - -def test_exception_formatting() -> None: - """Test that exceptions are included in output.""" - formatter = JsonContextFormatter() - - try: - raise ValueError("Test error") # noqa: TRY301 - except ValueError: - exc_info = sys.exc_info() - - record = logging.LogRecord( - name="test", - level=logging.ERROR, - pathname="test.py", - lineno=1, - msg="Error occurred", - args=(), - exc_info=exc_info, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "exception" in data - assert "ValueError" in data["exception"] diff --git a/.history/tests/formatters/test_simple_20251128212115.py b/.history/tests/formatters/test_simple_20251128212115.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/tests/formatters/test_simple_20251128212116.py b/.history/tests/formatters/test_simple_20251128212116.py deleted file mode 100644 index 301ecb5..0000000 --- a/.history/tests/formatters/test_simple_20251128212116.py +++ /dev/null @@ -1,189 +0,0 @@ -"""Tests for SimpleContextFormatter.""" - -import logging - -from fastapi_request_context.formatters import SimpleContextFormatter - - -def test_basic_formatting() -> None: - """Test basic local log formatting.""" - formatter = SimpleContextFormatter(fmt="%(levelname)s %(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "INFO" in output - assert "Test message" in output - - -def test_includes_context() -> None: - """Test that context is included in output.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123", "user_id": 456}) - - try: - formatter = SimpleContextFormatter(fmt="%(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=test-123" in output - assert "user_id=456" in output - finally: - adapter.exit_context() - - -def test_shorten_fields() -> None: - """Test shortening of specified fields.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "12345678901234567890"}) - - try: - formatter = SimpleContextFormatter( - fmt="%(context)s %(message)s", - shorten_fields={"request_id"}, - shorten_length=8, - ) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=12345678" in output - assert "12345678901234567890" not in output - finally: - adapter.exit_context() - - -def test_hidden_fields() -> None: - """Test hiding of specified fields.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context( - { - "request_id": "shown", - "correlation_id": "hidden", - }, - ) - - try: - formatter = SimpleContextFormatter( - fmt="%(context)s %(message)s", - hidden_fields={"correlation_id"}, - ) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=shown" in output - assert "correlation_id" not in output - finally: - adapter.exit_context() - - -def test_empty_context() -> None: - """Test formatting when context is empty.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({}) - - try: - formatter = SimpleContextFormatter(fmt="%(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - # Should not have empty brackets - assert "[]" not in output - assert "Test message" in output - finally: - adapter.exit_context() - - -def test_all_fields_hidden() -> None: - """Test formatting when all fields are hidden.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123", "user_id": "456"}) - - try: - formatter = SimpleContextFormatter( - fmt="%(context)s %(message)s", - hidden_fields={"request_id", "user_id"}, - ) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - # Should return empty string for context when all hidden - assert "[]" not in output - assert "request_id" not in output - assert "user_id" not in output - assert "Test message" in output - finally: - adapter.exit_context() - - -def test_default_format() -> None: - """Test default format string when none provided.""" - formatter = SimpleContextFormatter() - assert formatter._fmt is not None - assert "%(context)s" in formatter._fmt diff --git a/.history/tests/test_adapters_20251128203159.py b/.history/tests/test_adapters_20251128203159.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/tests/test_adapters_20251128203200.py b/.history/tests/test_adapters_20251128203200.py deleted file mode 100644 index fdcb81f..0000000 --- a/.history/tests/test_adapters_20251128203200.py +++ /dev/null @@ -1,166 +0,0 @@ -"""Tests for context adapters.""" - -import pytest - -from fastapi_request_context.adapters import ( - ContextAdapter, - ContextLoggingAdapter, - ContextVarsAdapter, -) - - -class TestContextVarsAdapter: - """Tests for ContextVarsAdapter.""" - - def test_set_and_get_value(self) -> None: - """Test basic set and get operations.""" - adapter = ContextVarsAdapter() - adapter.enter_context({}) - - adapter.set_value("key1", "value1") - assert adapter.get_value("key1") == "value1" - - adapter.exit_context() - - def test_get_nonexistent_value(self) -> None: - """Test getting a nonexistent key returns None.""" - adapter = ContextVarsAdapter() - adapter.enter_context({}) - - assert adapter.get_value("nonexistent") is None - - adapter.exit_context() - - def test_get_all(self) -> None: - """Test getting all values.""" - adapter = ContextVarsAdapter() - adapter.enter_context({"initial": "value"}) - - adapter.set_value("key1", "value1") - adapter.set_value("key2", "value2") - - all_values = adapter.get_all() - assert all_values == { - "initial": "value", - "key1": "value1", - "key2": "value2", - } - - adapter.exit_context() - - def test_enter_context_with_initial_values(self) -> None: - """Test entering context with initial values.""" - adapter = ContextVarsAdapter() - adapter.enter_context({"request_id": "123", "correlation_id": "456"}) - - assert adapter.get_value("request_id") == "123" - assert adapter.get_value("correlation_id") == "456" - - adapter.exit_context() - - def test_exit_context_resets_values(self) -> None: - """Test that exiting context resets values.""" - adapter = ContextVarsAdapter() - - # First context - adapter.enter_context({"key": "first"}) - assert adapter.get_value("key") == "first" - adapter.exit_context() - - # New context - adapter.enter_context({"key": "second"}) - assert adapter.get_value("key") == "second" - adapter.exit_context() - - def test_get_all_returns_copy(self) -> None: - """Test that get_all returns a copy, not the original dict.""" - adapter = ContextVarsAdapter() - adapter.enter_context({"key": "value"}) - - all_values = adapter.get_all() - all_values["new_key"] = "new_value" - - # Original should be unchanged - assert adapter.get_value("new_key") is None - - adapter.exit_context() - - def test_implements_protocol(self) -> None: - """Test that adapter implements ContextAdapter protocol.""" - adapter = ContextVarsAdapter() - assert isinstance(adapter, ContextAdapter) - - -class TestContextLoggingAdapter: - """Tests for ContextLoggingAdapter.""" - - def test_set_and_get_value(self) -> None: - """Test basic set and get operations.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({}) - - adapter.set_value("key1", "value1") - assert adapter.get_value("key1") == "value1" - - adapter.exit_context() - - def test_get_nonexistent_value(self) -> None: - """Test getting a nonexistent key returns None.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({}) - - assert adapter.get_value("nonexistent") is None - - adapter.exit_context() - - def test_get_all(self) -> None: - """Test getting all values.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({"initial": "value"}) - - adapter.set_value("key1", "value1") - - all_values = adapter.get_all() - assert "initial" in all_values - assert "key1" in all_values - - adapter.exit_context() - - def test_enter_context_with_initial_values(self) -> None: - """Test entering context with initial values.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({"request_id": "123"}) - - assert adapter.get_value("request_id") == "123" - - adapter.exit_context() - - def test_implements_protocol(self) -> None: - """Test that adapter implements ContextAdapter protocol.""" - adapter = ContextLoggingAdapter() - assert isinstance(adapter, ContextAdapter) - - -class TestContextLoggingAdapterImportError: - """Tests for ContextLoggingAdapter when context-logging is not installed.""" - - def test_import_error_message(self, monkeypatch: pytest.MonkeyPatch) -> None: - """Test that helpful error message is shown when context-logging missing.""" - import sys - - # Remove context_logging from modules to simulate it not being installed - original_modules = dict(sys.modules) - sys.modules["context_logging"] = None # type: ignore[assignment] - - try: - # Need to reimport to trigger the import check - import importlib - - from fastapi_request_context.adapters import context_logging - - importlib.reload(context_logging) - - with pytest.raises(ImportError, match="context-logging is required"): - context_logging.ContextLoggingAdapter() - finally: - sys.modules.update(original_modules) diff --git a/.history/tests/test_adapters_20251128203757.py b/.history/tests/test_adapters_20251128203757.py deleted file mode 100644 index 9f48c28..0000000 --- a/.history/tests/test_adapters_20251128203757.py +++ /dev/null @@ -1,168 +0,0 @@ -"""Tests for context adapters.""" - -import pytest - -from fastapi_request_context.adapters import ( - ContextAdapter, - ContextLoggingAdapter, - ContextVarsAdapter, -) - - -class TestContextVarsAdapter: - """Tests for ContextVarsAdapter.""" - - def test_set_and_get_value(self) -> None: - """Test basic set and get operations.""" - adapter = ContextVarsAdapter() - adapter.enter_context({}) - - try: - adapter.set_value("key1", "value1") - assert adapter.get_value("key1") == "value1" - finally: - adapter.exit_context() - - def test_get_nonexistent_value(self) -> None: - """Test getting a nonexistent key returns None.""" - adapter = ContextVarsAdapter() - adapter.enter_context({}) - - try: - assert adapter.get_value("nonexistent") is None - finally: - adapter.exit_context() - - def test_get_all(self) -> None: - """Test getting all values.""" - adapter = ContextVarsAdapter() - adapter.enter_context({"initial": "value"}) - - try: - adapter.set_value("key1", "value1") - adapter.set_value("key2", "value2") - - all_values = adapter.get_all() - assert all_values == { - "initial": "value", - "key1": "value1", - "key2": "value2", - } - finally: - adapter.exit_context() - - def test_enter_context_with_initial_values(self) -> None: - """Test entering context with initial values.""" - adapter = ContextVarsAdapter() - adapter.enter_context({"request_id": "123", "correlation_id": "456"}) - - try: - assert adapter.get_value("request_id") == "123" - assert adapter.get_value("correlation_id") == "456" - finally: - adapter.exit_context() - - def test_exit_context_clears_values(self) -> None: - """Test that exiting context clears values.""" - adapter = ContextVarsAdapter() - - adapter.enter_context({"key": "value"}) - assert adapter.get_value("key") == "value" - adapter.exit_context() - - # After exit, context is empty - assert adapter.get_value("key") is None - - def test_get_all_returns_copy(self) -> None: - """Test that get_all returns a copy, not the original dict.""" - adapter = ContextVarsAdapter() - adapter.enter_context({"key": "value"}) - - try: - all_values = adapter.get_all() - all_values["new_key"] = "new_value" - - # Original should be unchanged - assert adapter.get_value("new_key") is None - finally: - adapter.exit_context() - - def test_implements_protocol(self) -> None: - """Test that adapter implements ContextAdapter protocol.""" - adapter = ContextVarsAdapter() - assert isinstance(adapter, ContextAdapter) - - -class TestContextLoggingAdapter: - """Tests for ContextLoggingAdapter.""" - - def test_set_and_get_value(self) -> None: - """Test basic set and get operations.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({}) - - adapter.set_value("key1", "value1") - assert adapter.get_value("key1") == "value1" - - adapter.exit_context() - - def test_get_nonexistent_value(self) -> None: - """Test getting a nonexistent key returns None.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({}) - - assert adapter.get_value("nonexistent") is None - - adapter.exit_context() - - def test_get_all(self) -> None: - """Test getting all values.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({"initial": "value"}) - - adapter.set_value("key1", "value1") - - all_values = adapter.get_all() - assert "initial" in all_values - assert "key1" in all_values - - adapter.exit_context() - - def test_enter_context_with_initial_values(self) -> None: - """Test entering context with initial values.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({"request_id": "123"}) - - assert adapter.get_value("request_id") == "123" - - adapter.exit_context() - - def test_implements_protocol(self) -> None: - """Test that adapter implements ContextAdapter protocol.""" - adapter = ContextLoggingAdapter() - assert isinstance(adapter, ContextAdapter) - - -class TestContextLoggingAdapterImportError: - """Tests for ContextLoggingAdapter when context-logging is not installed.""" - - def test_import_error_message(self, monkeypatch: pytest.MonkeyPatch) -> None: - """Test that helpful error message is shown when context-logging missing.""" - import sys - - # Remove context_logging from modules to simulate it not being installed - original_modules = dict(sys.modules) - sys.modules["context_logging"] = None # type: ignore[assignment] - - try: - # Need to reimport to trigger the import check - import importlib - - from fastapi_request_context.adapters import context_logging - - importlib.reload(context_logging) - - with pytest.raises(ImportError, match="context-logging is required"): - context_logging.ContextLoggingAdapter() - finally: - sys.modules.update(original_modules) diff --git a/.history/tests/test_adapters_20251128203814.py b/.history/tests/test_adapters_20251128203814.py deleted file mode 100644 index 0f765bb..0000000 --- a/.history/tests/test_adapters_20251128203814.py +++ /dev/null @@ -1,172 +0,0 @@ -"""Tests for context adapters.""" - -import pytest - -from fastapi_request_context.adapters import ( - ContextAdapter, - ContextLoggingAdapter, - ContextVarsAdapter, -) - - -class TestContextVarsAdapter: - """Tests for ContextVarsAdapter.""" - - def test_set_and_get_value(self) -> None: - """Test basic set and get operations.""" - adapter = ContextVarsAdapter() - adapter.enter_context({}) - - try: - adapter.set_value("key1", "value1") - assert adapter.get_value("key1") == "value1" - finally: - adapter.exit_context() - - def test_get_nonexistent_value(self) -> None: - """Test getting a nonexistent key returns None.""" - adapter = ContextVarsAdapter() - adapter.enter_context({}) - - try: - assert adapter.get_value("nonexistent") is None - finally: - adapter.exit_context() - - def test_get_all(self) -> None: - """Test getting all values.""" - adapter = ContextVarsAdapter() - adapter.enter_context({"initial": "value"}) - - try: - adapter.set_value("key1", "value1") - adapter.set_value("key2", "value2") - - all_values = adapter.get_all() - assert all_values == { - "initial": "value", - "key1": "value1", - "key2": "value2", - } - finally: - adapter.exit_context() - - def test_enter_context_with_initial_values(self) -> None: - """Test entering context with initial values.""" - adapter = ContextVarsAdapter() - adapter.enter_context({"request_id": "123", "correlation_id": "456"}) - - try: - assert adapter.get_value("request_id") == "123" - assert adapter.get_value("correlation_id") == "456" - finally: - adapter.exit_context() - - def test_exit_context_clears_values(self) -> None: - """Test that exiting context clears values.""" - adapter = ContextVarsAdapter() - - adapter.enter_context({"key": "value"}) - assert adapter.get_value("key") == "value" - adapter.exit_context() - - # After exit, context is empty - assert adapter.get_value("key") is None - - def test_get_all_returns_copy(self) -> None: - """Test that get_all returns a copy, not the original dict.""" - adapter = ContextVarsAdapter() - adapter.enter_context({"key": "value"}) - - try: - all_values = adapter.get_all() - all_values["new_key"] = "new_value" - - # Original should be unchanged - assert adapter.get_value("new_key") is None - finally: - adapter.exit_context() - - def test_implements_protocol(self) -> None: - """Test that adapter implements ContextAdapter protocol.""" - adapter = ContextVarsAdapter() - assert isinstance(adapter, ContextAdapter) - - -class TestContextLoggingAdapter: - """Tests for ContextLoggingAdapter.""" - - def test_set_and_get_value(self) -> None: - """Test basic set and get operations.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({}) - - try: - adapter.set_value("key1", "value1") - assert adapter.get_value("key1") == "value1" - finally: - adapter.exit_context() - - def test_get_nonexistent_value(self) -> None: - """Test getting a nonexistent key returns None.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({}) - - try: - assert adapter.get_value("nonexistent") is None - finally: - adapter.exit_context() - - def test_get_all(self) -> None: - """Test getting all values.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({"initial": "value"}) - - try: - adapter.set_value("key1", "value1") - - all_values = adapter.get_all() - assert "initial" in all_values - assert "key1" in all_values - finally: - adapter.exit_context() - - def test_enter_context_with_initial_values(self) -> None: - """Test entering context with initial values.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({"request_id": "123"}) - - try: - assert adapter.get_value("request_id") == "123" - finally: - adapter.exit_context() - - def test_implements_protocol(self) -> None: - """Test that adapter implements ContextAdapter protocol.""" - adapter = ContextLoggingAdapter() - assert isinstance(adapter, ContextAdapter) - - -class TestContextLoggingAdapterImportError: - """Tests for ContextLoggingAdapter when context-logging is not installed.""" - - def test_import_error_message(self, monkeypatch: pytest.MonkeyPatch) -> None: - """Test that helpful error message is shown when context-logging missing.""" - import sys - - # Remove context_logging from modules to simulate it not being installed - original_modules = dict(sys.modules) - sys.modules["context_logging"] = None # type: ignore[assignment] - - try: - # Need to reimport to trigger the import check - import importlib - - from fastapi_request_context.adapters import context_logging - - importlib.reload(context_logging) - - with pytest.raises(ImportError, match="context-logging is required"): - context_logging.ContextLoggingAdapter() - finally: - sys.modules.update(original_modules) diff --git a/.history/tests/test_adapters_20251128211715.py b/.history/tests/test_adapters_20251128211715.py deleted file mode 100644 index fea2b60..0000000 --- a/.history/tests/test_adapters_20251128211715.py +++ /dev/null @@ -1,180 +0,0 @@ -"""Tests for context adapters.""" - -import pytest - -from fastapi_request_context.adapters import ( - ContextAdapter, - ContextLoggingAdapter, - ContextVarsAdapter, -) - - -class TestContextVarsAdapter: - """Tests for ContextVarsAdapter.""" - - def test_set_and_get_value(self) -> None: - """Test basic set and get operations.""" - adapter = ContextVarsAdapter() - adapter.enter_context({}) - - try: - adapter.set_value("key1", "value1") - assert adapter.get_value("key1") == "value1" - finally: - adapter.exit_context() - - def test_get_nonexistent_value(self) -> None: - """Test getting a nonexistent key returns None.""" - adapter = ContextVarsAdapter() - adapter.enter_context({}) - - try: - assert adapter.get_value("nonexistent") is None - finally: - adapter.exit_context() - - def test_get_all(self) -> None: - """Test getting all values.""" - adapter = ContextVarsAdapter() - adapter.enter_context({"initial": "value"}) - - try: - adapter.set_value("key1", "value1") - adapter.set_value("key2", "value2") - - all_values = adapter.get_all() - assert all_values == { - "initial": "value", - "key1": "value1", - "key2": "value2", - } - finally: - adapter.exit_context() - - def test_enter_context_with_initial_values(self) -> None: - """Test entering context with initial values.""" - adapter = ContextVarsAdapter() - adapter.enter_context({"request_id": "123", "correlation_id": "456"}) - - try: - assert adapter.get_value("request_id") == "123" - assert adapter.get_value("correlation_id") == "456" - finally: - adapter.exit_context() - - def test_exit_context_clears_values(self) -> None: - """Test that exiting context clears values.""" - adapter = ContextVarsAdapter() - - adapter.enter_context({"key": "value"}) - assert adapter.get_value("key") == "value" - adapter.exit_context() - - # After exit, context is empty - assert adapter.get_value("key") is None - - def test_get_all_returns_copy(self) -> None: - """Test that get_all returns a copy, not the original dict.""" - adapter = ContextVarsAdapter() - adapter.enter_context({"key": "value"}) - - try: - all_values = adapter.get_all() - all_values["new_key"] = "new_value" - - # Original should be unchanged - assert adapter.get_value("new_key") is None - finally: - adapter.exit_context() - - def test_implements_protocol(self) -> None: - """Test that adapter implements ContextAdapter protocol.""" - adapter = ContextVarsAdapter() - assert isinstance(adapter, ContextAdapter) - - def test_set_value_without_enter_context(self) -> None: - """Test that set_value does nothing when context not entered.""" - adapter = ContextVarsAdapter() - # Should not raise, just do nothing - adapter.set_value("key", "value") - # Value should be None since context was never entered - assert adapter.get_value("key") is None - - -class TestContextLoggingAdapter: - """Tests for ContextLoggingAdapter.""" - - def test_set_and_get_value(self) -> None: - """Test basic set and get operations.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({}) - - try: - adapter.set_value("key1", "value1") - assert adapter.get_value("key1") == "value1" - finally: - adapter.exit_context() - - def test_get_nonexistent_value(self) -> None: - """Test getting a nonexistent key returns None.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({}) - - try: - assert adapter.get_value("nonexistent") is None - finally: - adapter.exit_context() - - def test_get_all(self) -> None: - """Test getting all values.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({"initial": "value"}) - - try: - adapter.set_value("key1", "value1") - - all_values = adapter.get_all() - assert "initial" in all_values - assert "key1" in all_values - finally: - adapter.exit_context() - - def test_enter_context_with_initial_values(self) -> None: - """Test entering context with initial values.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({"request_id": "123"}) - - try: - assert adapter.get_value("request_id") == "123" - finally: - adapter.exit_context() - - def test_implements_protocol(self) -> None: - """Test that adapter implements ContextAdapter protocol.""" - adapter = ContextLoggingAdapter() - assert isinstance(adapter, ContextAdapter) - - -class TestContextLoggingAdapterImportError: - """Tests for ContextLoggingAdapter when context-logging is not installed.""" - - def test_import_error_message(self, monkeypatch: pytest.MonkeyPatch) -> None: - """Test that helpful error message is shown when context-logging missing.""" - import sys - - # Remove context_logging from modules to simulate it not being installed - original_modules = dict(sys.modules) - sys.modules["context_logging"] = None # type: ignore[assignment] - - try: - # Need to reimport to trigger the import check - import importlib - - from fastapi_request_context.adapters import context_logging - - importlib.reload(context_logging) - - with pytest.raises(ImportError, match="context-logging is required"): - context_logging.ContextLoggingAdapter() - finally: - sys.modules.update(original_modules) diff --git a/.history/tests/test_adapters_20251128211725.py b/.history/tests/test_adapters_20251128211725.py deleted file mode 100644 index 1d57c18..0000000 --- a/.history/tests/test_adapters_20251128211725.py +++ /dev/null @@ -1,186 +0,0 @@ -"""Tests for context adapters.""" - -import pytest - -from fastapi_request_context.adapters import ( - ContextAdapter, - ContextLoggingAdapter, - ContextVarsAdapter, -) - - -class TestContextVarsAdapter: - """Tests for ContextVarsAdapter.""" - - def test_set_and_get_value(self) -> None: - """Test basic set and get operations.""" - adapter = ContextVarsAdapter() - adapter.enter_context({}) - - try: - adapter.set_value("key1", "value1") - assert adapter.get_value("key1") == "value1" - finally: - adapter.exit_context() - - def test_get_nonexistent_value(self) -> None: - """Test getting a nonexistent key returns None.""" - adapter = ContextVarsAdapter() - adapter.enter_context({}) - - try: - assert adapter.get_value("nonexistent") is None - finally: - adapter.exit_context() - - def test_get_all(self) -> None: - """Test getting all values.""" - adapter = ContextVarsAdapter() - adapter.enter_context({"initial": "value"}) - - try: - adapter.set_value("key1", "value1") - adapter.set_value("key2", "value2") - - all_values = adapter.get_all() - assert all_values == { - "initial": "value", - "key1": "value1", - "key2": "value2", - } - finally: - adapter.exit_context() - - def test_enter_context_with_initial_values(self) -> None: - """Test entering context with initial values.""" - adapter = ContextVarsAdapter() - adapter.enter_context({"request_id": "123", "correlation_id": "456"}) - - try: - assert adapter.get_value("request_id") == "123" - assert adapter.get_value("correlation_id") == "456" - finally: - adapter.exit_context() - - def test_exit_context_clears_values(self) -> None: - """Test that exiting context clears values.""" - adapter = ContextVarsAdapter() - - adapter.enter_context({"key": "value"}) - assert adapter.get_value("key") == "value" - adapter.exit_context() - - # After exit, context is empty - assert adapter.get_value("key") is None - - def test_get_all_returns_copy(self) -> None: - """Test that get_all returns a copy, not the original dict.""" - adapter = ContextVarsAdapter() - adapter.enter_context({"key": "value"}) - - try: - all_values = adapter.get_all() - all_values["new_key"] = "new_value" - - # Original should be unchanged - assert adapter.get_value("new_key") is None - finally: - adapter.exit_context() - - def test_implements_protocol(self) -> None: - """Test that adapter implements ContextAdapter protocol.""" - adapter = ContextVarsAdapter() - assert isinstance(adapter, ContextAdapter) - - def test_set_value_without_enter_context(self) -> None: - """Test that set_value does nothing when context not entered.""" - adapter = ContextVarsAdapter() - # Should not raise, just do nothing - adapter.set_value("key", "value") - # Value should be None since context was never entered - assert adapter.get_value("key") is None - - -class TestContextLoggingAdapter: - """Tests for ContextLoggingAdapter.""" - - def test_set_and_get_value(self) -> None: - """Test basic set and get operations.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({}) - - try: - adapter.set_value("key1", "value1") - assert adapter.get_value("key1") == "value1" - finally: - adapter.exit_context() - - def test_get_nonexistent_value(self) -> None: - """Test getting a nonexistent key returns None.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({}) - - try: - assert adapter.get_value("nonexistent") is None - finally: - adapter.exit_context() - - def test_get_all(self) -> None: - """Test getting all values.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({"initial": "value"}) - - try: - adapter.set_value("key1", "value1") - - all_values = adapter.get_all() - assert "initial" in all_values - assert "key1" in all_values - finally: - adapter.exit_context() - - def test_enter_context_with_initial_values(self) -> None: - """Test entering context with initial values.""" - adapter = ContextLoggingAdapter() - adapter.enter_context({"request_id": "123"}) - - try: - assert adapter.get_value("request_id") == "123" - finally: - adapter.exit_context() - - def test_implements_protocol(self) -> None: - """Test that adapter implements ContextAdapter protocol.""" - adapter = ContextLoggingAdapter() - assert isinstance(adapter, ContextAdapter) - - def test_exit_context_without_enter(self) -> None: - """Test that exit_context does nothing when not entered.""" - adapter = ContextLoggingAdapter() - # Should not raise, just do nothing - adapter.exit_context() - - -class TestContextLoggingAdapterImportError: - """Tests for ContextLoggingAdapter when context-logging is not installed.""" - - def test_import_error_message(self, monkeypatch: pytest.MonkeyPatch) -> None: - """Test that helpful error message is shown when context-logging missing.""" - import sys - - # Remove context_logging from modules to simulate it not being installed - original_modules = dict(sys.modules) - sys.modules["context_logging"] = None # type: ignore[assignment] - - try: - # Need to reimport to trigger the import check - import importlib - - from fastapi_request_context.adapters import context_logging - - importlib.reload(context_logging) - - with pytest.raises(ImportError, match="context-logging is required"): - context_logging.ContextLoggingAdapter() - finally: - sys.modules.update(original_modules) diff --git a/.history/tests/test_context_20251128203140.py b/.history/tests/test_context_20251128203140.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/tests/test_context_20251128203141.py b/.history/tests/test_context_20251128203141.py deleted file mode 100644 index 2b379e2..0000000 --- a/.history/tests/test_context_20251128203141.py +++ /dev/null @@ -1,182 +0,0 @@ -"""Tests for context functions.""" - -from enum import StrEnum -from typing import Any - -import pytest -from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient - -from fastapi_request_context import ( - RequestContextMiddleware, - StandardContextField, - get_context, - get_full_context, - set_context, -) - - -class CustomField(StrEnum): - """Custom context field for testing.""" - - USER_ID = "user_id" - ORG_ID = "org_id" - - -@pytest.fixture -def app_with_custom_fields() -> FastAPI: - """Create app that sets and gets custom fields.""" - app = FastAPI() - - @app.get("/set-and-get") - async def set_and_get() -> dict[str, Any]: - # Set custom fields - set_context(CustomField.USER_ID, 123) - set_context(CustomField.ORG_ID, "org-456") - set_context("string_key", "string_value") - - # Get them back - return { - "user_id": get_context(CustomField.USER_ID), - "org_id": get_context(CustomField.ORG_ID), - "string_key": get_context("string_key"), - "request_id": get_context(StandardContextField.REQUEST_ID), - } - - @app.get("/full-context") - async def full_context() -> dict[str, Any]: - set_context(CustomField.USER_ID, 999) - return get_full_context() - - return app - - -@pytest.fixture -async def custom_client(app_with_custom_fields: FastAPI) -> AsyncClient: - """Create test client for custom fields app.""" - wrapped = RequestContextMiddleware(app_with_custom_fields) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - yield client - - -async def test_set_and_get_enum_fields(custom_client: AsyncClient) -> None: - """Test setting and getting context using StrEnum fields.""" - response = await custom_client.get("/set-and-get") - assert response.status_code == 200 - - data = response.json() - assert data["user_id"] == 123 - assert data["org_id"] == "org-456" - - -async def test_set_and_get_string_keys(custom_client: AsyncClient) -> None: - """Test setting and getting context using string keys.""" - response = await custom_client.get("/set-and-get") - assert response.status_code == 200 - - data = response.json() - assert data["string_key"] == "string_value" - - -async def test_standard_fields_available(custom_client: AsyncClient) -> None: - """Test that standard fields are always available.""" - response = await custom_client.get("/set-and-get") - assert response.status_code == 200 - - data = response.json() - assert data["request_id"] is not None - - -async def test_full_context_includes_all(custom_client: AsyncClient) -> None: - """Test that get_full_context includes all fields.""" - response = await custom_client.get("/full-context") - assert response.status_code == 200 - - data = response.json() - assert "request_id" in data - assert "correlation_id" in data - assert "user_id" in data - assert data["user_id"] == 999 - - -async def test_get_nonexistent_key(custom_client: AsyncClient) -> None: - """Test that getting a nonexistent key returns None.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - return {"value": get_context("nonexistent")} - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - assert response.status_code == 200 - assert response.json()["value"] is None - - -async def test_context_isolation_between_requests() -> None: - """Test that context is isolated between concurrent requests.""" - app = FastAPI() - import asyncio - - @app.get("/slow/{value}") - async def slow_route(value: str) -> dict[str, Any]: - set_context("my_value", value) - await asyncio.sleep(0.01) # Small delay - return { - "set_value": value, - "got_value": get_context("my_value"), - } - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - # Run two requests concurrently - responses = await asyncio.gather( - client.get("/slow/first"), - client.get("/slow/second"), - ) - - # Each request should see its own value - for response in responses: - data = response.json() - assert data["set_value"] == data["got_value"] - - -async def test_set_various_value_types(custom_client: AsyncClient) -> None: - """Test setting various types of values.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - set_context("int_val", 42) - set_context("float_val", 3.14) - set_context("bool_val", True) - set_context("list_val", [1, 2, 3]) - set_context("dict_val", {"key": "value"}) - set_context("none_val", None) - - return { - "int_val": get_context("int_val"), - "float_val": get_context("float_val"), - "bool_val": get_context("bool_val"), - "list_val": get_context("list_val"), - "dict_val": get_context("dict_val"), - "none_val": get_context("none_val"), - } - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - data = response.json() - assert data["int_val"] == 42 - assert data["float_val"] == 3.14 - assert data["bool_val"] is True - assert data["list_val"] == [1, 2, 3] - assert data["dict_val"] == {"key": "value"} - assert data["none_val"] is None diff --git a/.history/tests/test_context_20251128203532.py b/.history/tests/test_context_20251128203532.py deleted file mode 100644 index 3a853a5..0000000 --- a/.history/tests/test_context_20251128203532.py +++ /dev/null @@ -1,190 +0,0 @@ -"""Tests for context functions.""" - -import sys - -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from enum import Enum - - class StrEnum(str, Enum): - pass -from typing import Any - -import pytest -from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient - -from fastapi_request_context import ( - RequestContextMiddleware, - StandardContextField, - get_context, - get_full_context, - set_context, -) - - -class CustomField(StrEnum): - """Custom context field for testing.""" - - USER_ID = "user_id" - ORG_ID = "org_id" - - -@pytest.fixture -def app_with_custom_fields() -> FastAPI: - """Create app that sets and gets custom fields.""" - app = FastAPI() - - @app.get("/set-and-get") - async def set_and_get() -> dict[str, Any]: - # Set custom fields - set_context(CustomField.USER_ID, 123) - set_context(CustomField.ORG_ID, "org-456") - set_context("string_key", "string_value") - - # Get them back - return { - "user_id": get_context(CustomField.USER_ID), - "org_id": get_context(CustomField.ORG_ID), - "string_key": get_context("string_key"), - "request_id": get_context(StandardContextField.REQUEST_ID), - } - - @app.get("/full-context") - async def full_context() -> dict[str, Any]: - set_context(CustomField.USER_ID, 999) - return get_full_context() - - return app - - -@pytest.fixture -async def custom_client(app_with_custom_fields: FastAPI) -> AsyncClient: - """Create test client for custom fields app.""" - wrapped = RequestContextMiddleware(app_with_custom_fields) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - yield client - - -async def test_set_and_get_enum_fields(custom_client: AsyncClient) -> None: - """Test setting and getting context using StrEnum fields.""" - response = await custom_client.get("/set-and-get") - assert response.status_code == 200 - - data = response.json() - assert data["user_id"] == 123 - assert data["org_id"] == "org-456" - - -async def test_set_and_get_string_keys(custom_client: AsyncClient) -> None: - """Test setting and getting context using string keys.""" - response = await custom_client.get("/set-and-get") - assert response.status_code == 200 - - data = response.json() - assert data["string_key"] == "string_value" - - -async def test_standard_fields_available(custom_client: AsyncClient) -> None: - """Test that standard fields are always available.""" - response = await custom_client.get("/set-and-get") - assert response.status_code == 200 - - data = response.json() - assert data["request_id"] is not None - - -async def test_full_context_includes_all(custom_client: AsyncClient) -> None: - """Test that get_full_context includes all fields.""" - response = await custom_client.get("/full-context") - assert response.status_code == 200 - - data = response.json() - assert "request_id" in data - assert "correlation_id" in data - assert "user_id" in data - assert data["user_id"] == 999 - - -async def test_get_nonexistent_key(custom_client: AsyncClient) -> None: - """Test that getting a nonexistent key returns None.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - return {"value": get_context("nonexistent")} - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - assert response.status_code == 200 - assert response.json()["value"] is None - - -async def test_context_isolation_between_requests() -> None: - """Test that context is isolated between concurrent requests.""" - app = FastAPI() - import asyncio - - @app.get("/slow/{value}") - async def slow_route(value: str) -> dict[str, Any]: - set_context("my_value", value) - await asyncio.sleep(0.01) # Small delay - return { - "set_value": value, - "got_value": get_context("my_value"), - } - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - # Run two requests concurrently - responses = await asyncio.gather( - client.get("/slow/first"), - client.get("/slow/second"), - ) - - # Each request should see its own value - for response in responses: - data = response.json() - assert data["set_value"] == data["got_value"] - - -async def test_set_various_value_types(custom_client: AsyncClient) -> None: - """Test setting various types of values.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - set_context("int_val", 42) - set_context("float_val", 3.14) - set_context("bool_val", True) - set_context("list_val", [1, 2, 3]) - set_context("dict_val", {"key": "value"}) - set_context("none_val", None) - - return { - "int_val": get_context("int_val"), - "float_val": get_context("float_val"), - "bool_val": get_context("bool_val"), - "list_val": get_context("list_val"), - "dict_val": get_context("dict_val"), - "none_val": get_context("none_val"), - } - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - data = response.json() - assert data["int_val"] == 42 - assert data["float_val"] == 3.14 - assert data["bool_val"] is True - assert data["list_val"] == [1, 2, 3] - assert data["dict_val"] == {"key": "value"} - assert data["none_val"] is None diff --git a/.history/tests/test_context_20251128203541.py b/.history/tests/test_context_20251128203541.py deleted file mode 100644 index ad99a98..0000000 --- a/.history/tests/test_context_20251128203541.py +++ /dev/null @@ -1,191 +0,0 @@ -"""Tests for context functions.""" - -import sys - -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from enum import Enum - - class StrEnum(str, Enum): - pass - -from typing import Any - -import pytest -from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient - -from fastapi_request_context import ( - RequestContextMiddleware, - StandardContextField, - get_context, - get_full_context, - set_context, -) - - -class CustomField(StrEnum): - """Custom context field for testing.""" - - USER_ID = "user_id" - ORG_ID = "org_id" - - -@pytest.fixture -def app_with_custom_fields() -> FastAPI: - """Create app that sets and gets custom fields.""" - app = FastAPI() - - @app.get("/set-and-get") - async def set_and_get() -> dict[str, Any]: - # Set custom fields - set_context(CustomField.USER_ID, 123) - set_context(CustomField.ORG_ID, "org-456") - set_context("string_key", "string_value") - - # Get them back - return { - "user_id": get_context(CustomField.USER_ID), - "org_id": get_context(CustomField.ORG_ID), - "string_key": get_context("string_key"), - "request_id": get_context(StandardContextField.REQUEST_ID), - } - - @app.get("/full-context") - async def full_context() -> dict[str, Any]: - set_context(CustomField.USER_ID, 999) - return get_full_context() - - return app - - -@pytest.fixture -async def custom_client(app_with_custom_fields: FastAPI) -> AsyncClient: - """Create test client for custom fields app.""" - wrapped = RequestContextMiddleware(app_with_custom_fields) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - yield client - - -async def test_set_and_get_enum_fields(custom_client: AsyncClient) -> None: - """Test setting and getting context using StrEnum fields.""" - response = await custom_client.get("/set-and-get") - assert response.status_code == 200 - - data = response.json() - assert data["user_id"] == 123 - assert data["org_id"] == "org-456" - - -async def test_set_and_get_string_keys(custom_client: AsyncClient) -> None: - """Test setting and getting context using string keys.""" - response = await custom_client.get("/set-and-get") - assert response.status_code == 200 - - data = response.json() - assert data["string_key"] == "string_value" - - -async def test_standard_fields_available(custom_client: AsyncClient) -> None: - """Test that standard fields are always available.""" - response = await custom_client.get("/set-and-get") - assert response.status_code == 200 - - data = response.json() - assert data["request_id"] is not None - - -async def test_full_context_includes_all(custom_client: AsyncClient) -> None: - """Test that get_full_context includes all fields.""" - response = await custom_client.get("/full-context") - assert response.status_code == 200 - - data = response.json() - assert "request_id" in data - assert "correlation_id" in data - assert "user_id" in data - assert data["user_id"] == 999 - - -async def test_get_nonexistent_key(custom_client: AsyncClient) -> None: - """Test that getting a nonexistent key returns None.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - return {"value": get_context("nonexistent")} - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - assert response.status_code == 200 - assert response.json()["value"] is None - - -async def test_context_isolation_between_requests() -> None: - """Test that context is isolated between concurrent requests.""" - app = FastAPI() - import asyncio - - @app.get("/slow/{value}") - async def slow_route(value: str) -> dict[str, Any]: - set_context("my_value", value) - await asyncio.sleep(0.01) # Small delay - return { - "set_value": value, - "got_value": get_context("my_value"), - } - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - # Run two requests concurrently - responses = await asyncio.gather( - client.get("/slow/first"), - client.get("/slow/second"), - ) - - # Each request should see its own value - for response in responses: - data = response.json() - assert data["set_value"] == data["got_value"] - - -async def test_set_various_value_types(custom_client: AsyncClient) -> None: - """Test setting various types of values.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - set_context("int_val", 42) - set_context("float_val", 3.14) - set_context("bool_val", True) - set_context("list_val", [1, 2, 3]) - set_context("dict_val", {"key": "value"}) - set_context("none_val", None) - - return { - "int_val": get_context("int_val"), - "float_val": get_context("float_val"), - "bool_val": get_context("bool_val"), - "list_val": get_context("list_val"), - "dict_val": get_context("dict_val"), - "none_val": get_context("none_val"), - } - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - data = response.json() - assert data["int_val"] == 42 - assert data["float_val"] == 3.14 - assert data["bool_val"] is True - assert data["list_val"] == [1, 2, 3] - assert data["dict_val"] == {"key": "value"} - assert data["none_val"] is None diff --git a/.history/tests/test_context_20251128211433.py b/.history/tests/test_context_20251128211433.py deleted file mode 100644 index 4d5568c..0000000 --- a/.history/tests/test_context_20251128211433.py +++ /dev/null @@ -1,193 +0,0 @@ -"""Tests for context functions.""" - -import sys - -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from enum import Enum - - class StrEnum(str, Enum): - pass - - -from typing import Any - -import pytest -from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient - -from fastapi_request_context import ( - RequestContextMiddleware, - StandardContextField, - get_adapter, - get_context, - get_full_context, - set_context, -) - - -class CustomField(StrEnum): - """Custom context field for testing.""" - - USER_ID = "user_id" - ORG_ID = "org_id" - - -@pytest.fixture -def app_with_custom_fields() -> FastAPI: - """Create app that sets and gets custom fields.""" - app = FastAPI() - - @app.get("/set-and-get") - async def set_and_get() -> dict[str, Any]: - # Set custom fields - set_context(CustomField.USER_ID, 123) - set_context(CustomField.ORG_ID, "org-456") - set_context("string_key", "string_value") - - # Get them back - return { - "user_id": get_context(CustomField.USER_ID), - "org_id": get_context(CustomField.ORG_ID), - "string_key": get_context("string_key"), - "request_id": get_context(StandardContextField.REQUEST_ID), - } - - @app.get("/full-context") - async def full_context() -> dict[str, Any]: - set_context(CustomField.USER_ID, 999) - return get_full_context() - - return app - - -@pytest.fixture -async def custom_client(app_with_custom_fields: FastAPI) -> AsyncClient: - """Create test client for custom fields app.""" - wrapped = RequestContextMiddleware(app_with_custom_fields) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - yield client - - -async def test_set_and_get_enum_fields(custom_client: AsyncClient) -> None: - """Test setting and getting context using StrEnum fields.""" - response = await custom_client.get("/set-and-get") - assert response.status_code == 200 - - data = response.json() - assert data["user_id"] == 123 - assert data["org_id"] == "org-456" - - -async def test_set_and_get_string_keys(custom_client: AsyncClient) -> None: - """Test setting and getting context using string keys.""" - response = await custom_client.get("/set-and-get") - assert response.status_code == 200 - - data = response.json() - assert data["string_key"] == "string_value" - - -async def test_standard_fields_available(custom_client: AsyncClient) -> None: - """Test that standard fields are always available.""" - response = await custom_client.get("/set-and-get") - assert response.status_code == 200 - - data = response.json() - assert data["request_id"] is not None - - -async def test_full_context_includes_all(custom_client: AsyncClient) -> None: - """Test that get_full_context includes all fields.""" - response = await custom_client.get("/full-context") - assert response.status_code == 200 - - data = response.json() - assert "request_id" in data - assert "correlation_id" in data - assert "user_id" in data - assert data["user_id"] == 999 - - -async def test_get_nonexistent_key(custom_client: AsyncClient) -> None: - """Test that getting a nonexistent key returns None.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - return {"value": get_context("nonexistent")} - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - assert response.status_code == 200 - assert response.json()["value"] is None - - -async def test_context_isolation_between_requests() -> None: - """Test that context is isolated between concurrent requests.""" - app = FastAPI() - import asyncio - - @app.get("/slow/{value}") - async def slow_route(value: str) -> dict[str, Any]: - set_context("my_value", value) - await asyncio.sleep(0.01) # Small delay - return { - "set_value": value, - "got_value": get_context("my_value"), - } - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - # Run two requests concurrently - responses = await asyncio.gather( - client.get("/slow/first"), - client.get("/slow/second"), - ) - - # Each request should see its own value - for response in responses: - data = response.json() - assert data["set_value"] == data["got_value"] - - -async def test_set_various_value_types(custom_client: AsyncClient) -> None: - """Test setting various types of values.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - set_context("int_val", 42) - set_context("float_val", 3.14) - set_context("bool_val", True) - set_context("list_val", [1, 2, 3]) - set_context("dict_val", {"key": "value"}) - set_context("none_val", None) - - return { - "int_val": get_context("int_val"), - "float_val": get_context("float_val"), - "bool_val": get_context("bool_val"), - "list_val": get_context("list_val"), - "dict_val": get_context("dict_val"), - "none_val": get_context("none_val"), - } - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - data = response.json() - assert data["int_val"] == 42 - assert data["float_val"] == 3.14 - assert data["bool_val"] is True - assert data["list_val"] == [1, 2, 3] - assert data["dict_val"] == {"key": "value"} - assert data["none_val"] is None diff --git a/.history/tests/test_context_20251128211447.py b/.history/tests/test_context_20251128211447.py deleted file mode 100644 index fa9e23e..0000000 --- a/.history/tests/test_context_20251128211447.py +++ /dev/null @@ -1,201 +0,0 @@ -"""Tests for context functions.""" - -import sys - -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from enum import Enum - - class StrEnum(str, Enum): - pass - - -from typing import Any - -import pytest -from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient - -from fastapi_request_context import ( - RequestContextMiddleware, - StandardContextField, - get_adapter, - get_context, - get_full_context, - set_context, -) - - -class CustomField(StrEnum): - """Custom context field for testing.""" - - USER_ID = "user_id" - ORG_ID = "org_id" - - -@pytest.fixture -def app_with_custom_fields() -> FastAPI: - """Create app that sets and gets custom fields.""" - app = FastAPI() - - @app.get("/set-and-get") - async def set_and_get() -> dict[str, Any]: - # Set custom fields - set_context(CustomField.USER_ID, 123) - set_context(CustomField.ORG_ID, "org-456") - set_context("string_key", "string_value") - - # Get them back - return { - "user_id": get_context(CustomField.USER_ID), - "org_id": get_context(CustomField.ORG_ID), - "string_key": get_context("string_key"), - "request_id": get_context(StandardContextField.REQUEST_ID), - } - - @app.get("/full-context") - async def full_context() -> dict[str, Any]: - set_context(CustomField.USER_ID, 999) - return get_full_context() - - return app - - -@pytest.fixture -async def custom_client(app_with_custom_fields: FastAPI) -> AsyncClient: - """Create test client for custom fields app.""" - wrapped = RequestContextMiddleware(app_with_custom_fields) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - yield client - - -async def test_set_and_get_enum_fields(custom_client: AsyncClient) -> None: - """Test setting and getting context using StrEnum fields.""" - response = await custom_client.get("/set-and-get") - assert response.status_code == 200 - - data = response.json() - assert data["user_id"] == 123 - assert data["org_id"] == "org-456" - - -async def test_set_and_get_string_keys(custom_client: AsyncClient) -> None: - """Test setting and getting context using string keys.""" - response = await custom_client.get("/set-and-get") - assert response.status_code == 200 - - data = response.json() - assert data["string_key"] == "string_value" - - -async def test_standard_fields_available(custom_client: AsyncClient) -> None: - """Test that standard fields are always available.""" - response = await custom_client.get("/set-and-get") - assert response.status_code == 200 - - data = response.json() - assert data["request_id"] is not None - - -async def test_full_context_includes_all(custom_client: AsyncClient) -> None: - """Test that get_full_context includes all fields.""" - response = await custom_client.get("/full-context") - assert response.status_code == 200 - - data = response.json() - assert "request_id" in data - assert "correlation_id" in data - assert "user_id" in data - assert data["user_id"] == 999 - - -async def test_get_nonexistent_key(custom_client: AsyncClient) -> None: - """Test that getting a nonexistent key returns None.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - return {"value": get_context("nonexistent")} - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - assert response.status_code == 200 - assert response.json()["value"] is None - - -async def test_context_isolation_between_requests() -> None: - """Test that context is isolated between concurrent requests.""" - app = FastAPI() - import asyncio - - @app.get("/slow/{value}") - async def slow_route(value: str) -> dict[str, Any]: - set_context("my_value", value) - await asyncio.sleep(0.01) # Small delay - return { - "set_value": value, - "got_value": get_context("my_value"), - } - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - # Run two requests concurrently - responses = await asyncio.gather( - client.get("/slow/first"), - client.get("/slow/second"), - ) - - # Each request should see its own value - for response in responses: - data = response.json() - assert data["set_value"] == data["got_value"] - - -async def test_set_various_value_types(custom_client: AsyncClient) -> None: - """Test setting various types of values.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - set_context("int_val", 42) - set_context("float_val", 3.14) - set_context("bool_val", True) - set_context("list_val", [1, 2, 3]) - set_context("dict_val", {"key": "value"}) - set_context("none_val", None) - - return { - "int_val": get_context("int_val"), - "float_val": get_context("float_val"), - "bool_val": get_context("bool_val"), - "list_val": get_context("list_val"), - "dict_val": get_context("dict_val"), - "none_val": get_context("none_val"), - } - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - data = response.json() - assert data["int_val"] == 42 - assert data["float_val"] == 3.14 - assert data["bool_val"] is True - assert data["list_val"] == [1, 2, 3] - assert data["dict_val"] == {"key": "value"} - assert data["none_val"] is None - - -def test_get_adapter() -> None: - """Test that get_adapter returns the current adapter.""" - from fastapi_request_context.adapters import ContextVarsAdapter - - adapter = get_adapter() - assert isinstance(adapter, ContextVarsAdapter) diff --git a/.history/tests/test_context_20251128211949.py b/.history/tests/test_context_20251128211949.py deleted file mode 100644 index f7fa3c5..0000000 --- a/.history/tests/test_context_20251128211949.py +++ /dev/null @@ -1,191 +0,0 @@ -"""Tests for context functions.""" - -from enum import StrEnum -from typing import Any - -import pytest -from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient - -from fastapi_request_context import ( - RequestContextMiddleware, - StandardContextField, - get_adapter, - get_context, - get_full_context, - set_context, -) - - -class CustomField(StrEnum): - """Custom context field for testing.""" - - USER_ID = "user_id" - ORG_ID = "org_id" - - -@pytest.fixture -def app_with_custom_fields() -> FastAPI: - """Create app that sets and gets custom fields.""" - app = FastAPI() - - @app.get("/set-and-get") - async def set_and_get() -> dict[str, Any]: - # Set custom fields - set_context(CustomField.USER_ID, 123) - set_context(CustomField.ORG_ID, "org-456") - set_context("string_key", "string_value") - - # Get them back - return { - "user_id": get_context(CustomField.USER_ID), - "org_id": get_context(CustomField.ORG_ID), - "string_key": get_context("string_key"), - "request_id": get_context(StandardContextField.REQUEST_ID), - } - - @app.get("/full-context") - async def full_context() -> dict[str, Any]: - set_context(CustomField.USER_ID, 999) - return get_full_context() - - return app - - -@pytest.fixture -async def custom_client(app_with_custom_fields: FastAPI) -> AsyncClient: - """Create test client for custom fields app.""" - wrapped = RequestContextMiddleware(app_with_custom_fields) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - yield client - - -async def test_set_and_get_enum_fields(custom_client: AsyncClient) -> None: - """Test setting and getting context using StrEnum fields.""" - response = await custom_client.get("/set-and-get") - assert response.status_code == 200 - - data = response.json() - assert data["user_id"] == 123 - assert data["org_id"] == "org-456" - - -async def test_set_and_get_string_keys(custom_client: AsyncClient) -> None: - """Test setting and getting context using string keys.""" - response = await custom_client.get("/set-and-get") - assert response.status_code == 200 - - data = response.json() - assert data["string_key"] == "string_value" - - -async def test_standard_fields_available(custom_client: AsyncClient) -> None: - """Test that standard fields are always available.""" - response = await custom_client.get("/set-and-get") - assert response.status_code == 200 - - data = response.json() - assert data["request_id"] is not None - - -async def test_full_context_includes_all(custom_client: AsyncClient) -> None: - """Test that get_full_context includes all fields.""" - response = await custom_client.get("/full-context") - assert response.status_code == 200 - - data = response.json() - assert "request_id" in data - assert "correlation_id" in data - assert "user_id" in data - assert data["user_id"] == 999 - - -async def test_get_nonexistent_key(custom_client: AsyncClient) -> None: - """Test that getting a nonexistent key returns None.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - return {"value": get_context("nonexistent")} - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - assert response.status_code == 200 - assert response.json()["value"] is None - - -async def test_context_isolation_between_requests() -> None: - """Test that context is isolated between concurrent requests.""" - app = FastAPI() - import asyncio - - @app.get("/slow/{value}") - async def slow_route(value: str) -> dict[str, Any]: - set_context("my_value", value) - await asyncio.sleep(0.01) # Small delay - return { - "set_value": value, - "got_value": get_context("my_value"), - } - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - # Run two requests concurrently - responses = await asyncio.gather( - client.get("/slow/first"), - client.get("/slow/second"), - ) - - # Each request should see its own value - for response in responses: - data = response.json() - assert data["set_value"] == data["got_value"] - - -async def test_set_various_value_types(custom_client: AsyncClient) -> None: - """Test setting various types of values.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - set_context("int_val", 42) - set_context("float_val", 3.14) - set_context("bool_val", True) - set_context("list_val", [1, 2, 3]) - set_context("dict_val", {"key": "value"}) - set_context("none_val", None) - - return { - "int_val": get_context("int_val"), - "float_val": get_context("float_val"), - "bool_val": get_context("bool_val"), - "list_val": get_context("list_val"), - "dict_val": get_context("dict_val"), - "none_val": get_context("none_val"), - } - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - data = response.json() - assert data["int_val"] == 42 - assert data["float_val"] == 3.14 - assert data["bool_val"] is True - assert data["list_val"] == [1, 2, 3] - assert data["dict_val"] == {"key": "value"} - assert data["none_val"] is None - - -def test_get_adapter() -> None: - """Test that get_adapter returns the current adapter.""" - from fastapi_request_context.adapters import ContextVarsAdapter - - adapter = get_adapter() - assert isinstance(adapter, ContextVarsAdapter) diff --git a/.history/tests/test_formatters_20251128203233.py b/.history/tests/test_formatters_20251128203233.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/tests/test_formatters_20251128203234.py b/.history/tests/test_formatters_20251128203234.py deleted file mode 100644 index 658c662..0000000 --- a/.history/tests/test_formatters_20251128203234.py +++ /dev/null @@ -1,327 +0,0 @@ -"""Tests for logging formatters.""" - -import json -import logging -from typing import Any - -import pytest -from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient - -from fastapi_request_context import ( - RequestContextMiddleware, - StandardContextField, - set_context, -) -from fastapi_request_context.formatters import JsonContextFormatter, LocalContextFormatter - - -class TestJsonContextFormatter: - """Tests for JsonContextFormatter.""" - - def test_basic_formatting(self) -> None: - """Test basic JSON log formatting.""" - formatter = JsonContextFormatter() - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert data["message"] == "Test message" - assert data["level"] == "INFO" - assert data["logger"] == "test" - assert "timestamp" in data - - def test_includes_context(self) -> None: - """Test that context is included in JSON output.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123", "user_id": 456}) - - try: - formatter = JsonContextFormatter() - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert data["request_id"] == "test-123" - assert data["user_id"] == 456 - finally: - adapter.exit_context() - - def test_context_key_nesting(self) -> None: - """Test nesting context under a specific key.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123"}) - - try: - formatter = JsonContextFormatter(context_key="context") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "context" in data - assert data["context"]["request_id"] == "test-123" - finally: - adapter.exit_context() - - def test_exclude_standard_fields(self) -> None: - """Test excluding standard fields from output.""" - formatter = JsonContextFormatter(include_standard_fields=False) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "message" in data - assert "level" not in data - assert "logger" not in data - assert "timestamp" not in data - - def test_exception_formatting(self) -> None: - """Test that exceptions are included in output.""" - formatter = JsonContextFormatter() - - try: - raise ValueError("Test error") # noqa: TRY301 - except ValueError: - import sys - - exc_info = sys.exc_info() - - record = logging.LogRecord( - name="test", - level=logging.ERROR, - pathname="test.py", - lineno=1, - msg="Error occurred", - args=(), - exc_info=exc_info, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "exception" in data - assert "ValueError" in data["exception"] - - -class TestLocalContextFormatter: - """Tests for LocalContextFormatter.""" - - def test_basic_formatting(self) -> None: - """Test basic local log formatting.""" - formatter = LocalContextFormatter(fmt="%(levelname)s %(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "INFO" in output - assert "Test message" in output - - def test_includes_context(self) -> None: - """Test that context is included in output.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123", "user_id": 456}) - - try: - formatter = LocalContextFormatter(fmt="%(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=test-123" in output - assert "user_id=456" in output - finally: - adapter.exit_context() - - def test_shorten_fields(self) -> None: - """Test shortening of specified fields.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "12345678901234567890"}) - - try: - formatter = LocalContextFormatter( - fmt="%(context)s %(message)s", - shorten_fields={"request_id"}, - shorten_length=8, - ) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=12345678" in output - assert "12345678901234567890" not in output - finally: - adapter.exit_context() - - def test_hidden_fields(self) -> None: - """Test hiding of specified fields.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({ - "request_id": "shown", - "correlation_id": "hidden", - }) - - try: - formatter = LocalContextFormatter( - fmt="%(context)s %(message)s", - hidden_fields={"correlation_id"}, - ) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=shown" in output - assert "correlation_id" not in output - finally: - adapter.exit_context() - - def test_empty_context(self) -> None: - """Test formatting when context is empty.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({}) - - try: - formatter = LocalContextFormatter(fmt="%(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - # Should not have empty brackets - assert "[]" not in output - assert "Test message" in output - finally: - adapter.exit_context() - - def test_default_format(self) -> None: - """Test default format string when none provided.""" - formatter = LocalContextFormatter() - assert "%(context)s" in formatter._fmt # type: ignore[union-attr] - - -async def test_formatter_integration_with_middleware() -> None: - """Test formatters work correctly with middleware.""" - app = FastAPI() - log_records: list[str] = [] - - class CaptureHandler(logging.Handler): - def emit(self, record: logging.LogRecord) -> None: - log_records.append(self.format(record)) - - handler = CaptureHandler() - handler.setFormatter(JsonContextFormatter()) - logger = logging.getLogger("test_integration") - logger.addHandler(handler) - logger.setLevel(logging.INFO) - - @app.get("/") - async def root() -> dict[str, Any]: - set_context("user_id", 123) - logger.info("Processing request") - return {"status": "ok"} - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - await client.get("/") - - assert len(log_records) == 1 - data = json.loads(log_records[0]) - assert data["message"] == "Processing request" - assert data["user_id"] == 123 - assert "request_id" in data - - logger.removeHandler(handler) diff --git a/.history/tests/test_formatters_20251128203848.py b/.history/tests/test_formatters_20251128203848.py deleted file mode 100644 index 7f92a83..0000000 --- a/.history/tests/test_formatters_20251128203848.py +++ /dev/null @@ -1,325 +0,0 @@ -"""Tests for logging formatters.""" - -import json -import logging -from typing import Any - -from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient - -from fastapi_request_context import ( - RequestContextMiddleware, - set_context, -) -from fastapi_request_context.formatters import JsonContextFormatter, LocalContextFormatter - - -class TestJsonContextFormatter: - """Tests for JsonContextFormatter.""" - - def test_basic_formatting(self) -> None: - """Test basic JSON log formatting.""" - formatter = JsonContextFormatter() - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert data["message"] == "Test message" - assert data["level"] == "INFO" - assert data["logger"] == "test" - assert "timestamp" in data - - def test_includes_context(self) -> None: - """Test that context is included in JSON output.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123", "user_id": 456}) - - try: - formatter = JsonContextFormatter() - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert data["request_id"] == "test-123" - assert data["user_id"] == 456 - finally: - adapter.exit_context() - - def test_context_key_nesting(self) -> None: - """Test nesting context under a specific key.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123"}) - - try: - formatter = JsonContextFormatter(context_key="context") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "context" in data - assert data["context"]["request_id"] == "test-123" - finally: - adapter.exit_context() - - def test_exclude_standard_fields(self) -> None: - """Test excluding standard fields from output.""" - formatter = JsonContextFormatter(include_standard_fields=False) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "message" in data - assert "level" not in data - assert "logger" not in data - assert "timestamp" not in data - - def test_exception_formatting(self) -> None: - """Test that exceptions are included in output.""" - formatter = JsonContextFormatter() - - try: - raise ValueError("Test error") # noqa: TRY301 - except ValueError: - import sys - - exc_info = sys.exc_info() - - record = logging.LogRecord( - name="test", - level=logging.ERROR, - pathname="test.py", - lineno=1, - msg="Error occurred", - args=(), - exc_info=exc_info, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "exception" in data - assert "ValueError" in data["exception"] - - -class TestLocalContextFormatter: - """Tests for LocalContextFormatter.""" - - def test_basic_formatting(self) -> None: - """Test basic local log formatting.""" - formatter = LocalContextFormatter(fmt="%(levelname)s %(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "INFO" in output - assert "Test message" in output - - def test_includes_context(self) -> None: - """Test that context is included in output.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123", "user_id": 456}) - - try: - formatter = LocalContextFormatter(fmt="%(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=test-123" in output - assert "user_id=456" in output - finally: - adapter.exit_context() - - def test_shorten_fields(self) -> None: - """Test shortening of specified fields.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "12345678901234567890"}) - - try: - formatter = LocalContextFormatter( - fmt="%(context)s %(message)s", - shorten_fields={"request_id"}, - shorten_length=8, - ) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=12345678" in output - assert "12345678901234567890" not in output - finally: - adapter.exit_context() - - def test_hidden_fields(self) -> None: - """Test hiding of specified fields.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({ - "request_id": "shown", - "correlation_id": "hidden", - }) - - try: - formatter = LocalContextFormatter( - fmt="%(context)s %(message)s", - hidden_fields={"correlation_id"}, - ) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=shown" in output - assert "correlation_id" not in output - finally: - adapter.exit_context() - - def test_empty_context(self) -> None: - """Test formatting when context is empty.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({}) - - try: - formatter = LocalContextFormatter(fmt="%(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - # Should not have empty brackets - assert "[]" not in output - assert "Test message" in output - finally: - adapter.exit_context() - - def test_default_format(self) -> None: - """Test default format string when none provided.""" - formatter = LocalContextFormatter() - assert "%(context)s" in formatter._fmt # type: ignore[union-attr] - - -async def test_formatter_integration_with_middleware() -> None: - """Test formatters work correctly with middleware.""" - app = FastAPI() - log_records: list[str] = [] - - class CaptureHandler(logging.Handler): - def emit(self, record: logging.LogRecord) -> None: - log_records.append(self.format(record)) - - handler = CaptureHandler() - handler.setFormatter(JsonContextFormatter()) - logger = logging.getLogger("test_integration") - logger.addHandler(handler) - logger.setLevel(logging.INFO) - - @app.get("/") - async def root() -> dict[str, Any]: - set_context("user_id", 123) - logger.info("Processing request") - return {"status": "ok"} - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - await client.get("/") - - assert len(log_records) == 1 - data = json.loads(log_records[0]) - assert data["message"] == "Processing request" - assert data["user_id"] == 123 - assert "request_id" in data - - logger.removeHandler(handler) diff --git a/.history/tests/test_formatters_20251128204354.py b/.history/tests/test_formatters_20251128204354.py deleted file mode 100644 index 509c276..0000000 --- a/.history/tests/test_formatters_20251128204354.py +++ /dev/null @@ -1,328 +0,0 @@ -"""Tests for logging formatters.""" - -import json -import logging -from typing import Any - -from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient - -from fastapi_request_context import ( - RequestContextMiddleware, - set_context, -) -from fastapi_request_context.formatters import JsonContextFormatter, LocalContextFormatter - - -class TestJsonContextFormatter: - """Tests for JsonContextFormatter.""" - - def test_basic_formatting(self) -> None: - """Test basic JSON log formatting.""" - formatter = JsonContextFormatter() - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert data["message"] == "Test message" - assert data["level"] == "INFO" - assert data["logger"] == "test" - assert "timestamp" in data - - def test_includes_context(self) -> None: - """Test that context is included in JSON output.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123", "user_id": 456}) - - try: - formatter = JsonContextFormatter() - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert data["request_id"] == "test-123" - assert data["user_id"] == 456 - finally: - adapter.exit_context() - - def test_context_key_nesting(self) -> None: - """Test nesting context under a specific key.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123"}) - - try: - formatter = JsonContextFormatter(context_key="context") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "context" in data - assert data["context"]["request_id"] == "test-123" - finally: - adapter.exit_context() - - def test_exclude_standard_fields(self) -> None: - """Test excluding standard fields from output.""" - formatter = JsonContextFormatter(include_standard_fields=False) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "message" in data - assert "level" not in data - assert "logger" not in data - assert "timestamp" not in data - - def test_exception_formatting(self) -> None: - """Test that exceptions are included in output.""" - formatter = JsonContextFormatter() - - try: - raise ValueError("Test error") # noqa: TRY301 - except ValueError: - import sys - - exc_info = sys.exc_info() - - record = logging.LogRecord( - name="test", - level=logging.ERROR, - pathname="test.py", - lineno=1, - msg="Error occurred", - args=(), - exc_info=exc_info, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "exception" in data - assert "ValueError" in data["exception"] - - -class TestLocalContextFormatter: - """Tests for LocalContextFormatter.""" - - def test_basic_formatting(self) -> None: - """Test basic local log formatting.""" - formatter = LocalContextFormatter(fmt="%(levelname)s %(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "INFO" in output - assert "Test message" in output - - def test_includes_context(self) -> None: - """Test that context is included in output.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123", "user_id": 456}) - - try: - formatter = LocalContextFormatter(fmt="%(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=test-123" in output - assert "user_id=456" in output - finally: - adapter.exit_context() - - def test_shorten_fields(self) -> None: - """Test shortening of specified fields.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "12345678901234567890"}) - - try: - formatter = LocalContextFormatter( - fmt="%(context)s %(message)s", - shorten_fields={"request_id"}, - shorten_length=8, - ) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=12345678" in output - assert "12345678901234567890" not in output - finally: - adapter.exit_context() - - def test_hidden_fields(self) -> None: - """Test hiding of specified fields.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context( - { - "request_id": "shown", - "correlation_id": "hidden", - } - ) - - try: - formatter = LocalContextFormatter( - fmt="%(context)s %(message)s", - hidden_fields={"correlation_id"}, - ) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=shown" in output - assert "correlation_id" not in output - finally: - adapter.exit_context() - - def test_empty_context(self) -> None: - """Test formatting when context is empty.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({}) - - try: - formatter = LocalContextFormatter(fmt="%(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - # Should not have empty brackets - assert "[]" not in output - assert "Test message" in output - finally: - adapter.exit_context() - - def test_default_format(self) -> None: - """Test default format string when none provided.""" - formatter = LocalContextFormatter() - assert formatter._fmt is not None - assert "%(context)s" in formatter._fmt - - -async def test_formatter_integration_with_middleware() -> None: - """Test formatters work correctly with middleware.""" - app = FastAPI() - log_records: list[str] = [] - - class CaptureHandler(logging.Handler): - def emit(self, record: logging.LogRecord) -> None: - log_records.append(self.format(record)) - - handler = CaptureHandler() - handler.setFormatter(JsonContextFormatter()) - logger = logging.getLogger("test_integration") - logger.addHandler(handler) - logger.setLevel(logging.INFO) - - @app.get("/") - async def root() -> dict[str, Any]: - set_context("user_id", 123) - logger.info("Processing request") - return {"status": "ok"} - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - await client.get("/") - - assert len(log_records) == 1 - data = json.loads(log_records[0]) - assert data["message"] == "Processing request" - assert data["user_id"] == 123 - assert "request_id" in data - - logger.removeHandler(handler) diff --git a/.history/tests/test_formatters_20251128211220.py b/.history/tests/test_formatters_20251128211220.py deleted file mode 100644 index f0a8958..0000000 --- a/.history/tests/test_formatters_20251128211220.py +++ /dev/null @@ -1,328 +0,0 @@ -"""Tests for logging formatters.""" - -import json -import logging -from typing import Any - -from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient - -from fastapi_request_context import ( - RequestContextMiddleware, - set_context, -) -from fastapi_request_context.formatters import JsonContextFormatter, SimpleContextFormatter - - -class TestJsonContextFormatter: - """Tests for JsonContextFormatter.""" - - def test_basic_formatting(self) -> None: - """Test basic JSON log formatting.""" - formatter = JsonContextFormatter() - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert data["message"] == "Test message" - assert data["level"] == "INFO" - assert data["logger"] == "test" - assert "timestamp" in data - - def test_includes_context(self) -> None: - """Test that context is included in JSON output.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123", "user_id": 456}) - - try: - formatter = JsonContextFormatter() - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert data["request_id"] == "test-123" - assert data["user_id"] == 456 - finally: - adapter.exit_context() - - def test_context_key_nesting(self) -> None: - """Test nesting context under a specific key.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123"}) - - try: - formatter = JsonContextFormatter(context_key="context") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "context" in data - assert data["context"]["request_id"] == "test-123" - finally: - adapter.exit_context() - - def test_exclude_standard_fields(self) -> None: - """Test excluding standard fields from output.""" - formatter = JsonContextFormatter(include_standard_fields=False) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "message" in data - assert "level" not in data - assert "logger" not in data - assert "timestamp" not in data - - def test_exception_formatting(self) -> None: - """Test that exceptions are included in output.""" - formatter = JsonContextFormatter() - - try: - raise ValueError("Test error") # noqa: TRY301 - except ValueError: - import sys - - exc_info = sys.exc_info() - - record = logging.LogRecord( - name="test", - level=logging.ERROR, - pathname="test.py", - lineno=1, - msg="Error occurred", - args=(), - exc_info=exc_info, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "exception" in data - assert "ValueError" in data["exception"] - - -class TestLocalContextFormatter: - """Tests for LocalContextFormatter.""" - - def test_basic_formatting(self) -> None: - """Test basic local log formatting.""" - formatter = LocalContextFormatter(fmt="%(levelname)s %(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "INFO" in output - assert "Test message" in output - - def test_includes_context(self) -> None: - """Test that context is included in output.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123", "user_id": 456}) - - try: - formatter = LocalContextFormatter(fmt="%(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=test-123" in output - assert "user_id=456" in output - finally: - adapter.exit_context() - - def test_shorten_fields(self) -> None: - """Test shortening of specified fields.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "12345678901234567890"}) - - try: - formatter = LocalContextFormatter( - fmt="%(context)s %(message)s", - shorten_fields={"request_id"}, - shorten_length=8, - ) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=12345678" in output - assert "12345678901234567890" not in output - finally: - adapter.exit_context() - - def test_hidden_fields(self) -> None: - """Test hiding of specified fields.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context( - { - "request_id": "shown", - "correlation_id": "hidden", - }, - ) - - try: - formatter = LocalContextFormatter( - fmt="%(context)s %(message)s", - hidden_fields={"correlation_id"}, - ) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=shown" in output - assert "correlation_id" not in output - finally: - adapter.exit_context() - - def test_empty_context(self) -> None: - """Test formatting when context is empty.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({}) - - try: - formatter = LocalContextFormatter(fmt="%(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - # Should not have empty brackets - assert "[]" not in output - assert "Test message" in output - finally: - adapter.exit_context() - - def test_default_format(self) -> None: - """Test default format string when none provided.""" - formatter = LocalContextFormatter() - assert formatter._fmt is not None - assert "%(context)s" in formatter._fmt - - -async def test_formatter_integration_with_middleware() -> None: - """Test formatters work correctly with middleware.""" - app = FastAPI() - log_records: list[str] = [] - - class CaptureHandler(logging.Handler): - def emit(self, record: logging.LogRecord) -> None: - log_records.append(self.format(record)) - - handler = CaptureHandler() - handler.setFormatter(JsonContextFormatter()) - logger = logging.getLogger("test_integration") - logger.addHandler(handler) - logger.setLevel(logging.INFO) - - @app.get("/") - async def root() -> dict[str, Any]: - set_context("user_id", 123) - logger.info("Processing request") - return {"status": "ok"} - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - await client.get("/") - - assert len(log_records) == 1 - data = json.loads(log_records[0]) - assert data["message"] == "Processing request" - assert data["user_id"] == 123 - assert "request_id" in data - - logger.removeHandler(handler) diff --git a/.history/tests/test_formatters_20251128211227.py b/.history/tests/test_formatters_20251128211227.py deleted file mode 100644 index ab09d55..0000000 --- a/.history/tests/test_formatters_20251128211227.py +++ /dev/null @@ -1,328 +0,0 @@ -"""Tests for logging formatters.""" - -import json -import logging -from typing import Any - -from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient - -from fastapi_request_context import ( - RequestContextMiddleware, - set_context, -) -from fastapi_request_context.formatters import JsonContextFormatter, SimpleContextFormatter - - -class TestJsonContextFormatter: - """Tests for JsonContextFormatter.""" - - def test_basic_formatting(self) -> None: - """Test basic JSON log formatting.""" - formatter = JsonContextFormatter() - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert data["message"] == "Test message" - assert data["level"] == "INFO" - assert data["logger"] == "test" - assert "timestamp" in data - - def test_includes_context(self) -> None: - """Test that context is included in JSON output.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123", "user_id": 456}) - - try: - formatter = JsonContextFormatter() - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert data["request_id"] == "test-123" - assert data["user_id"] == 456 - finally: - adapter.exit_context() - - def test_context_key_nesting(self) -> None: - """Test nesting context under a specific key.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123"}) - - try: - formatter = JsonContextFormatter(context_key="context") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "context" in data - assert data["context"]["request_id"] == "test-123" - finally: - adapter.exit_context() - - def test_exclude_standard_fields(self) -> None: - """Test excluding standard fields from output.""" - formatter = JsonContextFormatter(include_standard_fields=False) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "message" in data - assert "level" not in data - assert "logger" not in data - assert "timestamp" not in data - - def test_exception_formatting(self) -> None: - """Test that exceptions are included in output.""" - formatter = JsonContextFormatter() - - try: - raise ValueError("Test error") # noqa: TRY301 - except ValueError: - import sys - - exc_info = sys.exc_info() - - record = logging.LogRecord( - name="test", - level=logging.ERROR, - pathname="test.py", - lineno=1, - msg="Error occurred", - args=(), - exc_info=exc_info, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "exception" in data - assert "ValueError" in data["exception"] - - -class TestSimpleContextFormatter: - """Tests for SimpleContextFormatter.""" - - def test_basic_formatting(self) -> None: - """Test basic local log formatting.""" - formatter = SimpleContextFormatter(fmt="%(levelname)s %(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "INFO" in output - assert "Test message" in output - - def test_includes_context(self) -> None: - """Test that context is included in output.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123", "user_id": 456}) - - try: - formatter = SimpleContextFormatter(fmt="%(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=test-123" in output - assert "user_id=456" in output - finally: - adapter.exit_context() - - def test_shorten_fields(self) -> None: - """Test shortening of specified fields.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "12345678901234567890"}) - - try: - formatter = SimpleContextFormatter( - fmt="%(context)s %(message)s", - shorten_fields={"request_id"}, - shorten_length=8, - ) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=12345678" in output - assert "12345678901234567890" not in output - finally: - adapter.exit_context() - - def test_hidden_fields(self) -> None: - """Test hiding of specified fields.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context( - { - "request_id": "shown", - "correlation_id": "hidden", - }, - ) - - try: - formatter = SimpleContextFormatter( - fmt="%(context)s %(message)s", - hidden_fields={"correlation_id"}, - ) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=shown" in output - assert "correlation_id" not in output - finally: - adapter.exit_context() - - def test_empty_context(self) -> None: - """Test formatting when context is empty.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({}) - - try: - formatter = SimpleContextFormatter(fmt="%(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - # Should not have empty brackets - assert "[]" not in output - assert "Test message" in output - finally: - adapter.exit_context() - - def test_default_format(self) -> None: - """Test default format string when none provided.""" - formatter = SimpleContextFormatter() - assert formatter._fmt is not None - assert "%(context)s" in formatter._fmt - - -async def test_formatter_integration_with_middleware() -> None: - """Test formatters work correctly with middleware.""" - app = FastAPI() - log_records: list[str] = [] - - class CaptureHandler(logging.Handler): - def emit(self, record: logging.LogRecord) -> None: - log_records.append(self.format(record)) - - handler = CaptureHandler() - handler.setFormatter(JsonContextFormatter()) - logger = logging.getLogger("test_integration") - logger.addHandler(handler) - logger.setLevel(logging.INFO) - - @app.get("/") - async def root() -> dict[str, Any]: - set_context("user_id", 123) - logger.info("Processing request") - return {"status": "ok"} - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - await client.get("/") - - assert len(log_records) == 1 - data = json.loads(log_records[0]) - assert data["message"] == "Processing request" - assert data["user_id"] == 123 - assert "request_id" in data - - logger.removeHandler(handler) diff --git a/.history/tests/test_formatters_20251128211505.py b/.history/tests/test_formatters_20251128211505.py deleted file mode 100644 index cdb443b..0000000 --- a/.history/tests/test_formatters_20251128211505.py +++ /dev/null @@ -1,361 +0,0 @@ -"""Tests for logging formatters.""" - -import json -import logging -from typing import Any - -from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient - -from fastapi_request_context import ( - RequestContextMiddleware, - set_context, -) -from fastapi_request_context.formatters import JsonContextFormatter, SimpleContextFormatter - - -class TestJsonContextFormatter: - """Tests for JsonContextFormatter.""" - - def test_basic_formatting(self) -> None: - """Test basic JSON log formatting.""" - formatter = JsonContextFormatter() - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert data["message"] == "Test message" - assert data["level"] == "INFO" - assert data["logger"] == "test" - assert "timestamp" in data - - def test_includes_context(self) -> None: - """Test that context is included in JSON output.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123", "user_id": 456}) - - try: - formatter = JsonContextFormatter() - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert data["request_id"] == "test-123" - assert data["user_id"] == 456 - finally: - adapter.exit_context() - - def test_context_key_nesting(self) -> None: - """Test nesting context under a specific key.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123"}) - - try: - formatter = JsonContextFormatter(context_key="context") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "context" in data - assert data["context"]["request_id"] == "test-123" - finally: - adapter.exit_context() - - def test_exclude_standard_fields(self) -> None: - """Test excluding standard fields from output.""" - formatter = JsonContextFormatter(include_standard_fields=False) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "message" in data - assert "level" not in data - assert "logger" not in data - assert "timestamp" not in data - - def test_exception_formatting(self) -> None: - """Test that exceptions are included in output.""" - formatter = JsonContextFormatter() - - try: - raise ValueError("Test error") # noqa: TRY301 - except ValueError: - import sys - - exc_info = sys.exc_info() - - record = logging.LogRecord( - name="test", - level=logging.ERROR, - pathname="test.py", - lineno=1, - msg="Error occurred", - args=(), - exc_info=exc_info, - ) - - output = formatter.format(record) - data = json.loads(output) - - assert "exception" in data - assert "ValueError" in data["exception"] - - -class TestSimpleContextFormatter: - """Tests for SimpleContextFormatter.""" - - def test_basic_formatting(self) -> None: - """Test basic local log formatting.""" - formatter = SimpleContextFormatter(fmt="%(levelname)s %(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "INFO" in output - assert "Test message" in output - - def test_includes_context(self) -> None: - """Test that context is included in output.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123", "user_id": 456}) - - try: - formatter = SimpleContextFormatter(fmt="%(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=test-123" in output - assert "user_id=456" in output - finally: - adapter.exit_context() - - def test_shorten_fields(self) -> None: - """Test shortening of specified fields.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "12345678901234567890"}) - - try: - formatter = SimpleContextFormatter( - fmt="%(context)s %(message)s", - shorten_fields={"request_id"}, - shorten_length=8, - ) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=12345678" in output - assert "12345678901234567890" not in output - finally: - adapter.exit_context() - - def test_hidden_fields(self) -> None: - """Test hiding of specified fields.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context( - { - "request_id": "shown", - "correlation_id": "hidden", - }, - ) - - try: - formatter = SimpleContextFormatter( - fmt="%(context)s %(message)s", - hidden_fields={"correlation_id"}, - ) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - assert "request_id=shown" in output - assert "correlation_id" not in output - finally: - adapter.exit_context() - - def test_empty_context(self) -> None: - """Test formatting when context is empty.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({}) - - try: - formatter = SimpleContextFormatter(fmt="%(context)s %(message)s") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - # Should not have empty brackets - assert "[]" not in output - assert "Test message" in output - finally: - adapter.exit_context() - - def test_all_fields_hidden(self) -> None: - """Test formatting when all fields are hidden.""" - from fastapi_request_context.adapters import ContextVarsAdapter - from fastapi_request_context.context import set_adapter - - adapter = ContextVarsAdapter() - set_adapter(adapter) - adapter.enter_context({"request_id": "test-123", "user_id": "456"}) - - try: - formatter = SimpleContextFormatter( - fmt="%(context)s %(message)s", - hidden_fields={"request_id", "user_id"}, - ) - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname="test.py", - lineno=1, - msg="Test message", - args=(), - exc_info=None, - ) - - output = formatter.format(record) - # Should return empty string for context when all hidden - assert "[]" not in output - assert "request_id" not in output - assert "user_id" not in output - assert "Test message" in output - finally: - adapter.exit_context() - - def test_default_format(self) -> None: - """Test default format string when none provided.""" - formatter = SimpleContextFormatter() - assert formatter._fmt is not None - assert "%(context)s" in formatter._fmt - - -async def test_formatter_integration_with_middleware() -> None: - """Test formatters work correctly with middleware.""" - app = FastAPI() - log_records: list[str] = [] - - class CaptureHandler(logging.Handler): - def emit(self, record: logging.LogRecord) -> None: - log_records.append(self.format(record)) - - handler = CaptureHandler() - handler.setFormatter(JsonContextFormatter()) - logger = logging.getLogger("test_integration") - logger.addHandler(handler) - logger.setLevel(logging.INFO) - - @app.get("/") - async def root() -> dict[str, Any]: - set_context("user_id", 123) - logger.info("Processing request") - return {"status": "ok"} - - wrapped = RequestContextMiddleware(app) - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - await client.get("/") - - assert len(log_records) == 1 - data = json.loads(log_records[0]) - assert data["message"] == "Processing request" - assert data["user_id"] == 123 - assert "request_id" in data - - logger.removeHandler(handler) diff --git a/.history/tests/test_middleware_20251128203118.py b/.history/tests/test_middleware_20251128203118.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/tests/test_middleware_20251128203119.py b/.history/tests/test_middleware_20251128203119.py deleted file mode 100644 index cfa6dad..0000000 --- a/.history/tests/test_middleware_20251128203119.py +++ /dev/null @@ -1,295 +0,0 @@ -"""Tests for RequestContextMiddleware.""" - -from typing import Any -from uuid import UUID - -import pytest -from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient - -from fastapi_request_context import ( - RequestContextConfig, - RequestContextMiddleware, - StandardContextField, - get_context, - get_full_context, -) - - -@pytest.fixture -def sample_app() -> FastAPI: - """Create a sample FastAPI app with a test route.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - return { - "request_id": get_context(StandardContextField.REQUEST_ID), - "correlation_id": get_context(StandardContextField.CORRELATION_ID), - } - - @app.get("/full-context") - async def full_context() -> dict[str, Any]: - return get_full_context() - - return app - - -@pytest.fixture -def wrapped_app(sample_app: FastAPI) -> RequestContextMiddleware: - """Create the sample app wrapped with middleware.""" - return RequestContextMiddleware(sample_app) - - -@pytest.fixture -async def test_client(wrapped_app: RequestContextMiddleware) -> AsyncClient: - """Create async test client.""" - transport = ASGITransport(app=wrapped_app) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - yield client - - -async def test_generates_request_id(test_client: AsyncClient) -> None: - """Test that request_id is always generated.""" - response = await test_client.get("/") - assert response.status_code == 200 - - data = response.json() - assert data["request_id"] is not None - # Verify it's a valid UUID - UUID(data["request_id"]) - - -async def test_generates_correlation_id(test_client: AsyncClient) -> None: - """Test that correlation_id is generated when not provided.""" - response = await test_client.get("/") - assert response.status_code == 200 - - data = response.json() - assert data["correlation_id"] is not None - UUID(data["correlation_id"]) - - -async def test_accepts_correlation_id_from_header(test_client: AsyncClient) -> None: - """Test that correlation_id is accepted from request header.""" - custom_correlation_id = "test-correlation-123" - response = await test_client.get( - "/", - headers={"X-Correlation-Id": custom_correlation_id}, - ) - assert response.status_code == 200 - - data = response.json() - assert data["correlation_id"] == custom_correlation_id - - -async def test_ignores_request_id_from_header(test_client: AsyncClient) -> None: - """Test that request_id from header is ignored (security best practice).""" - custom_request_id = "should-be-ignored" - response = await test_client.get( - "/", - headers={"X-Request-Id": custom_request_id}, - ) - assert response.status_code == 200 - - data = response.json() - # Should be a new UUID, not the provided value - assert data["request_id"] != custom_request_id - UUID(data["request_id"]) # Verify it's a valid UUID - - -async def test_adds_headers_to_response(test_client: AsyncClient) -> None: - """Test that request_id and correlation_id are added to response headers.""" - response = await test_client.get("/") - assert response.status_code == 200 - - # Check headers - assert "x-request-id" in response.headers - assert "x-correlation-id" in response.headers - - # Verify they match the context values - data = response.json() - assert response.headers["x-request-id"] == data["request_id"] - assert response.headers["x-correlation-id"] == data["correlation_id"] - - -async def test_unique_request_ids_per_request(test_client: AsyncClient) -> None: - """Test that each request gets a unique request_id.""" - response1 = await test_client.get("/") - response2 = await test_client.get("/") - - data1 = response1.json() - data2 = response2.json() - - assert data1["request_id"] != data2["request_id"] - - -async def test_correlation_id_case_insensitive(test_client: AsyncClient) -> None: - """Test that correlation_id header is case-insensitive.""" - custom_correlation_id = "test-correlation-lowercase" - - response = await test_client.get( - "/", - headers={"x-correlation-id": custom_correlation_id}, - ) - assert response.status_code == 200 - - data = response.json() - assert data["correlation_id"] == custom_correlation_id - - -async def test_full_context_includes_both_ids(test_client: AsyncClient) -> None: - """Test that full context includes both request_id and correlation_id.""" - response = await test_client.get("/full-context") - assert response.status_code == 200 - - data = response.json() - assert "request_id" in data - assert "correlation_id" in data - - -async def test_custom_id_generator() -> None: - """Test custom ID generator functions.""" - counter = {"value": 0} - - def custom_request_id() -> str: - counter["value"] += 1 - return f"req-{counter['value']}" - - def custom_correlation_id() -> str: - return "static-correlation" - - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - return { - "request_id": get_context(StandardContextField.REQUEST_ID), - "correlation_id": get_context(StandardContextField.CORRELATION_ID), - } - - config = RequestContextConfig( - request_id_generator=custom_request_id, - correlation_id_generator=custom_correlation_id, - ) - wrapped = RequestContextMiddleware(app, config=config) - - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response1 = await client.get("/") - response2 = await client.get("/") - - data1 = response1.json() - data2 = response2.json() - - assert data1["request_id"] == "req-1" - assert data2["request_id"] == "req-2" - assert data1["correlation_id"] == "static-correlation" - - -async def test_custom_header_names() -> None: - """Test custom header names configuration.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, str]: - return {"status": "ok"} - - config = RequestContextConfig( - request_id_header="X-Custom-Request-Id", - correlation_id_header="X-Custom-Correlation-Id", - ) - wrapped = RequestContextMiddleware(app, config=config) - - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - assert "x-custom-request-id" in response.headers - assert "x-custom-correlation-id" in response.headers - - -async def test_disable_response_headers() -> None: - """Test disabling response headers.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, str]: - return {"status": "ok"} - - config = RequestContextConfig(add_response_headers=False) - wrapped = RequestContextMiddleware(app, config=config) - - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - assert "x-request-id" not in response.headers - assert "x-correlation-id" not in response.headers - - -async def test_scope_type_filtering() -> None: - """Test that non-http scope types pass through unchanged.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, str]: - return {"status": "ok"} - - config = RequestContextConfig(scope_types={"http"}) # Only http, not websocket - wrapped = RequestContextMiddleware(app, config=config) - - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - # HTTP should work - assert response.status_code == 200 - - -async def test_adapter_string_contextvars() -> None: - """Test that 'contextvars' string works as adapter config.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - return {"request_id": get_context(StandardContextField.REQUEST_ID)} - - config = RequestContextConfig(context_adapter="contextvars") - wrapped = RequestContextMiddleware(app, config=config) - - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - assert response.status_code == 200 - assert response.json()["request_id"] is not None - - -async def test_adapter_string_context_logging() -> None: - """Test that 'context_logging' string works as adapter config.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - return {"request_id": get_context(StandardContextField.REQUEST_ID)} - - config = RequestContextConfig(context_adapter="context_logging") - wrapped = RequestContextMiddleware(app, config=config) - - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - assert response.status_code == 200 - assert response.json()["request_id"] is not None - - -async def test_adapter_string_invalid() -> None: - """Test that invalid adapter string raises ValueError.""" - app = FastAPI() - - config = RequestContextConfig(context_adapter="invalid") # type: ignore[arg-type] - - with pytest.raises(ValueError, match="Unknown adapter"): - RequestContextMiddleware(app, config=config) diff --git a/.history/tests/test_middleware_20251128211424.py b/.history/tests/test_middleware_20251128211424.py deleted file mode 100644 index 2ed659e..0000000 --- a/.history/tests/test_middleware_20251128211424.py +++ /dev/null @@ -1,322 +0,0 @@ -"""Tests for RequestContextMiddleware.""" - -from typing import Any -from uuid import UUID - -import pytest -from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient - -from fastapi_request_context import ( - RequestContextConfig, - RequestContextMiddleware, - StandardContextField, - get_context, - get_full_context, -) - - -@pytest.fixture -def sample_app() -> FastAPI: - """Create a sample FastAPI app with a test route.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - return { - "request_id": get_context(StandardContextField.REQUEST_ID), - "correlation_id": get_context(StandardContextField.CORRELATION_ID), - } - - @app.get("/full-context") - async def full_context() -> dict[str, Any]: - return get_full_context() - - return app - - -@pytest.fixture -def wrapped_app(sample_app: FastAPI) -> RequestContextMiddleware: - """Create the sample app wrapped with middleware.""" - return RequestContextMiddleware(sample_app) - - -@pytest.fixture -async def test_client(wrapped_app: RequestContextMiddleware) -> AsyncClient: - """Create async test client.""" - transport = ASGITransport(app=wrapped_app) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - yield client - - -async def test_generates_request_id(test_client: AsyncClient) -> None: - """Test that request_id is always generated.""" - response = await test_client.get("/") - assert response.status_code == 200 - - data = response.json() - assert data["request_id"] is not None - # Verify it's a valid UUID - UUID(data["request_id"]) - - -async def test_generates_correlation_id(test_client: AsyncClient) -> None: - """Test that correlation_id is generated when not provided.""" - response = await test_client.get("/") - assert response.status_code == 200 - - data = response.json() - assert data["correlation_id"] is not None - UUID(data["correlation_id"]) - - -async def test_accepts_correlation_id_from_header(test_client: AsyncClient) -> None: - """Test that correlation_id is accepted from request header.""" - custom_correlation_id = "test-correlation-123" - response = await test_client.get( - "/", - headers={"X-Correlation-Id": custom_correlation_id}, - ) - assert response.status_code == 200 - - data = response.json() - assert data["correlation_id"] == custom_correlation_id - - -async def test_ignores_request_id_from_header(test_client: AsyncClient) -> None: - """Test that request_id from header is ignored (security best practice).""" - custom_request_id = "should-be-ignored" - response = await test_client.get( - "/", - headers={"X-Request-Id": custom_request_id}, - ) - assert response.status_code == 200 - - data = response.json() - # Should be a new UUID, not the provided value - assert data["request_id"] != custom_request_id - UUID(data["request_id"]) # Verify it's a valid UUID - - -async def test_adds_headers_to_response(test_client: AsyncClient) -> None: - """Test that request_id and correlation_id are added to response headers.""" - response = await test_client.get("/") - assert response.status_code == 200 - - # Check headers - assert "x-request-id" in response.headers - assert "x-correlation-id" in response.headers - - # Verify they match the context values - data = response.json() - assert response.headers["x-request-id"] == data["request_id"] - assert response.headers["x-correlation-id"] == data["correlation_id"] - - -async def test_unique_request_ids_per_request(test_client: AsyncClient) -> None: - """Test that each request gets a unique request_id.""" - response1 = await test_client.get("/") - response2 = await test_client.get("/") - - data1 = response1.json() - data2 = response2.json() - - assert data1["request_id"] != data2["request_id"] - - -async def test_correlation_id_case_insensitive(test_client: AsyncClient) -> None: - """Test that correlation_id header is case-insensitive.""" - custom_correlation_id = "test-correlation-lowercase" - - response = await test_client.get( - "/", - headers={"x-correlation-id": custom_correlation_id}, - ) - assert response.status_code == 200 - - data = response.json() - assert data["correlation_id"] == custom_correlation_id - - -async def test_full_context_includes_both_ids(test_client: AsyncClient) -> None: - """Test that full context includes both request_id and correlation_id.""" - response = await test_client.get("/full-context") - assert response.status_code == 200 - - data = response.json() - assert "request_id" in data - assert "correlation_id" in data - - -async def test_custom_id_generator() -> None: - """Test custom ID generator functions.""" - counter = {"value": 0} - - def custom_request_id() -> str: - counter["value"] += 1 - return f"req-{counter['value']}" - - def custom_correlation_id() -> str: - return "static-correlation" - - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - return { - "request_id": get_context(StandardContextField.REQUEST_ID), - "correlation_id": get_context(StandardContextField.CORRELATION_ID), - } - - config = RequestContextConfig( - request_id_generator=custom_request_id, - correlation_id_generator=custom_correlation_id, - ) - wrapped = RequestContextMiddleware(app, config=config) - - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response1 = await client.get("/") - response2 = await client.get("/") - - data1 = response1.json() - data2 = response2.json() - - assert data1["request_id"] == "req-1" - assert data2["request_id"] == "req-2" - assert data1["correlation_id"] == "static-correlation" - - -async def test_custom_header_names() -> None: - """Test custom header names configuration.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, str]: - return {"status": "ok"} - - config = RequestContextConfig( - request_id_header="X-Custom-Request-Id", - correlation_id_header="X-Custom-Correlation-Id", - ) - wrapped = RequestContextMiddleware(app, config=config) - - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - assert "x-custom-request-id" in response.headers - assert "x-custom-correlation-id" in response.headers - - -async def test_disable_response_headers() -> None: - """Test disabling response headers.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, str]: - return {"status": "ok"} - - config = RequestContextConfig(add_response_headers=False) - wrapped = RequestContextMiddleware(app, config=config) - - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - assert "x-request-id" not in response.headers - assert "x-correlation-id" not in response.headers - - -async def test_scope_type_filtering() -> None: - """Test that non-http scope types pass through unchanged.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, str]: - return {"status": "ok"} - - config = RequestContextConfig(scope_types={"http"}) # Only http, not websocket - wrapped = RequestContextMiddleware(app, config=config) - - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - # HTTP should work - assert response.status_code == 200 - - -async def test_adapter_string_contextvars() -> None: - """Test that 'contextvars' string works as adapter config.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - return {"request_id": get_context(StandardContextField.REQUEST_ID)} - - config = RequestContextConfig(context_adapter="contextvars") - wrapped = RequestContextMiddleware(app, config=config) - - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - assert response.status_code == 200 - assert response.json()["request_id"] is not None - - -async def test_adapter_string_context_logging() -> None: - """Test that 'context_logging' string works as adapter config.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - return {"request_id": get_context(StandardContextField.REQUEST_ID)} - - config = RequestContextConfig(context_adapter="context_logging") - wrapped = RequestContextMiddleware(app, config=config) - - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - assert response.status_code == 200 - assert response.json()["request_id"] is not None - - -async def test_adapter_string_invalid() -> None: - """Test that invalid adapter string raises ValueError.""" - app = FastAPI() - - config = RequestContextConfig(context_adapter="invalid") # type: ignore[arg-type] - - with pytest.raises(ValueError, match="Unknown adapter"): - RequestContextMiddleware(app, config=config) - - -async def test_non_http_scope_passes_through() -> None: - """Test that non-HTTP scope types pass through without processing.""" - - async def mock_app( - scope: dict[str, Any], - receive: Any, - send: Any, - ) -> None: - # Just verify it was called - await send({"type": "lifespan.startup.complete"}) - - config = RequestContextConfig(scope_types={"http"}) - wrapped = RequestContextMiddleware(mock_app, config=config) - - received_messages: list[dict[str, Any]] = [] - - async def mock_send(message: dict[str, Any]) -> None: - received_messages.append(message) - - # Simulate a lifespan scope (not http) - await wrapped({"type": "lifespan"}, lambda: None, mock_send) - - # Should have passed through to mock_app - assert len(received_messages) == 1 - assert received_messages[0]["type"] == "lifespan.startup.complete" diff --git a/.history/tests/test_middleware_20251128211814.py b/.history/tests/test_middleware_20251128211814.py deleted file mode 100644 index 317ee35..0000000 --- a/.history/tests/test_middleware_20251128211814.py +++ /dev/null @@ -1,325 +0,0 @@ -"""Tests for RequestContextMiddleware.""" - -from typing import Any -from uuid import UUID - -import pytest -from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient - -from fastapi_request_context import ( - RequestContextConfig, - RequestContextMiddleware, - StandardContextField, - get_context, - get_full_context, -) - - -@pytest.fixture -def sample_app() -> FastAPI: - """Create a sample FastAPI app with a test route.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - return { - "request_id": get_context(StandardContextField.REQUEST_ID), - "correlation_id": get_context(StandardContextField.CORRELATION_ID), - } - - @app.get("/full-context") - async def full_context() -> dict[str, Any]: - return get_full_context() - - return app - - -@pytest.fixture -def wrapped_app(sample_app: FastAPI) -> RequestContextMiddleware: - """Create the sample app wrapped with middleware.""" - return RequestContextMiddleware(sample_app) - - -@pytest.fixture -async def test_client(wrapped_app: RequestContextMiddleware) -> AsyncClient: - """Create async test client.""" - transport = ASGITransport(app=wrapped_app) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - yield client - - -async def test_generates_request_id(test_client: AsyncClient) -> None: - """Test that request_id is always generated.""" - response = await test_client.get("/") - assert response.status_code == 200 - - data = response.json() - assert data["request_id"] is not None - # Verify it's a valid UUID - UUID(data["request_id"]) - - -async def test_generates_correlation_id(test_client: AsyncClient) -> None: - """Test that correlation_id is generated when not provided.""" - response = await test_client.get("/") - assert response.status_code == 200 - - data = response.json() - assert data["correlation_id"] is not None - UUID(data["correlation_id"]) - - -async def test_accepts_correlation_id_from_header(test_client: AsyncClient) -> None: - """Test that correlation_id is accepted from request header.""" - custom_correlation_id = "test-correlation-123" - response = await test_client.get( - "/", - headers={"X-Correlation-Id": custom_correlation_id}, - ) - assert response.status_code == 200 - - data = response.json() - assert data["correlation_id"] == custom_correlation_id - - -async def test_ignores_request_id_from_header(test_client: AsyncClient) -> None: - """Test that request_id from header is ignored (security best practice).""" - custom_request_id = "should-be-ignored" - response = await test_client.get( - "/", - headers={"X-Request-Id": custom_request_id}, - ) - assert response.status_code == 200 - - data = response.json() - # Should be a new UUID, not the provided value - assert data["request_id"] != custom_request_id - UUID(data["request_id"]) # Verify it's a valid UUID - - -async def test_adds_headers_to_response(test_client: AsyncClient) -> None: - """Test that request_id and correlation_id are added to response headers.""" - response = await test_client.get("/") - assert response.status_code == 200 - - # Check headers - assert "x-request-id" in response.headers - assert "x-correlation-id" in response.headers - - # Verify they match the context values - data = response.json() - assert response.headers["x-request-id"] == data["request_id"] - assert response.headers["x-correlation-id"] == data["correlation_id"] - - -async def test_unique_request_ids_per_request(test_client: AsyncClient) -> None: - """Test that each request gets a unique request_id.""" - response1 = await test_client.get("/") - response2 = await test_client.get("/") - - data1 = response1.json() - data2 = response2.json() - - assert data1["request_id"] != data2["request_id"] - - -async def test_correlation_id_case_insensitive(test_client: AsyncClient) -> None: - """Test that correlation_id header is case-insensitive.""" - custom_correlation_id = "test-correlation-lowercase" - - response = await test_client.get( - "/", - headers={"x-correlation-id": custom_correlation_id}, - ) - assert response.status_code == 200 - - data = response.json() - assert data["correlation_id"] == custom_correlation_id - - -async def test_full_context_includes_both_ids(test_client: AsyncClient) -> None: - """Test that full context includes both request_id and correlation_id.""" - response = await test_client.get("/full-context") - assert response.status_code == 200 - - data = response.json() - assert "request_id" in data - assert "correlation_id" in data - - -async def test_custom_id_generator() -> None: - """Test custom ID generator functions.""" - counter = {"value": 0} - - def custom_request_id() -> str: - counter["value"] += 1 - return f"req-{counter['value']}" - - def custom_correlation_id() -> str: - return "static-correlation" - - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - return { - "request_id": get_context(StandardContextField.REQUEST_ID), - "correlation_id": get_context(StandardContextField.CORRELATION_ID), - } - - config = RequestContextConfig( - request_id_generator=custom_request_id, - correlation_id_generator=custom_correlation_id, - ) - wrapped = RequestContextMiddleware(app, config=config) - - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response1 = await client.get("/") - response2 = await client.get("/") - - data1 = response1.json() - data2 = response2.json() - - assert data1["request_id"] == "req-1" - assert data2["request_id"] == "req-2" - assert data1["correlation_id"] == "static-correlation" - - -async def test_custom_header_names() -> None: - """Test custom header names configuration.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, str]: - return {"status": "ok"} - - config = RequestContextConfig( - request_id_header="X-Custom-Request-Id", - correlation_id_header="X-Custom-Correlation-Id", - ) - wrapped = RequestContextMiddleware(app, config=config) - - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - assert "x-custom-request-id" in response.headers - assert "x-custom-correlation-id" in response.headers - - -async def test_disable_response_headers() -> None: - """Test disabling response headers.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, str]: - return {"status": "ok"} - - config = RequestContextConfig(add_response_headers=False) - wrapped = RequestContextMiddleware(app, config=config) - - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - assert "x-request-id" not in response.headers - assert "x-correlation-id" not in response.headers - - -async def test_scope_type_filtering() -> None: - """Test that non-http scope types pass through unchanged.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, str]: - return {"status": "ok"} - - config = RequestContextConfig(scope_types={"http"}) # Only http, not websocket - wrapped = RequestContextMiddleware(app, config=config) - - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - # HTTP should work - assert response.status_code == 200 - - -async def test_adapter_string_contextvars() -> None: - """Test that 'contextvars' string works as adapter config.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - return {"request_id": get_context(StandardContextField.REQUEST_ID)} - - config = RequestContextConfig(context_adapter="contextvars") - wrapped = RequestContextMiddleware(app, config=config) - - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - assert response.status_code == 200 - assert response.json()["request_id"] is not None - - -async def test_adapter_string_context_logging() -> None: - """Test that 'context_logging' string works as adapter config.""" - app = FastAPI() - - @app.get("/") - async def root() -> dict[str, Any]: - return {"request_id": get_context(StandardContextField.REQUEST_ID)} - - config = RequestContextConfig(context_adapter="context_logging") - wrapped = RequestContextMiddleware(app, config=config) - - transport = ASGITransport(app=wrapped) # type: ignore[arg-type] - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/") - - assert response.status_code == 200 - assert response.json()["request_id"] is not None - - -async def test_adapter_string_invalid() -> None: - """Test that invalid adapter string raises ValueError.""" - app = FastAPI() - - config = RequestContextConfig(context_adapter="invalid") # type: ignore[arg-type] - - with pytest.raises(ValueError, match="Unknown adapter"): - RequestContextMiddleware(app, config=config) - - -async def test_non_http_scope_passes_through() -> None: - """Test that non-HTTP scope types pass through without processing.""" - - async def mock_app( - scope: Any, - receive: Any, - send: Any, - ) -> None: - # Just verify it was called - await send({"type": "lifespan.startup.complete"}) - - config = RequestContextConfig(scope_types={"http"}) - wrapped = RequestContextMiddleware(mock_app, config=config) # type: ignore[arg-type] - - received_messages: list[dict[str, Any]] = [] - - async def mock_send(message: Any) -> None: - received_messages.append(message) - - async def mock_receive() -> dict[str, Any]: - return {"type": "lifespan.startup"} - - # Simulate a lifespan scope (not http) - await wrapped({"type": "lifespan"}, mock_receive, mock_send) - - # Should have passed through to mock_app - assert len(received_messages) == 1 - assert received_messages[0]["type"] == "lifespan.startup.complete" diff --git a/.history/tests/test_validation_20251128203254.py b/.history/tests/test_validation_20251128203254.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/tests/test_validation_20251128203255.py b/.history/tests/test_validation_20251128203255.py deleted file mode 100644 index 818a437..0000000 --- a/.history/tests/test_validation_20251128203255.py +++ /dev/null @@ -1,199 +0,0 @@ -"""Tests for validation utilities.""" - -import pytest -from fastapi import Depends, FastAPI - -from fastapi_request_context.validation import ( - check_dependencies_are_async, - check_routes_and_dependencies_are_async, - is_async, -) - - -class TestIsAsync: - """Tests for is_async function.""" - - def test_async_function(self) -> None: - """Test that async functions are detected.""" - - async def async_func() -> None: - pass - - assert is_async(async_func) is True - - def test_sync_function(self) -> None: - """Test that sync functions are detected.""" - - def sync_func() -> None: - pass - - assert is_async(sync_func) is False - - def test_async_callable_class(self) -> None: - """Test that classes with async __call__ are detected.""" - - class AsyncCallable: - async def __call__(self) -> None: - pass - - assert is_async(AsyncCallable()) is True - - def test_sync_callable_class(self) -> None: - """Test that classes with sync __call__ are detected.""" - - class SyncCallable: - def __call__(self) -> None: - pass - - assert is_async(SyncCallable()) is False - - def test_lambda(self) -> None: - """Test that lambdas are detected as sync.""" - assert is_async(lambda: None) is False - - -class TestCheckDependenciesAreAsync: - """Tests for check_dependencies_are_async function.""" - - def test_all_async(self) -> None: - """Test that no warnings for all async dependencies.""" - - async def dep1() -> None: - pass - - async def dep2() -> None: - pass - - warnings = check_dependencies_are_async([dep1, dep2]) - assert warnings == [] - - def test_sync_dependency(self) -> None: - """Test that sync dependencies generate warnings.""" - - def sync_dep() -> None: - pass - - async def async_dep() -> None: - pass - - warnings = check_dependencies_are_async([sync_dep, async_dep]) - assert len(warnings) == 1 - assert "sync_dep" in warnings[0] - - def test_raise_on_sync(self) -> None: - """Test that raise_on_sync raises ValueError.""" - - def sync_dep() -> None: - pass - - with pytest.raises(ValueError, match="Sync dependencies found"): - check_dependencies_are_async([sync_dep], raise_on_sync=True) - - -class TestCheckRoutesAndDependenciesAreAsync: - """Tests for check_routes_and_dependencies_are_async function.""" - - def test_all_async_routes(self) -> None: - """Test that no warnings for all async routes.""" - app = FastAPI() - - @app.get("/") - async def async_route() -> dict[str, str]: - return {"status": "ok"} - - @app.post("/create") - async def async_create() -> dict[str, str]: - return {"status": "created"} - - warnings = check_routes_and_dependencies_are_async(app) - assert warnings == [] - - def test_sync_route(self) -> None: - """Test that sync routes generate warnings.""" - app = FastAPI() - - @app.get("/sync") - def sync_route() -> dict[str, str]: - return {"status": "ok"} - - warnings = check_routes_and_dependencies_are_async(app) - assert len(warnings) == 1 - assert "sync_route" in warnings[0] - assert "/sync" in warnings[0] - - def test_sync_dependency(self) -> None: - """Test that sync dependencies generate warnings.""" - - def sync_dep() -> int: - return 42 - - app = FastAPI() - - @app.get("/") - async def route(value: int = Depends(sync_dep)) -> dict[str, int]: - return {"value": value} - - warnings = check_routes_and_dependencies_are_async(app) - assert len(warnings) >= 1 - assert any("sync_dep" in w for w in warnings) - - def test_raise_on_sync_routes(self) -> None: - """Test that raise_on_sync raises ValueError for sync routes.""" - app = FastAPI() - - @app.get("/sync") - def sync_route() -> dict[str, str]: - return {"status": "ok"} - - with pytest.raises(ValueError, match="Sync routes/dependencies found"): - check_routes_and_dependencies_are_async(app, raise_on_sync=True) - - def test_mixed_routes(self) -> None: - """Test with mix of sync and async routes.""" - app = FastAPI() - - @app.get("/async") - async def async_route() -> dict[str, str]: - return {"status": "ok"} - - @app.get("/sync") - def sync_route() -> dict[str, str]: - return {"status": "ok"} - - warnings = check_routes_and_dependencies_are_async(app) - assert len(warnings) == 1 - assert "/sync" in warnings[0] - - def test_nested_dependencies(self) -> None: - """Test that nested dependencies are checked.""" - - def inner_sync_dep() -> int: - return 42 - - async def outer_dep(value: int = Depends(inner_sync_dep)) -> int: - return value * 2 - - app = FastAPI() - - @app.get("/") - async def route(value: int = Depends(outer_dep)) -> dict[str, int]: - return {"value": value} - - warnings = check_routes_and_dependencies_are_async(app) - # Should detect the sync inner dependency - assert any("inner_sync_dep" in w for w in warnings) - - def test_route_level_dependencies(self) -> None: - """Test that route-level dependencies are checked.""" - - def sync_dep() -> None: - pass - - app = FastAPI() - - @app.get("/", dependencies=[Depends(sync_dep)]) - async def route() -> dict[str, str]: - return {"status": "ok"} - - warnings = check_routes_and_dependencies_are_async(app) - assert any("sync_dep" in w for w in warnings) diff --git a/.history/tests/test_validation_20251128203733.py b/.history/tests/test_validation_20251128203733.py deleted file mode 100644 index 4d4c3ac..0000000 --- a/.history/tests/test_validation_20251128203733.py +++ /dev/null @@ -1,203 +0,0 @@ -"""Tests for validation utilities.""" - -import pytest -from fastapi import Depends, FastAPI - -from fastapi_request_context.validation import ( - check_dependencies_are_async, - check_routes_and_dependencies_are_async, - is_async, -) - - -class TestIsAsync: - """Tests for is_async function.""" - - def test_async_function(self) -> None: - """Test that async functions are detected.""" - - async def async_func() -> None: - pass - - assert is_async(async_func) is True - - def test_sync_function(self) -> None: - """Test that sync functions are detected.""" - - def sync_func() -> None: - pass - - assert is_async(sync_func) is False - - def test_async_callable_class(self) -> None: - """Test that classes with async __call__ are detected.""" - - class AsyncCallable: - async def __call__(self) -> None: - pass - - assert is_async(AsyncCallable()) is True - - def test_sync_callable_class(self) -> None: - """Test that classes with sync __call__ are detected.""" - - class SyncCallable: - def __call__(self) -> None: - pass - - assert is_async(SyncCallable()) is False - - def test_lambda(self) -> None: - """Test that lambdas are detected as sync.""" - assert is_async(lambda: None) is False - - -class TestCheckDependenciesAreAsync: - """Tests for check_dependencies_are_async function.""" - - def test_all_async(self) -> None: - """Test that no warnings for all async dependencies.""" - - async def dep1() -> None: - pass - - async def dep2() -> None: - pass - - warnings = check_dependencies_are_async([dep1, dep2]) - assert warnings == [] - - def test_sync_dependency(self) -> None: - """Test that sync dependencies generate warnings.""" - - def sync_dep() -> None: - pass - - async def async_dep() -> None: - pass - - warnings = check_dependencies_are_async([sync_dep, async_dep]) - assert len(warnings) == 1 - assert "sync_dep" in warnings[0] - - def test_raise_on_sync(self) -> None: - """Test that raise_on_sync raises ValueError.""" - - def sync_dep() -> None: - pass - - with pytest.raises(ValueError, match="Sync dependencies found"): - check_dependencies_are_async([sync_dep], raise_on_sync=True) - - -class TestCheckRoutesAndDependenciesAreAsync: - """Tests for check_routes_and_dependencies_are_async function.""" - - def test_all_async_routes(self) -> None: - """Test that no warnings for all async routes.""" - app = FastAPI() - - @app.get("/") - async def async_route() -> dict[str, str]: - return {"status": "ok"} - - @app.post("/create") - async def async_create() -> dict[str, str]: - return {"status": "created"} - - warnings = check_routes_and_dependencies_are_async(app) - assert warnings == [] - - def test_sync_route(self) -> None: - """Test that sync routes generate warnings.""" - app = FastAPI() - - @app.get("/sync") - def sync_route() -> dict[str, str]: - return {"status": "ok"} - - warnings = check_routes_and_dependencies_are_async(app) - assert len(warnings) == 1 - assert "sync_route" in warnings[0] - assert "/sync" in warnings[0] - - def test_sync_dependency(self) -> None: - """Test that sync dependencies generate warnings.""" - - def sync_dep() -> int: - return 42 - - app = FastAPI() - - @app.get("/") - async def route(value: int = Depends(sync_dep)) -> dict[str, int]: - return {"value": value} - - warnings = check_routes_and_dependencies_are_async(app) - assert len(warnings) >= 1 - assert any("sync_dep" in w for w in warnings) - - def test_raise_on_sync_routes(self) -> None: - """Test that raise_on_sync raises ValueError for sync routes.""" - app = FastAPI() - - @app.get("/sync") - def sync_route() -> dict[str, str]: - return {"status": "ok"} - - with pytest.raises(ValueError, match="Sync routes/dependencies found"): - check_routes_and_dependencies_are_async(app, raise_on_sync=True) - - def test_mixed_routes(self) -> None: - """Test with mix of sync and async routes.""" - app = FastAPI() - - @app.get("/async") - async def async_route() -> dict[str, str]: - return {"status": "ok"} - - @app.get("/sync") - def sync_route() -> dict[str, str]: - return {"status": "ok"} - - warnings = check_routes_and_dependencies_are_async(app) - assert len(warnings) == 1 - assert "/sync" in warnings[0] - - def test_multiple_sync_dependencies(self) -> None: - """Test that multiple sync dependencies generate warnings.""" - - def sync_dep1() -> int: - return 42 - - def sync_dep2() -> str: - return "hello" - - app = FastAPI() - - @app.get("/") - async def route( - v1: int = Depends(sync_dep1), - v2: str = Depends(sync_dep2), - ) -> dict[str, int | str]: - return {"v1": v1, "v2": v2} - - warnings = check_routes_and_dependencies_are_async(app) - # Should detect both sync dependencies - assert any("sync_dep1" in w for w in warnings) - assert any("sync_dep2" in w for w in warnings) - - def test_route_level_dependencies(self) -> None: - """Test that route-level dependencies are checked.""" - - def sync_dep() -> None: - pass - - app = FastAPI() - - @app.get("/", dependencies=[Depends(sync_dep)]) - async def route() -> dict[str, str]: - return {"status": "ok"} - - warnings = check_routes_and_dependencies_are_async(app) - assert any("sync_dep" in w for w in warnings) diff --git a/.history/tests/validation/__init___20251128212141.py b/.history/tests/validation/__init___20251128212141.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/tests/validation/__init___20251128212142.py b/.history/tests/validation/__init___20251128212142.py deleted file mode 100644 index 6f76831..0000000 --- a/.history/tests/validation/__init___20251128212142.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for validation utilities.""" diff --git a/.history/tests/validation/test_dependencies_20251128212150.py b/.history/tests/validation/test_dependencies_20251128212150.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/tests/validation/test_dependencies_20251128212151.py b/.history/tests/validation/test_dependencies_20251128212151.py deleted file mode 100644 index 7d7de74..0000000 --- a/.history/tests/validation/test_dependencies_20251128212151.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Tests for check_dependencies_are_async function.""" - -import pytest - -from fastapi_request_context.validation import check_dependencies_are_async - - -def test_all_async() -> None: - """Test that no warnings for all async dependencies.""" - - async def dep1() -> None: - pass - - async def dep2() -> None: - pass - - warnings = check_dependencies_are_async([dep1, dep2]) - assert warnings == [] - - -def test_sync_dependency() -> None: - """Test that sync dependencies generate warnings.""" - - def sync_dep() -> None: - pass - - async def async_dep() -> None: - pass - - warnings = check_dependencies_are_async([sync_dep, async_dep]) - assert len(warnings) == 1 - assert "sync_dep" in warnings[0] - - -def test_raise_on_sync() -> None: - """Test that raise_on_sync raises ValueError.""" - - def sync_dep() -> None: - pass - - with pytest.raises(ValueError, match="Sync dependencies found"): - check_dependencies_are_async([sync_dep], raise_on_sync=True) diff --git a/.history/tests/validation/test_is_async_20251128212146.py b/.history/tests/validation/test_is_async_20251128212146.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/tests/validation/test_is_async_20251128212147.py b/.history/tests/validation/test_is_async_20251128212147.py deleted file mode 100644 index b53796d..0000000 --- a/.history/tests/validation/test_is_async_20251128212147.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Tests for is_async function.""" - -from fastapi_request_context.validation import is_async - - -def test_async_function() -> None: - """Test that async functions are detected.""" - - async def async_func() -> None: - pass - - assert is_async(async_func) is True - - -def test_sync_function() -> None: - """Test that sync functions are detected.""" - - def sync_func() -> None: - pass - - assert is_async(sync_func) is False - - -def test_async_callable_class() -> None: - """Test that classes with async __call__ are detected.""" - - class AsyncCallable: - async def __call__(self) -> None: - pass - - assert is_async(AsyncCallable()) is True - - -def test_sync_callable_class() -> None: - """Test that classes with sync __call__ are detected.""" - - class SyncCallable: - def __call__(self) -> None: - pass - - assert is_async(SyncCallable()) is False - - -def test_lambda() -> None: - """Test that lambdas are detected as sync.""" - assert is_async(lambda: None) is False diff --git a/.history/tests/validation/test_routes_20251128212203.py b/.history/tests/validation/test_routes_20251128212203.py deleted file mode 100644 index e69de29..0000000 diff --git a/.history/tests/validation/test_routes_20251128212204.py b/.history/tests/validation/test_routes_20251128212204.py deleted file mode 100644 index 140f3de..0000000 --- a/.history/tests/validation/test_routes_20251128212204.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Tests for check_routes_and_dependencies_are_async function.""" - -import pytest -from fastapi import Depends, FastAPI - -from fastapi_request_context.validation import check_routes_and_dependencies_are_async - - -def test_all_async_routes() -> None: - """Test that no warnings for all async routes.""" - app = FastAPI() - - @app.get("/") - async def async_route() -> dict[str, str]: - return {"status": "ok"} - - @app.post("/create") - async def async_create() -> dict[str, str]: - return {"status": "created"} - - warnings = check_routes_and_dependencies_are_async(app) - assert warnings == [] - - -def test_sync_route() -> None: - """Test that sync routes generate warnings.""" - app = FastAPI() - - @app.get("/sync") - def sync_route() -> dict[str, str]: - return {"status": "ok"} - - warnings = check_routes_and_dependencies_are_async(app) - assert len(warnings) == 1 - assert "sync_route" in warnings[0] - assert "/sync" in warnings[0] - - -def test_sync_dependency() -> None: - """Test that sync dependencies generate warnings.""" - - def sync_dep() -> int: - return 42 - - app = FastAPI() - - @app.get("/") - async def route(value: int = Depends(sync_dep)) -> dict[str, int]: - return {"value": value} - - warnings = check_routes_and_dependencies_are_async(app) - assert len(warnings) >= 1 - assert any("sync_dep" in w for w in warnings) - - -def test_raise_on_sync_routes() -> None: - """Test that raise_on_sync raises ValueError for sync routes.""" - app = FastAPI() - - @app.get("/sync") - def sync_route() -> dict[str, str]: - return {"status": "ok"} - - with pytest.raises(ValueError, match="Sync routes/dependencies found"): - check_routes_and_dependencies_are_async(app, raise_on_sync=True) - - -def test_mixed_routes() -> None: - """Test with mix of sync and async routes.""" - app = FastAPI() - - @app.get("/async") - async def async_route() -> dict[str, str]: - return {"status": "ok"} - - @app.get("/sync") - def sync_route() -> dict[str, str]: - return {"status": "ok"} - - warnings = check_routes_and_dependencies_are_async(app) - assert len(warnings) == 1 - assert "/sync" in warnings[0] - - -def test_multiple_sync_dependencies() -> None: - """Test that multiple sync dependencies generate warnings.""" - - def sync_dep1() -> int: - return 42 - - def sync_dep2() -> str: - return "hello" - - app = FastAPI() - - @app.get("/") - async def route( - v1: int = Depends(sync_dep1), - v2: str = Depends(sync_dep2), - ) -> dict[str, int | str]: - return {"v1": v1, "v2": v2} - - warnings = check_routes_and_dependencies_are_async(app) - # Should detect both sync dependencies - assert any("sync_dep1" in w for w in warnings) - assert any("sync_dep2" in w for w in warnings) - - -def test_route_level_dependencies() -> None: - """Test that route-level dependencies are checked.""" - - def sync_dep() -> None: - pass - - app = FastAPI() - - @app.get("/", dependencies=[Depends(sync_dep)]) - async def route() -> dict[str, str]: - return {"status": "ok"} - - warnings = check_routes_and_dependencies_are_async(app) - assert any("sync_dep" in w for w in warnings) From c7192d8968fbb01d18fab4d5e333cdb9f1997bd0 Mon Sep 17 00:00:00 2001 From: Adrian Dankiv Date: Thu, 29 Jan 2026 14:49:17 +0100 Subject: [PATCH 4/4] feat: add middleware to propagate context to taskiq worker --- README.md | 61 +- examples/taskiq_integration.py | 117 ++++ fastapi_request_context/contrib/__init__.py | 1 + .../contrib/taskiq/__init__.py | 17 + .../contrib/taskiq/middleware.py | 118 ++++ fastapi_request_context/fields.py | 3 + pyproject.toml | 7 + tests/contrib/__init__.py | 1 + tests/contrib/test_taskiq_middleware.py | 442 ++++++++++++++ uv.lock | 537 +++++++++++++++++- 10 files changed, 1302 insertions(+), 2 deletions(-) create mode 100644 examples/taskiq_integration.py create mode 100644 fastapi_request_context/contrib/__init__.py create mode 100644 fastapi_request_context/contrib/taskiq/__init__.py create mode 100644 fastapi_request_context/contrib/taskiq/middleware.py create mode 100644 tests/contrib/__init__.py create mode 100644 tests/contrib/test_taskiq_middleware.py diff --git a/README.md b/README.md index ecef961..df6243c 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,9 @@ pip install fastapi-request-context[context-logging] # With JSON formatter support pip install fastapi-request-context[json-formatter] +# With Taskiq integration (background tasks) +pip install fastapi-request-context[taskiq] + # All optional dependencies pip install fastapi-request-context[all] ``` @@ -298,6 +301,62 @@ async def stream(): > **Note:** Requires `context-logging` extra: `pip install fastapi-request-context[context-logging]` +## Contrib Integrations + +### Taskiq Integration + +Automatically propagate request context to background tasks using [Taskiq](https://taskiq-python.github.io/): + +**Installation:** +```bash +pip install fastapi-request-context[taskiq] +``` + +**Usage:** +```python +from fastapi import FastAPI +from taskiq import InMemoryBroker +from fastapi_request_context import RequestContextMiddleware, get_context, set_context +from fastapi_request_context.contrib.taskiq import RequestContextTaskiqMiddleware +from fastapi_request_context.fields import StandardContextField + +# Set up Taskiq broker with middleware +broker = InMemoryBroker() +broker = broker.with_middlewares(RequestContextTaskiqMiddleware()) + +app = FastAPI() +app = RequestContextMiddleware(app) + +@broker.task +async def process_data(user_id: int): + # Context is automatically available in tasks + correlation_id = get_context(StandardContextField.CORRELATION_ID) + task_id = get_context(StandardContextField.TASK_ID) + custom_field = get_context("custom_field") + + # Your task logic here + return {"processed": user_id, "correlation_id": correlation_id} + +@app.post("/trigger") +async def trigger_task(): + # Set custom context in the request handler + set_context("custom_field", "custom_value") + + # Task inherits correlation_id and custom fields (but not request_id) + await process_data.kiq(user_id=123) + return {"status": "task queued"} +``` + +**Features:** +- ✅ Automatic context propagation to background tasks +- ✅ `CORRELATION_ID` preserved for distributed tracing +- ✅ Custom context fields propagated +- ✅ `TASK_ID` automatically injected (from Taskiq's task ID) +- ✅ `REQUEST_ID` excluded (each task environment has its own) +- ✅ Works with any Taskiq broker (InMemory, Redis, RabbitMQ, etc.) + +See [examples/taskiq_integration.py](examples/taskiq_integration.py) for a complete example. + ## API Reference ### Middleware @@ -317,7 +376,7 @@ async def stream(): ### Fields -- `StandardContextField` - Built-in fields (REQUEST_ID, CORRELATION_ID) +- `StandardContextField` - Built-in fields (REQUEST_ID, CORRELATION_ID, TASK_ID) ### Adapters diff --git a/examples/taskiq_integration.py b/examples/taskiq_integration.py new file mode 100644 index 0000000..a4a2eae --- /dev/null +++ b/examples/taskiq_integration.py @@ -0,0 +1,117 @@ +"""Example of using Taskiq with request context propagation. + +This example demonstrates how to: +1. Set up a Taskiq broker with the RequestContextTaskiqMiddleware +2. Send tasks from FastAPI endpoints with automatic context propagation +3. Access request context (correlation_id, custom fields) within tasks +4. Maintain distributed tracing across async task boundaries +""" + +import logging +import logging.config + +from fastapi import FastAPI +from taskiq import InMemoryBroker + +from fastapi_request_context import ( + RequestContextMiddleware, + get_context, + set_context, +) +from fastapi_request_context.contrib.taskiq import RequestContextTaskiqMiddleware +from fastapi_request_context.fields import StandardContextField +from fastapi_request_context.formatters.json import JsonContextFormatter + +logging.config.dictConfig( + { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "json": { + "()": JsonContextFormatter, + }, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "json", + "stream": "ext://sys.stdout", + }, + }, + "root": { + "level": "INFO", + "handlers": ["console"], + }, + }, +) + +logger = logging.getLogger(__name__) + +broker = InMemoryBroker() +broker = broker.with_middlewares(RequestContextTaskiqMiddleware()) + +app = FastAPI() +app = RequestContextMiddleware(app) + + +@broker.task +async def process_user_data(user_id: int) -> dict: + """Background task that processes user data. + + The request context from the originating request is automatically + available here, including correlation_id and any custom fields. + """ + correlation_id = get_context(StandardContextField.CORRELATION_ID) + task_id = get_context(StandardContextField.TASK_ID) + request_user_id = get_context("user_id") + + logger.info( + "Processing user data in background task", + extra={ + "user_id": user_id, + "request_user_id": request_user_id, + }, + ) + + return { + "correlation_id": correlation_id, + "task_id": task_id, + "user_id": user_id, + "status": "processed", + } + + +@app.post("/users/{user_id}/process") +async def trigger_processing(user_id: int) -> dict: + """Endpoint that triggers background processing. + + The current request context (correlation_id, custom fields) will be + automatically propagated to the background task. + """ + set_context("user_id", user_id) + + logger.info("Triggering background processing", extra={"user_id": user_id}) + + task = await process_user_data.kiq(user_id) + + correlation_id = get_context(StandardContextField.CORRELATION_ID) + request_id = get_context(StandardContextField.REQUEST_ID) + + return { + "message": "Processing started", + "task_id": task.task_id, + "correlation_id": correlation_id, + "request_id": request_id, + } + + +@app.get("/health") +async def health_check() -> dict: + """Health check endpoint.""" + return {"status": "healthy"} + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, port=8000) diff --git a/fastapi_request_context/contrib/__init__.py b/fastapi_request_context/contrib/__init__.py new file mode 100644 index 0000000..68415b3 --- /dev/null +++ b/fastapi_request_context/contrib/__init__.py @@ -0,0 +1 @@ +"""Contrib integrations for external libraries.""" diff --git a/fastapi_request_context/contrib/taskiq/__init__.py b/fastapi_request_context/contrib/taskiq/__init__.py new file mode 100644 index 0000000..0e29d91 --- /dev/null +++ b/fastapi_request_context/contrib/taskiq/__init__.py @@ -0,0 +1,17 @@ +"""Taskiq integration for request context propagation. + +This module provides middleware for Taskiq that propagates request context +from FastAPI requests to background tasks, enabling distributed tracing and +consistent logging across async task execution. + +Example: + >>> from taskiq import InMemoryBroker + >>> from fastapi_request_context.contrib.taskiq import RequestContextTaskiqMiddleware + >>> + >>> broker = InMemoryBroker() + >>> broker = broker.with_middlewares(RequestContextTaskiqMiddleware()) +""" + +from fastapi_request_context.contrib.taskiq.middleware import RequestContextTaskiqMiddleware + +__all__ = ["RequestContextTaskiqMiddleware"] diff --git a/fastapi_request_context/contrib/taskiq/middleware.py b/fastapi_request_context/contrib/taskiq/middleware.py new file mode 100644 index 0000000..c3af8e5 --- /dev/null +++ b/fastapi_request_context/contrib/taskiq/middleware.py @@ -0,0 +1,118 @@ +"""Taskiq middleware for request context propagation.""" + +import json +from contextvars import ContextVar +from typing import TYPE_CHECKING, Any + +from fastapi_request_context.context import get_adapter, get_full_context, set_context +from fastapi_request_context.fields import StandardContextField + +if TYPE_CHECKING: + from taskiq import TaskiqMessage, TaskiqResult + + from fastapi_request_context.adapters.base import ContextAdapter + + +try: + from taskiq import TaskiqMiddleware +except ImportError as e: + msg = "taskiq is required for RequestContextTaskiqMiddleware. Install with: pip install taskiq" + raise ImportError(msg) from e + + +class RequestContextTaskiqMiddleware(TaskiqMiddleware): + """Taskiq middleware that propagates request context to background tasks. + + This middleware: + 1. Captures the current request context when a task is sent + 2. Removes the REQUEST_ID (each task gets its own) + 3. Serializes the context and attaches it to the task message + 4. Restores the context when the task executes + 5. Adds the TASK_ID to the restored context + + Example: + >>> from taskiq import InMemoryBroker + >>> from fastapi_request_context.contrib.taskiq import RequestContextTaskiqMiddleware + >>> + >>> broker = InMemoryBroker() + >>> broker = broker.with_middlewares(RequestContextTaskiqMiddleware()) + >>> + >>> @broker.task + ... async def my_task(): + ... # Context from the originating request is available here + ... correlation_id = get_context(StandardContextField.CORRELATION_ID) + ... task_id = get_context(StandardContextField.TASK_ID) + + Note: + This middleware requires the context-logging adapter or a compatible + adapter that supports context manager protocol for proper cleanup. + """ + + REQUEST_CONTEXT_LABEL = "X-Request-Context" + + def __init__(self) -> None: + """Initialize the middleware with a context variable for tracking.""" + super().__init__() + self._current_context: ContextVar[ContextAdapter | None] = ContextVar( + "current_taskiq_context", + default=None, + ) + + def pre_send(self, message: "TaskiqMessage") -> "TaskiqMessage": + """Capture and attach current request context to the task message. + + Args: + message: The task message being sent. + + Returns: + Modified message with request context attached. + """ + full_context = get_full_context() + full_context.pop(StandardContextField.REQUEST_ID.value, None) + context_str = json.dumps(full_context) + + return message.model_copy( + update={ + "labels": message.labels | {self.REQUEST_CONTEXT_LABEL: context_str}, + }, + ) + + def pre_execute(self, message: "TaskiqMessage") -> "TaskiqMessage": + """Restore request context before task execution. + + Args: + message: The task message being executed. + + Returns: + The original message (unmodified). + """ + context_str = message.labels.get(self.REQUEST_CONTEXT_LABEL, "{}") + context_data = json.loads(context_str) + + context_data[StandardContextField.TASK_ID.value] = message.task_id + + adapter = get_adapter() + adapter.__enter__() + + for key, value in context_data.items(): + set_context(key, value) + + self._current_context.set(adapter) + + return message + + def post_save( + self, + message: "TaskiqMessage", # noqa: ARG002 + result: "TaskiqResult[Any]", # noqa: ARG002 + ) -> None: + """Clean up context after task execution. + + Args: + message: The task message that was executed. + result: The task execution result. + """ + adapter = self._current_context.get() + if adapter is not None: + adapter.__exit__(None, None, None) + self._current_context.set(None) diff --git a/fastapi_request_context/fields.py b/fastapi_request_context/fields.py index 137c865..97f75ff 100644 --- a/fastapi_request_context/fields.py +++ b/fastapi_request_context/fields.py @@ -23,3 +23,6 @@ class StandardContextField(StrEnum): CORRELATION_ID = "correlation_id" """Correlation ID for distributed tracing. May be from header or generated.""" + + TASK_ID = "task_id" + """Unique identifier for background task execution.""" diff --git a/pyproject.toml b/pyproject.toml index 4c4ee99..064311e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,10 +30,12 @@ dependencies = [ [project.optional-dependencies] context-logging = ["context-logging>=0.7.0"] json-formatter = ["python-json-logger>=2.0.0"] +taskiq = ["taskiq>=0.11.0"] build = ["uv>=0.9.0"] all = [ "context-logging>=0.7.0", "python-json-logger>=2.0.0", + "taskiq>=0.11.0", ] [project.urls] @@ -69,6 +71,7 @@ dev = [ "context-logging>=0.7.0", "python-json-logger>=2.0.0", "httpx>=0.27.0", + "taskiq>=0.11.0", ] [tool.tox] @@ -193,6 +196,10 @@ ignore_missing_imports = true module = "pythonjsonlogger.*" ignore_missing_imports = true +[[tool.mypy.overrides]] +module = "taskiq.*" +ignore_missing_imports = true + [tool.ruff] target-version = "py312" line-length = 100 diff --git a/tests/contrib/__init__.py b/tests/contrib/__init__.py new file mode 100644 index 0000000..c3f8e5a --- /dev/null +++ b/tests/contrib/__init__.py @@ -0,0 +1 @@ +"""Tests for contrib modules.""" diff --git a/tests/contrib/test_taskiq_middleware.py b/tests/contrib/test_taskiq_middleware.py new file mode 100644 index 0000000..14e97f2 --- /dev/null +++ b/tests/contrib/test_taskiq_middleware.py @@ -0,0 +1,442 @@ +"""Tests for Taskiq middleware integration.""" + +import json +from typing import Any + +import pytest +from taskiq import InMemoryBroker, TaskiqMessage, TaskiqResult + +from fastapi_request_context import ( + get_context, + get_full_context, + set_context, +) +from fastapi_request_context.adapters.base import ContextAdapter +from fastapi_request_context.adapters.contextvars import ContextVarsAdapter +from fastapi_request_context.context import set_adapter +from fastapi_request_context.contrib.taskiq import RequestContextTaskiqMiddleware +from fastapi_request_context.fields import StandardContextField + + +@pytest.fixture(autouse=True) +def reset_context_adapter() -> None: + """Reset context adapter before each test.""" + adapter = ContextVarsAdapter() + set_adapter(adapter) + + +def test_initialization() -> None: + """Test middleware initializes correctly.""" + middleware = RequestContextTaskiqMiddleware() + assert middleware.REQUEST_CONTEXT_LABEL == "X-Request-Context" + assert middleware._current_context.get() is None + + +def test_pre_send_captures_context() -> None: + """Test that pre_send captures and serializes current context.""" + middleware = RequestContextTaskiqMiddleware() + + adapter = ContextVarsAdapter() + with adapter: + set_context(StandardContextField.CORRELATION_ID, "test-correlation-123") + set_context(StandardContextField.REQUEST_ID, "should-be-removed") + set_context("user_id", 42) + set_context("custom_field", "custom_value") + + message = TaskiqMessage( + task_id="task-123", + task_name="test_task", + labels={}, + args=[], + kwargs={}, + ) + + result_message = middleware.pre_send(message) + + assert middleware.REQUEST_CONTEXT_LABEL in result_message.labels + context_str = result_message.labels[middleware.REQUEST_CONTEXT_LABEL] + context_data = json.loads(context_str) + + assert context_data["correlation_id"] == "test-correlation-123" + assert context_data["user_id"] == 42 + assert context_data["custom_field"] == "custom_value" + assert "request_id" not in context_data + + +def test_pre_send_preserves_existing_labels() -> None: + """Test that pre_send preserves existing message labels.""" + middleware = RequestContextTaskiqMiddleware() + + adapter = ContextVarsAdapter() + with adapter: + set_context("test_key", "test_value") + + message = TaskiqMessage( + task_id="task-123", + task_name="test_task", + labels={"existing_label": "existing_value"}, + args=[], + kwargs={}, + ) + + result_message = middleware.pre_send(message) + + assert "existing_label" in result_message.labels + assert result_message.labels["existing_label"] == "existing_value" + assert middleware.REQUEST_CONTEXT_LABEL in result_message.labels + + +def test_pre_send_with_empty_context() -> None: + """Test that pre_send works with empty context.""" + middleware = RequestContextTaskiqMiddleware() + + message = TaskiqMessage( + task_id="task-123", + task_name="test_task", + labels={}, + args=[], + kwargs={}, + ) + + result_message = middleware.pre_send(message) + + assert middleware.REQUEST_CONTEXT_LABEL in result_message.labels + context_str = result_message.labels[middleware.REQUEST_CONTEXT_LABEL] + context_data = json.loads(context_str) + assert isinstance(context_data, dict) + + +def test_pre_execute_restores_context() -> None: + """Test that pre_execute restores context from message.""" + middleware = RequestContextTaskiqMiddleware() + + context_data = { + "correlation_id": "restored-correlation-456", + "user_id": 99, + "org_id": "org-abc", + } + context_str = json.dumps(context_data) + + message = TaskiqMessage( + task_id="task-456", + task_name="test_task", + labels={middleware.REQUEST_CONTEXT_LABEL: context_str}, + args=[], + kwargs={}, + ) + + result_message = middleware.pre_execute(message) + + assert result_message == message + assert get_context(StandardContextField.CORRELATION_ID) == "restored-correlation-456" + assert get_context(StandardContextField.TASK_ID) == "task-456" + assert get_context("user_id") == 99 + assert get_context("org_id") == "org-abc" + + middleware.post_save( + message, + TaskiqResult(is_err=False, log=None, return_value=None, execution_time=0.0), + ) + + +def test_pre_execute_adds_task_id() -> None: + """Test that pre_execute adds TASK_ID to context.""" + middleware = RequestContextTaskiqMiddleware() + + message = TaskiqMessage( + task_id="task-789", + task_name="test_task", + labels={middleware.REQUEST_CONTEXT_LABEL: "{}"}, + args=[], + kwargs={}, + ) + + middleware.pre_execute(message) + + assert get_context(StandardContextField.TASK_ID) == "task-789" + + middleware.post_save( + message, + TaskiqResult(is_err=False, log=None, return_value=None, execution_time=0.0), + ) + + +def test_pre_execute_without_context_label() -> None: + """Test that pre_execute handles missing context label.""" + middleware = RequestContextTaskiqMiddleware() + + message = TaskiqMessage( + task_id="task-999", + task_name="test_task", + labels={}, + args=[], + kwargs={}, + ) + + result_message = middleware.pre_execute(message) + + assert result_message == message + assert get_context(StandardContextField.TASK_ID) == "task-999" + full_context = get_full_context() + assert StandardContextField.TASK_ID.value in full_context + + middleware.post_save( + message, + TaskiqResult(is_err=False, log=None, return_value=None, execution_time=0.0), + ) + + +def test_post_save_cleans_up_context() -> None: + """Test that post_save properly cleans up context.""" + middleware = RequestContextTaskiqMiddleware() + + message = TaskiqMessage( + task_id="task-cleanup", + task_name="test_task", + labels={middleware.REQUEST_CONTEXT_LABEL: json.dumps({"test_key": "test_value"})}, + args=[], + kwargs={}, + ) + + middleware.pre_execute(message) + assert get_context("test_key") == "test_value" + assert get_context(StandardContextField.TASK_ID) == "task-cleanup" + + result: TaskiqResult[Any] = TaskiqResult( + is_err=False, + log=None, + return_value={"status": "success"}, + execution_time=0.0, + ) + middleware.post_save(message, result) + + assert middleware._current_context.get() is None + + +def test_post_save_without_context() -> None: + """Test that post_save handles case when no context was set.""" + middleware = RequestContextTaskiqMiddleware() + + message = TaskiqMessage( + task_id="task-no-context", + task_name="test_task", + labels={}, + args=[], + kwargs={}, + ) + + result: TaskiqResult[Any] = TaskiqResult( + is_err=False, + log=None, + return_value=None, + execution_time=0.0, + ) + middleware.post_save(message, result) + + +def test_full_lifecycle() -> None: + """Test complete lifecycle: pre_send -> pre_execute -> post_save.""" + middleware = RequestContextTaskiqMiddleware() + + adapter = ContextVarsAdapter() + with adapter: + set_context(StandardContextField.CORRELATION_ID, "lifecycle-correlation") + set_context(StandardContextField.REQUEST_ID, "lifecycle-request-123") + set_context("user_id", 777) + + send_message = TaskiqMessage( + task_id="lifecycle-task", + task_name="test_task", + labels={}, + args=[], + kwargs={}, + ) + + sent_message = middleware.pre_send(send_message) + assert middleware.REQUEST_CONTEXT_LABEL in sent_message.labels + + context_str = sent_message.labels[middleware.REQUEST_CONTEXT_LABEL] + context_data = json.loads(context_str) + assert "request_id" not in context_data + assert context_data["correlation_id"] == "lifecycle-correlation" + assert context_data["user_id"] == 777 + + execute_message = TaskiqMessage( + task_id="lifecycle-task", + task_name="test_task", + labels=sent_message.labels, + args=[], + kwargs={}, + ) + + middleware.pre_execute(execute_message) + assert get_context(StandardContextField.CORRELATION_ID) == "lifecycle-correlation" + assert get_context(StandardContextField.TASK_ID) == "lifecycle-task" + assert get_context("user_id") == 777 + + result: TaskiqResult[Any] = TaskiqResult( + is_err=False, + log=None, + return_value="done", + execution_time=0.0, + ) + middleware.post_save(execute_message, result) + + +def test_context_isolation_between_tasks() -> None: + """Test that context is properly isolated between different task executions.""" + middleware = RequestContextTaskiqMiddleware() + + message1 = TaskiqMessage( + task_id="task-1", + task_name="test_task", + labels={middleware.REQUEST_CONTEXT_LABEL: json.dumps({"user_id": 1})}, + args=[], + kwargs={}, + ) + + middleware.pre_execute(message1) + assert get_context("user_id") == 1 + assert get_context(StandardContextField.TASK_ID) == "task-1" + + result1: TaskiqResult[Any] = TaskiqResult( + is_err=False, + log=None, + return_value=None, + execution_time=0.0, + ) + middleware.post_save(message1, result1) + + message2 = TaskiqMessage( + task_id="task-2", + task_name="test_task", + labels={middleware.REQUEST_CONTEXT_LABEL: json.dumps({"user_id": 2})}, + args=[], + kwargs={}, + ) + + middleware.pre_execute(message2) + assert get_context("user_id") == 2 + assert get_context(StandardContextField.TASK_ID) == "task-2" + + result2: TaskiqResult[Any] = TaskiqResult( + is_err=False, + log=None, + return_value=None, + execution_time=0.0, + ) + middleware.post_save(message2, result2) + + +def test_adapter_context_manager_called() -> None: + """Test that adapter __enter__ and __exit__ are called.""" + middleware = RequestContextTaskiqMiddleware() + + message = TaskiqMessage( + task_id="adapter-test", + task_name="test_task", + labels={middleware.REQUEST_CONTEXT_LABEL: json.dumps({"key": "value"})}, + args=[], + kwargs={}, + ) + + middleware.pre_execute(message) + adapter = middleware._current_context.get() + assert adapter is not None + assert isinstance(adapter, ContextAdapter) + + result: TaskiqResult[Any] = TaskiqResult( + is_err=False, + log=None, + return_value=None, + execution_time=0.0, + ) + middleware.post_save(message, result) + assert middleware._current_context.get() is None + + +async def test_integration_with_inmemory_broker() -> None: + """Test middleware integration with InMemoryBroker.""" + broker = InMemoryBroker() + broker = broker.with_middlewares(RequestContextTaskiqMiddleware()) + + @broker.task + async def test_task(value: int) -> dict[str, Any]: + correlation_id = get_context(StandardContextField.CORRELATION_ID) + task_id = get_context(StandardContextField.TASK_ID) + user_id = get_context("user_id") + + return { + "correlation_id": correlation_id, + "task_id": task_id, + "user_id": user_id, + "value": value, + } + + adapter = ContextVarsAdapter() + with adapter: + set_context(StandardContextField.CORRELATION_ID, "integration-test-123") + set_context(StandardContextField.REQUEST_ID, "should-not-propagate") + set_context("user_id", 999) + task = await test_task.kiq(42) + + result = await task.wait_result() + + assert not result.is_err + assert result.return_value["correlation_id"] == "integration-test-123" + assert result.return_value["user_id"] == 999 + assert result.return_value["value"] == 42 + assert result.return_value["task_id"] == task.task_id + + +async def test_multiple_tasks_with_different_contexts() -> None: + """Test that different tasks get their own context properly.""" + broker = InMemoryBroker() + broker = broker.with_middlewares(RequestContextTaskiqMiddleware()) + + @broker.task + async def context_reader() -> dict[str, Any]: + return { + "correlation_id": get_context(StandardContextField.CORRELATION_ID), + "task_id": get_context(StandardContextField.TASK_ID), + "user_id": get_context("user_id"), + } + + adapter = ContextVarsAdapter() + with adapter: + set_context(StandardContextField.CORRELATION_ID, "context-1") + set_context("user_id", 100) + task1 = await context_reader.kiq() + + set_context(StandardContextField.CORRELATION_ID, "context-2") + set_context("user_id", 200) + task2 = await context_reader.kiq() + + result1 = await task1.wait_result() + result2 = await task2.wait_result() + + assert result1.return_value["correlation_id"] == "context-1" + assert result1.return_value["user_id"] == 100 + assert result2.return_value["correlation_id"] == "context-2" + assert result2.return_value["user_id"] == 200 + + +def test_import_error_message() -> None: + """Test that helpful error message is raised when taskiq is not available.""" + import importlib + import sys + from unittest.mock import patch + + def reload_middleware() -> None: + import fastapi_request_context.contrib.taskiq.middleware + + importlib.reload(fastapi_request_context.contrib.taskiq.middleware) + + with ( + patch.dict(sys.modules, {"taskiq": None}), + pytest.raises( + ImportError, + match="taskiq is required", + ), + ): + reload_middleware() diff --git a/uv.lock b/uv.lock index eae851d..fa912bb 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,113 @@ version = 1 revision = 3 requires-python = ">=3.12" +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -34,6 +141,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, ] +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + [[package]] name = "cachetools" version = "6.2.2" @@ -211,6 +327,7 @@ dependencies = [ all = [ { name = "context-logging" }, { name = "python-json-logger" }, + { name = "taskiq" }, ] build = [ { name = "uv" }, @@ -221,6 +338,9 @@ context-logging = [ json-formatter = [ { name = "python-json-logger" }, ] +taskiq = [ + { name = "taskiq" }, +] [package.dev-dependencies] dev = [ @@ -232,6 +352,7 @@ dev = [ { name = "pytest-asyncio" }, { name = "python-json-logger" }, { name = "ruff" }, + { name = "taskiq" }, { name = "tox" }, { name = "tox-uv" }, ] @@ -243,9 +364,11 @@ requires-dist = [ { name = "fastapi", specifier = ">=0.100.0" }, { name = "python-json-logger", marker = "extra == 'all'", specifier = ">=2.0.0" }, { name = "python-json-logger", marker = "extra == 'json-formatter'", specifier = ">=2.0.0" }, + { name = "taskiq", marker = "extra == 'all'", specifier = ">=0.11.0" }, + { name = "taskiq", marker = "extra == 'taskiq'", specifier = ">=0.11.0" }, { name = "uv", marker = "extra == 'build'", specifier = ">=0.9.0" }, ] -provides-extras = ["context-logging", "json-formatter", "build", "all"] +provides-extras = ["context-logging", "json-formatter", "taskiq", "build", "all"] [package.metadata.requires-dev] dev = [ @@ -257,6 +380,7 @@ dev = [ { name = "pytest-asyncio", specifier = ">=0.23.0" }, { name = "python-json-logger", specifier = ">=2.0.0" }, { name = "ruff", specifier = ">=0.3.5" }, + { name = "taskiq", specifier = ">=0.11.0" }, { name = "tox", specifier = ">=4.14.2" }, { name = "tox-uv", specifier = ">=1.0.0" }, ] @@ -270,6 +394,95 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, ] +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -325,6 +538,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "izulu" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/58/6d6335c78b7ade54d8a6c6dbaa589e5c21b3fd916341d5a16f774c72652a/izulu-0.50.0.tar.gz", hash = "sha256:cc8e252d5e8560c70b95380295008eeb0786f7b745a405a40d3556ab3252d5f5", size = 48558, upload-time = "2025-03-24T15:52:21.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/9f/bf9d33546bbb6e5e80ebafe46f90b7d8b4a77410b7b05160b0ca8978c15a/izulu-0.50.0-py3-none-any.whl", hash = "sha256:4e9ae2508844e7c5f62c468a8b9e2deba2f60325ef63f01e65b39fd9a6b3fab4", size = 18095, upload-time = "2025-03-24T15:52:19.667Z" }, +] + [[package]] name = "librt" version = "0.6.2" @@ -377,6 +599,105 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/ee/9e30b435bc341844603fb209150594b1a801ced7ddb04be7dd2003a694d2/librt-0.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:de06350dfbf0649c0458e0af95fa516886120d0d11ed4ebbfcb7f67b038ab393", size = 20246, upload-time = "2025-11-18T16:51:04.724Z" }, ] +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + [[package]] name = "mypy" version = "1.19.0" @@ -455,6 +776,99 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "pycron" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5d/340be12ae4a69c33102dfb6ddc1dc6e53e69b2d504fa26b5d34a472c3057/pycron-3.2.0.tar.gz", hash = "sha256:e125a28aca0295769541a40633f70b602579df48c9cb357c36c28d2628ba2b13", size = 4248, upload-time = "2025-06-05T13:24:12.636Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/76/caf316909f4545e7158e0e1defd8956a1da49f4af04f5d16b18c358dfeac/pycron-3.2.0-py3-none-any.whl", hash = "sha256:6d2349746270bd642b71b9f7187cf13f4d9ee2412b4710396a507b5fe4f60dac", size = 4904, upload-time = "2025-06-05T13:24:11.477Z" }, +] + [[package]] name = "pydantic" version = "2.12.5" @@ -648,6 +1062,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, ] +[[package]] +name = "taskiq" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "anyio" }, + { name = "izulu" }, + { name = "packaging" }, + { name = "pycron" }, + { name = "pydantic" }, + { name = "taskiq-dependencies" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/cf/c4a47be05d85754f3e0ecc7b72131249adc067ea37517054459e94268fb1/taskiq-0.12.1.tar.gz", hash = "sha256:338dcf58eaca327e511a9380b2185bfa6a415dd79a5cf144546a2dbb95459298", size = 60536, upload-time = "2025-12-07T16:07:43.561Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/e4/a2fda3bcbb8b61108dc8e9db1a2d19a23578953db73e981f66b9d44f1207/taskiq-0.12.1-py3-none-any.whl", hash = "sha256:a8ade45e2e23edbadb972a88dec44e68c7daef83383d01fa3af48594a24a712a", size = 90668, upload-time = "2025-12-07T16:07:42.296Z" }, +] + +[[package]] +name = "taskiq-dependencies" +version = "1.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/90/47a627696e53bfdcacabc3e8c05b73bf1424685bcb5f17209cb8b12da1bf/taskiq_dependencies-1.5.7.tar.gz", hash = "sha256:0d3b240872ef152b719153b9526d866d2be978aeeaea6600e878414babc2dcb4", size = 14875, upload-time = "2025-02-26T22:07:39.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/6d/4a012f2de002c2e93273f5e7d3e3feea02f7fdbb7b75ca2ca1dd10703091/taskiq_dependencies-1.5.7-py3-none-any.whl", hash = "sha256:6fcee5d159bdb035ef915d4d848826169b6f06fe57cc2297a39b62ea3e76036f", size = 13801, upload-time = "2025-02-26T22:07:38.622Z" }, +] + [[package]] name = "tox" version = "4.32.0" @@ -811,3 +1252,97 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/99/8a06b8e17dddbf321325ae4eb12465804120f699cd1b8a355718300c62da/wrapt-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:35cdbd478607036fee40273be8ed54a451f5f23121bd9d4be515158f9498f7ad", size = 60634, upload-time = "2025-11-07T00:45:02.087Z" }, { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, ] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +]