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
@@ -1,10 +1,11 @@
|
|
1
1
|
"""ZIP archive implementation."""
|
2
2
|
|
3
|
-
import zipfile
|
4
3
|
from pathlib import Path
|
4
|
+
import zipfile
|
5
|
+
|
5
6
|
from attrs import define, field
|
6
7
|
|
7
|
-
from provide.foundation.archive.base import
|
8
|
+
from provide.foundation.archive.base import ArchiveError, BaseArchive
|
8
9
|
from provide.foundation.file import ensure_parent_dir
|
9
10
|
from provide.foundation.logger import get_logger
|
10
11
|
|
@@ -15,45 +16,46 @@ logger = get_logger(__name__)
|
|
15
16
|
class ZipArchive(BaseArchive):
|
16
17
|
"""
|
17
18
|
ZIP archive implementation.
|
18
|
-
|
19
|
+
|
19
20
|
Creates and extracts ZIP archives with optional compression and encryption.
|
20
21
|
Supports adding files to existing archives.
|
21
22
|
"""
|
22
|
-
|
23
|
+
|
23
24
|
compression_level: int = field(default=6) # Compression level 0-9 (0=store, 9=best)
|
24
25
|
compression_type: int = field(default=zipfile.ZIP_DEFLATED)
|
25
26
|
password: bytes | None = field(default=None)
|
26
|
-
|
27
|
+
|
27
28
|
@compression_level.validator
|
28
|
-
def _validate_level(self, attribute, value):
|
29
|
+
def _validate_level(self, attribute: object, value: int) -> None:
|
29
30
|
if not 0 <= value <= 9:
|
30
31
|
raise ValueError(f"Compression level must be 0-9, got {value}")
|
31
|
-
|
32
|
+
|
32
33
|
def create(self, source: Path, output: Path) -> Path:
|
33
34
|
"""
|
34
35
|
Create ZIP archive from source.
|
35
|
-
|
36
|
+
|
36
37
|
Args:
|
37
38
|
source: Source file or directory to archive
|
38
39
|
output: Output ZIP file path
|
39
|
-
|
40
|
+
|
40
41
|
Returns:
|
41
42
|
Path to created archive
|
42
|
-
|
43
|
+
|
43
44
|
Raises:
|
44
45
|
ArchiveError: If archive creation fails
|
45
46
|
"""
|
46
47
|
try:
|
47
48
|
ensure_parent_dir(output)
|
48
|
-
|
49
|
+
|
49
50
|
with zipfile.ZipFile(
|
50
|
-
output,
|
51
|
+
output,
|
52
|
+
"w",
|
51
53
|
compression=self.compression_type,
|
52
|
-
compresslevel=self.compression_level
|
54
|
+
compresslevel=self.compression_level,
|
53
55
|
) as zf:
|
54
56
|
if self.password:
|
55
57
|
zf.setpassword(self.password)
|
56
|
-
|
58
|
+
|
57
59
|
if source.is_dir():
|
58
60
|
# Add all files in directory
|
59
61
|
for item in sorted(source.rglob("*")):
|
@@ -63,141 +65,141 @@ class ZipArchive(BaseArchive):
|
|
63
65
|
else:
|
64
66
|
# Add single file
|
65
67
|
zf.write(source, source.name)
|
66
|
-
|
68
|
+
|
67
69
|
logger.debug(f"Created ZIP archive: {output}")
|
68
70
|
return output
|
69
|
-
|
71
|
+
|
70
72
|
except Exception as e:
|
71
73
|
raise ArchiveError(f"Failed to create ZIP archive: {e}") from e
|
72
74
|
|
73
75
|
def extract(self, archive: Path, output: Path) -> Path:
|
74
76
|
"""
|
75
77
|
Extract ZIP archive to output directory.
|
76
|
-
|
78
|
+
|
77
79
|
Args:
|
78
80
|
archive: ZIP archive file path
|
79
81
|
output: Output directory path
|
80
|
-
|
82
|
+
|
81
83
|
Returns:
|
82
84
|
Path to extraction directory
|
83
|
-
|
85
|
+
|
84
86
|
Raises:
|
85
87
|
ArchiveError: If extraction fails
|
86
88
|
"""
|
87
89
|
try:
|
88
90
|
output.mkdir(parents=True, exist_ok=True)
|
89
|
-
|
90
|
-
with zipfile.ZipFile(archive,
|
91
|
+
|
92
|
+
with zipfile.ZipFile(archive, "r") as zf:
|
91
93
|
if self.password:
|
92
94
|
zf.setpassword(self.password)
|
93
|
-
|
95
|
+
|
94
96
|
# Security check - prevent path traversal
|
95
97
|
for member in zf.namelist():
|
96
98
|
if member.startswith("/") or ".." in member:
|
97
99
|
raise ArchiveError(f"Unsafe path in archive: {member}")
|
98
|
-
|
100
|
+
|
99
101
|
# Extract all
|
100
102
|
zf.extractall(output)
|
101
|
-
|
103
|
+
|
102
104
|
logger.debug(f"Extracted ZIP archive to: {output}")
|
103
105
|
return output
|
104
|
-
|
106
|
+
|
105
107
|
except Exception as e:
|
106
108
|
raise ArchiveError(f"Failed to extract ZIP archive: {e}") from e
|
107
109
|
|
108
110
|
def validate(self, archive: Path) -> bool:
|
109
111
|
"""
|
110
112
|
Validate ZIP archive integrity.
|
111
|
-
|
113
|
+
|
112
114
|
Args:
|
113
115
|
archive: ZIP archive file path
|
114
|
-
|
116
|
+
|
115
117
|
Returns:
|
116
118
|
True if archive is valid, False otherwise
|
117
119
|
"""
|
118
120
|
try:
|
119
|
-
with zipfile.ZipFile(archive,
|
121
|
+
with zipfile.ZipFile(archive, "r") as zf:
|
120
122
|
# Test the archive
|
121
123
|
result = zf.testzip()
|
122
124
|
return result is None # None means no bad files
|
123
125
|
except Exception:
|
124
126
|
return False
|
125
|
-
|
127
|
+
|
126
128
|
def list_contents(self, archive: Path) -> list[str]:
|
127
129
|
"""
|
128
130
|
List contents of ZIP archive.
|
129
|
-
|
131
|
+
|
130
132
|
Args:
|
131
133
|
archive: ZIP archive file path
|
132
|
-
|
134
|
+
|
133
135
|
Returns:
|
134
136
|
List of file paths in archive
|
135
|
-
|
137
|
+
|
136
138
|
Raises:
|
137
139
|
ArchiveError: If listing fails
|
138
140
|
"""
|
139
141
|
try:
|
140
|
-
with zipfile.ZipFile(archive,
|
142
|
+
with zipfile.ZipFile(archive, "r") as zf:
|
141
143
|
return sorted(zf.namelist())
|
142
144
|
except Exception as e:
|
143
145
|
raise ArchiveError(f"Failed to list ZIP contents: {e}") from e
|
144
|
-
|
146
|
+
|
145
147
|
def add_file(self, archive: Path, file: Path, arcname: str | None = None) -> None:
|
146
148
|
"""
|
147
149
|
Add file to existing ZIP archive.
|
148
|
-
|
150
|
+
|
149
151
|
Args:
|
150
152
|
archive: ZIP archive file path
|
151
153
|
file: File to add
|
152
154
|
arcname: Name in archive (defaults to file name)
|
153
|
-
|
155
|
+
|
154
156
|
Raises:
|
155
157
|
ArchiveError: If adding file fails
|
156
158
|
"""
|
157
159
|
try:
|
158
|
-
with zipfile.ZipFile(archive,
|
160
|
+
with zipfile.ZipFile(archive, "a", compression=self.compression_type) as zf:
|
159
161
|
if self.password:
|
160
162
|
zf.setpassword(self.password)
|
161
|
-
|
163
|
+
|
162
164
|
zf.write(file, arcname or file.name)
|
163
|
-
|
165
|
+
|
164
166
|
logger.debug(f"Added {file} to ZIP archive {archive}")
|
165
|
-
|
167
|
+
|
166
168
|
except Exception as e:
|
167
169
|
raise ArchiveError(f"Failed to add file to ZIP: {e}") from e
|
168
|
-
|
170
|
+
|
169
171
|
def extract_file(self, archive: Path, member: str, output: Path) -> Path:
|
170
172
|
"""
|
171
173
|
Extract single file from ZIP archive.
|
172
|
-
|
174
|
+
|
173
175
|
Args:
|
174
176
|
archive: ZIP archive file path
|
175
177
|
member: Name of file in archive
|
176
178
|
output: Output directory or file path
|
177
|
-
|
179
|
+
|
178
180
|
Returns:
|
179
181
|
Path to extracted file
|
180
|
-
|
182
|
+
|
181
183
|
Raises:
|
182
184
|
ArchiveError: If extraction fails
|
183
185
|
"""
|
184
186
|
try:
|
185
|
-
with zipfile.ZipFile(archive,
|
187
|
+
with zipfile.ZipFile(archive, "r") as zf:
|
186
188
|
if self.password:
|
187
189
|
zf.setpassword(self.password)
|
188
|
-
|
190
|
+
|
189
191
|
# Security check
|
190
192
|
if member.startswith("/") or ".." in member:
|
191
193
|
raise ArchiveError(f"Unsafe path: {member}")
|
192
|
-
|
194
|
+
|
193
195
|
if output.is_dir():
|
194
196
|
zf.extract(member, output)
|
195
197
|
return output / member
|
196
198
|
else:
|
197
199
|
ensure_parent_dir(output)
|
198
|
-
with zf.open(member) as source, open(output,
|
200
|
+
with zf.open(member) as source, open(output, "wb") as target:
|
199
201
|
target.write(source.read())
|
200
202
|
return output
|
201
|
-
|
203
|
+
|
202
204
|
except Exception as e:
|
203
|
-
raise ArchiveError(f"Failed to extract file from ZIP: {e}") from e
|
205
|
+
raise ArchiveError(f"Failed to extract file from ZIP: {e}") from e
|
@@ -0,0 +1,20 @@
|
|
1
|
+
"""
|
2
|
+
Async utilities for Foundation.
|
3
|
+
|
4
|
+
Provides consistent async/await patterns, task management,
|
5
|
+
and async context utilities for Foundation applications.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from provide.foundation.asynctools.core import (
|
9
|
+
provide_gather,
|
10
|
+
provide_run,
|
11
|
+
provide_sleep_async,
|
12
|
+
provide_wait_for,
|
13
|
+
)
|
14
|
+
|
15
|
+
__all__ = [
|
16
|
+
"provide_gather",
|
17
|
+
"provide_run",
|
18
|
+
"provide_sleep_async",
|
19
|
+
"provide_wait_for",
|
20
|
+
]
|
@@ -0,0 +1,126 @@
|
|
1
|
+
"""Core async utilities for Foundation."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
from collections.abc import Awaitable, Callable
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
from provide.foundation.errors import ValidationError
|
8
|
+
|
9
|
+
|
10
|
+
async def provide_sleep_async(delay: float) -> None:
|
11
|
+
"""
|
12
|
+
Async sleep with Foundation tracking and cancellation support.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
delay: Number of seconds to sleep
|
16
|
+
|
17
|
+
Raises:
|
18
|
+
ValidationError: If delay is negative
|
19
|
+
|
20
|
+
Example:
|
21
|
+
>>> import asyncio
|
22
|
+
>>> async def main():
|
23
|
+
... await provide_sleep_async(0.1)
|
24
|
+
>>> asyncio.run(main())
|
25
|
+
"""
|
26
|
+
if delay < 0:
|
27
|
+
raise ValidationError("Sleep delay must be non-negative")
|
28
|
+
await asyncio.sleep(delay)
|
29
|
+
|
30
|
+
|
31
|
+
async def provide_gather(
|
32
|
+
*awaitables: Awaitable[Any], return_exceptions: bool = False
|
33
|
+
) -> list[Any]:
|
34
|
+
"""
|
35
|
+
Run awaitables concurrently with Foundation tracking.
|
36
|
+
|
37
|
+
Args:
|
38
|
+
*awaitables: Awaitable objects to run concurrently
|
39
|
+
return_exceptions: If True, exceptions are returned as results
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
List of results in the same order as input awaitables
|
43
|
+
|
44
|
+
Raises:
|
45
|
+
ValidationError: If no awaitables provided
|
46
|
+
|
47
|
+
Example:
|
48
|
+
>>> import asyncio
|
49
|
+
>>> async def fetch_data(n):
|
50
|
+
... await provide_sleep_async(0.1)
|
51
|
+
... return n * 2
|
52
|
+
>>> async def main():
|
53
|
+
... results = await provide_gather(
|
54
|
+
... fetch_data(1), fetch_data(2), fetch_data(3)
|
55
|
+
... )
|
56
|
+
... return results
|
57
|
+
>>> asyncio.run(main())
|
58
|
+
[2, 4, 6]
|
59
|
+
"""
|
60
|
+
if not awaitables:
|
61
|
+
raise ValidationError("At least one awaitable must be provided")
|
62
|
+
|
63
|
+
return await asyncio.gather(*awaitables, return_exceptions=return_exceptions)
|
64
|
+
|
65
|
+
|
66
|
+
async def provide_wait_for(awaitable: Awaitable[Any], timeout: float | None) -> Any:
|
67
|
+
"""
|
68
|
+
Wait for an awaitable with optional timeout.
|
69
|
+
|
70
|
+
Args:
|
71
|
+
awaitable: The awaitable to wait for
|
72
|
+
timeout: Timeout in seconds (None for no timeout)
|
73
|
+
|
74
|
+
Returns:
|
75
|
+
Result of the awaitable
|
76
|
+
|
77
|
+
Raises:
|
78
|
+
ValidationError: If timeout is negative
|
79
|
+
asyncio.TimeoutError: If timeout is exceeded
|
80
|
+
|
81
|
+
Example:
|
82
|
+
>>> import asyncio
|
83
|
+
>>> async def slow_task():
|
84
|
+
... await provide_sleep_async(0.2)
|
85
|
+
... return "done"
|
86
|
+
>>> async def main():
|
87
|
+
... try:
|
88
|
+
... result = await provide_wait_for(slow_task(), timeout=0.1)
|
89
|
+
... except asyncio.TimeoutError:
|
90
|
+
... result = "timed out"
|
91
|
+
... return result
|
92
|
+
>>> asyncio.run(main())
|
93
|
+
'timed out'
|
94
|
+
"""
|
95
|
+
if timeout is not None and timeout < 0:
|
96
|
+
raise ValidationError("Timeout must be non-negative")
|
97
|
+
|
98
|
+
return await asyncio.wait_for(awaitable, timeout=timeout)
|
99
|
+
|
100
|
+
|
101
|
+
def provide_run(main: Callable[[], Awaitable[Any]], *, debug: bool = False) -> Any:
|
102
|
+
"""
|
103
|
+
Run async function with Foundation tracking.
|
104
|
+
|
105
|
+
Args:
|
106
|
+
main: Async function to run
|
107
|
+
debug: Whether to run in debug mode
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
Result of the main function
|
111
|
+
|
112
|
+
Raises:
|
113
|
+
ValidationError: If main is not callable
|
114
|
+
|
115
|
+
Example:
|
116
|
+
>>> async def main():
|
117
|
+
... await provide_sleep_async(0.1)
|
118
|
+
... return "hello"
|
119
|
+
>>> result = provide_run(main)
|
120
|
+
>>> result
|
121
|
+
'hello'
|
122
|
+
"""
|
123
|
+
if not callable(main):
|
124
|
+
raise ValidationError("Main must be callable")
|
125
|
+
|
126
|
+
return asyncio.run(main(), debug=debug)
|
@@ -21,8 +21,6 @@ from provide.foundation.cli.testing import (
|
|
21
21
|
isolated_cli_runner,
|
22
22
|
temp_config_file,
|
23
23
|
)
|
24
|
-
from provide.foundation.testing.cli import MockContext
|
25
|
-
from provide.foundation.testing.logger import mock_logger
|
26
24
|
from provide.foundation.cli.utils import (
|
27
25
|
CliTestRunner,
|
28
26
|
assert_cli_error,
|
@@ -35,6 +33,8 @@ from provide.foundation.cli.utils import (
|
|
35
33
|
echo_warning,
|
36
34
|
setup_cli_logging,
|
37
35
|
)
|
36
|
+
from provide.foundation.testing.cli import MockContext
|
37
|
+
from provide.foundation.testing.logger import mock_logger
|
38
38
|
|
39
39
|
__all__ = [
|
40
40
|
"CliTestCase",
|
@@ -8,12 +8,12 @@ except ImportError:
|
|
8
8
|
click = None
|
9
9
|
_HAS_CLICK = False
|
10
10
|
|
11
|
-
from provide.foundation.utils.deps import check_optional_deps, has_dependency
|
12
11
|
from provide.foundation.console.output import pout
|
13
|
-
from provide.foundation.process import
|
12
|
+
from provide.foundation.process import exit_error, exit_success
|
13
|
+
from provide.foundation.utils.deps import check_optional_deps, has_dependency
|
14
14
|
|
15
15
|
|
16
|
-
def _require_click():
|
16
|
+
def _require_click() -> None:
|
17
17
|
"""Ensure click is available for CLI commands."""
|
18
18
|
if not _HAS_CLICK:
|
19
19
|
raise ImportError(
|
@@ -68,7 +68,7 @@ if _HAS_CLICK:
|
|
68
68
|
_deps_command_impl(quiet, check)
|
69
69
|
else:
|
70
70
|
# Stub for when click is not available
|
71
|
-
def deps_command(*args, **kwargs):
|
71
|
+
def deps_command(*args: object, **kwargs: object) -> None:
|
72
72
|
"""Deps command stub when click is not available."""
|
73
73
|
_require_click()
|
74
74
|
|
@@ -21,7 +21,7 @@ if _HAS_CLICK:
|
|
21
21
|
|
22
22
|
@click.group("logs", help="Send and query logs with OpenTelemetry integration")
|
23
23
|
@click.pass_context
|
24
|
-
def logs_group(ctx):
|
24
|
+
def logs_group(ctx: click.Context) -> None:
|
25
25
|
"""Logs management commands with OTEL correlation."""
|
26
26
|
# Store shared context
|
27
27
|
ctx.ensure_object(dict)
|
@@ -53,7 +53,7 @@ if _HAS_CLICK:
|
|
53
53
|
|
54
54
|
else:
|
55
55
|
# Stub when click is not available
|
56
|
-
def logs_group(*args, **kwargs):
|
56
|
+
def logs_group(*args: object, **kwargs: object) -> None:
|
57
57
|
"""Logs command stub when click is not available."""
|
58
58
|
raise ImportError(
|
59
59
|
"CLI commands require optional dependencies. "
|
@@ -205,7 +205,7 @@ def generate_logs_command(
|
|
205
205
|
error_rate: float,
|
206
206
|
enable_rate_limit: bool,
|
207
207
|
rate_limit: float,
|
208
|
-
):
|
208
|
+
) -> None:
|
209
209
|
"""Generate logs to test OpenObserve integration with Foundation's rate limiting."""
|
210
210
|
|
211
211
|
click.echo("🚀 Starting log generation...")
|
@@ -351,7 +351,7 @@ def generate_logs_command(
|
|
351
351
|
|
352
352
|
if not _HAS_CLICK:
|
353
353
|
|
354
|
-
def generate_logs_command(*args, **kwargs):
|
354
|
+
def generate_logs_command(*args: object, **kwargs: object) -> None:
|
355
355
|
raise ImportError(
|
356
356
|
"Click is required for CLI commands. Install with: pip install click"
|
357
357
|
)
|
@@ -66,8 +66,8 @@ if _HAS_CLICK:
|
|
66
66
|
)
|
67
67
|
@click.pass_context
|
68
68
|
def query_command(
|
69
|
-
ctx, sql, current_trace, trace_id, level, service, last, stream, size, format
|
70
|
-
):
|
69
|
+
ctx: click.Context, sql: str | None, current_trace: bool, trace_id: str | None, level: str | None, service: str | None, last: str, stream: str, size: int, format: str
|
70
|
+
) -> int | None:
|
71
71
|
"""Query logs from OpenObserve.
|
72
72
|
|
73
73
|
Examples:
|
@@ -166,7 +166,7 @@ if _HAS_CLICK:
|
|
166
166
|
|
167
167
|
else:
|
168
168
|
|
169
|
-
def query_command(*args, **kwargs):
|
169
|
+
def query_command(*args: object, **kwargs: object) -> None:
|
170
170
|
"""Query command stub when click is not available."""
|
171
171
|
raise ImportError(
|
172
172
|
"CLI commands require optional dependencies. "
|
@@ -66,8 +66,8 @@ if _HAS_CLICK:
|
|
66
66
|
)
|
67
67
|
@click.pass_context
|
68
68
|
def send_command(
|
69
|
-
ctx, message, level, service, json_attrs, attr, trace_id, span_id, use_otlp
|
70
|
-
):
|
69
|
+
ctx: click.Context, message: str | None, level: str, service: str | None, json_attrs: str | None, attr: tuple[str, ...], trace_id: str | None, span_id: str | None, use_otlp: bool
|
70
|
+
) -> int | None:
|
71
71
|
"""Send a log entry to OpenObserve.
|
72
72
|
|
73
73
|
Examples:
|
@@ -50,7 +50,7 @@ if _HAS_CLICK:
|
|
50
50
|
help="Output format",
|
51
51
|
)
|
52
52
|
@click.pass_context
|
53
|
-
def tail_command(ctx, stream, filter_sql, lines, follow, format):
|
53
|
+
def tail_command(ctx: click.Context, stream: str, filter_sql: str | None, lines: int, follow: bool, format: str) -> int | None:
|
54
54
|
"""Tail logs in real-time (like 'tail -f').
|
55
55
|
|
56
56
|
Examples:
|
@@ -104,7 +104,7 @@ if _HAS_CLICK:
|
|
104
104
|
|
105
105
|
else:
|
106
106
|
|
107
|
-
def tail_command(*args, **kwargs):
|
107
|
+
def tail_command(*args: object, **kwargs: object) -> None:
|
108
108
|
"""Tail command stub when click is not available."""
|
109
109
|
raise ImportError(
|
110
110
|
"CLI commands require optional dependencies. "
|
@@ -6,7 +6,6 @@ import os
|
|
6
6
|
from pathlib import Path
|
7
7
|
import tempfile
|
8
8
|
from typing import Any
|
9
|
-
from unittest.mock import MagicMock
|
10
9
|
|
11
10
|
import click
|
12
11
|
from click.testing import CliRunner
|
@@ -17,8 +16,6 @@ from provide.foundation.logger import get_logger
|
|
17
16
|
log = get_logger(__name__)
|
18
17
|
|
19
18
|
|
20
|
-
|
21
|
-
|
22
19
|
@contextmanager
|
23
20
|
def isolated_cli_runner(
|
24
21
|
env: dict[str, str] | None = None,
|
@@ -140,8 +137,6 @@ def create_test_cli(
|
|
140
137
|
return cli
|
141
138
|
|
142
139
|
|
143
|
-
|
144
|
-
|
145
140
|
class CliTestCase:
|
146
141
|
"""Base class for CLI test cases with common utilities."""
|
147
142
|
|
provide/foundation/cli/utils.py
CHANGED
@@ -1,12 +1,11 @@
|
|
1
1
|
"""Common CLI utilities for output, logging, and testing."""
|
2
2
|
|
3
|
-
import json
|
4
3
|
from typing import Any
|
5
4
|
|
6
5
|
import click
|
7
6
|
from click.testing import CliRunner, Result
|
8
7
|
|
9
|
-
from provide.foundation.console.output import
|
8
|
+
from provide.foundation.console.output import perr, pout
|
10
9
|
from provide.foundation.context import CLIContext
|
11
10
|
from provide.foundation.logger import (
|
12
11
|
LoggingConfig,
|
@@ -9,6 +9,25 @@ from provide.foundation.config.base import (
|
|
9
9
|
BaseConfig,
|
10
10
|
field,
|
11
11
|
)
|
12
|
+
from provide.foundation.config.converters import (
|
13
|
+
parse_bool_extended,
|
14
|
+
parse_comma_list,
|
15
|
+
parse_console_formatter,
|
16
|
+
parse_float_with_validation,
|
17
|
+
parse_headers,
|
18
|
+
parse_json_dict,
|
19
|
+
parse_json_list,
|
20
|
+
parse_log_level,
|
21
|
+
parse_module_levels,
|
22
|
+
parse_rate_limits,
|
23
|
+
parse_sample_rate,
|
24
|
+
validate_log_level,
|
25
|
+
validate_non_negative,
|
26
|
+
validate_overflow_policy,
|
27
|
+
validate_port,
|
28
|
+
validate_positive,
|
29
|
+
validate_sample_rate,
|
30
|
+
)
|
12
31
|
from provide.foundation.config.env import (
|
13
32
|
RuntimeConfig,
|
14
33
|
env_field,
|
@@ -45,25 +64,6 @@ from provide.foundation.config.types import (
|
|
45
64
|
ConfigSource,
|
46
65
|
ConfigValue,
|
47
66
|
)
|
48
|
-
from provide.foundation.config.converters import (
|
49
|
-
parse_bool_extended,
|
50
|
-
parse_comma_list,
|
51
|
-
parse_console_formatter,
|
52
|
-
parse_float_with_validation,
|
53
|
-
parse_headers,
|
54
|
-
parse_json_dict,
|
55
|
-
parse_json_list,
|
56
|
-
parse_log_level,
|
57
|
-
parse_module_levels,
|
58
|
-
parse_rate_limits,
|
59
|
-
parse_sample_rate,
|
60
|
-
validate_log_level,
|
61
|
-
validate_non_negative,
|
62
|
-
validate_overflow_policy,
|
63
|
-
validate_port,
|
64
|
-
validate_positive,
|
65
|
-
validate_sample_rate,
|
66
|
-
)
|
67
67
|
from provide.foundation.config.validators import (
|
68
68
|
validate_choice,
|
69
69
|
validate_range,
|
@@ -87,7 +87,7 @@ class BaseConfig:
|
|
87
87
|
_source_map: dict[str, ConfigSource] = attrs_field(init=False, factory=lambda: {})
|
88
88
|
_original_values: dict[str, Any] = attrs_field(init=False, factory=lambda: {})
|
89
89
|
|
90
|
-
def __attrs_post_init__(self):
|
90
|
+
def __attrs_post_init__(self) -> None:
|
91
91
|
"""Post-initialization hook for subclasses."""
|
92
92
|
# The _source_map and _original_values are now handled by attrs with factory
|
93
93
|
# Note: validate() is now async, so we can't call it here
|
@@ -175,7 +175,7 @@ class BaseConfig:
|
|
175
175
|
Configuration instance
|
176
176
|
"""
|
177
177
|
# Filter data to only include fields defined in the class, excluding private fields
|
178
|
-
field_names = {f.name for f in fields(cls) if not f.name.startswith(
|
178
|
+
field_names = {f.name for f in fields(cls) if not f.name.startswith("_")}
|
179
179
|
filtered_data = {k: v for k, v in data.items() if k in field_names}
|
180
180
|
|
181
181
|
# Create instance
|