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