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,80 @@
1
+ """
2
+ Configuration field validators.
3
+
4
+ Provides common validation functions for configuration fields.
5
+ Domain-specific validators should be implemented in their respective packages.
6
+ """
7
+
8
+ from collections.abc import Callable
9
+ from typing import Any
10
+
11
+ from provide.foundation.errors.config import ValidationError
12
+
13
+
14
+ def validate_choice(choices: list[Any]) -> Callable[[Any, Any, Any], None]:
15
+ """
16
+ Create a validator that ensures the value is one of the allowed choices.
17
+
18
+ Args:
19
+ choices: List of allowed values
20
+
21
+ Returns:
22
+ Validator function
23
+ """
24
+
25
+ def validator(instance, attribute, value):
26
+ if value not in choices:
27
+ raise ValidationError(
28
+ f"Invalid value '{value}' for {attribute.name}. "
29
+ f"Must be one of: {choices}"
30
+ )
31
+
32
+ return validator
33
+
34
+
35
+ def validate_range(
36
+ min_value: float, max_value: float
37
+ ) -> Callable[[Any, Any, Any], None]:
38
+ """
39
+ Create a validator that ensures the value is within a numeric range.
40
+
41
+ Args:
42
+ min_value: Minimum allowed value (inclusive)
43
+ max_value: Maximum allowed value (inclusive)
44
+
45
+ Returns:
46
+ Validator function
47
+ """
48
+
49
+ def validator(instance, attribute, value):
50
+ if not isinstance(value, (int, float)):
51
+ raise ValidationError(f"Value must be a number, got {type(value).__name__}")
52
+
53
+ if not (min_value <= value <= max_value):
54
+ raise ValidationError(
55
+ f"Value must be between {min_value} and {max_value}, got {value}"
56
+ )
57
+
58
+ return validator
59
+
60
+
61
+ def validate_positive(instance, attribute, value):
62
+ """
63
+ Validate that a numeric value is positive.
64
+ """
65
+ if not isinstance(value, (int, float)):
66
+ raise ValidationError(f"Value must be a number, got {type(value).__name__}")
67
+
68
+ if value <= 0:
69
+ raise ValidationError(f"Value must be positive, got {value}")
70
+
71
+
72
+ def validate_non_negative(instance, attribute, value):
73
+ """
74
+ Validate that a numeric value is non-negative.
75
+ """
76
+ if not isinstance(value, (int, float)):
77
+ raise ValidationError(f"Value must be a number, got {type(value).__name__}")
78
+
79
+ if value < 0:
80
+ raise ValidationError(f"Value must be non-negative, got {value}")
@@ -0,0 +1,29 @@
1
+ """
2
+ Console I/O utilities for standardized CLI input/output.
3
+
4
+ Provides pout(), perr(), and pin() functions for consistent I/O handling.
5
+ """
6
+
7
+ from provide.foundation.console.input import (
8
+ apin,
9
+ apin_lines,
10
+ apin_stream,
11
+ pin,
12
+ pin_lines,
13
+ pin_stream,
14
+ )
15
+ from provide.foundation.console.output import perr, pout
16
+
17
+ __all__ = [
18
+ # Output functions
19
+ "perr",
20
+ "pout",
21
+ # Input functions
22
+ "pin",
23
+ "pin_lines",
24
+ "pin_stream",
25
+ # Async input functions
26
+ "apin",
27
+ "apin_lines",
28
+ "apin_stream",
29
+ ]
@@ -0,0 +1,364 @@
1
+ """
2
+ Core console input functions for standardized CLI input.
3
+
4
+ Provides pin() and async variants for consistent input handling with support
5
+ for JSON mode, streaming, and proper integration with the foundation's patterns.
6
+ """
7
+
8
+ import asyncio
9
+ from collections.abc import AsyncIterator, Iterator
10
+ import json
11
+ import sys
12
+ from typing import Any, TypeVar
13
+
14
+ try:
15
+ import click
16
+
17
+ _HAS_CLICK = True
18
+ except ImportError:
19
+ click = None
20
+ _HAS_CLICK = False
21
+
22
+ from provide.foundation.context import Context
23
+ from provide.foundation.logger import get_logger
24
+
25
+ plog = get_logger(__name__)
26
+
27
+ T = TypeVar("T")
28
+
29
+
30
+ def _get_context() -> Context | None:
31
+ """Get current context from Click or environment."""
32
+ if not _HAS_CLICK:
33
+ return None
34
+ ctx = click.get_current_context(silent=True)
35
+ if ctx and hasattr(ctx, "obj") and isinstance(ctx.obj, Context):
36
+ return ctx.obj
37
+ return None
38
+
39
+
40
+ def _should_use_json(ctx: Context | None = None) -> bool:
41
+ """Determine if JSON output should be used."""
42
+ if ctx is None:
43
+ ctx = _get_context()
44
+ return ctx.json_output if ctx else False
45
+
46
+
47
+ def _should_use_color(ctx: Context | None = None) -> bool:
48
+ """Determine if color output should be used."""
49
+ if ctx is None:
50
+ ctx = _get_context()
51
+
52
+ # Check if stdin is a TTY
53
+ return sys.stdin.isatty()
54
+
55
+
56
+ def pin(prompt: str = "", **kwargs: Any) -> str | Any:
57
+ """
58
+ Input from stdin with optional prompt.
59
+
60
+ Args:
61
+ prompt: Prompt to display before input
62
+ **kwargs: Optional formatting arguments:
63
+ type: Type to convert input to (int, float, bool, etc.)
64
+ default: Default value if no input provided
65
+ password: Hide input for passwords (default: False)
66
+ confirmation_prompt: Ask for confirmation (for passwords)
67
+ hide_input: Hide the input (same as password)
68
+ show_default: Show default value in prompt
69
+ value_proc: Callable to process the value
70
+ json_key: Key for JSON output mode
71
+ ctx: Override context
72
+ color: Color for prompt (red, green, yellow, blue, cyan, magenta, white)
73
+ bold: Bold prompt text
74
+
75
+ Returns:
76
+ User input as string or converted type
77
+
78
+ Examples:
79
+ name = pin("Enter name: ")
80
+ age = pin("Age: ", type=int, default=0)
81
+ password = pin("Password: ", password=True)
82
+
83
+ In JSON mode, returns structured input data.
84
+ """
85
+ ctx = kwargs.get("ctx") or _get_context()
86
+
87
+ if _should_use_json(ctx):
88
+ # JSON mode - read from stdin and parse
89
+ try:
90
+ if sys.stdin.isatty():
91
+ # Interactive mode, still show prompt to stderr
92
+ if prompt:
93
+ if _HAS_CLICK:
94
+ click.echo(prompt, err=True, nl=False)
95
+ else:
96
+ print(prompt, file=sys.stderr, end="")
97
+
98
+ line = sys.stdin.readline().strip()
99
+
100
+ # Try to parse as JSON first
101
+ try:
102
+ data = json.loads(line)
103
+ except json.JSONDecodeError:
104
+ # Treat as plain string
105
+ data = line
106
+
107
+ # Apply type conversion if specified
108
+ if type_func := kwargs.get("type"):
109
+ try:
110
+ data = type_func(data)
111
+ except (TypeError, ValueError):
112
+ pass
113
+
114
+ if json_key := kwargs.get("json_key"):
115
+ return {json_key: data}
116
+ return data
117
+
118
+ except Exception as e:
119
+ plog.error("Failed to read JSON input", error=str(e))
120
+ if json_key := kwargs.get("json_key"):
121
+ return {json_key: None, "error": str(e)}
122
+ return None
123
+ else:
124
+ # Regular interactive mode - use click.prompt
125
+ prompt_kwargs = {}
126
+
127
+ # Map our kwargs to click.prompt kwargs
128
+ if "type" in kwargs:
129
+ prompt_kwargs["type"] = kwargs["type"]
130
+ if "default" in kwargs:
131
+ prompt_kwargs["default"] = kwargs["default"]
132
+ if kwargs.get("password") or kwargs.get("hide_input"):
133
+ prompt_kwargs["hide_input"] = True
134
+ if "confirmation_prompt" in kwargs:
135
+ prompt_kwargs["confirmation_prompt"] = kwargs["confirmation_prompt"]
136
+ if "show_default" in kwargs:
137
+ prompt_kwargs["show_default"] = kwargs["show_default"]
138
+ if "value_proc" in kwargs:
139
+ prompt_kwargs["value_proc"] = kwargs["value_proc"]
140
+
141
+ if _HAS_CLICK:
142
+ # Apply color/formatting to prompt if requested and supported
143
+ styled_prompt = prompt
144
+ if _should_use_color(ctx):
145
+ color = kwargs.get("color")
146
+ bold = kwargs.get("bold", False)
147
+ if color or bold:
148
+ styled_prompt = click.style(prompt, fg=color, bold=bold)
149
+
150
+ return click.prompt(styled_prompt, **prompt_kwargs)
151
+ else:
152
+ # Fallback to standard Python input
153
+ display_prompt = prompt
154
+ if kwargs.get("default") and kwargs.get("show_default", True):
155
+ display_prompt = f"{prompt} [{kwargs['default']}]: "
156
+ elif prompt and not prompt.endswith(": "):
157
+ display_prompt = f"{prompt}: "
158
+
159
+ if kwargs.get("password") or kwargs.get("hide_input"):
160
+ import getpass
161
+
162
+ user_input = getpass.getpass(display_prompt)
163
+ else:
164
+ user_input = input(display_prompt)
165
+
166
+ # Handle default value
167
+ if not user_input and "default" in kwargs:
168
+ user_input = str(kwargs["default"])
169
+
170
+ # Type conversion
171
+ if type_func := kwargs.get("type"):
172
+ try:
173
+ return type_func(user_input)
174
+ except (TypeError, ValueError):
175
+ return user_input
176
+
177
+ return user_input
178
+
179
+
180
+ def pin_stream() -> Iterator[str]:
181
+ """
182
+ Stream input line by line from stdin.
183
+
184
+ Yields:
185
+ Lines from stdin (without trailing newline)
186
+
187
+ Examples:
188
+ for line in pin_stream():
189
+ process(line)
190
+
191
+ Note: This blocks on each line. For non-blocking, use apin_stream().
192
+ """
193
+ ctx = _get_context()
194
+
195
+ if _should_use_json(ctx):
196
+ # In JSON mode, try to read as JSON first
197
+ stdin_content = sys.stdin.read()
198
+ try:
199
+ # Try to parse as JSON array/object
200
+ data = json.loads(stdin_content)
201
+ if isinstance(data, list):
202
+ for item in data:
203
+ yield json.dumps(item) if not isinstance(item, str) else item
204
+ else:
205
+ yield json.dumps(data)
206
+ except json.JSONDecodeError:
207
+ # Fall back to line-by-line reading
208
+ for line in stdin_content.splitlines():
209
+ if line: # Skip empty lines
210
+ yield line
211
+ else:
212
+ # Regular mode - yield lines as they come
213
+ plog.debug("📥 Starting input stream")
214
+ line_count = 0
215
+ try:
216
+ for line in sys.stdin:
217
+ line = line.rstrip("\n\r")
218
+ line_count += 1
219
+ plog.trace("📥 Stream line", line_num=line_count, length=len(line))
220
+ yield line
221
+ finally:
222
+ plog.debug("📥 Input stream ended", lines=line_count)
223
+
224
+
225
+ async def apin(prompt: str = "", **kwargs: Any) -> str | Any:
226
+ """
227
+ Async input from stdin with optional prompt.
228
+
229
+ Args:
230
+ prompt: Prompt to display before input
231
+ **kwargs: Same as pin()
232
+
233
+ Returns:
234
+ User input as string or converted type
235
+
236
+ Examples:
237
+ name = await apin("Enter name: ")
238
+ age = await apin("Age: ", type=int)
239
+
240
+ Note: This runs the blocking input in a thread pool to avoid blocking the event loop.
241
+ """
242
+ import functools
243
+
244
+ loop = asyncio.get_event_loop()
245
+ func = functools.partial(pin, prompt, **kwargs)
246
+ return await loop.run_in_executor(None, func)
247
+
248
+
249
+ async def apin_stream() -> AsyncIterator[str]:
250
+ """
251
+ Async stream input line by line from stdin.
252
+
253
+ Yields:
254
+ Lines from stdin (without trailing newline)
255
+
256
+ Examples:
257
+ async for line in apin_stream():
258
+ await process(line)
259
+
260
+ This provides non-blocking line-by-line input streaming.
261
+ """
262
+ ctx = _get_context()
263
+
264
+ if _should_use_json(ctx):
265
+ # In JSON mode, read all input and yield parsed lines
266
+ loop = asyncio.get_event_loop()
267
+
268
+ def read_json():
269
+ try:
270
+ data = json.load(sys.stdin)
271
+ if isinstance(data, list):
272
+ return [
273
+ json.dumps(item) if not isinstance(item, str) else item
274
+ for item in data
275
+ ]
276
+ else:
277
+ return [json.dumps(data)]
278
+ except json.JSONDecodeError:
279
+ # Fall back to line-by-line reading
280
+ return [line.rstrip("\n\r") for line in sys.stdin]
281
+
282
+ lines = await loop.run_in_executor(None, read_json)
283
+ for line in lines:
284
+ yield line
285
+ else:
286
+ # Regular mode - async line streaming
287
+ plog.debug("📥 Starting async input stream")
288
+ line_count = 0
289
+
290
+ # Create async reader for stdin
291
+ loop = asyncio.get_event_loop()
292
+ reader = asyncio.StreamReader()
293
+ protocol = asyncio.StreamReaderProtocol(reader)
294
+
295
+ await loop.connect_read_pipe(lambda: protocol, sys.stdin)
296
+
297
+ try:
298
+ while True:
299
+ try:
300
+ line_bytes = await reader.readline()
301
+ if not line_bytes:
302
+ break
303
+
304
+ line = line_bytes.decode("utf-8").rstrip("\n\r")
305
+ line_count += 1
306
+ plog.trace(
307
+ "📥 Async stream line", line_num=line_count, length=len(line)
308
+ )
309
+ yield line
310
+
311
+ except asyncio.CancelledError:
312
+ plog.debug("📥 Async stream cancelled", lines=line_count)
313
+ break
314
+ except Exception as e:
315
+ plog.error("📥 Async stream error", error=str(e), lines=line_count)
316
+ break
317
+ finally:
318
+ plog.debug("📥 Async input stream ended", lines=line_count)
319
+
320
+
321
+ def pin_lines(count: int | None = None) -> list[str]:
322
+ """
323
+ Read multiple lines from stdin.
324
+
325
+ Args:
326
+ count: Number of lines to read (None for all until EOF)
327
+
328
+ Returns:
329
+ List of input lines
330
+
331
+ Examples:
332
+ lines = pin_lines(3) # Read exactly 3 lines
333
+ all_lines = pin_lines() # Read until EOF
334
+ """
335
+ lines = []
336
+ for i, line in enumerate(pin_stream()):
337
+ lines.append(line)
338
+ if count is not None and i + 1 >= count:
339
+ break
340
+ return lines
341
+
342
+
343
+ async def apin_lines(count: int | None = None) -> list[str]:
344
+ """
345
+ Async read multiple lines from stdin.
346
+
347
+ Args:
348
+ count: Number of lines to read (None for all until EOF)
349
+
350
+ Returns:
351
+ List of input lines
352
+
353
+ Examples:
354
+ lines = await apin_lines(3) # Read exactly 3 lines
355
+ all_lines = await apin_lines() # Read until EOF
356
+ """
357
+ lines = []
358
+ i = 0
359
+ async for line in apin_stream():
360
+ lines.append(line)
361
+ i += 1
362
+ if count is not None and i >= count:
363
+ break
364
+ return lines
@@ -0,0 +1,178 @@
1
+ """
2
+ Core console output functions for standardized CLI output.
3
+
4
+ Provides pout() and perr() for consistent output handling with support
5
+ for JSON mode, colors, and proper stream separation.
6
+ """
7
+
8
+ import json
9
+ import sys
10
+ from typing import Any
11
+
12
+ try:
13
+ import click
14
+
15
+ _HAS_CLICK = True
16
+ except ImportError:
17
+ click = None
18
+ _HAS_CLICK = False
19
+
20
+ from provide.foundation.context import Context
21
+ from provide.foundation.logger import get_logger
22
+
23
+ log = get_logger(__name__)
24
+
25
+
26
+ def _get_context() -> Context | None:
27
+ """Get current context from Click or environment."""
28
+ if not _HAS_CLICK:
29
+ return None
30
+ ctx = click.get_current_context(silent=True)
31
+ if ctx and hasattr(ctx, "obj") and isinstance(ctx.obj, Context):
32
+ return ctx.obj
33
+ return None
34
+
35
+
36
+ def _should_use_json(ctx: Context | None = None) -> bool:
37
+ """Determine if JSON output should be used."""
38
+ if ctx is None:
39
+ ctx = _get_context()
40
+ return ctx.json_output if ctx else False
41
+
42
+
43
+ def _should_use_color(ctx: Context | None = None, stream=None) -> bool:
44
+ """Determine if color output should be used."""
45
+ if ctx is None:
46
+ ctx = _get_context()
47
+
48
+ # Check if stream is a TTY
49
+ if stream:
50
+ return getattr(stream, "isatty", lambda: False)()
51
+
52
+ return sys.stdout.isatty() or sys.stderr.isatty()
53
+
54
+
55
+ def _output_json(data: Any, stream=sys.stdout) -> None:
56
+ """Output data as JSON."""
57
+ try:
58
+ json_str = json.dumps(data, indent=2, default=str)
59
+ click.echo(json_str, file=stream)
60
+ except (TypeError, ValueError) as e:
61
+ # Fallback to string representation
62
+ click.echo(
63
+ json.dumps({"error": f"JSON encoding failed: {e}", "data": str(data)}),
64
+ file=stream,
65
+ )
66
+
67
+
68
+ def pout(message: Any, **kwargs: Any) -> None:
69
+ """
70
+ Output message to stdout.
71
+
72
+ Args:
73
+ message: Content to output (any type - will be stringified or JSON-encoded)
74
+ **kwargs: Optional formatting arguments:
75
+ color: Color name (red, green, yellow, blue, cyan, magenta, white)
76
+ bold: Bold text
77
+ dim: Dim text
78
+ nl/newline: Add newline (default: True)
79
+ json_key: Key for JSON output mode
80
+ prefix: Optional prefix string
81
+ ctx: Override context
82
+
83
+ Examples:
84
+ pout("Hello world")
85
+ pout({"data": "value"}) # Auto-JSON if dict/list
86
+ pout("Success", color="green", bold=True)
87
+ pout(results, json_key="results")
88
+ """
89
+ ctx = kwargs.get("ctx") or _get_context()
90
+
91
+ # Handle newline option (support both nl and newline)
92
+ nl = kwargs.get("nl", kwargs.get("newline", True))
93
+
94
+ if _should_use_json(ctx):
95
+ # JSON mode
96
+ if kwargs.get("json_key"):
97
+ _output_json({kwargs["json_key"]: message}, sys.stdout)
98
+ else:
99
+ _output_json(message, sys.stdout)
100
+ else:
101
+ # Regular output mode
102
+ # Add optional prefix
103
+ output = str(message)
104
+ if prefix := kwargs.get("prefix"):
105
+ output = f"{prefix} {output}"
106
+
107
+ # Apply color/formatting if requested and supported
108
+ color = kwargs.get("color")
109
+ bold = kwargs.get("bold", False)
110
+ dim = kwargs.get("dim", False)
111
+
112
+ if _HAS_CLICK:
113
+ if (color or bold or dim) and _should_use_color(ctx, sys.stdout):
114
+ click.secho(output, fg=color, bold=bold, dim=dim, nl=nl)
115
+ else:
116
+ click.echo(output, nl=nl)
117
+ else:
118
+ # Fallback to standard Python print
119
+ if nl:
120
+ print(output, file=sys.stdout)
121
+ else:
122
+ print(output, file=sys.stdout, end="")
123
+
124
+
125
+ def perr(message: Any, **kwargs: Any) -> None:
126
+ """
127
+ Output message to stderr.
128
+
129
+ Args:
130
+ message: Content to output (any type - will be stringified or JSON-encoded)
131
+ **kwargs: Optional formatting arguments:
132
+ color: Color name (red, green, yellow, blue, cyan, magenta, white)
133
+ bold: Bold text
134
+ dim: Dim text
135
+ nl/newline: Add newline (default: True)
136
+ json_key: Key for JSON output mode
137
+ prefix: Optional prefix string
138
+ ctx: Override context
139
+
140
+ Examples:
141
+ perr("Error occurred")
142
+ perr("Warning", color="yellow")
143
+ perr({"error": details}, json_key="error")
144
+ """
145
+ ctx = kwargs.get("ctx") or _get_context()
146
+
147
+ # Handle newline option (support both nl and newline)
148
+ nl = kwargs.get("nl", kwargs.get("newline", True))
149
+
150
+ if _should_use_json(ctx):
151
+ # JSON mode
152
+ if kwargs.get("json_key"):
153
+ _output_json({kwargs["json_key"]: message}, sys.stderr)
154
+ else:
155
+ _output_json(message, sys.stderr)
156
+ else:
157
+ # Regular output mode
158
+ # Add optional prefix
159
+ output = str(message)
160
+ if prefix := kwargs.get("prefix"):
161
+ output = f"{prefix} {output}"
162
+
163
+ # Apply color/formatting if requested and supported
164
+ color = kwargs.get("color")
165
+ bold = kwargs.get("bold", False)
166
+ dim = kwargs.get("dim", False)
167
+
168
+ if _HAS_CLICK:
169
+ if (color or bold or dim) and _should_use_color(ctx, sys.stderr):
170
+ click.secho(output, fg=color, bold=bold, dim=dim, err=True, nl=nl)
171
+ else:
172
+ click.echo(output, err=True, nl=nl)
173
+ else:
174
+ # Fallback to standard Python print
175
+ if nl:
176
+ print(output, file=sys.stderr)
177
+ else:
178
+ print(output, file=sys.stderr, end="")
@@ -0,0 +1,12 @@
1
+ """
2
+ Core context management for provide-foundation.
3
+
4
+ Provides unified application context that bridges configuration, runtime state,
5
+ and presentation concerns across the foundation library.
6
+ """
7
+
8
+ from provide.foundation.context.core import Context
9
+
10
+ __all__ = [
11
+ "Context",
12
+ ]