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.
- provide/__init__.py +15 -0
- provide/foundation/__init__.py +155 -0
- provide/foundation/_version.py +58 -0
- provide/foundation/cli/__init__.py +67 -0
- provide/foundation/cli/commands/__init__.py +3 -0
- provide/foundation/cli/commands/deps.py +71 -0
- provide/foundation/cli/commands/logs/__init__.py +63 -0
- provide/foundation/cli/commands/logs/generate.py +357 -0
- provide/foundation/cli/commands/logs/generate_old.py +569 -0
- provide/foundation/cli/commands/logs/query.py +174 -0
- provide/foundation/cli/commands/logs/send.py +166 -0
- provide/foundation/cli/commands/logs/tail.py +112 -0
- provide/foundation/cli/decorators.py +262 -0
- provide/foundation/cli/main.py +65 -0
- provide/foundation/cli/testing.py +220 -0
- provide/foundation/cli/utils.py +210 -0
- provide/foundation/config/__init__.py +106 -0
- provide/foundation/config/base.py +295 -0
- provide/foundation/config/env.py +369 -0
- provide/foundation/config/loader.py +311 -0
- provide/foundation/config/manager.py +387 -0
- provide/foundation/config/schema.py +284 -0
- provide/foundation/config/sync.py +281 -0
- provide/foundation/config/types.py +78 -0
- provide/foundation/config/validators.py +80 -0
- provide/foundation/console/__init__.py +29 -0
- provide/foundation/console/input.py +364 -0
- provide/foundation/console/output.py +178 -0
- provide/foundation/context/__init__.py +12 -0
- provide/foundation/context/core.py +356 -0
- provide/foundation/core.py +20 -0
- provide/foundation/crypto/__init__.py +182 -0
- provide/foundation/crypto/algorithms.py +111 -0
- provide/foundation/crypto/certificates.py +896 -0
- provide/foundation/crypto/checksums.py +301 -0
- provide/foundation/crypto/constants.py +57 -0
- provide/foundation/crypto/hashing.py +265 -0
- provide/foundation/crypto/keys.py +188 -0
- provide/foundation/crypto/signatures.py +144 -0
- provide/foundation/crypto/utils.py +164 -0
- provide/foundation/errors/__init__.py +96 -0
- provide/foundation/errors/auth.py +73 -0
- provide/foundation/errors/base.py +81 -0
- provide/foundation/errors/config.py +103 -0
- provide/foundation/errors/context.py +299 -0
- provide/foundation/errors/decorators.py +484 -0
- provide/foundation/errors/handlers.py +360 -0
- provide/foundation/errors/integration.py +105 -0
- provide/foundation/errors/platform.py +37 -0
- provide/foundation/errors/process.py +140 -0
- provide/foundation/errors/resources.py +133 -0
- provide/foundation/errors/runtime.py +160 -0
- provide/foundation/errors/safe_decorators.py +133 -0
- provide/foundation/errors/types.py +276 -0
- provide/foundation/file/__init__.py +79 -0
- provide/foundation/file/atomic.py +157 -0
- provide/foundation/file/directory.py +134 -0
- provide/foundation/file/formats.py +236 -0
- provide/foundation/file/lock.py +175 -0
- provide/foundation/file/safe.py +179 -0
- provide/foundation/file/utils.py +170 -0
- provide/foundation/hub/__init__.py +88 -0
- provide/foundation/hub/click_builder.py +310 -0
- provide/foundation/hub/commands.py +42 -0
- provide/foundation/hub/components.py +640 -0
- provide/foundation/hub/decorators.py +244 -0
- provide/foundation/hub/info.py +32 -0
- provide/foundation/hub/manager.py +446 -0
- provide/foundation/hub/registry.py +279 -0
- provide/foundation/hub/type_mapping.py +54 -0
- provide/foundation/hub/types.py +28 -0
- provide/foundation/logger/__init__.py +41 -0
- provide/foundation/logger/base.py +22 -0
- provide/foundation/logger/config/__init__.py +16 -0
- provide/foundation/logger/config/base.py +40 -0
- provide/foundation/logger/config/logging.py +394 -0
- provide/foundation/logger/config/telemetry.py +188 -0
- provide/foundation/logger/core.py +239 -0
- provide/foundation/logger/custom_processors.py +172 -0
- provide/foundation/logger/emoji/__init__.py +44 -0
- provide/foundation/logger/emoji/matrix.py +209 -0
- provide/foundation/logger/emoji/sets.py +458 -0
- provide/foundation/logger/emoji/types.py +56 -0
- provide/foundation/logger/factories.py +56 -0
- provide/foundation/logger/processors/__init__.py +13 -0
- provide/foundation/logger/processors/main.py +254 -0
- provide/foundation/logger/processors/trace.py +113 -0
- provide/foundation/logger/ratelimit/__init__.py +31 -0
- provide/foundation/logger/ratelimit/limiters.py +294 -0
- provide/foundation/logger/ratelimit/processor.py +203 -0
- provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
- provide/foundation/logger/setup/__init__.py +29 -0
- provide/foundation/logger/setup/coordinator.py +138 -0
- provide/foundation/logger/setup/emoji_resolver.py +64 -0
- provide/foundation/logger/setup/processors.py +85 -0
- provide/foundation/logger/setup/testing.py +39 -0
- provide/foundation/logger/trace.py +38 -0
- provide/foundation/metrics/__init__.py +119 -0
- provide/foundation/metrics/otel.py +122 -0
- provide/foundation/metrics/simple.py +165 -0
- provide/foundation/observability/__init__.py +53 -0
- provide/foundation/observability/openobserve/__init__.py +79 -0
- provide/foundation/observability/openobserve/auth.py +72 -0
- provide/foundation/observability/openobserve/client.py +307 -0
- provide/foundation/observability/openobserve/commands.py +357 -0
- provide/foundation/observability/openobserve/exceptions.py +41 -0
- provide/foundation/observability/openobserve/formatters.py +298 -0
- provide/foundation/observability/openobserve/models.py +134 -0
- provide/foundation/observability/openobserve/otlp.py +320 -0
- provide/foundation/observability/openobserve/search.py +222 -0
- provide/foundation/observability/openobserve/streaming.py +235 -0
- provide/foundation/platform/__init__.py +44 -0
- provide/foundation/platform/detection.py +193 -0
- provide/foundation/platform/info.py +157 -0
- provide/foundation/process/__init__.py +39 -0
- provide/foundation/process/async_runner.py +373 -0
- provide/foundation/process/lifecycle.py +406 -0
- provide/foundation/process/runner.py +390 -0
- provide/foundation/setup/__init__.py +101 -0
- provide/foundation/streams/__init__.py +44 -0
- provide/foundation/streams/console.py +57 -0
- provide/foundation/streams/core.py +65 -0
- provide/foundation/streams/file.py +104 -0
- provide/foundation/testing/__init__.py +166 -0
- provide/foundation/testing/cli.py +227 -0
- provide/foundation/testing/crypto.py +163 -0
- provide/foundation/testing/fixtures.py +49 -0
- provide/foundation/testing/hub.py +23 -0
- provide/foundation/testing/logger.py +106 -0
- provide/foundation/testing/streams.py +54 -0
- provide/foundation/tracer/__init__.py +49 -0
- provide/foundation/tracer/context.py +115 -0
- provide/foundation/tracer/otel.py +135 -0
- provide/foundation/tracer/spans.py +174 -0
- provide/foundation/types.py +32 -0
- provide/foundation/utils/__init__.py +97 -0
- provide/foundation/utils/deps.py +195 -0
- provide/foundation/utils/env.py +491 -0
- provide/foundation/utils/formatting.py +483 -0
- provide/foundation/utils/parsing.py +235 -0
- provide/foundation/utils/rate_limiting.py +112 -0
- provide/foundation/utils/streams.py +67 -0
- provide/foundation/utils/timing.py +93 -0
- provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
- provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
- provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
- provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
- provide_foundation-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,390 @@
|
|
1
|
+
"""Core subprocess execution utilities."""
|
2
|
+
|
3
|
+
from collections.abc import Iterator, Mapping
|
4
|
+
import os
|
5
|
+
from pathlib import Path
|
6
|
+
import subprocess
|
7
|
+
from typing import Any
|
8
|
+
|
9
|
+
from attrs import define
|
10
|
+
|
11
|
+
from provide.foundation.errors.integration import TimeoutError
|
12
|
+
from provide.foundation.errors.runtime import ProcessError
|
13
|
+
from provide.foundation.logger import get_logger
|
14
|
+
|
15
|
+
plog = get_logger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
@define
|
19
|
+
class CompletedProcess:
|
20
|
+
"""Result of a completed process."""
|
21
|
+
|
22
|
+
args: list[str]
|
23
|
+
returncode: int
|
24
|
+
stdout: str
|
25
|
+
stderr: str
|
26
|
+
cwd: str | None = None
|
27
|
+
env: dict[str, str] | None = None
|
28
|
+
|
29
|
+
|
30
|
+
def run_command(
|
31
|
+
cmd: list[str] | str,
|
32
|
+
cwd: str | Path | None = None,
|
33
|
+
env: Mapping[str, str] | None = None,
|
34
|
+
capture_output: bool = True,
|
35
|
+
check: bool = True,
|
36
|
+
timeout: float | None = None,
|
37
|
+
text: bool = True,
|
38
|
+
input: str | bytes | None = None,
|
39
|
+
shell: bool = False,
|
40
|
+
**kwargs: Any,
|
41
|
+
) -> CompletedProcess:
|
42
|
+
"""
|
43
|
+
Run a subprocess command with consistent error handling and logging.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
cmd: Command and arguments as a list
|
47
|
+
cwd: Working directory for the command
|
48
|
+
env: Environment variables (if None, uses current environment)
|
49
|
+
capture_output: Whether to capture stdout/stderr
|
50
|
+
check: Whether to raise exception on non-zero exit
|
51
|
+
timeout: Command timeout in seconds
|
52
|
+
text: Whether to decode output as text
|
53
|
+
input: Input to send to the process
|
54
|
+
shell: Whether to run command through shell
|
55
|
+
**kwargs: Additional arguments passed to subprocess.run
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
CompletedProcess with results
|
59
|
+
|
60
|
+
Raises:
|
61
|
+
ProcessError: If command fails and check=True
|
62
|
+
TimeoutError: If timeout is exceeded
|
63
|
+
"""
|
64
|
+
cmd_str = " ".join(cmd) if isinstance(cmd, list) else str(cmd)
|
65
|
+
plog.info("🚀 Running command", command=cmd_str, cwd=str(cwd) if cwd else None)
|
66
|
+
|
67
|
+
# Prepare environment, disabling foundation telemetry by default
|
68
|
+
run_env = os.environ.copy()
|
69
|
+
if env is not None:
|
70
|
+
run_env.update(env)
|
71
|
+
run_env.setdefault("PROVIDE_TELEMETRY_DISABLED", "true")
|
72
|
+
|
73
|
+
# Convert Path to string
|
74
|
+
if isinstance(cwd, Path):
|
75
|
+
cwd = str(cwd)
|
76
|
+
|
77
|
+
# If command is a string, we need shell=True
|
78
|
+
if isinstance(cmd, str) and not shell:
|
79
|
+
shell = True
|
80
|
+
|
81
|
+
try:
|
82
|
+
# Prepare command for subprocess
|
83
|
+
if shell:
|
84
|
+
# For shell commands, ensure it's a string
|
85
|
+
subprocess_cmd = cmd_str
|
86
|
+
else:
|
87
|
+
# For non-shell, use the original cmd (list or string)
|
88
|
+
subprocess_cmd = cmd
|
89
|
+
|
90
|
+
# Handle input based on text mode
|
91
|
+
if input is not None and text and isinstance(input, bytes):
|
92
|
+
# Convert bytes to string if text mode is enabled
|
93
|
+
subprocess_input = input.decode("utf-8")
|
94
|
+
elif input is not None and not text and isinstance(input, str):
|
95
|
+
# Convert string to bytes if text mode is disabled
|
96
|
+
subprocess_input = input.encode("utf-8")
|
97
|
+
else:
|
98
|
+
subprocess_input = input
|
99
|
+
|
100
|
+
result = subprocess.run(
|
101
|
+
subprocess_cmd,
|
102
|
+
cwd=cwd,
|
103
|
+
env=run_env,
|
104
|
+
capture_output=capture_output,
|
105
|
+
text=text,
|
106
|
+
input=subprocess_input,
|
107
|
+
timeout=timeout,
|
108
|
+
check=False, # We'll handle the check ourselves
|
109
|
+
shell=shell,
|
110
|
+
**kwargs,
|
111
|
+
)
|
112
|
+
|
113
|
+
completed = CompletedProcess(
|
114
|
+
args=cmd if isinstance(cmd, list) else [cmd],
|
115
|
+
returncode=result.returncode,
|
116
|
+
stdout=result.stdout if capture_output else "",
|
117
|
+
stderr=result.stderr if capture_output else "",
|
118
|
+
cwd=cwd,
|
119
|
+
env=dict(run_env) if env else None,
|
120
|
+
)
|
121
|
+
|
122
|
+
if check and result.returncode != 0:
|
123
|
+
plog.error(
|
124
|
+
"❌ Command failed",
|
125
|
+
command=cmd_str,
|
126
|
+
returncode=result.returncode,
|
127
|
+
stderr=result.stderr if capture_output else None,
|
128
|
+
)
|
129
|
+
raise ProcessError(
|
130
|
+
f"Command failed with exit code {result.returncode}: {cmd_str}",
|
131
|
+
code="PROCESS_COMMAND_FAILED",
|
132
|
+
command=cmd_str,
|
133
|
+
returncode=result.returncode,
|
134
|
+
stdout=result.stdout if capture_output else None,
|
135
|
+
stderr=result.stderr if capture_output else None,
|
136
|
+
)
|
137
|
+
|
138
|
+
plog.debug(
|
139
|
+
"✅ Command completed",
|
140
|
+
command=cmd_str,
|
141
|
+
returncode=result.returncode,
|
142
|
+
)
|
143
|
+
|
144
|
+
return completed
|
145
|
+
|
146
|
+
except subprocess.TimeoutExpired as e:
|
147
|
+
plog.error(
|
148
|
+
"⏱️ Command timed out",
|
149
|
+
command=cmd_str,
|
150
|
+
timeout=timeout,
|
151
|
+
)
|
152
|
+
raise TimeoutError(
|
153
|
+
f"Command timed out after {timeout}s: {cmd_str}",
|
154
|
+
code="PROCESS_TIMEOUT",
|
155
|
+
command=cmd_str,
|
156
|
+
timeout=timeout,
|
157
|
+
) from e
|
158
|
+
except Exception as e:
|
159
|
+
if isinstance(e, ProcessError | TimeoutError):
|
160
|
+
raise
|
161
|
+
plog.error(
|
162
|
+
"💥 Command execution failed",
|
163
|
+
command=cmd_str,
|
164
|
+
error=str(e),
|
165
|
+
)
|
166
|
+
raise ProcessError(
|
167
|
+
f"Failed to execute command: {cmd_str}",
|
168
|
+
code="PROCESS_EXECUTION_FAILED",
|
169
|
+
command=cmd_str,
|
170
|
+
error=str(e),
|
171
|
+
) from e
|
172
|
+
|
173
|
+
|
174
|
+
def run_command_simple(
|
175
|
+
cmd: list[str],
|
176
|
+
cwd: str | Path | None = None,
|
177
|
+
**kwargs: Any,
|
178
|
+
) -> str:
|
179
|
+
"""
|
180
|
+
Simple wrapper for run_command that returns stdout as a string.
|
181
|
+
|
182
|
+
Args:
|
183
|
+
cmd: Command and arguments as a list
|
184
|
+
cwd: Working directory for the command
|
185
|
+
**kwargs: Additional arguments passed to run_command
|
186
|
+
|
187
|
+
Returns:
|
188
|
+
Stdout as a stripped string
|
189
|
+
|
190
|
+
Raises:
|
191
|
+
ProcessError: If command fails
|
192
|
+
"""
|
193
|
+
result = run_command(cmd, cwd=cwd, capture_output=True, check=True, **kwargs)
|
194
|
+
return result.stdout.strip()
|
195
|
+
|
196
|
+
|
197
|
+
def stream_command(
|
198
|
+
cmd: list[str],
|
199
|
+
cwd: str | Path | None = None,
|
200
|
+
env: Mapping[str, str] | None = None,
|
201
|
+
timeout: float | None = None,
|
202
|
+
stream_stderr: bool = False,
|
203
|
+
**kwargs: Any,
|
204
|
+
) -> Iterator[str]:
|
205
|
+
"""
|
206
|
+
Stream command output line by line.
|
207
|
+
|
208
|
+
Args:
|
209
|
+
cmd: Command and arguments as a list
|
210
|
+
cwd: Working directory for the command
|
211
|
+
env: Environment variables
|
212
|
+
timeout: Command timeout in seconds
|
213
|
+
stream_stderr: Whether to stream stderr (merged with stdout)
|
214
|
+
**kwargs: Additional arguments passed to subprocess.Popen
|
215
|
+
|
216
|
+
Yields:
|
217
|
+
Lines of output from the command
|
218
|
+
|
219
|
+
Raises:
|
220
|
+
ProcessError: If command fails
|
221
|
+
TimeoutError: If timeout is exceeded
|
222
|
+
"""
|
223
|
+
import fcntl
|
224
|
+
import os
|
225
|
+
import select
|
226
|
+
import time
|
227
|
+
|
228
|
+
cmd_str = " ".join(cmd) if isinstance(cmd, list) else str(cmd)
|
229
|
+
plog.info("🌊 Streaming command", command=cmd_str, cwd=str(cwd) if cwd else None)
|
230
|
+
|
231
|
+
# Prepare environment, disabling foundation telemetry by default
|
232
|
+
run_env = os.environ.copy()
|
233
|
+
if env is not None:
|
234
|
+
run_env.update(env)
|
235
|
+
run_env.setdefault("PROVIDE_TELEMETRY_DISABLED", "true")
|
236
|
+
|
237
|
+
# Convert Path to string
|
238
|
+
if isinstance(cwd, Path):
|
239
|
+
cwd = str(cwd)
|
240
|
+
|
241
|
+
try:
|
242
|
+
process = subprocess.Popen(
|
243
|
+
cmd,
|
244
|
+
cwd=cwd,
|
245
|
+
env=run_env,
|
246
|
+
stdout=subprocess.PIPE,
|
247
|
+
stderr=subprocess.STDOUT if stream_stderr else subprocess.PIPE,
|
248
|
+
text=True,
|
249
|
+
bufsize=1,
|
250
|
+
universal_newlines=True,
|
251
|
+
**kwargs,
|
252
|
+
)
|
253
|
+
|
254
|
+
if timeout is not None:
|
255
|
+
start_time = time.time()
|
256
|
+
|
257
|
+
if process.stdout:
|
258
|
+
# Use non-blocking I/O with timeout
|
259
|
+
# Make stdout non-blocking
|
260
|
+
fd = process.stdout.fileno()
|
261
|
+
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
|
262
|
+
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
263
|
+
|
264
|
+
buffer = ""
|
265
|
+
while True:
|
266
|
+
elapsed = time.time() - start_time
|
267
|
+
if elapsed >= timeout:
|
268
|
+
process.kill()
|
269
|
+
process.wait()
|
270
|
+
plog.error(
|
271
|
+
"⏱️ Stream timed out", command=cmd_str, timeout=timeout
|
272
|
+
)
|
273
|
+
raise TimeoutError(
|
274
|
+
f"Command timed out after {timeout}s: {cmd_str}",
|
275
|
+
code="PROCESS_STREAM_TIMEOUT",
|
276
|
+
command=cmd_str,
|
277
|
+
timeout=timeout,
|
278
|
+
)
|
279
|
+
|
280
|
+
# Use select with timeout
|
281
|
+
remaining = timeout - elapsed
|
282
|
+
ready, _, _ = select.select(
|
283
|
+
[process.stdout], [], [], min(0.1, remaining)
|
284
|
+
)
|
285
|
+
|
286
|
+
if ready:
|
287
|
+
try:
|
288
|
+
chunk = process.stdout.read(1024)
|
289
|
+
if not chunk:
|
290
|
+
break # EOF
|
291
|
+
buffer += chunk
|
292
|
+
|
293
|
+
# Yield complete lines
|
294
|
+
while "\n" in buffer:
|
295
|
+
line, buffer = buffer.split("\n", 1)
|
296
|
+
yield line.rstrip()
|
297
|
+
except OSError:
|
298
|
+
# No data available yet
|
299
|
+
pass
|
300
|
+
|
301
|
+
# Check if process ended
|
302
|
+
if process.poll() is not None:
|
303
|
+
# Read any remaining data
|
304
|
+
remaining_data = process.stdout.read()
|
305
|
+
if remaining_data:
|
306
|
+
buffer += remaining_data
|
307
|
+
|
308
|
+
# Yield any remaining lines
|
309
|
+
for line in buffer.split("\n"):
|
310
|
+
if line:
|
311
|
+
yield line.rstrip()
|
312
|
+
break
|
313
|
+
|
314
|
+
# Wait for process to complete
|
315
|
+
returncode = process.poll()
|
316
|
+
if returncode is None:
|
317
|
+
returncode = process.wait()
|
318
|
+
else:
|
319
|
+
# No timeout - use blocking I/O
|
320
|
+
if process.stdout:
|
321
|
+
for line in process.stdout:
|
322
|
+
yield line.rstrip()
|
323
|
+
|
324
|
+
# Wait for process to complete
|
325
|
+
returncode = process.wait()
|
326
|
+
|
327
|
+
if returncode != 0:
|
328
|
+
raise ProcessError(
|
329
|
+
f"Command failed with exit code {returncode}: {cmd_str}",
|
330
|
+
code="PROCESS_STREAM_FAILED",
|
331
|
+
command=cmd_str,
|
332
|
+
returncode=returncode,
|
333
|
+
)
|
334
|
+
|
335
|
+
plog.debug("✅ Stream completed", command=cmd_str)
|
336
|
+
except Exception as e:
|
337
|
+
if isinstance(e, ProcessError | TimeoutError):
|
338
|
+
raise
|
339
|
+
plog.error("💥 Stream failed", command=cmd_str, error=str(e))
|
340
|
+
raise ProcessError(
|
341
|
+
f"Failed to stream command: {cmd_str}",
|
342
|
+
code="PROCESS_STREAM_ERROR",
|
343
|
+
command=cmd_str,
|
344
|
+
error=str(e),
|
345
|
+
) from e
|
346
|
+
|
347
|
+
|
348
|
+
def run_shell(
|
349
|
+
cmd: str,
|
350
|
+
cwd: str | Path | None = None,
|
351
|
+
env: Mapping[str, str] | None = None,
|
352
|
+
capture_output: bool = True,
|
353
|
+
check: bool = True,
|
354
|
+
timeout: float | None = None,
|
355
|
+
**kwargs: Any,
|
356
|
+
) -> CompletedProcess:
|
357
|
+
"""Run a shell command.
|
358
|
+
|
359
|
+
Args:
|
360
|
+
cmd: Shell command string
|
361
|
+
cwd: Working directory
|
362
|
+
env: Environment variables
|
363
|
+
capture_output: Whether to capture output
|
364
|
+
check: Whether to raise on non-zero exit
|
365
|
+
timeout: Command timeout
|
366
|
+
**kwargs: Additional subprocess arguments
|
367
|
+
|
368
|
+
Returns:
|
369
|
+
CompletedProcess with results
|
370
|
+
"""
|
371
|
+
return run_command(
|
372
|
+
cmd,
|
373
|
+
cwd=cwd,
|
374
|
+
env=env,
|
375
|
+
capture_output=capture_output,
|
376
|
+
check=check,
|
377
|
+
timeout=timeout,
|
378
|
+
shell=True,
|
379
|
+
**kwargs,
|
380
|
+
)
|
381
|
+
|
382
|
+
|
383
|
+
# Export all public functions
|
384
|
+
__all__ = [
|
385
|
+
"CompletedProcess",
|
386
|
+
"run_command",
|
387
|
+
"run_command_simple",
|
388
|
+
"run_shell",
|
389
|
+
"stream_command",
|
390
|
+
]
|
@@ -0,0 +1,101 @@
|
|
1
|
+
#
|
2
|
+
# __init__.py
|
3
|
+
#
|
4
|
+
"""
|
5
|
+
Foundation Setup Module.
|
6
|
+
|
7
|
+
This module provides the main setup API for Foundation,
|
8
|
+
orchestrating logging, tracing, and other subsystems.
|
9
|
+
"""
|
10
|
+
|
11
|
+
from provide.foundation.logger.config import TelemetryConfig
|
12
|
+
from provide.foundation.logger.setup import internal_setup
|
13
|
+
from provide.foundation.logger.setup.coordinator import _PROVIDE_SETUP_LOCK
|
14
|
+
from provide.foundation.logger.setup.testing import reset_foundation_setup_for_testing
|
15
|
+
from provide.foundation.metrics.otel import (
|
16
|
+
setup_opentelemetry_metrics,
|
17
|
+
shutdown_opentelemetry_metrics,
|
18
|
+
)
|
19
|
+
from provide.foundation.streams.file import configure_file_logging, flush_log_streams
|
20
|
+
from provide.foundation.tracer.otel import (
|
21
|
+
setup_opentelemetry_tracing,
|
22
|
+
shutdown_opentelemetry,
|
23
|
+
)
|
24
|
+
|
25
|
+
_EXPLICIT_SETUP_DONE = False
|
26
|
+
|
27
|
+
|
28
|
+
def setup_foundation(config: TelemetryConfig | None = None) -> None:
|
29
|
+
"""
|
30
|
+
Initialize the Foundation system with all its subsystems.
|
31
|
+
|
32
|
+
This orchestrates:
|
33
|
+
- Logging system setup
|
34
|
+
- Stream configuration
|
35
|
+
- Future: Tracer initialization
|
36
|
+
|
37
|
+
Args:
|
38
|
+
config: Optional configuration to use. If None, loads from environment.
|
39
|
+
"""
|
40
|
+
global _EXPLICIT_SETUP_DONE
|
41
|
+
|
42
|
+
with _PROVIDE_SETUP_LOCK:
|
43
|
+
current_config = config if config is not None else TelemetryConfig.from_env()
|
44
|
+
|
45
|
+
# Configure file logging if specified
|
46
|
+
log_file_path = getattr(current_config.logging, "log_file", None)
|
47
|
+
configure_file_logging(log_file_path)
|
48
|
+
|
49
|
+
# Run the main logging setup
|
50
|
+
internal_setup(current_config, is_explicit_call=True)
|
51
|
+
|
52
|
+
# Initialize OpenTelemetry tracing and metrics if available and enabled
|
53
|
+
setup_opentelemetry_tracing(current_config)
|
54
|
+
setup_opentelemetry_metrics(current_config)
|
55
|
+
|
56
|
+
_EXPLICIT_SETUP_DONE = True
|
57
|
+
|
58
|
+
|
59
|
+
def setup_telemetry(config: TelemetryConfig | None = None) -> None:
|
60
|
+
"""
|
61
|
+
Legacy alias for setup_foundation.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
config: Optional configuration to use. If None, loads from environment.
|
65
|
+
"""
|
66
|
+
setup_foundation(config)
|
67
|
+
|
68
|
+
|
69
|
+
async def shutdown_foundation(timeout_millis: int = 5000) -> None:
|
70
|
+
"""
|
71
|
+
Gracefully shutdown all Foundation subsystems.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
timeout_millis: Timeout for shutdown (currently unused)
|
75
|
+
"""
|
76
|
+
with _PROVIDE_SETUP_LOCK:
|
77
|
+
# Shutdown OpenTelemetry tracing and metrics
|
78
|
+
shutdown_opentelemetry()
|
79
|
+
shutdown_opentelemetry_metrics()
|
80
|
+
|
81
|
+
# Flush logging streams
|
82
|
+
flush_log_streams()
|
83
|
+
|
84
|
+
|
85
|
+
async def shutdown_foundation_telemetry(timeout_millis: int = 5000) -> None:
|
86
|
+
"""
|
87
|
+
Legacy alias for shutdown_foundation.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
timeout_millis: Timeout for shutdown (currently unused)
|
91
|
+
"""
|
92
|
+
await shutdown_foundation(timeout_millis)
|
93
|
+
|
94
|
+
|
95
|
+
__all__ = [
|
96
|
+
"reset_foundation_setup_for_testing",
|
97
|
+
"setup_foundation",
|
98
|
+
"setup_telemetry", # Legacy alias
|
99
|
+
"shutdown_foundation",
|
100
|
+
"shutdown_foundation_telemetry", # Legacy alias
|
101
|
+
]
|
@@ -0,0 +1,44 @@
|
|
1
|
+
#
|
2
|
+
# __init__.py
|
3
|
+
#
|
4
|
+
"""
|
5
|
+
Foundation Streams Module.
|
6
|
+
|
7
|
+
Provides stream management functionality including console, file,
|
8
|
+
and core stream operations.
|
9
|
+
"""
|
10
|
+
|
11
|
+
from provide.foundation.streams.console import (
|
12
|
+
get_console_stream,
|
13
|
+
is_tty,
|
14
|
+
supports_color,
|
15
|
+
write_to_console,
|
16
|
+
)
|
17
|
+
from provide.foundation.streams.core import (
|
18
|
+
ensure_stderr_default,
|
19
|
+
get_log_stream,
|
20
|
+
set_log_stream_for_testing,
|
21
|
+
)
|
22
|
+
from provide.foundation.streams.file import (
|
23
|
+
close_log_streams,
|
24
|
+
configure_file_logging,
|
25
|
+
flush_log_streams,
|
26
|
+
reset_streams,
|
27
|
+
)
|
28
|
+
|
29
|
+
__all__ = [
|
30
|
+
# Core stream functions
|
31
|
+
"get_log_stream",
|
32
|
+
"set_log_stream_for_testing",
|
33
|
+
"ensure_stderr_default",
|
34
|
+
# File stream functions
|
35
|
+
"configure_file_logging",
|
36
|
+
"flush_log_streams",
|
37
|
+
"close_log_streams",
|
38
|
+
"reset_streams",
|
39
|
+
# Console stream functions
|
40
|
+
"get_console_stream",
|
41
|
+
"is_tty",
|
42
|
+
"supports_color",
|
43
|
+
"write_to_console",
|
44
|
+
]
|
@@ -0,0 +1,57 @@
|
|
1
|
+
#
|
2
|
+
# console.py
|
3
|
+
#
|
4
|
+
"""
|
5
|
+
Console stream utilities for Foundation.
|
6
|
+
Handles console-specific stream operations and formatting.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import sys
|
10
|
+
from typing import TextIO
|
11
|
+
|
12
|
+
from provide.foundation.streams.core import get_log_stream
|
13
|
+
|
14
|
+
|
15
|
+
def get_console_stream() -> TextIO:
|
16
|
+
"""Get the appropriate console stream for output."""
|
17
|
+
return get_log_stream()
|
18
|
+
|
19
|
+
|
20
|
+
def is_tty() -> bool:
|
21
|
+
"""Check if the current stream is a TTY (terminal)."""
|
22
|
+
stream = get_log_stream()
|
23
|
+
return hasattr(stream, "isatty") and stream.isatty()
|
24
|
+
|
25
|
+
|
26
|
+
def supports_color() -> bool:
|
27
|
+
"""Check if the current stream supports color output."""
|
28
|
+
import os
|
29
|
+
|
30
|
+
# Check NO_COLOR environment variable
|
31
|
+
if os.getenv("NO_COLOR"):
|
32
|
+
return False
|
33
|
+
|
34
|
+
# Check FORCE_COLOR environment variable
|
35
|
+
if os.getenv("FORCE_COLOR"):
|
36
|
+
return True
|
37
|
+
|
38
|
+
# Check if we're in a TTY
|
39
|
+
return is_tty()
|
40
|
+
|
41
|
+
|
42
|
+
def write_to_console(message: str, stream: TextIO | None = None) -> None:
|
43
|
+
"""
|
44
|
+
Write a message to the console stream.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
message: Message to write
|
48
|
+
stream: Optional specific stream to write to, defaults to current console stream
|
49
|
+
"""
|
50
|
+
target_stream = stream or get_console_stream()
|
51
|
+
try:
|
52
|
+
target_stream.write(message)
|
53
|
+
target_stream.flush()
|
54
|
+
except Exception:
|
55
|
+
# Fallback to stderr
|
56
|
+
sys.stderr.write(message)
|
57
|
+
sys.stderr.flush()
|
@@ -0,0 +1,65 @@
|
|
1
|
+
#
|
2
|
+
# core.py
|
3
|
+
#
|
4
|
+
"""
|
5
|
+
Core stream management for Foundation.
|
6
|
+
Handles log streams, file handles, and output configuration.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import sys
|
10
|
+
import threading
|
11
|
+
from typing import TextIO
|
12
|
+
|
13
|
+
_PROVIDE_LOG_STREAM: TextIO = sys.stderr
|
14
|
+
_LOG_FILE_HANDLE: TextIO | None = None
|
15
|
+
_STREAM_LOCK = threading.Lock()
|
16
|
+
|
17
|
+
|
18
|
+
def _is_in_click_testing() -> bool:
|
19
|
+
"""Check if we're running inside Click's testing framework."""
|
20
|
+
import inspect
|
21
|
+
import os
|
22
|
+
|
23
|
+
# Check environment variables for Click testing
|
24
|
+
if os.getenv("CLICK_TESTING"):
|
25
|
+
return True
|
26
|
+
|
27
|
+
# Check the call stack for Click's testing module or CLI integration tests
|
28
|
+
for frame_info in inspect.stack():
|
29
|
+
module = frame_info.frame.f_globals.get("__name__", "")
|
30
|
+
filename = frame_info.filename or ""
|
31
|
+
|
32
|
+
if "click.testing" in module or "test_cli_integration" in filename:
|
33
|
+
return True
|
34
|
+
|
35
|
+
# Also check for common Click testing patterns
|
36
|
+
locals_self = frame_info.frame.f_locals.get("self")
|
37
|
+
if hasattr(locals_self, "runner"):
|
38
|
+
runner = locals_self.runner
|
39
|
+
if hasattr(runner, "invoke") and "CliRunner" in str(type(runner)):
|
40
|
+
return True
|
41
|
+
|
42
|
+
return False
|
43
|
+
|
44
|
+
|
45
|
+
def get_log_stream() -> TextIO:
|
46
|
+
"""Get the current log stream."""
|
47
|
+
return _PROVIDE_LOG_STREAM
|
48
|
+
|
49
|
+
|
50
|
+
def set_log_stream_for_testing(stream: TextIO | None) -> None:
|
51
|
+
"""Set the log stream for testing purposes."""
|
52
|
+
global _PROVIDE_LOG_STREAM
|
53
|
+
with _STREAM_LOCK:
|
54
|
+
# Don't modify streams if we're in Click testing context
|
55
|
+
if _is_in_click_testing():
|
56
|
+
return
|
57
|
+
_PROVIDE_LOG_STREAM = stream if stream is not None else sys.stderr
|
58
|
+
|
59
|
+
|
60
|
+
def ensure_stderr_default() -> None:
|
61
|
+
"""Ensure the log stream defaults to stderr if it's stdout."""
|
62
|
+
global _PROVIDE_LOG_STREAM
|
63
|
+
with _STREAM_LOCK:
|
64
|
+
if _PROVIDE_LOG_STREAM is sys.stdout:
|
65
|
+
_PROVIDE_LOG_STREAM = sys.stderr
|