provide-foundation 0.0.0.dev0__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.
Files changed (149) hide show
  1. provide/__init__.py +15 -0
  2. provide/foundation/__init__.py +155 -0
  3. provide/foundation/_version.py +58 -0
  4. provide/foundation/cli/__init__.py +67 -0
  5. provide/foundation/cli/commands/__init__.py +3 -0
  6. provide/foundation/cli/commands/deps.py +71 -0
  7. provide/foundation/cli/commands/logs/__init__.py +63 -0
  8. provide/foundation/cli/commands/logs/generate.py +357 -0
  9. provide/foundation/cli/commands/logs/generate_old.py +569 -0
  10. provide/foundation/cli/commands/logs/query.py +174 -0
  11. provide/foundation/cli/commands/logs/send.py +166 -0
  12. provide/foundation/cli/commands/logs/tail.py +112 -0
  13. provide/foundation/cli/decorators.py +262 -0
  14. provide/foundation/cli/main.py +65 -0
  15. provide/foundation/cli/testing.py +220 -0
  16. provide/foundation/cli/utils.py +210 -0
  17. provide/foundation/config/__init__.py +106 -0
  18. provide/foundation/config/base.py +295 -0
  19. provide/foundation/config/env.py +369 -0
  20. provide/foundation/config/loader.py +311 -0
  21. provide/foundation/config/manager.py +387 -0
  22. provide/foundation/config/schema.py +284 -0
  23. provide/foundation/config/sync.py +281 -0
  24. provide/foundation/config/types.py +78 -0
  25. provide/foundation/config/validators.py +80 -0
  26. provide/foundation/console/__init__.py +29 -0
  27. provide/foundation/console/input.py +364 -0
  28. provide/foundation/console/output.py +178 -0
  29. provide/foundation/context/__init__.py +12 -0
  30. provide/foundation/context/core.py +356 -0
  31. provide/foundation/core.py +20 -0
  32. provide/foundation/crypto/__init__.py +182 -0
  33. provide/foundation/crypto/algorithms.py +111 -0
  34. provide/foundation/crypto/certificates.py +896 -0
  35. provide/foundation/crypto/checksums.py +301 -0
  36. provide/foundation/crypto/constants.py +57 -0
  37. provide/foundation/crypto/hashing.py +265 -0
  38. provide/foundation/crypto/keys.py +188 -0
  39. provide/foundation/crypto/signatures.py +144 -0
  40. provide/foundation/crypto/utils.py +164 -0
  41. provide/foundation/errors/__init__.py +96 -0
  42. provide/foundation/errors/auth.py +73 -0
  43. provide/foundation/errors/base.py +81 -0
  44. provide/foundation/errors/config.py +103 -0
  45. provide/foundation/errors/context.py +299 -0
  46. provide/foundation/errors/decorators.py +484 -0
  47. provide/foundation/errors/handlers.py +360 -0
  48. provide/foundation/errors/integration.py +105 -0
  49. provide/foundation/errors/platform.py +37 -0
  50. provide/foundation/errors/process.py +140 -0
  51. provide/foundation/errors/resources.py +133 -0
  52. provide/foundation/errors/runtime.py +160 -0
  53. provide/foundation/errors/safe_decorators.py +133 -0
  54. provide/foundation/errors/types.py +276 -0
  55. provide/foundation/file/__init__.py +79 -0
  56. provide/foundation/file/atomic.py +157 -0
  57. provide/foundation/file/directory.py +134 -0
  58. provide/foundation/file/formats.py +236 -0
  59. provide/foundation/file/lock.py +175 -0
  60. provide/foundation/file/safe.py +179 -0
  61. provide/foundation/file/utils.py +170 -0
  62. provide/foundation/hub/__init__.py +88 -0
  63. provide/foundation/hub/click_builder.py +310 -0
  64. provide/foundation/hub/commands.py +42 -0
  65. provide/foundation/hub/components.py +640 -0
  66. provide/foundation/hub/decorators.py +244 -0
  67. provide/foundation/hub/info.py +32 -0
  68. provide/foundation/hub/manager.py +446 -0
  69. provide/foundation/hub/registry.py +279 -0
  70. provide/foundation/hub/type_mapping.py +54 -0
  71. provide/foundation/hub/types.py +28 -0
  72. provide/foundation/logger/__init__.py +41 -0
  73. provide/foundation/logger/base.py +22 -0
  74. provide/foundation/logger/config/__init__.py +16 -0
  75. provide/foundation/logger/config/base.py +40 -0
  76. provide/foundation/logger/config/logging.py +394 -0
  77. provide/foundation/logger/config/telemetry.py +188 -0
  78. provide/foundation/logger/core.py +239 -0
  79. provide/foundation/logger/custom_processors.py +172 -0
  80. provide/foundation/logger/emoji/__init__.py +44 -0
  81. provide/foundation/logger/emoji/matrix.py +209 -0
  82. provide/foundation/logger/emoji/sets.py +458 -0
  83. provide/foundation/logger/emoji/types.py +56 -0
  84. provide/foundation/logger/factories.py +56 -0
  85. provide/foundation/logger/processors/__init__.py +13 -0
  86. provide/foundation/logger/processors/main.py +254 -0
  87. provide/foundation/logger/processors/trace.py +113 -0
  88. provide/foundation/logger/ratelimit/__init__.py +31 -0
  89. provide/foundation/logger/ratelimit/limiters.py +294 -0
  90. provide/foundation/logger/ratelimit/processor.py +203 -0
  91. provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
  92. provide/foundation/logger/setup/__init__.py +29 -0
  93. provide/foundation/logger/setup/coordinator.py +138 -0
  94. provide/foundation/logger/setup/emoji_resolver.py +64 -0
  95. provide/foundation/logger/setup/processors.py +85 -0
  96. provide/foundation/logger/setup/testing.py +39 -0
  97. provide/foundation/logger/trace.py +38 -0
  98. provide/foundation/metrics/__init__.py +119 -0
  99. provide/foundation/metrics/otel.py +122 -0
  100. provide/foundation/metrics/simple.py +165 -0
  101. provide/foundation/observability/__init__.py +53 -0
  102. provide/foundation/observability/openobserve/__init__.py +79 -0
  103. provide/foundation/observability/openobserve/auth.py +72 -0
  104. provide/foundation/observability/openobserve/client.py +307 -0
  105. provide/foundation/observability/openobserve/commands.py +357 -0
  106. provide/foundation/observability/openobserve/exceptions.py +41 -0
  107. provide/foundation/observability/openobserve/formatters.py +298 -0
  108. provide/foundation/observability/openobserve/models.py +134 -0
  109. provide/foundation/observability/openobserve/otlp.py +320 -0
  110. provide/foundation/observability/openobserve/search.py +222 -0
  111. provide/foundation/observability/openobserve/streaming.py +235 -0
  112. provide/foundation/platform/__init__.py +44 -0
  113. provide/foundation/platform/detection.py +193 -0
  114. provide/foundation/platform/info.py +157 -0
  115. provide/foundation/process/__init__.py +39 -0
  116. provide/foundation/process/async_runner.py +373 -0
  117. provide/foundation/process/lifecycle.py +406 -0
  118. provide/foundation/process/runner.py +390 -0
  119. provide/foundation/setup/__init__.py +101 -0
  120. provide/foundation/streams/__init__.py +44 -0
  121. provide/foundation/streams/console.py +57 -0
  122. provide/foundation/streams/core.py +65 -0
  123. provide/foundation/streams/file.py +104 -0
  124. provide/foundation/testing/__init__.py +166 -0
  125. provide/foundation/testing/cli.py +227 -0
  126. provide/foundation/testing/crypto.py +163 -0
  127. provide/foundation/testing/fixtures.py +49 -0
  128. provide/foundation/testing/hub.py +23 -0
  129. provide/foundation/testing/logger.py +106 -0
  130. provide/foundation/testing/streams.py +54 -0
  131. provide/foundation/tracer/__init__.py +49 -0
  132. provide/foundation/tracer/context.py +115 -0
  133. provide/foundation/tracer/otel.py +135 -0
  134. provide/foundation/tracer/spans.py +174 -0
  135. provide/foundation/types.py +32 -0
  136. provide/foundation/utils/__init__.py +97 -0
  137. provide/foundation/utils/deps.py +195 -0
  138. provide/foundation/utils/env.py +491 -0
  139. provide/foundation/utils/formatting.py +483 -0
  140. provide/foundation/utils/parsing.py +235 -0
  141. provide/foundation/utils/rate_limiting.py +112 -0
  142. provide/foundation/utils/streams.py +67 -0
  143. provide/foundation/utils/timing.py +93 -0
  144. provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
  145. provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
  146. provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
  147. provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  148. provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
  149. provide_foundation-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,373 @@
1
+ """Async subprocess execution utilities."""
2
+
3
+ import asyncio
4
+ import builtins
5
+ from collections.abc import AsyncIterator, Mapping
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from provide.foundation.logger import get_logger
11
+ from provide.foundation.process.runner import (
12
+ CompletedProcess,
13
+ ProcessError,
14
+ TimeoutError,
15
+ )
16
+
17
+ plog = get_logger(__name__)
18
+
19
+
20
+ def _filter_subprocess_kwargs(kwargs: dict) -> dict:
21
+ """Filter kwargs to only include valid subprocess parameters."""
22
+ valid_subprocess_kwargs = {
23
+ "stdin",
24
+ "stdout",
25
+ "stderr",
26
+ "shell",
27
+ "cwd",
28
+ "env",
29
+ "universal_newlines",
30
+ "startupinfo",
31
+ "creationflags",
32
+ "restore_signals",
33
+ "start_new_session",
34
+ "pass_fds",
35
+ "encoding",
36
+ "errors",
37
+ "text",
38
+ "executable",
39
+ "preexec_fn",
40
+ "close_fds",
41
+ "group",
42
+ "extra_groups",
43
+ "user",
44
+ "umask",
45
+ }
46
+ return {k: v for k, v in kwargs.items() if k in valid_subprocess_kwargs}
47
+
48
+
49
+ async def async_run_command(
50
+ cmd: list[str] | str,
51
+ cwd: str | Path | None = None,
52
+ env: Mapping[str, str] | None = None,
53
+ capture_output: bool = True,
54
+ check: bool = True,
55
+ timeout: float | None = None,
56
+ input: bytes | None = None,
57
+ shell: bool = False,
58
+ **kwargs: Any,
59
+ ) -> CompletedProcess:
60
+ """
61
+ Run a subprocess command asynchronously.
62
+
63
+ Args:
64
+ cmd: Command and arguments as a list
65
+ cwd: Working directory for the command
66
+ env: Environment variables (if None, uses current environment)
67
+ capture_output: Whether to capture stdout/stderr
68
+ check: Whether to raise exception on non-zero exit
69
+ timeout: Command timeout in seconds
70
+ input: Input to send to the process
71
+ **kwargs: Additional arguments
72
+
73
+ Returns:
74
+ CompletedProcess with results
75
+
76
+ Raises:
77
+ ProcessError: If command fails and check=True
78
+ TimeoutError: If timeout is exceeded
79
+ """
80
+ cmd_str = " ".join(cmd) if isinstance(cmd, list) else str(cmd)
81
+ plog.info(
82
+ "🚀 Running async command", command=cmd_str, cwd=str(cwd) if cwd else None
83
+ )
84
+
85
+ # Prepare environment, disabling foundation telemetry by default
86
+ run_env = os.environ.copy()
87
+ if env is not None:
88
+ run_env.update(env)
89
+ run_env.setdefault("PROVIDE_TELEMETRY_DISABLED", "true")
90
+
91
+ # Convert Path to string
92
+ if isinstance(cwd, Path):
93
+ cwd = str(cwd)
94
+
95
+ try:
96
+ # Create subprocess
97
+ if shell:
98
+ # For shell commands, use create_subprocess_shell with string command
99
+ process = await asyncio.create_subprocess_shell(
100
+ cmd_str,
101
+ cwd=cwd,
102
+ env=run_env,
103
+ stdout=asyncio.subprocess.PIPE if capture_output else None,
104
+ stderr=asyncio.subprocess.PIPE if capture_output else None,
105
+ stdin=asyncio.subprocess.PIPE if input else None,
106
+ **_filter_subprocess_kwargs(kwargs),
107
+ )
108
+ else:
109
+ # For non-shell commands, use create_subprocess_exec with unpacked args
110
+ process = await asyncio.create_subprocess_exec(
111
+ *(cmd if isinstance(cmd, list) else [cmd]),
112
+ cwd=cwd,
113
+ env=run_env,
114
+ stdout=asyncio.subprocess.PIPE if capture_output else None,
115
+ stderr=asyncio.subprocess.PIPE if capture_output else None,
116
+ stdin=asyncio.subprocess.PIPE if input else None,
117
+ **_filter_subprocess_kwargs(kwargs),
118
+ )
119
+
120
+ # Communicate with process
121
+ if timeout:
122
+ try:
123
+ stdout, stderr = await asyncio.wait_for(
124
+ process.communicate(input=input),
125
+ timeout=timeout,
126
+ )
127
+ except builtins.TimeoutError:
128
+ process.kill()
129
+ await process.wait()
130
+ plog.error(
131
+ "⏱️ Async command timed out", command=cmd_str, timeout=timeout
132
+ )
133
+ raise TimeoutError(
134
+ f"Command timed out after {timeout}s: {cmd_str}",
135
+ code="PROCESS_ASYNC_TIMEOUT",
136
+ command=cmd_str,
137
+ timeout=timeout,
138
+ )
139
+ else:
140
+ stdout, stderr = await process.communicate(input=input)
141
+
142
+ # Decode output
143
+ stdout_str = stdout.decode(errors="replace") if stdout else ""
144
+ stderr_str = stderr.decode(errors="replace") if stderr else ""
145
+
146
+ completed = CompletedProcess(
147
+ args=cmd,
148
+ returncode=process.returncode or 0,
149
+ stdout=stdout_str,
150
+ stderr=stderr_str,
151
+ cwd=cwd,
152
+ env=dict(run_env) if env else None,
153
+ )
154
+
155
+ if check and process.returncode != 0:
156
+ plog.error(
157
+ "❌ Async command failed",
158
+ command=cmd_str,
159
+ returncode=process.returncode,
160
+ stderr=stderr_str if capture_output else None,
161
+ )
162
+ raise ProcessError(
163
+ f"Command failed with exit code {process.returncode}: {cmd_str}",
164
+ code="PROCESS_ASYNC_FAILED",
165
+ command=cmd_str,
166
+ returncode=process.returncode,
167
+ stdout=stdout_str if capture_output else None,
168
+ stderr=stderr_str if capture_output else None,
169
+ )
170
+
171
+ plog.debug(
172
+ "✅ Async command completed",
173
+ command=cmd_str,
174
+ returncode=process.returncode,
175
+ )
176
+
177
+ return completed
178
+
179
+ except Exception as e:
180
+ if isinstance(e, ProcessError | TimeoutError):
181
+ raise
182
+
183
+ plog.error(
184
+ "💥 Async command execution failed",
185
+ command=cmd_str,
186
+ error=str(e),
187
+ )
188
+ raise ProcessError(
189
+ f"Failed to execute async command: {cmd_str}",
190
+ code="PROCESS_ASYNC_EXECUTION_FAILED",
191
+ command=cmd_str,
192
+ error=str(e),
193
+ ) from e
194
+
195
+
196
+ async def async_stream_command(
197
+ cmd: list[str],
198
+ cwd: str | Path | None = None,
199
+ env: Mapping[str, str] | None = None,
200
+ timeout: float | None = None,
201
+ stream_stderr: bool = False,
202
+ **kwargs: Any,
203
+ ) -> AsyncIterator[str]:
204
+ """
205
+ Stream command output line by line asynchronously.
206
+
207
+ Args:
208
+ cmd: Command and arguments as a list
209
+ cwd: Working directory for the command
210
+ env: Environment variables
211
+ timeout: Command timeout in seconds
212
+ **kwargs: Additional arguments
213
+
214
+ Yields:
215
+ Lines of output from the command
216
+
217
+ Raises:
218
+ ProcessError: If command fails
219
+ TimeoutError: If timeout is exceeded
220
+ """
221
+ cmd_str = " ".join(cmd) if isinstance(cmd, list) else str(cmd)
222
+ plog.info(
223
+ "🌊 Streaming async command", command=cmd_str, cwd=str(cwd) if cwd else None
224
+ )
225
+
226
+ # Prepare environment, disabling foundation telemetry by default
227
+ run_env = os.environ.copy()
228
+ if env is not None:
229
+ run_env.update(env)
230
+ run_env.setdefault("PROVIDE_TELEMETRY_DISABLED", "true")
231
+
232
+ # Convert Path to string
233
+ if isinstance(cwd, Path):
234
+ cwd = str(cwd)
235
+
236
+ try:
237
+ # Create subprocess
238
+ # Merge stderr to stdout for streaming, as we always want to see errors
239
+ stderr_handling = (
240
+ asyncio.subprocess.STDOUT if stream_stderr else asyncio.subprocess.PIPE
241
+ )
242
+ process = await asyncio.create_subprocess_exec(
243
+ *(cmd if isinstance(cmd, list) else cmd.split()),
244
+ cwd=cwd,
245
+ env=run_env,
246
+ stdout=asyncio.subprocess.PIPE,
247
+ stderr=stderr_handling,
248
+ **_filter_subprocess_kwargs(kwargs),
249
+ )
250
+
251
+ # Stream output with optional timeout
252
+ if timeout:
253
+ # For timeout, we need to handle it differently
254
+ # Create a task to read lines with timeout
255
+ async def read_with_timeout():
256
+ lines = []
257
+ if process.stdout:
258
+ try:
259
+ # Use wait_for on each readline operation
260
+ remaining_timeout = timeout
261
+ start_time = asyncio.get_event_loop().time()
262
+
263
+ while True:
264
+ elapsed = asyncio.get_event_loop().time() - start_time
265
+ remaining_timeout = timeout - elapsed
266
+
267
+ if remaining_timeout <= 0:
268
+ raise builtins.TimeoutError()
269
+
270
+ # Wait for a line with remaining timeout
271
+ line = await asyncio.wait_for(
272
+ process.stdout.readline(), timeout=remaining_timeout
273
+ )
274
+
275
+ if not line:
276
+ break # EOF
277
+
278
+ lines.append(line.decode(errors="replace").rstrip())
279
+ except builtins.TimeoutError:
280
+ process.kill()
281
+ await process.wait()
282
+ plog.error(
283
+ "⏱️ Async stream timed out", command=cmd_str, timeout=timeout
284
+ )
285
+ raise TimeoutError(
286
+ f"Command timed out after {timeout}s: {cmd_str}",
287
+ code="PROCESS_ASYNC_STREAM_TIMEOUT",
288
+ command=cmd_str,
289
+ timeout=timeout,
290
+ )
291
+
292
+ # Wait for process to complete
293
+ await process.wait()
294
+
295
+ if process.returncode != 0:
296
+ raise ProcessError(
297
+ f"Command failed with exit code {process.returncode}: {cmd_str}",
298
+ code="PROCESS_ASYNC_STREAM_FAILED",
299
+ command=cmd_str,
300
+ returncode=process.returncode,
301
+ )
302
+
303
+ return lines
304
+
305
+ # Yield lines as they were read
306
+ lines = await read_with_timeout()
307
+ for line in lines:
308
+ yield line
309
+ else:
310
+ # No timeout - stream normally
311
+ if process.stdout:
312
+ async for line in process.stdout:
313
+ yield line.decode(errors="replace").rstrip()
314
+
315
+ # Wait for process to complete
316
+ await process.wait()
317
+
318
+ if process.returncode != 0:
319
+ raise ProcessError(
320
+ f"Command failed with exit code {process.returncode}: {cmd_str}",
321
+ code="PROCESS_ASYNC_STREAM_FAILED",
322
+ command=cmd_str,
323
+ returncode=process.returncode,
324
+ )
325
+
326
+ plog.debug("✅ Async stream completed", command=cmd_str)
327
+
328
+ except Exception as e:
329
+ if isinstance(e, ProcessError | TimeoutError):
330
+ raise
331
+
332
+ plog.error("💥 Async stream failed", command=cmd_str, error=str(e))
333
+ raise ProcessError(
334
+ f"Failed to stream async command: {cmd_str}",
335
+ code="PROCESS_ASYNC_STREAM_ERROR",
336
+ command=cmd_str,
337
+ error=str(e),
338
+ ) from e
339
+
340
+
341
+ async def async_run_shell(
342
+ cmd: str,
343
+ cwd: str | Path | None = None,
344
+ env: Mapping[str, str] | None = None,
345
+ capture_output: bool = True,
346
+ check: bool = True,
347
+ timeout: float | None = None,
348
+ **kwargs: Any,
349
+ ) -> CompletedProcess:
350
+ """Run a shell command asynchronously.
351
+
352
+ Args:
353
+ cmd: Shell command string
354
+ cwd: Working directory
355
+ env: Environment variables
356
+ capture_output: Whether to capture output
357
+ check: Whether to raise on non-zero exit
358
+ timeout: Command timeout
359
+ **kwargs: Additional subprocess arguments
360
+
361
+ Returns:
362
+ CompletedProcess with results
363
+ """
364
+ return await async_run_command(
365
+ cmd,
366
+ cwd=cwd,
367
+ env=env,
368
+ capture_output=capture_output,
369
+ check=check,
370
+ timeout=timeout,
371
+ shell=True,
372
+ **kwargs,
373
+ )