httpware 0.8.3__tar.gz → 0.8.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {httpware-0.8.3 → httpware-0.8.5}/PKG-INFO +1 -1
- {httpware-0.8.3 → httpware-0.8.5}/pyproject.toml +1 -1
- httpware-0.8.5/src/httpware/_internal/import_checker.py +26 -0
- {httpware-0.8.3 → httpware-0.8.5}/src/httpware/_internal/observability.py +9 -3
- {httpware-0.8.3 → httpware-0.8.5}/src/httpware/decoders/pydantic.py +8 -7
- {httpware-0.8.3 → httpware-0.8.5}/src/httpware/middleware/chain.py +5 -7
- httpware-0.8.3/src/httpware/_internal/import_checker.py +0 -8
- {httpware-0.8.3 → httpware-0.8.5}/README.md +0 -0
- {httpware-0.8.3 → httpware-0.8.5}/src/httpware/__init__.py +0 -0
- {httpware-0.8.3 → httpware-0.8.5}/src/httpware/_internal/__init__.py +0 -0
- {httpware-0.8.3 → httpware-0.8.5}/src/httpware/_internal/exception_mapping.py +0 -0
- {httpware-0.8.3 → httpware-0.8.5}/src/httpware/_internal/status.py +0 -0
- {httpware-0.8.3 → httpware-0.8.5}/src/httpware/client.py +0 -0
- {httpware-0.8.3 → httpware-0.8.5}/src/httpware/decoders/__init__.py +0 -0
- {httpware-0.8.3 → httpware-0.8.5}/src/httpware/decoders/msgspec.py +0 -0
- {httpware-0.8.3 → httpware-0.8.5}/src/httpware/errors.py +0 -0
- {httpware-0.8.3 → httpware-0.8.5}/src/httpware/middleware/__init__.py +0 -0
- {httpware-0.8.3 → httpware-0.8.5}/src/httpware/middleware/resilience/__init__.py +0 -0
- {httpware-0.8.3 → httpware-0.8.5}/src/httpware/middleware/resilience/_backoff.py +0 -0
- {httpware-0.8.3 → httpware-0.8.5}/src/httpware/middleware/resilience/budget.py +0 -0
- {httpware-0.8.3 → httpware-0.8.5}/src/httpware/middleware/resilience/bulkhead.py +0 -0
- {httpware-0.8.3 → httpware-0.8.5}/src/httpware/middleware/resilience/retry.py +0 -0
- {httpware-0.8.3 → httpware-0.8.5}/src/httpware/py.typed +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Detect optional extras without importing them. Used by adapter modules to gate hard imports."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, distribution
|
|
4
|
+
from importlib.util import find_spec
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _is_distribution_installed(name: str) -> bool:
|
|
8
|
+
"""Probe the package registry for a distribution by name. No sys.modules side effects."""
|
|
9
|
+
try:
|
|
10
|
+
distribution(name)
|
|
11
|
+
except PackageNotFoundError:
|
|
12
|
+
return False
|
|
13
|
+
return True
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
is_msgspec_installed = find_spec("msgspec") is not None
|
|
17
|
+
is_pydantic_installed = find_spec("pydantic") is not None
|
|
18
|
+
# opentelemetry/ is a PEP 420 namespace package — instrumentation packages create the
|
|
19
|
+
# directory even without opentelemetry-api. find_spec("opentelemetry") therefore returns
|
|
20
|
+
# non-None regardless of whether the api package is present, and
|
|
21
|
+
# find_spec("opentelemetry.trace") populates sys.modules with the namespace parent as a
|
|
22
|
+
# CPython side-effect, breaking the isolation guarantee.
|
|
23
|
+
# importlib.metadata.distribution probes the package registry instead: it returns the
|
|
24
|
+
# distribution when opentelemetry-api is installed and raises PackageNotFoundError when
|
|
25
|
+
# it is absent, with no sys.modules side effects.
|
|
26
|
+
is_otel_installed = _is_distribution_installed("opentelemetry-api")
|
|
@@ -29,7 +29,9 @@ def _emit_event(
|
|
|
29
29
|
``trace.get_current_span().add_event(event_name, attributes=attributes)``.
|
|
30
30
|
When no tracer is active, ``get_current_span()`` returns a ``NonRecordingSpan``
|
|
31
31
|
whose ``add_event`` is a documented no-op — so the call is unconditional
|
|
32
|
-
behind the install gate.
|
|
32
|
+
behind the install gate. If the install gate is wrong (the namespace exists
|
|
33
|
+
but the api package is missing or broken), the lazy import raises
|
|
34
|
+
``ImportError``; we degrade silently to log-only emission.
|
|
33
35
|
|
|
34
36
|
The lazy ``from opentelemetry import trace`` inside the if-block preserves
|
|
35
37
|
the optional-extras isolation invariant: ``import httpware`` must not pull
|
|
@@ -37,6 +39,10 @@ def _emit_event(
|
|
|
37
39
|
"""
|
|
38
40
|
logger.log(level, message, extra=attributes)
|
|
39
41
|
if import_checker.is_otel_installed:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
try:
|
|
43
|
+
from opentelemetry import trace # noqa: PLC0415 — lazy by design (optional-extras isolation)
|
|
44
|
+
except ImportError:
|
|
45
|
+
# opentelemetry namespace exists but the api package is broken or missing —
|
|
46
|
+
# degrade to log-only emission. The structured log record above has already fired.
|
|
47
|
+
return
|
|
42
48
|
trace.get_current_span().add_event(event_name, attributes=attributes)
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
"""PydanticDecoder — module-level cached TypeAdapter adapter for ResponseDecoder.
|
|
2
2
|
|
|
3
|
-
Requires the `pydantic` extra: `pip install httpware[pydantic]`.
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Requires the `pydantic` extra: `pip install httpware[pydantic]`. The optional-extras
|
|
4
|
+
gate is enforced upstream — `client.py:_default_pydantic_decoder()` raises
|
|
5
|
+
ImportError when pydantic is absent, so this module is never imported in that
|
|
6
|
+
path. Tests simulating "pydantic not installed" patch
|
|
7
|
+
`import_checker.is_pydantic_installed=False` at runtime, after this module is
|
|
8
|
+
already loaded; `PydanticDecoder.__init__` then raises ImportError with the
|
|
6
9
|
install hint.
|
|
7
10
|
"""
|
|
8
11
|
|
|
9
12
|
import functools
|
|
10
13
|
from typing import TypeVar
|
|
11
14
|
|
|
12
|
-
from
|
|
13
|
-
|
|
15
|
+
from pydantic import TypeAdapter
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
from pydantic import TypeAdapter
|
|
17
|
+
from httpware._internal import import_checker
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
MISSING_DEPENDENCY_MESSAGE = (
|
|
@@ -5,16 +5,14 @@ from collections.abc import Awaitable, Callable, Sequence
|
|
|
5
5
|
|
|
6
6
|
import httpx2
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
if typing.TYPE_CHECKING:
|
|
10
|
-
from httpware.middleware import AsyncMiddleware, Middleware
|
|
8
|
+
from httpware.middleware import AsyncMiddleware, Middleware
|
|
11
9
|
|
|
12
10
|
|
|
13
11
|
_AsyncNext: typing.TypeAlias = Callable[[httpx2.Request], Awaitable[httpx2.Response]]
|
|
14
12
|
_Next: typing.TypeAlias = Callable[[httpx2.Request], httpx2.Response]
|
|
15
13
|
|
|
16
14
|
|
|
17
|
-
def compose_async(middleware:
|
|
15
|
+
def compose_async(middleware: Sequence[AsyncMiddleware], terminal: _AsyncNext) -> _AsyncNext:
|
|
18
16
|
"""Fold `middleware` into a single callable around `terminal`.
|
|
19
17
|
|
|
20
18
|
The first middleware in the sequence is the outermost wrapper.
|
|
@@ -25,14 +23,14 @@ def compose_async(middleware: "Sequence[AsyncMiddleware]", terminal: _AsyncNext)
|
|
|
25
23
|
return dispatch
|
|
26
24
|
|
|
27
25
|
|
|
28
|
-
def _wrap(layer:
|
|
26
|
+
def _wrap(layer: AsyncMiddleware, inner: _AsyncNext) -> _AsyncNext:
|
|
29
27
|
async def call(request: httpx2.Request) -> httpx2.Response:
|
|
30
28
|
return await layer(request, inner)
|
|
31
29
|
|
|
32
30
|
return call
|
|
33
31
|
|
|
34
32
|
|
|
35
|
-
def compose(middleware:
|
|
33
|
+
def compose(middleware: Sequence[Middleware], terminal: _Next) -> _Next:
|
|
36
34
|
"""Fold sync `middleware` into a single callable around sync `terminal`.
|
|
37
35
|
|
|
38
36
|
The first middleware in the sequence is the outermost wrapper.
|
|
@@ -43,7 +41,7 @@ def compose(middleware: "Sequence[Middleware]", terminal: _Next) -> _Next:
|
|
|
43
41
|
return dispatch
|
|
44
42
|
|
|
45
43
|
|
|
46
|
-
def _wrap_sync(layer:
|
|
44
|
+
def _wrap_sync(layer: Middleware, inner: _Next) -> _Next:
|
|
47
45
|
def call(request: httpx2.Request) -> httpx2.Response:
|
|
48
46
|
return layer(request, inner)
|
|
49
47
|
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
"""Detect optional extras without importing them. Used by adapter modules to gate hard imports."""
|
|
2
|
-
|
|
3
|
-
from importlib.util import find_spec
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
is_msgspec_installed = find_spec("msgspec") is not None
|
|
7
|
-
is_pydantic_installed = find_spec("pydantic") is not None
|
|
8
|
-
is_otel_installed = find_spec("opentelemetry") is not None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|