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
@@ -6,7 +6,6 @@ checksum algorithms and GPG/PGP signatures.
|
|
6
6
|
"""
|
7
7
|
|
8
8
|
import hashlib
|
9
|
-
import re
|
10
9
|
from pathlib import Path
|
11
10
|
from typing import Literal
|
12
11
|
|
@@ -18,7 +17,7 @@ log = get_logger(__name__)
|
|
18
17
|
|
19
18
|
class VerificationError(FoundationError):
|
20
19
|
"""Raised when verification fails."""
|
21
|
-
|
20
|
+
|
22
21
|
pass
|
23
22
|
|
24
23
|
|
@@ -28,125 +27,115 @@ HashAlgo = Literal["sha256", "sha512", "md5", "blake2b"]
|
|
28
27
|
class ToolVerifier:
|
29
28
|
"""
|
30
29
|
Verify tool artifacts using checksums and signatures.
|
31
|
-
|
30
|
+
|
32
31
|
Supports multiple checksum algorithms and GPG/PGP signatures
|
33
32
|
for ensuring artifact integrity and authenticity.
|
34
33
|
"""
|
35
|
-
|
34
|
+
|
36
35
|
SUPPORTED_ALGORITHMS = ["sha256", "sha512", "md5", "blake2b"]
|
37
36
|
CHUNK_SIZE = 8192 # Read files in 8KB chunks
|
38
|
-
|
37
|
+
|
39
38
|
def verify_checksum(
|
40
|
-
self,
|
41
|
-
file_path: Path,
|
42
|
-
expected: str,
|
43
|
-
algo: HashAlgo = "sha256"
|
39
|
+
self, file_path: Path, expected: str, algo: HashAlgo = "sha256"
|
44
40
|
) -> bool:
|
45
41
|
"""
|
46
42
|
Verify file checksum.
|
47
|
-
|
43
|
+
|
48
44
|
Args:
|
49
45
|
file_path: Path to file to verify.
|
50
46
|
expected: Expected checksum (hex string).
|
51
47
|
algo: Hash algorithm to use.
|
52
|
-
|
48
|
+
|
53
49
|
Returns:
|
54
50
|
True if checksum matches, False otherwise.
|
55
|
-
|
51
|
+
|
56
52
|
Raises:
|
57
53
|
ValueError: If algorithm is not supported.
|
58
54
|
FileNotFoundError: If file doesn't exist.
|
59
55
|
"""
|
60
56
|
if algo not in self.SUPPORTED_ALGORITHMS:
|
61
57
|
raise ValueError(f"Unsupported hash algorithm: {algo}")
|
62
|
-
|
58
|
+
|
63
59
|
if not file_path.exists():
|
64
60
|
raise FileNotFoundError(f"File not found: {file_path}")
|
65
|
-
|
61
|
+
|
66
62
|
log.debug(f"Verifying {algo} checksum for {file_path}")
|
67
|
-
|
63
|
+
|
68
64
|
# Create hasher
|
69
65
|
hasher = hashlib.new(algo)
|
70
|
-
|
66
|
+
|
71
67
|
# Read file in chunks
|
72
68
|
with file_path.open("rb") as f:
|
73
69
|
while chunk := f.read(self.CHUNK_SIZE):
|
74
70
|
hasher.update(chunk)
|
75
|
-
|
71
|
+
|
76
72
|
actual = hasher.hexdigest()
|
77
73
|
matches = actual == expected
|
78
|
-
|
74
|
+
|
79
75
|
if not matches:
|
80
76
|
log.warning(
|
81
77
|
f"Checksum mismatch for {file_path.name}: "
|
82
78
|
f"expected {expected}, got {actual}"
|
83
79
|
)
|
84
|
-
|
80
|
+
|
85
81
|
return matches
|
86
|
-
|
87
|
-
def verify_shasums_file(
|
88
|
-
self,
|
89
|
-
shasums_file: Path,
|
90
|
-
target_file: Path
|
91
|
-
) -> bool:
|
82
|
+
|
83
|
+
def verify_shasums_file(self, shasums_file: Path, target_file: Path) -> bool:
|
92
84
|
"""
|
93
85
|
Verify using a shasums file (common for Go/Terraform).
|
94
|
-
|
86
|
+
|
95
87
|
Args:
|
96
88
|
shasums_file: Path to shasums file.
|
97
89
|
target_file: Path to file to verify.
|
98
|
-
|
90
|
+
|
99
91
|
Returns:
|
100
92
|
True if file is listed and checksum matches, False otherwise.
|
101
93
|
"""
|
102
94
|
log.debug(f"Verifying {target_file.name} using {shasums_file}")
|
103
|
-
|
95
|
+
|
104
96
|
with shasums_file.open() as f:
|
105
97
|
for line in f:
|
106
98
|
line = line.strip()
|
107
99
|
if not line:
|
108
100
|
continue
|
109
|
-
|
101
|
+
|
110
102
|
# Parse line: "checksum filename" or "checksum *filename"
|
111
103
|
parts = line.split(None, 1)
|
112
104
|
if len(parts) != 2:
|
113
105
|
continue
|
114
|
-
|
106
|
+
|
115
107
|
checksum, filename = parts
|
116
108
|
# Remove asterisk prefix if present (binary mode indicator)
|
117
109
|
filename = filename.lstrip("*")
|
118
|
-
|
110
|
+
|
119
111
|
# Check if this is our file
|
120
112
|
if filename == target_file.name:
|
121
113
|
return self.verify_checksum(target_file, checksum)
|
122
|
-
|
114
|
+
|
123
115
|
# File not found in shasums
|
124
116
|
log.warning(f"{target_file.name} not found in {shasums_file}")
|
125
117
|
return False
|
126
|
-
|
118
|
+
|
127
119
|
def verify_signature(
|
128
|
-
self,
|
129
|
-
file_path: Path,
|
130
|
-
signature: str,
|
131
|
-
public_key: str | None = None
|
120
|
+
self, file_path: Path, signature: str, public_key: str | None = None
|
132
121
|
) -> bool:
|
133
122
|
"""
|
134
123
|
Verify GPG/PGP signature.
|
135
|
-
|
124
|
+
|
136
125
|
Args:
|
137
126
|
file_path: Path to file to verify.
|
138
127
|
signature: Signature data.
|
139
128
|
public_key: Optional public key for verification.
|
140
|
-
|
129
|
+
|
141
130
|
Returns:
|
142
131
|
True if signature is valid, False otherwise.
|
143
132
|
"""
|
144
133
|
log.debug(f"Verifying signature for {file_path}")
|
145
|
-
|
134
|
+
|
146
135
|
try:
|
147
136
|
# Use foundation's crypto module
|
148
137
|
from provide.foundation.crypto import verify_signature
|
149
|
-
|
138
|
+
|
150
139
|
return verify_signature(file_path, signature, public_key)
|
151
140
|
except ImportError:
|
152
141
|
log.warning("Crypto module not available, skipping signature verification")
|
@@ -154,33 +143,33 @@ class ToolVerifier:
|
|
154
143
|
except Exception as e:
|
155
144
|
log.error(f"Signature verification failed: {e}")
|
156
145
|
return False
|
157
|
-
|
146
|
+
|
158
147
|
def extract_checksum(self, checksum_string: str) -> str:
|
159
148
|
"""
|
160
149
|
Extract checksum from various string formats.
|
161
|
-
|
150
|
+
|
162
151
|
Handles formats like:
|
163
152
|
- "abc123"
|
164
153
|
- "abc123 filename.tar.gz"
|
165
154
|
- "sha256:abc123"
|
166
155
|
- "SHA256:def456"
|
167
|
-
|
156
|
+
|
168
157
|
Args:
|
169
158
|
checksum_string: String containing checksum.
|
170
|
-
|
159
|
+
|
171
160
|
Returns:
|
172
161
|
Extracted checksum hex string.
|
173
162
|
"""
|
174
163
|
checksum_string = checksum_string.strip()
|
175
|
-
|
164
|
+
|
176
165
|
# Remove algorithm prefix if present
|
177
166
|
if ":" in checksum_string:
|
178
167
|
checksum_string = checksum_string.split(":", 1)[1]
|
179
|
-
|
168
|
+
|
180
169
|
# Take first word (checksum is before any whitespace)
|
181
170
|
checksum = checksum_string.split()[0]
|
182
|
-
|
171
|
+
|
183
172
|
# Remove any asterisk prefix (binary mode indicator)
|
184
173
|
checksum = checksum.lstrip("*")
|
185
|
-
|
186
|
-
return checksum
|
174
|
+
|
175
|
+
return checksum
|
@@ -47,9 +47,7 @@ class Span:
|
|
47
47
|
error: str | None = None
|
48
48
|
|
49
49
|
# Internal OpenTelemetry span (when available)
|
50
|
-
_otel_span: "otel_trace.Span | None" = field(
|
51
|
-
default=None, init=False, repr=False
|
52
|
-
)
|
50
|
+
_otel_span: "otel_trace.Span | None" = field(default=None, init=False, repr=False)
|
53
51
|
_active: bool = field(default=True, init=False, repr=False)
|
54
52
|
|
55
53
|
def __post_init__(self) -> None:
|
@@ -162,13 +160,3 @@ class Span:
|
|
162
160
|
"status": self.status,
|
163
161
|
"error": self.error,
|
164
162
|
}
|
165
|
-
|
166
|
-
def __enter__(self):
|
167
|
-
"""Context manager entry."""
|
168
|
-
return self
|
169
|
-
|
170
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
171
|
-
"""Context manager exit."""
|
172
|
-
if exc_type is not None:
|
173
|
-
self.set_error(f"{exc_type.__name__}: {exc_val}")
|
174
|
-
self.finish()
|
@@ -14,28 +14,28 @@ Key Features:
|
|
14
14
|
|
15
15
|
Example Usage:
|
16
16
|
>>> from provide.foundation.transport import get, post
|
17
|
-
>>>
|
17
|
+
>>>
|
18
18
|
>>> # Simple requests
|
19
19
|
>>> response = await get("https://api.example.com/users")
|
20
20
|
>>> data = response.json()
|
21
|
-
>>>
|
21
|
+
>>>
|
22
22
|
>>> # POST with JSON body
|
23
23
|
>>> response = await post(
|
24
24
|
... "https://api.example.com/users",
|
25
25
|
... body={"name": "John", "email": "john@example.com"}
|
26
26
|
... )
|
27
|
-
>>>
|
27
|
+
>>>
|
28
28
|
>>> # Using client for multiple requests
|
29
29
|
>>> from provide.foundation.transport import UniversalClient
|
30
|
-
>>>
|
30
|
+
>>>
|
31
31
|
>>> async with UniversalClient() as client:
|
32
32
|
... users = await client.get("https://api.example.com/users")
|
33
33
|
... posts = await client.get("https://api.example.com/posts")
|
34
|
-
>>>
|
34
|
+
>>>
|
35
35
|
>>> # Custom transport registration
|
36
36
|
>>> from provide.foundation.transport import register_transport
|
37
37
|
>>> from provide.foundation.transport.types import TransportType
|
38
|
-
>>>
|
38
|
+
>>>
|
39
39
|
>>> register_transport(TransportType("custom"), MyCustomTransport)
|
40
40
|
|
41
41
|
Environment Configuration:
|
@@ -43,7 +43,7 @@ Environment Configuration:
|
|
43
43
|
TRANSPORT_MAX_RETRIES=3
|
44
44
|
TRANSPORT_RETRY_BACKOFF_FACTOR=0.5
|
45
45
|
TRANSPORT_VERIFY_SSL=true
|
46
|
-
|
46
|
+
|
47
47
|
HTTP_POOL_CONNECTIONS=10
|
48
48
|
HTTP_POOL_MAXSIZE=100
|
49
49
|
HTTP_FOLLOW_REDIRECTS=true
|
@@ -54,9 +54,23 @@ Environment Configuration:
|
|
54
54
|
# Core transport abstractions
|
55
55
|
from provide.foundation.transport.base import Request, Response
|
56
56
|
|
57
|
+
# High-level client API
|
58
|
+
from provide.foundation.transport.client import (
|
59
|
+
UniversalClient,
|
60
|
+
delete,
|
61
|
+
get,
|
62
|
+
get_default_client,
|
63
|
+
head,
|
64
|
+
options,
|
65
|
+
patch,
|
66
|
+
post,
|
67
|
+
put,
|
68
|
+
request,
|
69
|
+
stream,
|
70
|
+
)
|
71
|
+
|
57
72
|
# Transport types and configuration
|
58
73
|
from provide.foundation.transport.config import HTTPConfig, TransportConfig
|
59
|
-
from provide.foundation.transport.types import HTTPMethod, TransportType
|
60
74
|
|
61
75
|
# Error types
|
62
76
|
from provide.foundation.transport.errors import (
|
@@ -87,45 +101,26 @@ from provide.foundation.transport.registry import (
|
|
87
101
|
list_registered_transports,
|
88
102
|
register_transport,
|
89
103
|
)
|
90
|
-
|
91
|
-
# High-level client API
|
92
|
-
from provide.foundation.transport.client import (
|
93
|
-
UniversalClient,
|
94
|
-
delete,
|
95
|
-
get,
|
96
|
-
get_default_client,
|
97
|
-
head,
|
98
|
-
options,
|
99
|
-
patch,
|
100
|
-
post,
|
101
|
-
put,
|
102
|
-
request,
|
103
|
-
stream,
|
104
|
-
)
|
104
|
+
from provide.foundation.transport.types import HTTPMethod, TransportType
|
105
105
|
|
106
106
|
__all__ = [
|
107
107
|
# Core abstractions
|
108
108
|
"Request",
|
109
109
|
"Response",
|
110
|
-
|
111
110
|
# Configuration
|
112
111
|
"TransportConfig",
|
113
112
|
"HTTPConfig",
|
114
|
-
|
115
113
|
# Types
|
116
114
|
"TransportType",
|
117
115
|
"HTTPMethod",
|
118
|
-
|
119
116
|
# Errors
|
120
117
|
"TransportError",
|
121
118
|
"TransportConnectionError",
|
122
|
-
"TransportTimeoutError",
|
119
|
+
"TransportTimeoutError",
|
123
120
|
"HTTPResponseError",
|
124
121
|
"TransportNotFoundError",
|
125
|
-
|
126
122
|
# Transport implementations
|
127
123
|
"HTTPTransport",
|
128
|
-
|
129
124
|
# Middleware
|
130
125
|
"Middleware",
|
131
126
|
"MiddlewarePipeline",
|
@@ -133,13 +128,11 @@ __all__ = [
|
|
133
128
|
"RetryMiddleware",
|
134
129
|
"MetricsMiddleware",
|
135
130
|
"create_default_pipeline",
|
136
|
-
|
137
131
|
# Registry
|
138
132
|
"register_transport",
|
139
133
|
"get_transport",
|
140
134
|
"get_transport_info",
|
141
135
|
"list_registered_transports",
|
142
|
-
|
143
136
|
# Client API
|
144
137
|
"UniversalClient",
|
145
138
|
"get_default_client",
|
@@ -147,9 +140,9 @@ __all__ = [
|
|
147
140
|
"get",
|
148
141
|
"post",
|
149
142
|
"put",
|
150
|
-
"patch",
|
143
|
+
"patch",
|
151
144
|
"delete",
|
152
145
|
"head",
|
153
146
|
"options",
|
154
147
|
"stream",
|
155
|
-
]
|
148
|
+
]
|
@@ -2,7 +2,6 @@
|
|
2
2
|
Core transport abstractions.
|
3
3
|
"""
|
4
4
|
|
5
|
-
import time
|
6
5
|
from abc import ABC, abstractmethod
|
7
6
|
from collections.abc import AsyncIterator
|
8
7
|
from typing import Any, Protocol, runtime_checkable
|
@@ -18,7 +17,7 @@ log = get_logger(__name__)
|
|
18
17
|
@define
|
19
18
|
class Request:
|
20
19
|
"""Protocol-agnostic request."""
|
21
|
-
|
20
|
+
|
22
21
|
uri: str
|
23
22
|
method: str = "GET"
|
24
23
|
headers: Headers = field(factory=dict)
|
@@ -26,7 +25,7 @@ class Request:
|
|
26
25
|
body: Data = None
|
27
26
|
timeout: float | None = None
|
28
27
|
metadata: dict[str, Any] = field(factory=dict)
|
29
|
-
|
28
|
+
|
30
29
|
@property
|
31
30
|
def transport_type(self) -> TransportType:
|
32
31
|
"""Infer transport type from URI scheme."""
|
@@ -36,7 +35,7 @@ class Request:
|
|
36
35
|
except ValueError:
|
37
36
|
log.trace(f"Unknown scheme '{scheme}', defaulting to HTTP")
|
38
37
|
return TransportType.HTTP
|
39
|
-
|
38
|
+
|
40
39
|
@property
|
41
40
|
def base_url(self) -> str:
|
42
41
|
"""Extract base URL from URI."""
|
@@ -49,43 +48,44 @@ class Request:
|
|
49
48
|
@define
|
50
49
|
class Response:
|
51
50
|
"""Protocol-agnostic response."""
|
52
|
-
|
51
|
+
|
53
52
|
status: int
|
54
53
|
headers: Headers = field(factory=dict)
|
55
54
|
body: bytes | str | None = None
|
56
55
|
metadata: dict[str, Any] = field(factory=dict)
|
57
56
|
elapsed_ms: float = 0
|
58
57
|
request: Request | None = None
|
59
|
-
|
58
|
+
|
60
59
|
def is_success(self) -> bool:
|
61
60
|
"""Check if response indicates success."""
|
62
61
|
return 200 <= self.status < 300
|
63
|
-
|
62
|
+
|
64
63
|
def json(self) -> Any:
|
65
64
|
"""Parse response body as JSON."""
|
66
65
|
import json
|
67
|
-
|
66
|
+
|
68
67
|
if isinstance(self.body, bytes):
|
69
|
-
return json.loads(self.body.decode(
|
68
|
+
return json.loads(self.body.decode("utf-8"))
|
70
69
|
elif isinstance(self.body, str):
|
71
70
|
return json.loads(self.body)
|
72
71
|
else:
|
73
72
|
raise ValueError("Response body is not JSON-parseable")
|
74
|
-
|
73
|
+
|
75
74
|
@property
|
76
75
|
def text(self) -> str:
|
77
76
|
"""Get response body as text."""
|
78
77
|
if isinstance(self.body, bytes):
|
79
|
-
return self.body.decode(
|
78
|
+
return self.body.decode("utf-8")
|
80
79
|
elif isinstance(self.body, str):
|
81
80
|
return self.body
|
82
81
|
else:
|
83
82
|
return str(self.body or "")
|
84
|
-
|
83
|
+
|
85
84
|
def raise_for_status(self) -> None:
|
86
85
|
"""Raise error if response status indicates failure."""
|
87
86
|
if not self.is_success():
|
88
87
|
from provide.foundation.transport.errors import HTTPResponseError
|
88
|
+
|
89
89
|
raise HTTPResponseError(
|
90
90
|
f"Request failed with status {self.status}",
|
91
91
|
status_code=self.status,
|
@@ -96,32 +96,32 @@ class Response:
|
|
96
96
|
@runtime_checkable
|
97
97
|
class Transport(Protocol):
|
98
98
|
"""Abstract transport protocol."""
|
99
|
-
|
99
|
+
|
100
100
|
async def execute(self, request: Request) -> Response:
|
101
101
|
"""Execute a request and return response."""
|
102
102
|
...
|
103
|
-
|
103
|
+
|
104
104
|
async def stream(self, request: Request) -> AsyncIterator[bytes]:
|
105
105
|
"""Stream response data."""
|
106
106
|
...
|
107
|
-
|
107
|
+
|
108
108
|
async def connect(self) -> None:
|
109
109
|
"""Establish connection if needed."""
|
110
110
|
...
|
111
|
-
|
111
|
+
|
112
112
|
async def disconnect(self) -> None:
|
113
113
|
"""Close connection if needed."""
|
114
114
|
...
|
115
|
-
|
115
|
+
|
116
116
|
def supports(self, transport_type: TransportType) -> bool:
|
117
117
|
"""Check if this transport handles the given type."""
|
118
118
|
...
|
119
|
-
|
119
|
+
|
120
120
|
async def __aenter__(self) -> "Transport":
|
121
121
|
"""Context manager entry."""
|
122
122
|
await self.connect()
|
123
123
|
return self
|
124
|
-
|
124
|
+
|
125
125
|
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
126
126
|
"""Context manager exit."""
|
127
127
|
await self.disconnect()
|
@@ -129,43 +129,45 @@ class Transport(Protocol):
|
|
129
129
|
|
130
130
|
class TransportBase(ABC):
|
131
131
|
"""Base class for transport implementations."""
|
132
|
-
|
132
|
+
|
133
133
|
def __init__(self):
|
134
134
|
self._logger = get_logger(self.__class__.__name__)
|
135
|
-
|
135
|
+
|
136
136
|
@abstractmethod
|
137
137
|
async def execute(self, request: Request) -> Response:
|
138
138
|
"""Execute a request and return response."""
|
139
139
|
pass
|
140
|
-
|
140
|
+
|
141
141
|
@abstractmethod
|
142
142
|
def supports(self, transport_type: TransportType) -> bool:
|
143
143
|
"""Check if this transport handles the given type."""
|
144
144
|
pass
|
145
|
-
|
145
|
+
|
146
146
|
async def connect(self) -> None:
|
147
147
|
"""Default connect implementation."""
|
148
148
|
self._logger.trace("Transport connecting")
|
149
|
-
|
149
|
+
|
150
150
|
async def disconnect(self) -> None:
|
151
151
|
"""Default disconnect implementation."""
|
152
152
|
self._logger.trace("Transport disconnecting")
|
153
|
-
|
153
|
+
|
154
154
|
async def stream(self, request: Request) -> AsyncIterator[bytes]:
|
155
155
|
"""Default streaming implementation (not supported)."""
|
156
|
-
raise NotImplementedError(
|
157
|
-
|
156
|
+
raise NotImplementedError(
|
157
|
+
f"{self.__class__.__name__} does not support streaming"
|
158
|
+
)
|
159
|
+
|
158
160
|
async def __aenter__(self) -> "TransportBase":
|
159
161
|
await self.connect()
|
160
162
|
return self
|
161
|
-
|
163
|
+
|
162
164
|
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
163
165
|
await self.disconnect()
|
164
166
|
|
165
167
|
|
166
168
|
__all__ = [
|
167
169
|
"Request",
|
168
|
-
"Response",
|
170
|
+
"Response",
|
169
171
|
"Transport",
|
170
172
|
"TransportBase",
|
171
|
-
]
|
173
|
+
]
|