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,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