provide-foundation 0.0.0.dev2__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 +20 -20
- 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 +90 -91
- 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 +4 -4
- provide/foundation/cli/commands/logs/__init__.py +2 -2
- provide/foundation/cli/commands/logs/generate.py +2 -2
- provide/foundation/cli/commands/logs/query.py +3 -3
- provide/foundation/cli/commands/logs/send.py +2 -2
- provide/foundation/cli/commands/logs/tail.py +2 -2
- provide/foundation/cli/decorators.py +0 -1
- provide/foundation/cli/testing.py +0 -5
- provide/foundation/cli/utils.py +1 -2
- provide/foundation/config/__init__.py +19 -19
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +81 -83
- provide/foundation/config/defaults.py +1 -1
- provide/foundation/config/env.py +2 -1
- provide/foundation/config/loader.py +1 -1
- provide/foundation/config/sync.py +8 -6
- provide/foundation/config/types.py +5 -5
- provide/foundation/config/validators.py +4 -4
- provide/foundation/console/output.py +7 -7
- provide/foundation/context/core.py +19 -17
- 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 -2
- provide/foundation/errors/decorators.py +0 -3
- provide/foundation/errors/types.py +0 -1
- 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 +1 -1
- provide/foundation/file/lock.py +2 -3
- provide/foundation/hub/components.py +19 -21
- provide/foundation/hub/config.py +25 -19
- provide/foundation/hub/discovery.py +5 -4
- provide/foundation/hub/handlers.py +13 -5
- provide/foundation/hub/lifecycle.py +10 -9
- provide/foundation/hub/manager.py +3 -0
- provide/foundation/hub/processors.py +8 -3
- provide/foundation/integrations/__init__.py +1 -1
- provide/foundation/integrations/openobserve/client.py +2 -2
- provide/foundation/integrations/openobserve/commands.py +9 -9
- provide/foundation/integrations/openobserve/config.py +2 -2
- provide/foundation/integrations/openobserve/otlp.py +2 -2
- provide/foundation/integrations/openobserve/search.py +1 -2
- provide/foundation/integrations/openobserve/streaming.py +1 -1
- provide/foundation/logger/__init__.py +0 -1
- provide/foundation/logger/config/base.py +1 -1
- provide/foundation/logger/config/logging.py +19 -19
- provide/foundation/logger/config/telemetry.py +11 -13
- 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 +38 -24
- 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 +1 -1
- provide/foundation/process/__init__.py +1 -1
- provide/foundation/process/exit.py +6 -5
- provide/foundation/process/lifecycle.py +41 -18
- provide/foundation/resilience/__init__.py +6 -5
- provide/foundation/resilience/circuit.py +32 -30
- provide/foundation/resilience/decorators.py +58 -42
- provide/foundation/resilience/fallback.py +55 -40
- provide/foundation/resilience/retry.py +67 -65
- provide/foundation/serialization/__init__.py +16 -0
- provide/foundation/serialization/core.py +70 -0
- provide/foundation/streams/config.py +8 -9
- provide/foundation/streams/console.py +3 -3
- provide/foundation/streams/core.py +2 -2
- provide/foundation/streams/file.py +1 -1
- provide/foundation/testing/__init__.py +22 -7
- provide/foundation/testing/archive/__init__.py +7 -7
- provide/foundation/testing/archive/fixtures.py +58 -54
- provide/foundation/testing/cli.py +3 -6
- provide/foundation/testing/common/__init__.py +13 -13
- provide/foundation/testing/common/fixtures.py +27 -30
- provide/foundation/testing/file/__init__.py +15 -15
- provide/foundation/testing/file/content_fixtures.py +65 -92
- provide/foundation/testing/file/directory_fixtures.py +19 -19
- provide/foundation/testing/file/fixtures.py +14 -17
- provide/foundation/testing/file/special_fixtures.py +34 -42
- provide/foundation/testing/logger.py +28 -23
- 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 +89 -80
- provide/foundation/testing/process/fixtures.py +11 -13
- provide/foundation/testing/process/subprocess_fixtures.py +41 -40
- provide/foundation/testing/threading/__init__.py +17 -17
- provide/foundation/testing/threading/basic_fixtures.py +21 -17
- provide/foundation/testing/threading/data_fixtures.py +18 -16
- provide/foundation/testing/threading/execution_fixtures.py +67 -52
- provide/foundation/testing/threading/fixtures.py +10 -14
- provide/foundation/testing/threading/sync_fixtures.py +21 -18
- provide/foundation/testing/time/__init__.py +11 -11
- provide/foundation/testing/time/fixtures.py +91 -79
- 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 +62 -69
- provide/foundation/tools/downloader.py +51 -56
- 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 +1 -13
- 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 +11 -13
- provide/foundation/transport/errors.py +13 -27
- provide/foundation/transport/http.py +69 -55
- provide/foundation/transport/middleware.py +86 -81
- provide/foundation/transport/registry.py +29 -27
- provide/foundation/transport/types.py +6 -6
- provide/foundation/utils/deps.py +3 -2
- provide/foundation/utils/parsing.py +7 -7
- {provide_foundation-0.0.0.dev2.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.dev2.dist-info/RECORD +0 -225
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/top_level.txt +0 -0
@@ -9,7 +9,10 @@ from attrs import define, field
|
|
9
9
|
|
10
10
|
from provide.foundation.logger import get_logger
|
11
11
|
from provide.foundation.transport.base import Request, Response
|
12
|
-
from provide.foundation.transport.middleware import
|
12
|
+
from provide.foundation.transport.middleware import (
|
13
|
+
MiddlewarePipeline,
|
14
|
+
create_default_pipeline,
|
15
|
+
)
|
13
16
|
from provide.foundation.transport.registry import get_transport
|
14
17
|
from provide.foundation.transport.types import Data, Headers, HTTPMethod, Params
|
15
18
|
|
@@ -19,12 +22,12 @@ log = get_logger(__name__)
|
|
19
22
|
@define
|
20
23
|
class UniversalClient:
|
21
24
|
"""Universal client that works with any transport via Hub registry."""
|
22
|
-
|
25
|
+
|
23
26
|
middleware: MiddlewarePipeline = field(factory=create_default_pipeline)
|
24
27
|
default_headers: Headers = field(factory=dict)
|
25
28
|
default_timeout: float | None = field(default=None)
|
26
29
|
_transports: dict[str, Any] = field(factory=dict, init=False)
|
27
|
-
|
30
|
+
|
28
31
|
async def request(
|
29
32
|
self,
|
30
33
|
uri: str,
|
@@ -34,11 +37,11 @@ class UniversalClient:
|
|
34
37
|
params: Params | None = None,
|
35
38
|
body: Data = None,
|
36
39
|
timeout: float | None = None,
|
37
|
-
**kwargs
|
40
|
+
**kwargs,
|
38
41
|
) -> Response:
|
39
42
|
"""
|
40
43
|
Make a request using appropriate transport.
|
41
|
-
|
44
|
+
|
42
45
|
Args:
|
43
46
|
uri: Full URI to make request to
|
44
47
|
method: HTTP method or protocol-specific method
|
@@ -47,19 +50,19 @@ class UniversalClient:
|
|
47
50
|
body: Request body (dict for JSON, str/bytes for raw)
|
48
51
|
timeout: Request timeout override
|
49
52
|
**kwargs: Additional request metadata
|
50
|
-
|
53
|
+
|
51
54
|
Returns:
|
52
55
|
Response from the transport
|
53
56
|
"""
|
54
57
|
# Normalize method
|
55
58
|
if isinstance(method, HTTPMethod):
|
56
59
|
method = method.value
|
57
|
-
|
60
|
+
|
58
61
|
# Merge headers
|
59
62
|
request_headers = dict(self.default_headers)
|
60
63
|
if headers:
|
61
64
|
request_headers.update(headers)
|
62
|
-
|
65
|
+
|
63
66
|
# Create request object
|
64
67
|
request = Request(
|
65
68
|
uri=uri,
|
@@ -70,92 +73,86 @@ class UniversalClient:
|
|
70
73
|
timeout=timeout or self.default_timeout,
|
71
74
|
metadata=kwargs,
|
72
75
|
)
|
73
|
-
|
76
|
+
|
74
77
|
# Process through middleware
|
75
78
|
request = await self.middleware.process_request(request)
|
76
|
-
|
79
|
+
|
77
80
|
try:
|
78
81
|
# Get transport for this URI
|
79
82
|
transport = await self._get_transport(request.transport_type.value)
|
80
|
-
|
83
|
+
|
81
84
|
# Execute request
|
82
85
|
response = await transport.execute(request)
|
83
|
-
|
86
|
+
|
84
87
|
# Process response through middleware
|
85
88
|
response = await self.middleware.process_response(response)
|
86
|
-
|
89
|
+
|
87
90
|
return response
|
88
|
-
|
91
|
+
|
89
92
|
except Exception as e:
|
90
93
|
# Process error through middleware
|
91
94
|
e = await self.middleware.process_error(e, request)
|
92
95
|
raise e
|
93
|
-
|
96
|
+
|
94
97
|
async def stream(
|
95
|
-
self,
|
96
|
-
uri: str,
|
97
|
-
method: str | HTTPMethod = HTTPMethod.GET,
|
98
|
-
**kwargs
|
98
|
+
self, uri: str, method: str | HTTPMethod = HTTPMethod.GET, **kwargs
|
99
99
|
) -> AsyncIterator[bytes]:
|
100
100
|
"""
|
101
101
|
Stream data from URI.
|
102
|
-
|
102
|
+
|
103
103
|
Args:
|
104
104
|
uri: URI to stream from
|
105
105
|
method: HTTP method or protocol-specific method
|
106
106
|
**kwargs: Additional request parameters
|
107
|
-
|
107
|
+
|
108
108
|
Yields:
|
109
109
|
Chunks of response data
|
110
110
|
"""
|
111
111
|
# Normalize method
|
112
112
|
if isinstance(method, HTTPMethod):
|
113
113
|
method = method.value
|
114
|
-
|
114
|
+
|
115
115
|
# Create request
|
116
116
|
request = Request(
|
117
|
-
uri=uri,
|
118
|
-
method=method,
|
119
|
-
headers=dict(self.default_headers),
|
120
|
-
**kwargs
|
117
|
+
uri=uri, method=method, headers=dict(self.default_headers), **kwargs
|
121
118
|
)
|
122
|
-
|
119
|
+
|
123
120
|
# Get transport
|
124
121
|
transport = await self._get_transport(request.transport_type.value)
|
125
|
-
|
122
|
+
|
126
123
|
# Stream response
|
127
124
|
log.info(f"🌊 Streaming {method} {uri}")
|
128
125
|
async for chunk in transport.stream(request):
|
129
126
|
yield chunk
|
130
|
-
|
127
|
+
|
131
128
|
async def get(self, uri: str, **kwargs) -> Response:
|
132
129
|
"""GET request."""
|
133
130
|
return await self.request(uri, HTTPMethod.GET, **kwargs)
|
134
|
-
|
131
|
+
|
135
132
|
async def post(self, uri: str, **kwargs) -> Response:
|
136
133
|
"""POST request."""
|
137
134
|
return await self.request(uri, HTTPMethod.POST, **kwargs)
|
138
|
-
|
135
|
+
|
139
136
|
async def put(self, uri: str, **kwargs) -> Response:
|
140
137
|
"""PUT request."""
|
141
138
|
return await self.request(uri, HTTPMethod.PUT, **kwargs)
|
142
|
-
|
139
|
+
|
143
140
|
async def patch(self, uri: str, **kwargs) -> Response:
|
144
141
|
"""PATCH request."""
|
145
142
|
return await self.request(uri, HTTPMethod.PATCH, **kwargs)
|
146
|
-
|
143
|
+
|
147
144
|
async def delete(self, uri: str, **kwargs) -> Response:
|
148
145
|
"""DELETE request."""
|
149
146
|
return await self.request(uri, HTTPMethod.DELETE, **kwargs)
|
150
|
-
|
147
|
+
|
151
148
|
async def head(self, uri: str, **kwargs) -> Response:
|
152
149
|
"""HEAD request."""
|
153
150
|
return await self.request(uri, HTTPMethod.HEAD, **kwargs)
|
154
|
-
|
151
|
+
|
155
152
|
async def options(self, uri: str, **kwargs) -> Response:
|
156
153
|
"""OPTIONS request."""
|
157
154
|
return await self.request(uri, HTTPMethod.OPTIONS, **kwargs)
|
158
|
-
|
155
|
+
|
159
156
|
async def _get_transport(self, scheme: str) -> Any:
|
160
157
|
"""Get or create transport for scheme."""
|
161
158
|
if scheme not in self._transports:
|
@@ -163,13 +160,13 @@ class UniversalClient:
|
|
163
160
|
transport = get_transport(f"{scheme}://example.com")
|
164
161
|
await transport.connect()
|
165
162
|
self._transports[scheme] = transport
|
166
|
-
|
163
|
+
|
167
164
|
return self._transports[scheme]
|
168
|
-
|
165
|
+
|
169
166
|
async def __aenter__(self) -> "UniversalClient":
|
170
167
|
"""Context manager entry."""
|
171
168
|
return self
|
172
|
-
|
169
|
+
|
173
170
|
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
174
171
|
"""Context manager exit - cleanup all transports."""
|
175
172
|
for transport in self._transports.values():
|
@@ -193,9 +190,7 @@ def get_default_client() -> UniversalClient:
|
|
193
190
|
|
194
191
|
|
195
192
|
async def request(
|
196
|
-
uri: str,
|
197
|
-
method: str | HTTPMethod = HTTPMethod.GET,
|
198
|
-
**kwargs
|
193
|
+
uri: str, method: str | HTTPMethod = HTTPMethod.GET, **kwargs
|
199
194
|
) -> Response:
|
200
195
|
"""Make a request using the default client."""
|
201
196
|
client = get_default_client()
|
@@ -253,14 +248,14 @@ async def stream(uri: str, **kwargs) -> AsyncIterator[bytes]:
|
|
253
248
|
|
254
249
|
__all__ = [
|
255
250
|
"UniversalClient",
|
256
|
-
"get_default_client",
|
257
|
-
"request",
|
258
|
-
"get",
|
259
|
-
"post",
|
260
|
-
"put",
|
261
|
-
"patch",
|
262
251
|
"delete",
|
252
|
+
"get",
|
253
|
+
"get_default_client",
|
263
254
|
"head",
|
264
255
|
"options",
|
256
|
+
"patch",
|
257
|
+
"post",
|
258
|
+
"put",
|
259
|
+
"request",
|
265
260
|
"stream",
|
266
|
-
]
|
261
|
+
]
|
@@ -4,7 +4,6 @@ Transport configuration with Foundation config integration.
|
|
4
4
|
|
5
5
|
from attrs import define
|
6
6
|
|
7
|
-
from provide.foundation.config.env import RuntimeConfig
|
8
7
|
from provide.foundation.config.base import field
|
9
8
|
from provide.foundation.config.converters import (
|
10
9
|
parse_bool_extended,
|
@@ -12,6 +11,7 @@ from provide.foundation.config.converters import (
|
|
12
11
|
validate_non_negative,
|
13
12
|
validate_positive,
|
14
13
|
)
|
14
|
+
from provide.foundation.config.env import RuntimeConfig
|
15
15
|
from provide.foundation.config.loader import RuntimeConfigLoader
|
16
16
|
from provide.foundation.config.manager import register_config
|
17
17
|
from provide.foundation.logger import get_logger
|
@@ -22,7 +22,7 @@ log = get_logger(__name__)
|
|
22
22
|
@define(slots=True, repr=False)
|
23
23
|
class TransportConfig(RuntimeConfig):
|
24
24
|
"""Base configuration for all transports."""
|
25
|
-
|
25
|
+
|
26
26
|
timeout: float = field(
|
27
27
|
default=30.0,
|
28
28
|
env_var="PROVIDE_TRANSPORT_TIMEOUT",
|
@@ -50,13 +50,12 @@ class TransportConfig(RuntimeConfig):
|
|
50
50
|
converter=parse_bool_extended,
|
51
51
|
description="Whether to verify SSL certificates",
|
52
52
|
)
|
53
|
-
|
54
53
|
|
55
54
|
|
56
55
|
@define(slots=True, repr=False)
|
57
56
|
class HTTPConfig(TransportConfig):
|
58
57
|
"""HTTP-specific configuration."""
|
59
|
-
|
58
|
+
|
60
59
|
pool_connections: int = field(
|
61
60
|
default=10,
|
62
61
|
env_var="PROVIDE_HTTP_POOL_CONNECTIONS",
|
@@ -90,7 +89,6 @@ class HTTPConfig(TransportConfig):
|
|
90
89
|
validator=validate_non_negative,
|
91
90
|
description="Maximum number of redirects to follow",
|
92
91
|
)
|
93
|
-
|
94
92
|
|
95
93
|
|
96
94
|
async def register_transport_configs() -> None:
|
@@ -106,10 +104,10 @@ async def register_transport_configs() -> None:
|
|
106
104
|
"max_retries": 3,
|
107
105
|
"retry_backoff_factor": 0.5,
|
108
106
|
"verify_ssl": True,
|
109
|
-
}
|
107
|
+
},
|
110
108
|
)
|
111
|
-
|
112
|
-
# Register HTTPConfig
|
109
|
+
|
110
|
+
# Register HTTPConfig
|
113
111
|
await register_config(
|
114
112
|
name="transport.http",
|
115
113
|
config=None, # Will be loaded on demand
|
@@ -124,17 +122,17 @@ async def register_transport_configs() -> None:
|
|
124
122
|
"follow_redirects": True,
|
125
123
|
"http2": False,
|
126
124
|
"max_redirects": 5,
|
127
|
-
}
|
125
|
+
},
|
128
126
|
)
|
129
|
-
|
127
|
+
|
130
128
|
log.trace("Successfully registered transport configurations with ConfigManager")
|
131
|
-
|
129
|
+
|
132
130
|
except Exception as e:
|
133
131
|
log.warning("Failed to register transport configurations", error=str(e))
|
134
132
|
|
135
133
|
|
136
134
|
__all__ = [
|
137
|
-
"TransportConfig",
|
138
135
|
"HTTPConfig",
|
136
|
+
"TransportConfig",
|
139
137
|
"register_transport_configs",
|
140
|
-
]
|
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
|
+
]
|