provide-foundation 0.0.0.dev1__py3-none-any.whl → 0.0.0.dev3__py3-none-any.whl
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.
- provide/foundation/__init__.py +36 -10
- provide/foundation/archive/__init__.py +1 -1
- provide/foundation/archive/base.py +15 -14
- provide/foundation/archive/bzip2.py +40 -40
- provide/foundation/archive/gzip.py +42 -42
- provide/foundation/archive/operations.py +93 -96
- provide/foundation/archive/tar.py +33 -31
- provide/foundation/archive/zip.py +52 -50
- provide/foundation/asynctools/__init__.py +20 -0
- provide/foundation/asynctools/core.py +126 -0
- provide/foundation/cli/__init__.py +2 -2
- provide/foundation/cli/commands/deps.py +15 -9
- provide/foundation/cli/commands/logs/__init__.py +3 -3
- provide/foundation/cli/commands/logs/generate.py +2 -2
- provide/foundation/cli/commands/logs/query.py +4 -4
- provide/foundation/cli/commands/logs/send.py +3 -3
- provide/foundation/cli/commands/logs/tail.py +3 -3
- provide/foundation/cli/decorators.py +11 -11
- provide/foundation/cli/main.py +1 -1
- provide/foundation/cli/testing.py +2 -40
- provide/foundation/cli/utils.py +21 -18
- provide/foundation/config/__init__.py +35 -2
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +477 -0
- provide/foundation/config/defaults.py +67 -0
- provide/foundation/config/env.py +6 -20
- provide/foundation/config/loader.py +10 -4
- provide/foundation/config/sync.py +8 -6
- provide/foundation/config/types.py +5 -5
- provide/foundation/config/validators.py +4 -4
- provide/foundation/console/input.py +5 -5
- provide/foundation/console/output.py +36 -14
- provide/foundation/context/__init__.py +8 -4
- provide/foundation/context/core.py +88 -110
- provide/foundation/crypto/certificates/__init__.py +9 -5
- provide/foundation/crypto/certificates/base.py +2 -2
- provide/foundation/crypto/certificates/certificate.py +48 -19
- provide/foundation/crypto/certificates/factory.py +26 -18
- provide/foundation/crypto/certificates/generator.py +24 -23
- provide/foundation/crypto/certificates/loader.py +24 -16
- provide/foundation/crypto/certificates/operations.py +17 -10
- provide/foundation/crypto/certificates/trust.py +21 -21
- provide/foundation/env/__init__.py +28 -0
- provide/foundation/env/core.py +218 -0
- provide/foundation/errors/__init__.py +3 -3
- provide/foundation/errors/decorators.py +0 -234
- provide/foundation/errors/types.py +0 -98
- provide/foundation/eventsets/display.py +13 -14
- provide/foundation/eventsets/registry.py +61 -31
- provide/foundation/eventsets/resolver.py +50 -46
- provide/foundation/eventsets/sets/das.py +8 -8
- provide/foundation/eventsets/sets/database.py +14 -14
- provide/foundation/eventsets/sets/http.py +21 -21
- provide/foundation/eventsets/sets/llm.py +16 -16
- provide/foundation/eventsets/sets/task_queue.py +13 -13
- provide/foundation/eventsets/types.py +7 -7
- provide/foundation/file/directory.py +14 -23
- provide/foundation/file/lock.py +4 -3
- provide/foundation/hub/components.py +75 -389
- provide/foundation/hub/config.py +157 -0
- provide/foundation/hub/discovery.py +63 -0
- provide/foundation/hub/handlers.py +89 -0
- provide/foundation/hub/lifecycle.py +195 -0
- provide/foundation/hub/manager.py +7 -4
- provide/foundation/hub/processors.py +49 -0
- provide/foundation/integrations/__init__.py +11 -0
- provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
- provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/client.py +14 -14
- provide/foundation/{observability → integrations}/openobserve/commands.py +12 -12
- provide/foundation/integrations/openobserve/config.py +37 -0
- provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/otlp.py +2 -2
- provide/foundation/{observability → integrations}/openobserve/search.py +2 -3
- provide/foundation/{observability → integrations}/openobserve/streaming.py +5 -5
- provide/foundation/logger/__init__.py +0 -1
- provide/foundation/logger/config/base.py +1 -1
- provide/foundation/logger/config/logging.py +69 -299
- provide/foundation/logger/config/telemetry.py +39 -121
- provide/foundation/logger/factories.py +2 -2
- provide/foundation/logger/processors/main.py +12 -10
- provide/foundation/logger/ratelimit/limiters.py +4 -4
- provide/foundation/logger/ratelimit/processor.py +1 -1
- provide/foundation/logger/setup/coordinator.py +39 -25
- provide/foundation/logger/setup/processors.py +3 -3
- provide/foundation/logger/setup/testing.py +14 -0
- provide/foundation/logger/trace.py +5 -5
- provide/foundation/metrics/__init__.py +1 -1
- provide/foundation/metrics/otel.py +3 -1
- provide/foundation/observability/__init__.py +3 -3
- provide/foundation/process/__init__.py +9 -0
- provide/foundation/process/exit.py +48 -0
- provide/foundation/process/lifecycle.py +69 -46
- provide/foundation/resilience/__init__.py +36 -0
- provide/foundation/resilience/circuit.py +166 -0
- provide/foundation/resilience/decorators.py +236 -0
- provide/foundation/resilience/fallback.py +208 -0
- provide/foundation/resilience/retry.py +327 -0
- provide/foundation/serialization/__init__.py +16 -0
- provide/foundation/serialization/core.py +70 -0
- provide/foundation/streams/config.py +78 -0
- provide/foundation/streams/console.py +4 -5
- provide/foundation/streams/core.py +5 -2
- provide/foundation/streams/file.py +12 -2
- provide/foundation/testing/__init__.py +29 -9
- provide/foundation/testing/archive/__init__.py +7 -7
- provide/foundation/testing/archive/fixtures.py +58 -54
- provide/foundation/testing/cli.py +30 -20
- provide/foundation/testing/common/__init__.py +13 -15
- provide/foundation/testing/common/fixtures.py +27 -57
- provide/foundation/testing/file/__init__.py +15 -15
- provide/foundation/testing/file/content_fixtures.py +289 -0
- provide/foundation/testing/file/directory_fixtures.py +107 -0
- provide/foundation/testing/file/fixtures.py +42 -516
- provide/foundation/testing/file/special_fixtures.py +145 -0
- provide/foundation/testing/logger.py +89 -8
- provide/foundation/testing/mocking/__init__.py +21 -21
- provide/foundation/testing/mocking/fixtures.py +80 -67
- provide/foundation/testing/process/__init__.py +23 -23
- provide/foundation/testing/process/async_fixtures.py +414 -0
- provide/foundation/testing/process/fixtures.py +48 -571
- provide/foundation/testing/process/subprocess_fixtures.py +210 -0
- provide/foundation/testing/threading/__init__.py +17 -17
- provide/foundation/testing/threading/basic_fixtures.py +105 -0
- provide/foundation/testing/threading/data_fixtures.py +101 -0
- provide/foundation/testing/threading/execution_fixtures.py +278 -0
- provide/foundation/testing/threading/fixtures.py +32 -502
- provide/foundation/testing/threading/sync_fixtures.py +100 -0
- provide/foundation/testing/time/__init__.py +11 -11
- provide/foundation/testing/time/fixtures.py +95 -83
- provide/foundation/testing/transport/__init__.py +9 -9
- provide/foundation/testing/transport/fixtures.py +54 -54
- provide/foundation/time/__init__.py +18 -0
- provide/foundation/time/core.py +63 -0
- provide/foundation/tools/__init__.py +2 -2
- provide/foundation/tools/base.py +68 -67
- provide/foundation/tools/cache.py +69 -74
- provide/foundation/tools/downloader.py +68 -62
- provide/foundation/tools/installer.py +51 -57
- provide/foundation/tools/registry.py +38 -45
- provide/foundation/tools/resolver.py +70 -68
- provide/foundation/tools/verifier.py +39 -50
- provide/foundation/tracer/spans.py +2 -14
- provide/foundation/transport/__init__.py +26 -33
- provide/foundation/transport/base.py +32 -30
- provide/foundation/transport/client.py +44 -49
- provide/foundation/transport/config.py +36 -107
- provide/foundation/transport/errors.py +13 -27
- provide/foundation/transport/http.py +69 -55
- provide/foundation/transport/middleware.py +113 -114
- provide/foundation/transport/registry.py +29 -27
- provide/foundation/transport/types.py +6 -6
- provide/foundation/utils/deps.py +17 -14
- provide/foundation/utils/parsing.py +49 -4
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
- provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
- provide_foundation-0.0.0.dev1.dist-info/RECORD +0 -200
- /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
- /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/top_level.txt +0 -0
@@ -2,164 +2,93 @@
|
|
2
2
|
Transport configuration with Foundation config integration.
|
3
3
|
"""
|
4
4
|
|
5
|
-
import os
|
6
|
-
|
7
5
|
from attrs import define
|
8
6
|
|
9
|
-
from provide.foundation.config import
|
7
|
+
from provide.foundation.config.base import field
|
8
|
+
from provide.foundation.config.converters import (
|
9
|
+
parse_bool_extended,
|
10
|
+
parse_float_with_validation,
|
11
|
+
validate_non_negative,
|
12
|
+
validate_positive,
|
13
|
+
)
|
14
|
+
from provide.foundation.config.env import RuntimeConfig
|
10
15
|
from provide.foundation.config.loader import RuntimeConfigLoader
|
11
16
|
from provide.foundation.config.manager import register_config
|
12
|
-
from provide.foundation.config.types import ConfigSource
|
13
17
|
from provide.foundation.logger import get_logger
|
14
18
|
|
15
19
|
log = get_logger(__name__)
|
16
20
|
|
17
21
|
|
18
22
|
@define(slots=True, repr=False)
|
19
|
-
class TransportConfig(
|
23
|
+
class TransportConfig(RuntimeConfig):
|
20
24
|
"""Base configuration for all transports."""
|
21
|
-
|
25
|
+
|
22
26
|
timeout: float = field(
|
23
27
|
default=30.0,
|
24
28
|
env_var="PROVIDE_TRANSPORT_TIMEOUT",
|
29
|
+
converter=lambda x: parse_float_with_validation(x, min_val=0.0) if x else 30.0,
|
30
|
+
validator=validate_positive,
|
25
31
|
description="Request timeout in seconds",
|
26
32
|
)
|
27
33
|
max_retries: int = field(
|
28
34
|
default=3,
|
29
35
|
env_var="PROVIDE_TRANSPORT_MAX_RETRIES",
|
36
|
+
converter=int,
|
37
|
+
validator=validate_non_negative,
|
30
38
|
description="Maximum number of retry attempts",
|
31
39
|
)
|
32
40
|
retry_backoff_factor: float = field(
|
33
41
|
default=0.5,
|
34
|
-
env_var="PROVIDE_TRANSPORT_RETRY_BACKOFF_FACTOR",
|
42
|
+
env_var="PROVIDE_TRANSPORT_RETRY_BACKOFF_FACTOR",
|
43
|
+
converter=lambda x: parse_float_with_validation(x, min_val=0.0) if x else 0.5,
|
44
|
+
validator=validate_non_negative,
|
35
45
|
description="Backoff multiplier for retries",
|
36
46
|
)
|
37
47
|
verify_ssl: bool = field(
|
38
48
|
default=True,
|
39
49
|
env_var="PROVIDE_TRANSPORT_VERIFY_SSL",
|
50
|
+
converter=parse_bool_extended,
|
40
51
|
description="Whether to verify SSL certificates",
|
41
52
|
)
|
42
|
-
|
43
|
-
@classmethod
|
44
|
-
def from_env(cls, strict: bool = True) -> "TransportConfig":
|
45
|
-
"""Load configuration from environment variables."""
|
46
|
-
config_dict = {}
|
47
|
-
|
48
|
-
if timeout := os.getenv("PROVIDE_TRANSPORT_TIMEOUT"):
|
49
|
-
try:
|
50
|
-
config_dict["timeout"] = float(timeout)
|
51
|
-
except ValueError:
|
52
|
-
if strict:
|
53
|
-
log.warning(
|
54
|
-
"Invalid transport timeout value, using field default",
|
55
|
-
invalid_value=timeout,
|
56
|
-
)
|
57
|
-
|
58
|
-
if max_retries := os.getenv("PROVIDE_TRANSPORT_MAX_RETRIES"):
|
59
|
-
try:
|
60
|
-
config_dict["max_retries"] = int(max_retries)
|
61
|
-
except ValueError:
|
62
|
-
if strict:
|
63
|
-
log.warning(
|
64
|
-
"Invalid max retries value, using field default",
|
65
|
-
invalid_value=max_retries,
|
66
|
-
)
|
67
|
-
|
68
|
-
if backoff := os.getenv("PROVIDE_TRANSPORT_RETRY_BACKOFF_FACTOR"):
|
69
|
-
try:
|
70
|
-
config_dict["retry_backoff_factor"] = float(backoff)
|
71
|
-
except ValueError:
|
72
|
-
if strict:
|
73
|
-
log.warning(
|
74
|
-
"Invalid backoff factor value, using field default",
|
75
|
-
invalid_value=backoff,
|
76
|
-
)
|
77
|
-
|
78
|
-
if verify_ssl := os.getenv("PROVIDE_TRANSPORT_VERIFY_SSL"):
|
79
|
-
config_dict["verify_ssl"] = verify_ssl.lower() == "true"
|
80
|
-
|
81
|
-
config = cls.from_dict(config_dict, source=ConfigSource.ENV)
|
82
|
-
log.trace("Loaded transport configuration from environment", config_dict=config_dict)
|
83
|
-
return config
|
84
53
|
|
85
54
|
|
86
55
|
@define(slots=True, repr=False)
|
87
56
|
class HTTPConfig(TransportConfig):
|
88
57
|
"""HTTP-specific configuration."""
|
89
|
-
|
58
|
+
|
90
59
|
pool_connections: int = field(
|
91
60
|
default=10,
|
92
61
|
env_var="PROVIDE_HTTP_POOL_CONNECTIONS",
|
62
|
+
converter=int,
|
63
|
+
validator=validate_positive,
|
93
64
|
description="Number of connection pools to cache",
|
94
65
|
)
|
95
66
|
pool_maxsize: int = field(
|
96
67
|
default=100,
|
97
|
-
env_var="PROVIDE_HTTP_POOL_MAXSIZE",
|
68
|
+
env_var="PROVIDE_HTTP_POOL_MAXSIZE",
|
69
|
+
converter=int,
|
70
|
+
validator=validate_positive,
|
98
71
|
description="Maximum number of connections per pool",
|
99
72
|
)
|
100
73
|
follow_redirects: bool = field(
|
101
74
|
default=True,
|
102
75
|
env_var="PROVIDE_HTTP_FOLLOW_REDIRECTS",
|
76
|
+
converter=parse_bool_extended,
|
103
77
|
description="Whether to automatically follow redirects",
|
104
78
|
)
|
105
79
|
http2: bool = field(
|
106
80
|
default=False,
|
107
81
|
env_var="PROVIDE_HTTP_USE_HTTP2",
|
82
|
+
converter=parse_bool_extended,
|
108
83
|
description="Enable HTTP/2 support",
|
109
84
|
)
|
110
85
|
max_redirects: int = field(
|
111
86
|
default=5,
|
112
87
|
env_var="PROVIDE_HTTP_MAX_REDIRECTS",
|
88
|
+
converter=int,
|
89
|
+
validator=validate_non_negative,
|
113
90
|
description="Maximum number of redirects to follow",
|
114
91
|
)
|
115
|
-
|
116
|
-
@classmethod
|
117
|
-
def from_env(cls, strict: bool = True) -> "HTTPConfig":
|
118
|
-
"""Load HTTP configuration from environment variables."""
|
119
|
-
# Start with base transport config
|
120
|
-
base_config = TransportConfig.from_env(strict=strict)
|
121
|
-
config_dict = base_config.to_dict(include_sensitive=True)
|
122
|
-
|
123
|
-
# Add HTTP-specific settings
|
124
|
-
if pool_connections := os.getenv("PROVIDE_HTTP_POOL_CONNECTIONS"):
|
125
|
-
try:
|
126
|
-
config_dict["pool_connections"] = int(pool_connections)
|
127
|
-
except ValueError:
|
128
|
-
if strict:
|
129
|
-
log.warning(
|
130
|
-
"Invalid pool connections value, using field default",
|
131
|
-
invalid_value=pool_connections,
|
132
|
-
)
|
133
|
-
|
134
|
-
if pool_maxsize := os.getenv("PROVIDE_HTTP_POOL_MAXSIZE"):
|
135
|
-
try:
|
136
|
-
config_dict["pool_maxsize"] = int(pool_maxsize)
|
137
|
-
except ValueError:
|
138
|
-
if strict:
|
139
|
-
log.warning(
|
140
|
-
"Invalid pool maxsize value, using field default",
|
141
|
-
invalid_value=pool_maxsize,
|
142
|
-
)
|
143
|
-
|
144
|
-
if follow_redirects := os.getenv("PROVIDE_HTTP_FOLLOW_REDIRECTS"):
|
145
|
-
config_dict["follow_redirects"] = follow_redirects.lower() == "true"
|
146
|
-
|
147
|
-
if http2 := os.getenv("PROVIDE_HTTP_USE_HTTP2"):
|
148
|
-
config_dict["http2"] = http2.lower() == "true"
|
149
|
-
|
150
|
-
if max_redirects := os.getenv("PROVIDE_HTTP_MAX_REDIRECTS"):
|
151
|
-
try:
|
152
|
-
config_dict["max_redirects"] = int(max_redirects)
|
153
|
-
except ValueError:
|
154
|
-
if strict:
|
155
|
-
log.warning(
|
156
|
-
"Invalid max redirects value, using field default",
|
157
|
-
invalid_value=max_redirects,
|
158
|
-
)
|
159
|
-
|
160
|
-
config = cls.from_dict(config_dict, source=ConfigSource.ENV)
|
161
|
-
log.trace("Loaded HTTP configuration from environment", config_dict=config_dict)
|
162
|
-
return config
|
163
92
|
|
164
93
|
|
165
94
|
async def register_transport_configs() -> None:
|
@@ -175,10 +104,10 @@ async def register_transport_configs() -> None:
|
|
175
104
|
"max_retries": 3,
|
176
105
|
"retry_backoff_factor": 0.5,
|
177
106
|
"verify_ssl": True,
|
178
|
-
}
|
107
|
+
},
|
179
108
|
)
|
180
|
-
|
181
|
-
# Register HTTPConfig
|
109
|
+
|
110
|
+
# Register HTTPConfig
|
182
111
|
await register_config(
|
183
112
|
name="transport.http",
|
184
113
|
config=None, # Will be loaded on demand
|
@@ -193,17 +122,17 @@ async def register_transport_configs() -> None:
|
|
193
122
|
"follow_redirects": True,
|
194
123
|
"http2": False,
|
195
124
|
"max_redirects": 5,
|
196
|
-
}
|
125
|
+
},
|
197
126
|
)
|
198
|
-
|
127
|
+
|
199
128
|
log.trace("Successfully registered transport configurations with ConfigManager")
|
200
|
-
|
129
|
+
|
201
130
|
except Exception as e:
|
202
131
|
log.warning("Failed to register transport configurations", error=str(e))
|
203
132
|
|
204
133
|
|
205
134
|
__all__ = [
|
206
|
-
"TransportConfig",
|
207
135
|
"HTTPConfig",
|
136
|
+
"TransportConfig",
|
208
137
|
"register_transport_configs",
|
209
|
-
]
|
138
|
+
]
|
@@ -12,38 +12,29 @@ if TYPE_CHECKING:
|
|
12
12
|
|
13
13
|
class TransportError(FoundationError):
|
14
14
|
"""Base transport error."""
|
15
|
-
|
16
|
-
def __init__(
|
17
|
-
self,
|
18
|
-
message: str,
|
19
|
-
*,
|
20
|
-
request: "Request | None" = None,
|
21
|
-
**kwargs
|
22
|
-
):
|
15
|
+
|
16
|
+
def __init__(self, message: str, *, request: "Request | None" = None, **kwargs):
|
23
17
|
super().__init__(message, **kwargs)
|
24
18
|
self.request = request
|
25
19
|
|
26
20
|
|
27
21
|
class TransportConnectionError(TransportError):
|
28
22
|
"""Transport connection failed."""
|
23
|
+
|
29
24
|
pass
|
30
25
|
|
31
26
|
|
32
27
|
class TransportTimeoutError(TransportError):
|
33
28
|
"""Transport request timed out."""
|
29
|
+
|
34
30
|
pass
|
35
31
|
|
36
32
|
|
37
33
|
class HTTPResponseError(TransportError):
|
38
34
|
"""HTTP response error (4xx/5xx status codes)."""
|
39
|
-
|
35
|
+
|
40
36
|
def __init__(
|
41
|
-
self,
|
42
|
-
message: str,
|
43
|
-
*,
|
44
|
-
status_code: int,
|
45
|
-
response: "Response",
|
46
|
-
**kwargs
|
37
|
+
self, message: str, *, status_code: int, response: "Response", **kwargs
|
47
38
|
):
|
48
39
|
super().__init__(message, **kwargs)
|
49
40
|
self.status_code = status_code
|
@@ -52,28 +43,23 @@ class HTTPResponseError(TransportError):
|
|
52
43
|
|
53
44
|
class TransportConfigurationError(TransportError):
|
54
45
|
"""Transport configuration error."""
|
46
|
+
|
55
47
|
pass
|
56
48
|
|
57
49
|
|
58
50
|
class TransportNotFoundError(TransportError):
|
59
51
|
"""No transport found for the given URI scheme."""
|
60
|
-
|
61
|
-
def __init__(
|
62
|
-
self,
|
63
|
-
message: str,
|
64
|
-
*,
|
65
|
-
scheme: str,
|
66
|
-
**kwargs
|
67
|
-
):
|
52
|
+
|
53
|
+
def __init__(self, message: str, *, scheme: str, **kwargs):
|
68
54
|
super().__init__(message, **kwargs)
|
69
55
|
self.scheme = scheme
|
70
56
|
|
71
57
|
|
72
58
|
__all__ = [
|
73
|
-
"TransportError",
|
74
|
-
"TransportConnectionError",
|
75
|
-
"TransportTimeoutError",
|
76
59
|
"HTTPResponseError",
|
77
60
|
"TransportConfigurationError",
|
61
|
+
"TransportConnectionError",
|
62
|
+
"TransportError",
|
78
63
|
"TransportNotFoundError",
|
79
|
-
|
64
|
+
"TransportTimeoutError",
|
65
|
+
]
|
@@ -2,18 +2,16 @@
|
|
2
2
|
HTTP/HTTPS transport implementation using httpx.
|
3
3
|
"""
|
4
4
|
|
5
|
-
import time
|
6
5
|
from collections.abc import AsyncIterator
|
7
|
-
|
6
|
+
import time
|
8
7
|
|
9
|
-
import httpx
|
10
8
|
from attrs import define, field
|
9
|
+
import httpx
|
11
10
|
|
12
11
|
from provide.foundation.logger import get_logger
|
13
12
|
from provide.foundation.transport.base import Request, Response, TransportBase
|
14
13
|
from provide.foundation.transport.config import HTTPConfig
|
15
14
|
from provide.foundation.transport.errors import (
|
16
|
-
HTTPResponseError,
|
17
15
|
TransportConnectionError,
|
18
16
|
TransportTimeoutError,
|
19
17
|
)
|
@@ -25,28 +23,28 @@ log = get_logger(__name__)
|
|
25
23
|
@define
|
26
24
|
class HTTPTransport(TransportBase):
|
27
25
|
"""HTTP/HTTPS transport using httpx backend."""
|
28
|
-
|
26
|
+
|
29
27
|
SCHEMES = ["http", "https"]
|
30
|
-
|
28
|
+
|
31
29
|
config: HTTPConfig = field(factory=HTTPConfig.from_env)
|
32
30
|
_client: httpx.AsyncClient | None = field(default=None, init=False)
|
33
|
-
|
31
|
+
|
34
32
|
def supports(self, transport_type: TransportType) -> bool:
|
35
33
|
"""Check if this transport supports the given type."""
|
36
34
|
return transport_type.value in self.SCHEMES
|
37
|
-
|
35
|
+
|
38
36
|
async def connect(self) -> None:
|
39
37
|
"""Initialize httpx client with configuration."""
|
40
38
|
if self._client is not None:
|
41
39
|
return
|
42
|
-
|
40
|
+
|
43
41
|
limits = httpx.Limits(
|
44
42
|
max_connections=self.config.pool_connections,
|
45
43
|
max_keepalive_connections=self.config.pool_maxsize,
|
46
44
|
)
|
47
|
-
|
45
|
+
|
48
46
|
timeout = httpx.Timeout(self.config.timeout)
|
49
|
-
|
47
|
+
|
50
48
|
self._client = httpx.AsyncClient(
|
51
49
|
limits=limits,
|
52
50
|
timeout=timeout,
|
@@ -55,35 +53,37 @@ class HTTPTransport(TransportBase):
|
|
55
53
|
max_redirects=self.config.max_redirects,
|
56
54
|
http2=self.config.http2,
|
57
55
|
)
|
58
|
-
|
59
|
-
log.trace(
|
60
|
-
|
61
|
-
|
62
|
-
|
56
|
+
|
57
|
+
log.trace(
|
58
|
+
"HTTP transport connected",
|
59
|
+
pool_connections=self.config.pool_connections,
|
60
|
+
http2=self.config.http2,
|
61
|
+
)
|
62
|
+
|
63
63
|
async def disconnect(self) -> None:
|
64
64
|
"""Close httpx client."""
|
65
65
|
if self._client is not None:
|
66
66
|
await self._client.aclose()
|
67
67
|
self._client = None
|
68
68
|
log.trace("HTTP transport disconnected")
|
69
|
-
|
69
|
+
|
70
70
|
async def execute(self, request: Request) -> Response:
|
71
71
|
"""Execute HTTP request."""
|
72
72
|
await self.connect()
|
73
|
-
|
73
|
+
|
74
74
|
if self._client is None:
|
75
75
|
raise TransportConnectionError("HTTP client not connected")
|
76
|
-
|
76
|
+
|
77
77
|
# Log request with emoji
|
78
78
|
log.info(f"🚀 {request.method} {request.uri}")
|
79
|
-
|
79
|
+
|
80
80
|
start_time = time.perf_counter()
|
81
|
-
|
81
|
+
|
82
82
|
try:
|
83
83
|
# Determine request body format
|
84
84
|
json_data = None
|
85
85
|
data = None
|
86
|
-
|
86
|
+
|
87
87
|
if request.body is not None:
|
88
88
|
if isinstance(request.body, dict):
|
89
89
|
json_data = request.body
|
@@ -91,9 +91,8 @@ class HTTPTransport(TransportBase):
|
|
91
91
|
data = request.body
|
92
92
|
else:
|
93
93
|
# Try to serialize as JSON
|
94
|
-
import json
|
95
94
|
json_data = request.body
|
96
|
-
|
95
|
+
|
97
96
|
# Make the request
|
98
97
|
httpx_response = await self._client.request(
|
99
98
|
method=request.method,
|
@@ -104,13 +103,15 @@ class HTTPTransport(TransportBase):
|
|
104
103
|
data=data,
|
105
104
|
timeout=request.timeout or self.config.timeout,
|
106
105
|
)
|
107
|
-
|
106
|
+
|
108
107
|
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
109
|
-
|
108
|
+
|
110
109
|
# Log response with status emoji
|
111
110
|
status_emoji = self._get_status_emoji(httpx_response.status_code)
|
112
|
-
log.info(
|
113
|
-
|
111
|
+
log.info(
|
112
|
+
f"{status_emoji} {httpx_response.status_code} ({elapsed_ms:.0f}ms)"
|
113
|
+
)
|
114
|
+
|
114
115
|
# Create response object
|
115
116
|
response = Response(
|
116
117
|
status=httpx_response.status_code,
|
@@ -126,35 +127,43 @@ class HTTPTransport(TransportBase):
|
|
126
127
|
elapsed_ms=elapsed_ms,
|
127
128
|
request=request,
|
128
129
|
)
|
129
|
-
|
130
|
+
|
130
131
|
return response
|
131
|
-
|
132
|
+
|
132
133
|
except httpx.ConnectError as e:
|
133
134
|
log.error(f"❌ Connection failed: {e}")
|
134
|
-
raise TransportConnectionError(
|
135
|
-
|
135
|
+
raise TransportConnectionError(
|
136
|
+
f"Failed to connect: {e}", request=request
|
137
|
+
) from e
|
138
|
+
|
136
139
|
except httpx.TimeoutException as e:
|
137
140
|
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
138
141
|
log.error(f"⏱️ Request timed out ({elapsed_ms:.0f}ms)")
|
139
|
-
raise TransportTimeoutError(
|
140
|
-
|
142
|
+
raise TransportTimeoutError(
|
143
|
+
f"Request timed out: {e}", request=request
|
144
|
+
) from e
|
145
|
+
|
141
146
|
except httpx.RequestError as e:
|
142
147
|
log.error(f"❌ Request failed: {e}")
|
143
|
-
raise TransportConnectionError(
|
144
|
-
|
148
|
+
raise TransportConnectionError(
|
149
|
+
f"Request failed: {e}", request=request
|
150
|
+
) from e
|
151
|
+
|
145
152
|
except Exception as e:
|
146
153
|
log.error(f"❌ Unexpected error: {e}", exc_info=True)
|
147
|
-
raise TransportConnectionError(
|
148
|
-
|
154
|
+
raise TransportConnectionError(
|
155
|
+
f"Unexpected error: {e}", request=request
|
156
|
+
) from e
|
157
|
+
|
149
158
|
async def stream(self, request: Request) -> AsyncIterator[bytes]:
|
150
159
|
"""Stream HTTP response."""
|
151
160
|
await self.connect()
|
152
|
-
|
161
|
+
|
153
162
|
if self._client is None:
|
154
163
|
raise TransportConnectionError("HTTP client not connected")
|
155
|
-
|
164
|
+
|
156
165
|
log.info(f"🌊 Streaming {request.method} {request.uri}")
|
157
|
-
|
166
|
+
|
158
167
|
try:
|
159
168
|
async with self._client.stream(
|
160
169
|
method=request.method,
|
@@ -163,24 +172,29 @@ class HTTPTransport(TransportBase):
|
|
163
172
|
params=request.params,
|
164
173
|
timeout=request.timeout or self.config.timeout,
|
165
174
|
) as response:
|
166
|
-
|
167
175
|
# Log response start
|
168
176
|
status_emoji = self._get_status_emoji(response.status_code)
|
169
177
|
log.info(f"{status_emoji} {response.status_code} (streaming)")
|
170
|
-
|
178
|
+
|
171
179
|
# Stream the response
|
172
180
|
async for chunk in response.aiter_bytes():
|
173
181
|
yield chunk
|
174
|
-
|
182
|
+
|
175
183
|
except httpx.ConnectError as e:
|
176
|
-
raise TransportConnectionError(
|
177
|
-
|
184
|
+
raise TransportConnectionError(
|
185
|
+
f"Failed to connect: {e}", request=request
|
186
|
+
) from e
|
187
|
+
|
178
188
|
except httpx.TimeoutException as e:
|
179
|
-
raise TransportTimeoutError(
|
180
|
-
|
189
|
+
raise TransportTimeoutError(
|
190
|
+
f"Stream timed out: {e}", request=request
|
191
|
+
) from e
|
192
|
+
|
181
193
|
except httpx.RequestError as e:
|
182
|
-
raise TransportConnectionError(
|
183
|
-
|
194
|
+
raise TransportConnectionError(
|
195
|
+
f"Stream failed: {e}", request=request
|
196
|
+
) from e
|
197
|
+
|
184
198
|
def _get_status_emoji(self, status_code: int) -> str:
|
185
199
|
"""Get emoji for HTTP status code."""
|
186
200
|
if 200 <= status_code < 300:
|
@@ -200,7 +214,7 @@ def _register_http_transport():
|
|
200
214
|
"""Register HTTP transport with the Hub."""
|
201
215
|
try:
|
202
216
|
from provide.foundation.transport.registry import register_transport
|
203
|
-
|
217
|
+
|
204
218
|
register_transport(
|
205
219
|
TransportType.HTTP,
|
206
220
|
HTTPTransport,
|
@@ -208,16 +222,16 @@ def _register_http_transport():
|
|
208
222
|
description="HTTP/HTTPS transport using httpx",
|
209
223
|
version="1.0.0",
|
210
224
|
)
|
211
|
-
|
225
|
+
|
212
226
|
# Also register HTTPS explicitly
|
213
227
|
register_transport(
|
214
228
|
TransportType.HTTPS,
|
215
229
|
HTTPTransport,
|
216
230
|
schemes=HTTPTransport.SCHEMES,
|
217
|
-
description="HTTP/HTTPS transport using httpx",
|
231
|
+
description="HTTP/HTTPS transport using httpx",
|
218
232
|
version="1.0.0",
|
219
233
|
)
|
220
|
-
|
234
|
+
|
221
235
|
except ImportError:
|
222
236
|
# Registry not available yet, will be registered later
|
223
237
|
pass
|
@@ -229,4 +243,4 @@ _register_http_transport()
|
|
229
243
|
|
230
244
|
__all__ = [
|
231
245
|
"HTTPTransport",
|
232
|
-
]
|
246
|
+
]
|