provide-foundation 0.0.0.dev1__py3-none-any.whl → 0.0.0.dev3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- provide/foundation/__init__.py +36 -10
- provide/foundation/archive/__init__.py +1 -1
- provide/foundation/archive/base.py +15 -14
- provide/foundation/archive/bzip2.py +40 -40
- provide/foundation/archive/gzip.py +42 -42
- provide/foundation/archive/operations.py +93 -96
- provide/foundation/archive/tar.py +33 -31
- provide/foundation/archive/zip.py +52 -50
- provide/foundation/asynctools/__init__.py +20 -0
- provide/foundation/asynctools/core.py +126 -0
- provide/foundation/cli/__init__.py +2 -2
- provide/foundation/cli/commands/deps.py +15 -9
- provide/foundation/cli/commands/logs/__init__.py +3 -3
- provide/foundation/cli/commands/logs/generate.py +2 -2
- provide/foundation/cli/commands/logs/query.py +4 -4
- provide/foundation/cli/commands/logs/send.py +3 -3
- provide/foundation/cli/commands/logs/tail.py +3 -3
- provide/foundation/cli/decorators.py +11 -11
- provide/foundation/cli/main.py +1 -1
- provide/foundation/cli/testing.py +2 -40
- provide/foundation/cli/utils.py +21 -18
- provide/foundation/config/__init__.py +35 -2
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +477 -0
- provide/foundation/config/defaults.py +67 -0
- provide/foundation/config/env.py +6 -20
- provide/foundation/config/loader.py +10 -4
- provide/foundation/config/sync.py +8 -6
- provide/foundation/config/types.py +5 -5
- provide/foundation/config/validators.py +4 -4
- provide/foundation/console/input.py +5 -5
- provide/foundation/console/output.py +36 -14
- provide/foundation/context/__init__.py +8 -4
- provide/foundation/context/core.py +88 -110
- provide/foundation/crypto/certificates/__init__.py +9 -5
- provide/foundation/crypto/certificates/base.py +2 -2
- provide/foundation/crypto/certificates/certificate.py +48 -19
- provide/foundation/crypto/certificates/factory.py +26 -18
- provide/foundation/crypto/certificates/generator.py +24 -23
- provide/foundation/crypto/certificates/loader.py +24 -16
- provide/foundation/crypto/certificates/operations.py +17 -10
- provide/foundation/crypto/certificates/trust.py +21 -21
- provide/foundation/env/__init__.py +28 -0
- provide/foundation/env/core.py +218 -0
- provide/foundation/errors/__init__.py +3 -3
- provide/foundation/errors/decorators.py +0 -234
- provide/foundation/errors/types.py +0 -98
- provide/foundation/eventsets/display.py +13 -14
- provide/foundation/eventsets/registry.py +61 -31
- provide/foundation/eventsets/resolver.py +50 -46
- provide/foundation/eventsets/sets/das.py +8 -8
- provide/foundation/eventsets/sets/database.py +14 -14
- provide/foundation/eventsets/sets/http.py +21 -21
- provide/foundation/eventsets/sets/llm.py +16 -16
- provide/foundation/eventsets/sets/task_queue.py +13 -13
- provide/foundation/eventsets/types.py +7 -7
- provide/foundation/file/directory.py +14 -23
- provide/foundation/file/lock.py +4 -3
- provide/foundation/hub/components.py +75 -389
- provide/foundation/hub/config.py +157 -0
- provide/foundation/hub/discovery.py +63 -0
- provide/foundation/hub/handlers.py +89 -0
- provide/foundation/hub/lifecycle.py +195 -0
- provide/foundation/hub/manager.py +7 -4
- provide/foundation/hub/processors.py +49 -0
- provide/foundation/integrations/__init__.py +11 -0
- provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
- provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/client.py +14 -14
- provide/foundation/{observability → integrations}/openobserve/commands.py +12 -12
- provide/foundation/integrations/openobserve/config.py +37 -0
- provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/otlp.py +2 -2
- provide/foundation/{observability → integrations}/openobserve/search.py +2 -3
- provide/foundation/{observability → integrations}/openobserve/streaming.py +5 -5
- provide/foundation/logger/__init__.py +0 -1
- provide/foundation/logger/config/base.py +1 -1
- provide/foundation/logger/config/logging.py +69 -299
- provide/foundation/logger/config/telemetry.py +39 -121
- provide/foundation/logger/factories.py +2 -2
- provide/foundation/logger/processors/main.py +12 -10
- provide/foundation/logger/ratelimit/limiters.py +4 -4
- provide/foundation/logger/ratelimit/processor.py +1 -1
- provide/foundation/logger/setup/coordinator.py +39 -25
- provide/foundation/logger/setup/processors.py +3 -3
- provide/foundation/logger/setup/testing.py +14 -0
- provide/foundation/logger/trace.py +5 -5
- provide/foundation/metrics/__init__.py +1 -1
- provide/foundation/metrics/otel.py +3 -1
- provide/foundation/observability/__init__.py +3 -3
- provide/foundation/process/__init__.py +9 -0
- provide/foundation/process/exit.py +48 -0
- provide/foundation/process/lifecycle.py +69 -46
- provide/foundation/resilience/__init__.py +36 -0
- provide/foundation/resilience/circuit.py +166 -0
- provide/foundation/resilience/decorators.py +236 -0
- provide/foundation/resilience/fallback.py +208 -0
- provide/foundation/resilience/retry.py +327 -0
- provide/foundation/serialization/__init__.py +16 -0
- provide/foundation/serialization/core.py +70 -0
- provide/foundation/streams/config.py +78 -0
- provide/foundation/streams/console.py +4 -5
- provide/foundation/streams/core.py +5 -2
- provide/foundation/streams/file.py +12 -2
- provide/foundation/testing/__init__.py +29 -9
- provide/foundation/testing/archive/__init__.py +7 -7
- provide/foundation/testing/archive/fixtures.py +58 -54
- provide/foundation/testing/cli.py +30 -20
- provide/foundation/testing/common/__init__.py +13 -15
- provide/foundation/testing/common/fixtures.py +27 -57
- provide/foundation/testing/file/__init__.py +15 -15
- provide/foundation/testing/file/content_fixtures.py +289 -0
- provide/foundation/testing/file/directory_fixtures.py +107 -0
- provide/foundation/testing/file/fixtures.py +42 -516
- provide/foundation/testing/file/special_fixtures.py +145 -0
- provide/foundation/testing/logger.py +89 -8
- provide/foundation/testing/mocking/__init__.py +21 -21
- provide/foundation/testing/mocking/fixtures.py +80 -67
- provide/foundation/testing/process/__init__.py +23 -23
- provide/foundation/testing/process/async_fixtures.py +414 -0
- provide/foundation/testing/process/fixtures.py +48 -571
- provide/foundation/testing/process/subprocess_fixtures.py +210 -0
- provide/foundation/testing/threading/__init__.py +17 -17
- provide/foundation/testing/threading/basic_fixtures.py +105 -0
- provide/foundation/testing/threading/data_fixtures.py +101 -0
- provide/foundation/testing/threading/execution_fixtures.py +278 -0
- provide/foundation/testing/threading/fixtures.py +32 -502
- provide/foundation/testing/threading/sync_fixtures.py +100 -0
- provide/foundation/testing/time/__init__.py +11 -11
- provide/foundation/testing/time/fixtures.py +95 -83
- provide/foundation/testing/transport/__init__.py +9 -9
- provide/foundation/testing/transport/fixtures.py +54 -54
- provide/foundation/time/__init__.py +18 -0
- provide/foundation/time/core.py +63 -0
- provide/foundation/tools/__init__.py +2 -2
- provide/foundation/tools/base.py +68 -67
- provide/foundation/tools/cache.py +69 -74
- provide/foundation/tools/downloader.py +68 -62
- provide/foundation/tools/installer.py +51 -57
- provide/foundation/tools/registry.py +38 -45
- provide/foundation/tools/resolver.py +70 -68
- provide/foundation/tools/verifier.py +39 -50
- provide/foundation/tracer/spans.py +2 -14
- provide/foundation/transport/__init__.py +26 -33
- provide/foundation/transport/base.py +32 -30
- provide/foundation/transport/client.py +44 -49
- provide/foundation/transport/config.py +36 -107
- provide/foundation/transport/errors.py +13 -27
- provide/foundation/transport/http.py +69 -55
- provide/foundation/transport/middleware.py +113 -114
- provide/foundation/transport/registry.py +29 -27
- provide/foundation/transport/types.py +6 -6
- provide/foundation/utils/deps.py +17 -14
- provide/foundation/utils/parsing.py +49 -4
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
- provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
- provide_foundation-0.0.0.dev1.dist-info/RECORD +0 -200
- /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
- /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/top_level.txt +0 -0
@@ -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)
|
@@ -17,10 +17,8 @@ from provide.foundation.cli.decorators import (
|
|
17
17
|
)
|
18
18
|
from provide.foundation.cli.testing import (
|
19
19
|
CliTestCase,
|
20
|
-
MockContext,
|
21
20
|
create_test_cli,
|
22
21
|
isolated_cli_runner,
|
23
|
-
mock_logger,
|
24
22
|
temp_config_file,
|
25
23
|
)
|
26
24
|
from provide.foundation.cli.utils import (
|
@@ -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,10 +8,12 @@ except ImportError:
|
|
8
8
|
click = None
|
9
9
|
_HAS_CLICK = False
|
10
10
|
|
11
|
-
from provide.foundation.
|
11
|
+
from provide.foundation.console.output import pout
|
12
|
+
from provide.foundation.process import exit_error, exit_success
|
13
|
+
from provide.foundation.utils.deps import check_optional_deps, has_dependency
|
12
14
|
|
13
15
|
|
14
|
-
def _require_click():
|
16
|
+
def _require_click() -> None:
|
15
17
|
"""Ensure click is available for CLI commands."""
|
16
18
|
if not _HAS_CLICK:
|
17
19
|
raise ImportError(
|
@@ -23,21 +25,25 @@ def _require_click():
|
|
23
25
|
def _deps_command_impl(quiet: bool, check: str | None) -> None:
|
24
26
|
"""Implementation of deps command logic."""
|
25
27
|
if check:
|
26
|
-
from provide.foundation.utils.deps import has_dependency
|
27
|
-
|
28
28
|
available = has_dependency(check)
|
29
29
|
if not quiet:
|
30
30
|
status = "✅" if available else "❌"
|
31
|
-
|
31
|
+
pout(f"{status} {check}: {'Available' if available else 'Missing'}")
|
32
32
|
if not available:
|
33
|
-
|
34
|
-
|
33
|
+
pout(f"Install with: pip install 'provide-foundation[{check}]'")
|
34
|
+
if available:
|
35
|
+
exit_success()
|
36
|
+
else:
|
37
|
+
exit_error("Dependency check failed")
|
35
38
|
else:
|
36
39
|
# Check all dependencies
|
37
40
|
deps = check_optional_deps(quiet=quiet, return_status=True)
|
38
41
|
available_count = sum(1 for dep in deps if dep.available)
|
39
42
|
total_count = len(deps)
|
40
|
-
|
43
|
+
if available_count == total_count:
|
44
|
+
exit_success()
|
45
|
+
else:
|
46
|
+
exit_error(f"Missing {total_count - available_count} dependencies")
|
41
47
|
|
42
48
|
|
43
49
|
if _HAS_CLICK:
|
@@ -62,7 +68,7 @@ if _HAS_CLICK:
|
|
62
68
|
_deps_command_impl(quiet, check)
|
63
69
|
else:
|
64
70
|
# Stub for when click is not available
|
65
|
-
def deps_command(*args, **kwargs):
|
71
|
+
def deps_command(*args: object, **kwargs: object) -> None:
|
66
72
|
"""Deps command stub when click is not available."""
|
67
73
|
_require_click()
|
68
74
|
|
@@ -21,14 +21,14 @@ 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)
|
28
28
|
|
29
29
|
# Try to get OpenObserve client if available
|
30
30
|
try:
|
31
|
-
from provide.foundation.
|
31
|
+
from provide.foundation.integrations.openobserve import OpenObserveClient
|
32
32
|
|
33
33
|
ctx.obj["client"] = OpenObserveClient.from_config()
|
34
34
|
except Exception as e:
|
@@ -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:
|
@@ -89,7 +89,7 @@ if _HAS_CLICK:
|
|
89
89
|
# Custom SQL query
|
90
90
|
foundation logs query --sql "SELECT * FROM default WHERE duration_ms > 1000"
|
91
91
|
"""
|
92
|
-
from provide.foundation.
|
92
|
+
from provide.foundation.integrations.openobserve import (
|
93
93
|
format_output,
|
94
94
|
search_logs,
|
95
95
|
)
|
@@ -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:
|
@@ -83,7 +83,7 @@ if _HAS_CLICK:
|
|
83
83
|
# Send with JSON attributes
|
84
84
|
foundation logs send -m "Error occurred" -j '{"error_code": 500, "path": "/api/users"}'
|
85
85
|
"""
|
86
|
-
from provide.foundation.
|
86
|
+
from provide.foundation.integrations.openobserve.otlp import send_log
|
87
87
|
|
88
88
|
# Get message from stdin if not provided
|
89
89
|
if not message:
|
@@ -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:
|
@@ -69,7 +69,7 @@ if _HAS_CLICK:
|
|
69
69
|
# Tail with JSON output
|
70
70
|
foundation logs tail --format json
|
71
71
|
"""
|
72
|
-
from provide.foundation.
|
72
|
+
from provide.foundation.integrations.openobserve import (
|
73
73
|
format_output,
|
74
74
|
tail_logs,
|
75
75
|
)
|
@@ -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. "
|
@@ -3,7 +3,6 @@
|
|
3
3
|
from collections.abc import Callable
|
4
4
|
import functools
|
5
5
|
from pathlib import Path
|
6
|
-
import sys
|
7
6
|
from typing import Any, TypeVar
|
8
7
|
|
9
8
|
try:
|
@@ -11,7 +10,8 @@ try:
|
|
11
10
|
except ImportError:
|
12
11
|
click = None
|
13
12
|
|
14
|
-
from provide.foundation.context import
|
13
|
+
from provide.foundation.context import CLIContext
|
14
|
+
from provide.foundation.process import exit_error, exit_interrupted
|
15
15
|
|
16
16
|
F = TypeVar("F", bound=Callable[..., Any])
|
17
17
|
|
@@ -162,7 +162,7 @@ def error_handler(f: F) -> F:
|
|
162
162
|
except KeyboardInterrupt:
|
163
163
|
if not json_output:
|
164
164
|
click.secho("\nInterrupted by user", fg="yellow", err=True)
|
165
|
-
|
165
|
+
exit_interrupted()
|
166
166
|
except Exception as e:
|
167
167
|
if debug:
|
168
168
|
# In debug mode, show full traceback
|
@@ -179,16 +179,16 @@ def error_handler(f: F) -> F:
|
|
179
179
|
else:
|
180
180
|
click.secho(f"Error: {e}", fg="red", err=True)
|
181
181
|
|
182
|
-
|
182
|
+
exit_error(f"Command failed: {str(e)}")
|
183
183
|
|
184
184
|
return wrapper
|
185
185
|
|
186
186
|
|
187
187
|
def pass_context(f: F) -> F:
|
188
188
|
"""
|
189
|
-
Decorator to pass the foundation
|
189
|
+
Decorator to pass the foundation CLIContext to a command.
|
190
190
|
|
191
|
-
Creates or retrieves a
|
191
|
+
Creates or retrieves a CLIContext from Click's context object
|
192
192
|
and passes it as the first argument to the decorated function.
|
193
193
|
"""
|
194
194
|
|
@@ -197,15 +197,15 @@ def pass_context(f: F) -> F:
|
|
197
197
|
def wrapper(ctx: click.Context, *args, **kwargs):
|
198
198
|
# Get or create foundation context
|
199
199
|
if not hasattr(ctx, "obj") or ctx.obj is None:
|
200
|
-
ctx.obj =
|
201
|
-
elif not isinstance(ctx.obj,
|
200
|
+
ctx.obj = CLIContext()
|
201
|
+
elif not isinstance(ctx.obj, CLIContext):
|
202
202
|
# If obj exists but isn't a Context, wrap it
|
203
203
|
if isinstance(ctx.obj, dict):
|
204
|
-
ctx.obj =
|
204
|
+
ctx.obj = CLIContext.from_dict(ctx.obj)
|
205
205
|
else:
|
206
|
-
# Store existing obj and create new
|
206
|
+
# Store existing obj and create new CLIContext
|
207
207
|
old_obj = ctx.obj
|
208
|
-
ctx.obj =
|
208
|
+
ctx.obj = CLIContext()
|
209
209
|
ctx.obj._cli_data = old_obj
|
210
210
|
|
211
211
|
# Update context from command options
|
provide/foundation/cli/main.py
CHANGED