provide-foundation 0.0.0.dev0__py3-none-any.whl → 0.0.0.dev1__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 +12 -20
- provide/foundation/archive/__init__.py +23 -0
- provide/foundation/archive/base.py +70 -0
- provide/foundation/archive/bzip2.py +157 -0
- provide/foundation/archive/gzip.py +159 -0
- provide/foundation/archive/operations.py +336 -0
- provide/foundation/archive/tar.py +164 -0
- provide/foundation/archive/zip.py +203 -0
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/sync.py +19 -4
- provide/foundation/core.py +1 -2
- provide/foundation/crypto/__init__.py +2 -0
- provide/foundation/crypto/certificates/__init__.py +34 -0
- provide/foundation/crypto/certificates/base.py +173 -0
- provide/foundation/crypto/certificates/certificate.py +290 -0
- provide/foundation/crypto/certificates/factory.py +213 -0
- provide/foundation/crypto/certificates/generator.py +138 -0
- provide/foundation/crypto/certificates/loader.py +130 -0
- provide/foundation/crypto/certificates/operations.py +198 -0
- provide/foundation/crypto/certificates/trust.py +107 -0
- provide/foundation/eventsets/__init__.py +0 -0
- provide/foundation/eventsets/display.py +84 -0
- provide/foundation/eventsets/registry.py +160 -0
- provide/foundation/eventsets/resolver.py +192 -0
- provide/foundation/eventsets/sets/das.py +128 -0
- provide/foundation/eventsets/sets/database.py +125 -0
- provide/foundation/eventsets/sets/http.py +153 -0
- provide/foundation/eventsets/sets/llm.py +139 -0
- provide/foundation/eventsets/sets/task_queue.py +107 -0
- provide/foundation/eventsets/types.py +70 -0
- provide/foundation/hub/components.py +7 -133
- provide/foundation/logger/__init__.py +3 -10
- provide/foundation/logger/config/logging.py +6 -6
- provide/foundation/logger/core.py +0 -2
- provide/foundation/logger/custom_processors.py +1 -0
- provide/foundation/logger/factories.py +11 -2
- provide/foundation/logger/processors/main.py +20 -84
- provide/foundation/logger/setup/__init__.py +5 -1
- provide/foundation/logger/setup/coordinator.py +75 -23
- provide/foundation/logger/setup/processors.py +2 -9
- provide/foundation/logger/trace.py +27 -0
- provide/foundation/metrics/otel.py +10 -10
- provide/foundation/process/lifecycle.py +82 -26
- provide/foundation/testing/__init__.py +77 -0
- provide/foundation/testing/archive/__init__.py +24 -0
- provide/foundation/testing/archive/fixtures.py +217 -0
- provide/foundation/testing/common/__init__.py +34 -0
- provide/foundation/testing/common/fixtures.py +263 -0
- provide/foundation/testing/file/__init__.py +40 -0
- provide/foundation/testing/file/fixtures.py +523 -0
- provide/foundation/testing/logger.py +41 -11
- provide/foundation/testing/mocking/__init__.py +46 -0
- provide/foundation/testing/mocking/fixtures.py +331 -0
- provide/foundation/testing/process/__init__.py +48 -0
- provide/foundation/testing/process/fixtures.py +577 -0
- provide/foundation/testing/threading/__init__.py +38 -0
- provide/foundation/testing/threading/fixtures.py +520 -0
- provide/foundation/testing/time/__init__.py +32 -0
- provide/foundation/testing/time/fixtures.py +409 -0
- provide/foundation/testing/transport/__init__.py +30 -0
- provide/foundation/testing/transport/fixtures.py +280 -0
- provide/foundation/tools/__init__.py +58 -0
- provide/foundation/tools/base.py +348 -0
- provide/foundation/tools/cache.py +266 -0
- provide/foundation/tools/downloader.py +213 -0
- provide/foundation/tools/installer.py +254 -0
- provide/foundation/tools/registry.py +223 -0
- provide/foundation/tools/resolver.py +321 -0
- provide/foundation/tools/verifier.py +186 -0
- provide/foundation/tracer/otel.py +7 -11
- provide/foundation/transport/__init__.py +155 -0
- provide/foundation/transport/base.py +171 -0
- provide/foundation/transport/client.py +266 -0
- provide/foundation/transport/config.py +209 -0
- provide/foundation/transport/errors.py +79 -0
- provide/foundation/transport/http.py +232 -0
- provide/foundation/transport/middleware.py +366 -0
- provide/foundation/transport/registry.py +167 -0
- provide/foundation/transport/types.py +45 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev1.dist-info}/METADATA +5 -28
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev1.dist-info}/RECORD +85 -34
- provide/foundation/cli/commands/logs/generate_old.py +0 -569
- provide/foundation/crypto/certificates.py +0 -896
- provide/foundation/logger/emoji/__init__.py +0 -44
- provide/foundation/logger/emoji/matrix.py +0 -209
- provide/foundation/logger/emoji/sets.py +0 -458
- provide/foundation/logger/emoji/types.py +0 -56
- provide/foundation/logger/setup/emoji_resolver.py +0 -64
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev1.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev1.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev1.dist-info}/licenses/LICENSE +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,209 @@
|
|
1
|
+
"""
|
2
|
+
Transport configuration with Foundation config integration.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import os
|
6
|
+
|
7
|
+
from attrs import define
|
8
|
+
|
9
|
+
from provide.foundation.config import BaseConfig, field
|
10
|
+
from provide.foundation.config.loader import RuntimeConfigLoader
|
11
|
+
from provide.foundation.config.manager import register_config
|
12
|
+
from provide.foundation.config.types import ConfigSource
|
13
|
+
from provide.foundation.logger import get_logger
|
14
|
+
|
15
|
+
log = get_logger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
@define(slots=True, repr=False)
|
19
|
+
class TransportConfig(BaseConfig):
|
20
|
+
"""Base configuration for all transports."""
|
21
|
+
|
22
|
+
timeout: float = field(
|
23
|
+
default=30.0,
|
24
|
+
env_var="PROVIDE_TRANSPORT_TIMEOUT",
|
25
|
+
description="Request timeout in seconds",
|
26
|
+
)
|
27
|
+
max_retries: int = field(
|
28
|
+
default=3,
|
29
|
+
env_var="PROVIDE_TRANSPORT_MAX_RETRIES",
|
30
|
+
description="Maximum number of retry attempts",
|
31
|
+
)
|
32
|
+
retry_backoff_factor: float = field(
|
33
|
+
default=0.5,
|
34
|
+
env_var="PROVIDE_TRANSPORT_RETRY_BACKOFF_FACTOR",
|
35
|
+
description="Backoff multiplier for retries",
|
36
|
+
)
|
37
|
+
verify_ssl: bool = field(
|
38
|
+
default=True,
|
39
|
+
env_var="PROVIDE_TRANSPORT_VERIFY_SSL",
|
40
|
+
description="Whether to verify SSL certificates",
|
41
|
+
)
|
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
|
+
|
85
|
+
|
86
|
+
@define(slots=True, repr=False)
|
87
|
+
class HTTPConfig(TransportConfig):
|
88
|
+
"""HTTP-specific configuration."""
|
89
|
+
|
90
|
+
pool_connections: int = field(
|
91
|
+
default=10,
|
92
|
+
env_var="PROVIDE_HTTP_POOL_CONNECTIONS",
|
93
|
+
description="Number of connection pools to cache",
|
94
|
+
)
|
95
|
+
pool_maxsize: int = field(
|
96
|
+
default=100,
|
97
|
+
env_var="PROVIDE_HTTP_POOL_MAXSIZE",
|
98
|
+
description="Maximum number of connections per pool",
|
99
|
+
)
|
100
|
+
follow_redirects: bool = field(
|
101
|
+
default=True,
|
102
|
+
env_var="PROVIDE_HTTP_FOLLOW_REDIRECTS",
|
103
|
+
description="Whether to automatically follow redirects",
|
104
|
+
)
|
105
|
+
http2: bool = field(
|
106
|
+
default=False,
|
107
|
+
env_var="PROVIDE_HTTP_USE_HTTP2",
|
108
|
+
description="Enable HTTP/2 support",
|
109
|
+
)
|
110
|
+
max_redirects: int = field(
|
111
|
+
default=5,
|
112
|
+
env_var="PROVIDE_HTTP_MAX_REDIRECTS",
|
113
|
+
description="Maximum number of redirects to follow",
|
114
|
+
)
|
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
|
+
|
164
|
+
|
165
|
+
async def register_transport_configs() -> None:
|
166
|
+
"""Register transport configurations with the global ConfigManager."""
|
167
|
+
try:
|
168
|
+
# Register TransportConfig
|
169
|
+
await register_config(
|
170
|
+
name="transport",
|
171
|
+
config=None, # Will be loaded on demand
|
172
|
+
loader=RuntimeConfigLoader(prefix="PROVIDE_TRANSPORT"),
|
173
|
+
defaults={
|
174
|
+
"timeout": 30.0,
|
175
|
+
"max_retries": 3,
|
176
|
+
"retry_backoff_factor": 0.5,
|
177
|
+
"verify_ssl": True,
|
178
|
+
}
|
179
|
+
)
|
180
|
+
|
181
|
+
# Register HTTPConfig
|
182
|
+
await register_config(
|
183
|
+
name="transport.http",
|
184
|
+
config=None, # Will be loaded on demand
|
185
|
+
loader=RuntimeConfigLoader(prefix="PROVIDE_HTTP"),
|
186
|
+
defaults={
|
187
|
+
"timeout": 30.0,
|
188
|
+
"max_retries": 3,
|
189
|
+
"retry_backoff_factor": 0.5,
|
190
|
+
"verify_ssl": True,
|
191
|
+
"pool_connections": 10,
|
192
|
+
"pool_maxsize": 100,
|
193
|
+
"follow_redirects": True,
|
194
|
+
"http2": False,
|
195
|
+
"max_redirects": 5,
|
196
|
+
}
|
197
|
+
)
|
198
|
+
|
199
|
+
log.trace("Successfully registered transport configurations with ConfigManager")
|
200
|
+
|
201
|
+
except Exception as e:
|
202
|
+
log.warning("Failed to register transport configurations", error=str(e))
|
203
|
+
|
204
|
+
|
205
|
+
__all__ = [
|
206
|
+
"TransportConfig",
|
207
|
+
"HTTPConfig",
|
208
|
+
"register_transport_configs",
|
209
|
+
]
|
@@ -0,0 +1,79 @@
|
|
1
|
+
"""
|
2
|
+
Transport-specific error types.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import TYPE_CHECKING
|
6
|
+
|
7
|
+
from provide.foundation.errors.base import FoundationError
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from provide.foundation.transport.base import Request, Response
|
11
|
+
|
12
|
+
|
13
|
+
class TransportError(FoundationError):
|
14
|
+
"""Base transport error."""
|
15
|
+
|
16
|
+
def __init__(
|
17
|
+
self,
|
18
|
+
message: str,
|
19
|
+
*,
|
20
|
+
request: "Request | None" = None,
|
21
|
+
**kwargs
|
22
|
+
):
|
23
|
+
super().__init__(message, **kwargs)
|
24
|
+
self.request = request
|
25
|
+
|
26
|
+
|
27
|
+
class TransportConnectionError(TransportError):
|
28
|
+
"""Transport connection failed."""
|
29
|
+
pass
|
30
|
+
|
31
|
+
|
32
|
+
class TransportTimeoutError(TransportError):
|
33
|
+
"""Transport request timed out."""
|
34
|
+
pass
|
35
|
+
|
36
|
+
|
37
|
+
class HTTPResponseError(TransportError):
|
38
|
+
"""HTTP response error (4xx/5xx status codes)."""
|
39
|
+
|
40
|
+
def __init__(
|
41
|
+
self,
|
42
|
+
message: str,
|
43
|
+
*,
|
44
|
+
status_code: int,
|
45
|
+
response: "Response",
|
46
|
+
**kwargs
|
47
|
+
):
|
48
|
+
super().__init__(message, **kwargs)
|
49
|
+
self.status_code = status_code
|
50
|
+
self.response = response
|
51
|
+
|
52
|
+
|
53
|
+
class TransportConfigurationError(TransportError):
|
54
|
+
"""Transport configuration error."""
|
55
|
+
pass
|
56
|
+
|
57
|
+
|
58
|
+
class TransportNotFoundError(TransportError):
|
59
|
+
"""No transport found for the given URI scheme."""
|
60
|
+
|
61
|
+
def __init__(
|
62
|
+
self,
|
63
|
+
message: str,
|
64
|
+
*,
|
65
|
+
scheme: str,
|
66
|
+
**kwargs
|
67
|
+
):
|
68
|
+
super().__init__(message, **kwargs)
|
69
|
+
self.scheme = scheme
|
70
|
+
|
71
|
+
|
72
|
+
__all__ = [
|
73
|
+
"TransportError",
|
74
|
+
"TransportConnectionError",
|
75
|
+
"TransportTimeoutError",
|
76
|
+
"HTTPResponseError",
|
77
|
+
"TransportConfigurationError",
|
78
|
+
"TransportNotFoundError",
|
79
|
+
]
|
@@ -0,0 +1,232 @@
|
|
1
|
+
"""
|
2
|
+
HTTP/HTTPS transport implementation using httpx.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import time
|
6
|
+
from collections.abc import AsyncIterator
|
7
|
+
from typing import Any
|
8
|
+
|
9
|
+
import httpx
|
10
|
+
from attrs import define, field
|
11
|
+
|
12
|
+
from provide.foundation.logger import get_logger
|
13
|
+
from provide.foundation.transport.base import Request, Response, TransportBase
|
14
|
+
from provide.foundation.transport.config import HTTPConfig
|
15
|
+
from provide.foundation.transport.errors import (
|
16
|
+
HTTPResponseError,
|
17
|
+
TransportConnectionError,
|
18
|
+
TransportTimeoutError,
|
19
|
+
)
|
20
|
+
from provide.foundation.transport.types import TransportType
|
21
|
+
|
22
|
+
log = get_logger(__name__)
|
23
|
+
|
24
|
+
|
25
|
+
@define
|
26
|
+
class HTTPTransport(TransportBase):
|
27
|
+
"""HTTP/HTTPS transport using httpx backend."""
|
28
|
+
|
29
|
+
SCHEMES = ["http", "https"]
|
30
|
+
|
31
|
+
config: HTTPConfig = field(factory=HTTPConfig.from_env)
|
32
|
+
_client: httpx.AsyncClient | None = field(default=None, init=False)
|
33
|
+
|
34
|
+
def supports(self, transport_type: TransportType) -> bool:
|
35
|
+
"""Check if this transport supports the given type."""
|
36
|
+
return transport_type.value in self.SCHEMES
|
37
|
+
|
38
|
+
async def connect(self) -> None:
|
39
|
+
"""Initialize httpx client with configuration."""
|
40
|
+
if self._client is not None:
|
41
|
+
return
|
42
|
+
|
43
|
+
limits = httpx.Limits(
|
44
|
+
max_connections=self.config.pool_connections,
|
45
|
+
max_keepalive_connections=self.config.pool_maxsize,
|
46
|
+
)
|
47
|
+
|
48
|
+
timeout = httpx.Timeout(self.config.timeout)
|
49
|
+
|
50
|
+
self._client = httpx.AsyncClient(
|
51
|
+
limits=limits,
|
52
|
+
timeout=timeout,
|
53
|
+
verify=self.config.verify_ssl,
|
54
|
+
follow_redirects=self.config.follow_redirects,
|
55
|
+
max_redirects=self.config.max_redirects,
|
56
|
+
http2=self.config.http2,
|
57
|
+
)
|
58
|
+
|
59
|
+
log.trace("HTTP transport connected",
|
60
|
+
pool_connections=self.config.pool_connections,
|
61
|
+
http2=self.config.http2)
|
62
|
+
|
63
|
+
async def disconnect(self) -> None:
|
64
|
+
"""Close httpx client."""
|
65
|
+
if self._client is not None:
|
66
|
+
await self._client.aclose()
|
67
|
+
self._client = None
|
68
|
+
log.trace("HTTP transport disconnected")
|
69
|
+
|
70
|
+
async def execute(self, request: Request) -> Response:
|
71
|
+
"""Execute HTTP request."""
|
72
|
+
await self.connect()
|
73
|
+
|
74
|
+
if self._client is None:
|
75
|
+
raise TransportConnectionError("HTTP client not connected")
|
76
|
+
|
77
|
+
# Log request with emoji
|
78
|
+
log.info(f"🚀 {request.method} {request.uri}")
|
79
|
+
|
80
|
+
start_time = time.perf_counter()
|
81
|
+
|
82
|
+
try:
|
83
|
+
# Determine request body format
|
84
|
+
json_data = None
|
85
|
+
data = None
|
86
|
+
|
87
|
+
if request.body is not None:
|
88
|
+
if isinstance(request.body, dict):
|
89
|
+
json_data = request.body
|
90
|
+
elif isinstance(request.body, (str, bytes)):
|
91
|
+
data = request.body
|
92
|
+
else:
|
93
|
+
# Try to serialize as JSON
|
94
|
+
import json
|
95
|
+
json_data = request.body
|
96
|
+
|
97
|
+
# Make the request
|
98
|
+
httpx_response = await self._client.request(
|
99
|
+
method=request.method,
|
100
|
+
url=request.uri,
|
101
|
+
headers=request.headers,
|
102
|
+
params=request.params,
|
103
|
+
json=json_data,
|
104
|
+
data=data,
|
105
|
+
timeout=request.timeout or self.config.timeout,
|
106
|
+
)
|
107
|
+
|
108
|
+
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
109
|
+
|
110
|
+
# Log response with status emoji
|
111
|
+
status_emoji = self._get_status_emoji(httpx_response.status_code)
|
112
|
+
log.info(f"{status_emoji} {httpx_response.status_code} ({elapsed_ms:.0f}ms)")
|
113
|
+
|
114
|
+
# Create response object
|
115
|
+
response = Response(
|
116
|
+
status=httpx_response.status_code,
|
117
|
+
headers=dict(httpx_response.headers),
|
118
|
+
body=httpx_response.content,
|
119
|
+
metadata={
|
120
|
+
"http_version": str(httpx_response.http_version),
|
121
|
+
"reason_phrase": httpx_response.reason_phrase,
|
122
|
+
"encoding": httpx_response.encoding,
|
123
|
+
"is_redirect": httpx_response.is_redirect,
|
124
|
+
"url": str(httpx_response.url),
|
125
|
+
},
|
126
|
+
elapsed_ms=elapsed_ms,
|
127
|
+
request=request,
|
128
|
+
)
|
129
|
+
|
130
|
+
return response
|
131
|
+
|
132
|
+
except httpx.ConnectError as e:
|
133
|
+
log.error(f"❌ Connection failed: {e}")
|
134
|
+
raise TransportConnectionError(f"Failed to connect: {e}", request=request) from e
|
135
|
+
|
136
|
+
except httpx.TimeoutException as e:
|
137
|
+
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
138
|
+
log.error(f"⏱️ Request timed out ({elapsed_ms:.0f}ms)")
|
139
|
+
raise TransportTimeoutError(f"Request timed out: {e}", request=request) from e
|
140
|
+
|
141
|
+
except httpx.RequestError as e:
|
142
|
+
log.error(f"❌ Request failed: {e}")
|
143
|
+
raise TransportConnectionError(f"Request failed: {e}", request=request) from e
|
144
|
+
|
145
|
+
except Exception as e:
|
146
|
+
log.error(f"❌ Unexpected error: {e}", exc_info=True)
|
147
|
+
raise TransportConnectionError(f"Unexpected error: {e}", request=request) from e
|
148
|
+
|
149
|
+
async def stream(self, request: Request) -> AsyncIterator[bytes]:
|
150
|
+
"""Stream HTTP response."""
|
151
|
+
await self.connect()
|
152
|
+
|
153
|
+
if self._client is None:
|
154
|
+
raise TransportConnectionError("HTTP client not connected")
|
155
|
+
|
156
|
+
log.info(f"🌊 Streaming {request.method} {request.uri}")
|
157
|
+
|
158
|
+
try:
|
159
|
+
async with self._client.stream(
|
160
|
+
method=request.method,
|
161
|
+
url=request.uri,
|
162
|
+
headers=request.headers,
|
163
|
+
params=request.params,
|
164
|
+
timeout=request.timeout or self.config.timeout,
|
165
|
+
) as response:
|
166
|
+
|
167
|
+
# Log response start
|
168
|
+
status_emoji = self._get_status_emoji(response.status_code)
|
169
|
+
log.info(f"{status_emoji} {response.status_code} (streaming)")
|
170
|
+
|
171
|
+
# Stream the response
|
172
|
+
async for chunk in response.aiter_bytes():
|
173
|
+
yield chunk
|
174
|
+
|
175
|
+
except httpx.ConnectError as e:
|
176
|
+
raise TransportConnectionError(f"Failed to connect: {e}", request=request) from e
|
177
|
+
|
178
|
+
except httpx.TimeoutException as e:
|
179
|
+
raise TransportTimeoutError(f"Stream timed out: {e}", request=request) from e
|
180
|
+
|
181
|
+
except httpx.RequestError as e:
|
182
|
+
raise TransportConnectionError(f"Stream failed: {e}", request=request) from e
|
183
|
+
|
184
|
+
def _get_status_emoji(self, status_code: int) -> str:
|
185
|
+
"""Get emoji for HTTP status code."""
|
186
|
+
if 200 <= status_code < 300:
|
187
|
+
return "✅" # Success
|
188
|
+
elif 300 <= status_code < 400:
|
189
|
+
return "↩️" # Redirect
|
190
|
+
elif 400 <= status_code < 500:
|
191
|
+
return "⚠️" # Client error
|
192
|
+
elif 500 <= status_code < 600:
|
193
|
+
return "❌" # Server error
|
194
|
+
else:
|
195
|
+
return "❓" # Unknown
|
196
|
+
|
197
|
+
|
198
|
+
# Auto-register HTTP transport
|
199
|
+
def _register_http_transport():
|
200
|
+
"""Register HTTP transport with the Hub."""
|
201
|
+
try:
|
202
|
+
from provide.foundation.transport.registry import register_transport
|
203
|
+
|
204
|
+
register_transport(
|
205
|
+
TransportType.HTTP,
|
206
|
+
HTTPTransport,
|
207
|
+
schemes=HTTPTransport.SCHEMES,
|
208
|
+
description="HTTP/HTTPS transport using httpx",
|
209
|
+
version="1.0.0",
|
210
|
+
)
|
211
|
+
|
212
|
+
# Also register HTTPS explicitly
|
213
|
+
register_transport(
|
214
|
+
TransportType.HTTPS,
|
215
|
+
HTTPTransport,
|
216
|
+
schemes=HTTPTransport.SCHEMES,
|
217
|
+
description="HTTP/HTTPS transport using httpx",
|
218
|
+
version="1.0.0",
|
219
|
+
)
|
220
|
+
|
221
|
+
except ImportError:
|
222
|
+
# Registry not available yet, will be registered later
|
223
|
+
pass
|
224
|
+
|
225
|
+
|
226
|
+
# Register when module is imported
|
227
|
+
_register_http_transport()
|
228
|
+
|
229
|
+
|
230
|
+
__all__ = [
|
231
|
+
"HTTPTransport",
|
232
|
+
]
|