provide-foundation 0.0.0.dev1__py3-none-any.whl → 0.0.0.dev3__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/foundation/__init__.py +36 -10
- provide/foundation/archive/__init__.py +1 -1
- provide/foundation/archive/base.py +15 -14
- provide/foundation/archive/bzip2.py +40 -40
- provide/foundation/archive/gzip.py +42 -42
- provide/foundation/archive/operations.py +93 -96
- provide/foundation/archive/tar.py +33 -31
- provide/foundation/archive/zip.py +52 -50
- provide/foundation/asynctools/__init__.py +20 -0
- provide/foundation/asynctools/core.py +126 -0
- provide/foundation/cli/__init__.py +2 -2
- provide/foundation/cli/commands/deps.py +15 -9
- provide/foundation/cli/commands/logs/__init__.py +3 -3
- provide/foundation/cli/commands/logs/generate.py +2 -2
- provide/foundation/cli/commands/logs/query.py +4 -4
- provide/foundation/cli/commands/logs/send.py +3 -3
- provide/foundation/cli/commands/logs/tail.py +3 -3
- provide/foundation/cli/decorators.py +11 -11
- provide/foundation/cli/main.py +1 -1
- provide/foundation/cli/testing.py +2 -40
- provide/foundation/cli/utils.py +21 -18
- provide/foundation/config/__init__.py +35 -2
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +477 -0
- provide/foundation/config/defaults.py +67 -0
- provide/foundation/config/env.py +6 -20
- provide/foundation/config/loader.py +10 -4
- provide/foundation/config/sync.py +8 -6
- provide/foundation/config/types.py +5 -5
- provide/foundation/config/validators.py +4 -4
- provide/foundation/console/input.py +5 -5
- provide/foundation/console/output.py +36 -14
- provide/foundation/context/__init__.py +8 -4
- provide/foundation/context/core.py +88 -110
- provide/foundation/crypto/certificates/__init__.py +9 -5
- provide/foundation/crypto/certificates/base.py +2 -2
- provide/foundation/crypto/certificates/certificate.py +48 -19
- provide/foundation/crypto/certificates/factory.py +26 -18
- provide/foundation/crypto/certificates/generator.py +24 -23
- provide/foundation/crypto/certificates/loader.py +24 -16
- provide/foundation/crypto/certificates/operations.py +17 -10
- provide/foundation/crypto/certificates/trust.py +21 -21
- provide/foundation/env/__init__.py +28 -0
- provide/foundation/env/core.py +218 -0
- provide/foundation/errors/__init__.py +3 -3
- provide/foundation/errors/decorators.py +0 -234
- provide/foundation/errors/types.py +0 -98
- provide/foundation/eventsets/display.py +13 -14
- provide/foundation/eventsets/registry.py +61 -31
- provide/foundation/eventsets/resolver.py +50 -46
- provide/foundation/eventsets/sets/das.py +8 -8
- provide/foundation/eventsets/sets/database.py +14 -14
- provide/foundation/eventsets/sets/http.py +21 -21
- provide/foundation/eventsets/sets/llm.py +16 -16
- provide/foundation/eventsets/sets/task_queue.py +13 -13
- provide/foundation/eventsets/types.py +7 -7
- provide/foundation/file/directory.py +14 -23
- provide/foundation/file/lock.py +4 -3
- provide/foundation/hub/components.py +75 -389
- provide/foundation/hub/config.py +157 -0
- provide/foundation/hub/discovery.py +63 -0
- provide/foundation/hub/handlers.py +89 -0
- provide/foundation/hub/lifecycle.py +195 -0
- provide/foundation/hub/manager.py +7 -4
- provide/foundation/hub/processors.py +49 -0
- provide/foundation/integrations/__init__.py +11 -0
- provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
- provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/client.py +14 -14
- provide/foundation/{observability → integrations}/openobserve/commands.py +12 -12
- provide/foundation/integrations/openobserve/config.py +37 -0
- provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/otlp.py +2 -2
- provide/foundation/{observability → integrations}/openobserve/search.py +2 -3
- provide/foundation/{observability → integrations}/openobserve/streaming.py +5 -5
- provide/foundation/logger/__init__.py +0 -1
- provide/foundation/logger/config/base.py +1 -1
- provide/foundation/logger/config/logging.py +69 -299
- provide/foundation/logger/config/telemetry.py +39 -121
- provide/foundation/logger/factories.py +2 -2
- provide/foundation/logger/processors/main.py +12 -10
- provide/foundation/logger/ratelimit/limiters.py +4 -4
- provide/foundation/logger/ratelimit/processor.py +1 -1
- provide/foundation/logger/setup/coordinator.py +39 -25
- provide/foundation/logger/setup/processors.py +3 -3
- provide/foundation/logger/setup/testing.py +14 -0
- provide/foundation/logger/trace.py +5 -5
- provide/foundation/metrics/__init__.py +1 -1
- provide/foundation/metrics/otel.py +3 -1
- provide/foundation/observability/__init__.py +3 -3
- provide/foundation/process/__init__.py +9 -0
- provide/foundation/process/exit.py +48 -0
- provide/foundation/process/lifecycle.py +69 -46
- provide/foundation/resilience/__init__.py +36 -0
- provide/foundation/resilience/circuit.py +166 -0
- provide/foundation/resilience/decorators.py +236 -0
- provide/foundation/resilience/fallback.py +208 -0
- provide/foundation/resilience/retry.py +327 -0
- provide/foundation/serialization/__init__.py +16 -0
- provide/foundation/serialization/core.py +70 -0
- provide/foundation/streams/config.py +78 -0
- provide/foundation/streams/console.py +4 -5
- provide/foundation/streams/core.py +5 -2
- provide/foundation/streams/file.py +12 -2
- provide/foundation/testing/__init__.py +29 -9
- provide/foundation/testing/archive/__init__.py +7 -7
- provide/foundation/testing/archive/fixtures.py +58 -54
- provide/foundation/testing/cli.py +30 -20
- provide/foundation/testing/common/__init__.py +13 -15
- provide/foundation/testing/common/fixtures.py +27 -57
- provide/foundation/testing/file/__init__.py +15 -15
- provide/foundation/testing/file/content_fixtures.py +289 -0
- provide/foundation/testing/file/directory_fixtures.py +107 -0
- provide/foundation/testing/file/fixtures.py +42 -516
- provide/foundation/testing/file/special_fixtures.py +145 -0
- provide/foundation/testing/logger.py +89 -8
- provide/foundation/testing/mocking/__init__.py +21 -21
- provide/foundation/testing/mocking/fixtures.py +80 -67
- provide/foundation/testing/process/__init__.py +23 -23
- provide/foundation/testing/process/async_fixtures.py +414 -0
- provide/foundation/testing/process/fixtures.py +48 -571
- provide/foundation/testing/process/subprocess_fixtures.py +210 -0
- provide/foundation/testing/threading/__init__.py +17 -17
- provide/foundation/testing/threading/basic_fixtures.py +105 -0
- provide/foundation/testing/threading/data_fixtures.py +101 -0
- provide/foundation/testing/threading/execution_fixtures.py +278 -0
- provide/foundation/testing/threading/fixtures.py +32 -502
- provide/foundation/testing/threading/sync_fixtures.py +100 -0
- provide/foundation/testing/time/__init__.py +11 -11
- provide/foundation/testing/time/fixtures.py +95 -83
- provide/foundation/testing/transport/__init__.py +9 -9
- provide/foundation/testing/transport/fixtures.py +54 -54
- provide/foundation/time/__init__.py +18 -0
- provide/foundation/time/core.py +63 -0
- provide/foundation/tools/__init__.py +2 -2
- provide/foundation/tools/base.py +68 -67
- provide/foundation/tools/cache.py +69 -74
- provide/foundation/tools/downloader.py +68 -62
- provide/foundation/tools/installer.py +51 -57
- provide/foundation/tools/registry.py +38 -45
- provide/foundation/tools/resolver.py +70 -68
- provide/foundation/tools/verifier.py +39 -50
- provide/foundation/tracer/spans.py +2 -14
- provide/foundation/transport/__init__.py +26 -33
- provide/foundation/transport/base.py +32 -30
- provide/foundation/transport/client.py +44 -49
- provide/foundation/transport/config.py +36 -107
- provide/foundation/transport/errors.py +13 -27
- provide/foundation/transport/http.py +69 -55
- provide/foundation/transport/middleware.py +113 -114
- provide/foundation/transport/registry.py +29 -27
- provide/foundation/transport/types.py +6 -6
- provide/foundation/utils/deps.py +17 -14
- provide/foundation/utils/parsing.py +49 -4
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
- provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
- provide_foundation-0.0.0.dev1.dist-info/RECORD +0 -200
- /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
- /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/top_level.txt +0 -0
@@ -20,31 +20,31 @@ class ConfigSource(Enum):
|
|
20
20
|
ENV = 20
|
21
21
|
RUNTIME = 30 # Highest precedence
|
22
22
|
|
23
|
-
def __lt__(self, other):
|
23
|
+
def __lt__(self, other: object) -> bool:
|
24
24
|
"""Enable comparison for precedence."""
|
25
25
|
if not isinstance(other, ConfigSource):
|
26
26
|
return NotImplemented
|
27
27
|
return self.value < other.value
|
28
28
|
|
29
|
-
def __le__(self, other):
|
29
|
+
def __le__(self, other: object) -> bool:
|
30
30
|
"""Enable <= comparison for precedence."""
|
31
31
|
if not isinstance(other, ConfigSource):
|
32
32
|
return NotImplemented
|
33
33
|
return self.value <= other.value
|
34
34
|
|
35
|
-
def __gt__(self, other):
|
35
|
+
def __gt__(self, other: object) -> bool:
|
36
36
|
"""Enable > comparison for precedence."""
|
37
37
|
if not isinstance(other, ConfigSource):
|
38
38
|
return NotImplemented
|
39
39
|
return self.value > other.value
|
40
40
|
|
41
|
-
def __ge__(self, other):
|
41
|
+
def __ge__(self, other: object) -> bool:
|
42
42
|
"""Enable >= comparison for precedence."""
|
43
43
|
if not isinstance(other, ConfigSource):
|
44
44
|
return NotImplemented
|
45
45
|
return self.value >= other.value
|
46
46
|
|
47
|
-
def __eq__(self, other):
|
47
|
+
def __eq__(self, other: object) -> bool:
|
48
48
|
"""Enable == comparison for precedence."""
|
49
49
|
if not isinstance(other, ConfigSource):
|
50
50
|
return NotImplemented
|
@@ -22,7 +22,7 @@ def validate_choice(choices: list[Any]) -> Callable[[Any, Any, Any], None]:
|
|
22
22
|
Validator function
|
23
23
|
"""
|
24
24
|
|
25
|
-
def validator(instance, attribute, value):
|
25
|
+
def validator(instance: object, attribute: object, value: Any) -> None:
|
26
26
|
if value not in choices:
|
27
27
|
raise ValidationError(
|
28
28
|
f"Invalid value '{value}' for {attribute.name}. "
|
@@ -46,7 +46,7 @@ def validate_range(
|
|
46
46
|
Validator function
|
47
47
|
"""
|
48
48
|
|
49
|
-
def validator(instance, attribute, value):
|
49
|
+
def validator(instance: object, attribute: object, value: Any) -> None:
|
50
50
|
if not isinstance(value, (int, float)):
|
51
51
|
raise ValidationError(f"Value must be a number, got {type(value).__name__}")
|
52
52
|
|
@@ -58,7 +58,7 @@ def validate_range(
|
|
58
58
|
return validator
|
59
59
|
|
60
60
|
|
61
|
-
def validate_positive(instance, attribute, value):
|
61
|
+
def validate_positive(instance: object, attribute: object, value: Any) -> None:
|
62
62
|
"""
|
63
63
|
Validate that a numeric value is positive.
|
64
64
|
"""
|
@@ -69,7 +69,7 @@ def validate_positive(instance, attribute, value):
|
|
69
69
|
raise ValidationError(f"Value must be positive, got {value}")
|
70
70
|
|
71
71
|
|
72
|
-
def validate_non_negative(instance, attribute, value):
|
72
|
+
def validate_non_negative(instance: object, attribute: object, value: Any) -> None:
|
73
73
|
"""
|
74
74
|
Validate that a numeric value is non-negative.
|
75
75
|
"""
|
@@ -19,7 +19,7 @@ except ImportError:
|
|
19
19
|
click = None
|
20
20
|
_HAS_CLICK = False
|
21
21
|
|
22
|
-
from provide.foundation.context import
|
22
|
+
from provide.foundation.context import CLIContext
|
23
23
|
from provide.foundation.logger import get_logger
|
24
24
|
|
25
25
|
plog = get_logger(__name__)
|
@@ -27,24 +27,24 @@ plog = get_logger(__name__)
|
|
27
27
|
T = TypeVar("T")
|
28
28
|
|
29
29
|
|
30
|
-
def _get_context() ->
|
30
|
+
def _get_context() -> CLIContext | None:
|
31
31
|
"""Get current context from Click or environment."""
|
32
32
|
if not _HAS_CLICK:
|
33
33
|
return None
|
34
34
|
ctx = click.get_current_context(silent=True)
|
35
|
-
if ctx and hasattr(ctx, "obj") and isinstance(ctx.obj,
|
35
|
+
if ctx and hasattr(ctx, "obj") and isinstance(ctx.obj, CLIContext):
|
36
36
|
return ctx.obj
|
37
37
|
return None
|
38
38
|
|
39
39
|
|
40
|
-
def _should_use_json(ctx:
|
40
|
+
def _should_use_json(ctx: CLIContext | None = None) -> bool:
|
41
41
|
"""Determine if JSON output should be used."""
|
42
42
|
if ctx is None:
|
43
43
|
ctx = _get_context()
|
44
44
|
return ctx.json_output if ctx else False
|
45
45
|
|
46
46
|
|
47
|
-
def _should_use_color(ctx:
|
47
|
+
def _should_use_color(ctx: CLIContext | None = None) -> bool:
|
48
48
|
"""Determine if color output should be used."""
|
49
49
|
if ctx is None:
|
50
50
|
ctx = _get_context()
|
@@ -6,6 +6,7 @@ for JSON mode, colors, and proper stream separation.
|
|
6
6
|
"""
|
7
7
|
|
8
8
|
import json
|
9
|
+
import os
|
9
10
|
import sys
|
10
11
|
from typing import Any
|
11
12
|
|
@@ -17,34 +18,48 @@ except ImportError:
|
|
17
18
|
click = None
|
18
19
|
_HAS_CLICK = False
|
19
20
|
|
20
|
-
from provide.foundation.context import
|
21
|
+
from provide.foundation.context import CLIContext
|
22
|
+
from provide.foundation.errors.decorators import with_error_handling
|
21
23
|
from provide.foundation.logger import get_logger
|
22
24
|
|
23
25
|
log = get_logger(__name__)
|
24
26
|
|
25
27
|
|
26
|
-
def _get_context() ->
|
28
|
+
def _get_context() -> CLIContext | None:
|
27
29
|
"""Get current context from Click or environment."""
|
28
30
|
if not _HAS_CLICK:
|
29
31
|
return None
|
30
32
|
ctx = click.get_current_context(silent=True)
|
31
|
-
if ctx and hasattr(ctx, "obj") and isinstance(ctx.obj,
|
33
|
+
if ctx and hasattr(ctx, "obj") and isinstance(ctx.obj, CLIContext):
|
32
34
|
return ctx.obj
|
33
35
|
return None
|
34
36
|
|
35
37
|
|
36
|
-
def _should_use_json(ctx:
|
38
|
+
def _should_use_json(ctx: CLIContext | None = None) -> bool:
|
37
39
|
"""Determine if JSON output should be used."""
|
38
40
|
if ctx is None:
|
39
41
|
ctx = _get_context()
|
40
42
|
return ctx.json_output if ctx else False
|
41
43
|
|
42
44
|
|
43
|
-
def _should_use_color(ctx:
|
45
|
+
def _should_use_color(ctx: CLIContext | None = None, stream: Any = None) -> bool:
|
44
46
|
"""Determine if color output should be used."""
|
45
47
|
if ctx is None:
|
46
48
|
ctx = _get_context()
|
47
49
|
|
50
|
+
# Check FORCE_COLOR first (enables color even for non-TTY)
|
51
|
+
force_color = os.environ.get("FORCE_COLOR", "").lower()
|
52
|
+
if force_color in ("1", "true", "yes"):
|
53
|
+
return True
|
54
|
+
|
55
|
+
# Check NO_COLOR (disables color even for TTY)
|
56
|
+
if os.environ.get("NO_COLOR"):
|
57
|
+
return False
|
58
|
+
|
59
|
+
# Check context no_color setting
|
60
|
+
if ctx and ctx.no_color:
|
61
|
+
return False
|
62
|
+
|
48
63
|
# Check if stream is a TTY
|
49
64
|
if stream:
|
50
65
|
return getattr(stream, "isatty", lambda: False)()
|
@@ -52,19 +67,21 @@ def _should_use_color(ctx: Context | None = None, stream=None) -> bool:
|
|
52
67
|
return sys.stdout.isatty() or sys.stderr.isatty()
|
53
68
|
|
54
69
|
|
55
|
-
|
70
|
+
@with_error_handling(fallback=None, suppress=(TypeError, ValueError, AttributeError))
|
71
|
+
def _output_json(data: Any, stream: Any = sys.stdout) -> None:
|
56
72
|
"""Output data as JSON."""
|
57
|
-
|
58
|
-
|
73
|
+
json_str = json.dumps(data, indent=2, default=str)
|
74
|
+
if _HAS_CLICK:
|
59
75
|
click.echo(json_str, file=stream)
|
60
|
-
|
61
|
-
|
62
|
-
click.echo(
|
63
|
-
json.dumps({"error": f"JSON encoding failed: {e}", "data": str(data)}),
|
64
|
-
file=stream,
|
65
|
-
)
|
76
|
+
else:
|
77
|
+
print(json_str, file=stream)
|
66
78
|
|
67
79
|
|
80
|
+
@with_error_handling(
|
81
|
+
fallback=None,
|
82
|
+
suppress=(OSError, IOError, UnicodeEncodeError),
|
83
|
+
context_provider=lambda: {"function": "pout"},
|
84
|
+
)
|
68
85
|
def pout(message: Any, **kwargs: Any) -> None:
|
69
86
|
"""
|
70
87
|
Output message to stdout.
|
@@ -122,6 +139,11 @@ def pout(message: Any, **kwargs: Any) -> None:
|
|
122
139
|
print(output, file=sys.stdout, end="")
|
123
140
|
|
124
141
|
|
142
|
+
@with_error_handling(
|
143
|
+
fallback=None,
|
144
|
+
suppress=(OSError, IOError, UnicodeEncodeError),
|
145
|
+
context_provider=lambda: {"function": "perr"},
|
146
|
+
)
|
125
147
|
def perr(message: Any, **kwargs: Any) -> None:
|
126
148
|
"""
|
127
149
|
Output message to stderr.
|
@@ -1,12 +1,16 @@
|
|
1
1
|
"""
|
2
2
|
Core context management for provide-foundation.
|
3
3
|
|
4
|
-
Provides
|
5
|
-
|
4
|
+
Provides CLI runtime context for managing command execution state,
|
5
|
+
output formatting, and CLI-specific settings.
|
6
6
|
"""
|
7
7
|
|
8
|
-
from provide.foundation.context.core import
|
8
|
+
from provide.foundation.context.core import CLIContext
|
9
|
+
|
10
|
+
# Backward compatibility
|
11
|
+
Context = CLIContext
|
9
12
|
|
10
13
|
__all__ = [
|
11
|
-
"
|
14
|
+
"CLIContext",
|
15
|
+
"Context", # Backward compatibility
|
12
16
|
]
|
@@ -2,15 +2,15 @@
|
|
2
2
|
|
3
3
|
import copy
|
4
4
|
import json
|
5
|
-
import os
|
6
5
|
from pathlib import Path
|
7
6
|
from typing import Any
|
8
7
|
|
9
8
|
from attrs import define, field, fields, validators
|
10
9
|
|
10
|
+
from provide.foundation.config.base import ConfigSource, field as config_field
|
11
|
+
from provide.foundation.config.converters import parse_bool_strict
|
12
|
+
from provide.foundation.config.env import RuntimeConfig
|
11
13
|
from provide.foundation.logger import get_logger
|
12
|
-
from provide.foundation.logger.config import TelemetryConfig
|
13
|
-
from provide.foundation.utils.parsing import parse_bool
|
14
14
|
|
15
15
|
try:
|
16
16
|
import tomli as tomllib
|
@@ -29,36 +29,68 @@ except ImportError:
|
|
29
29
|
VALID_LOG_LEVELS = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
30
30
|
|
31
31
|
|
32
|
-
@define(slots=True,
|
33
|
-
class
|
32
|
+
@define(slots=True, repr=False)
|
33
|
+
class CLIContext(RuntimeConfig):
|
34
34
|
"""
|
35
|
-
|
35
|
+
Runtime context for CLI execution and state management.
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
and programmatic updates.
|
37
|
+
Manages CLI-specific settings, output formatting, and runtime state
|
38
|
+
during command execution. Supports loading from files, environment variables,
|
39
|
+
and programmatic updates during CLI command execution.
|
40
40
|
"""
|
41
41
|
|
42
|
-
log_level: str =
|
43
|
-
default="INFO",
|
42
|
+
log_level: str = config_field(
|
43
|
+
default="INFO",
|
44
|
+
env_var="PROVIDE_LOG_LEVEL",
|
45
|
+
converter=str.upper,
|
46
|
+
validator=validators.in_(VALID_LOG_LEVELS),
|
47
|
+
description="Logging level for CLI output",
|
44
48
|
)
|
45
|
-
profile: str =
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
+
profile: str = config_field(
|
50
|
+
default="default",
|
51
|
+
env_var="PROVIDE_PROFILE",
|
52
|
+
description="Configuration profile to use",
|
49
53
|
)
|
50
|
-
|
51
|
-
default=
|
54
|
+
debug: bool = config_field(
|
55
|
+
default=False,
|
56
|
+
env_var="PROVIDE_DEBUG",
|
57
|
+
converter=parse_bool_strict,
|
58
|
+
description="Enable debug mode",
|
52
59
|
)
|
53
|
-
|
54
|
-
default=
|
60
|
+
json_output: bool = config_field(
|
61
|
+
default=False,
|
62
|
+
env_var="PROVIDE_JSON_OUTPUT",
|
63
|
+
converter=parse_bool_strict,
|
64
|
+
description="Output in JSON format",
|
55
65
|
)
|
56
|
-
|
57
|
-
|
58
|
-
|
66
|
+
config_file: Path | None = config_field(
|
67
|
+
default=None,
|
68
|
+
env_var="PROVIDE_CONFIG_FILE",
|
69
|
+
converter=lambda x: Path(x) if x else None,
|
70
|
+
description="Path to configuration file",
|
59
71
|
)
|
60
|
-
|
61
|
-
default=
|
72
|
+
log_file: Path | None = config_field(
|
73
|
+
default=None,
|
74
|
+
env_var="PROVIDE_LOG_FILE",
|
75
|
+
converter=lambda x: Path(x) if x else None,
|
76
|
+
description="Path to log file",
|
77
|
+
)
|
78
|
+
log_format: str = config_field(
|
79
|
+
default="key_value",
|
80
|
+
env_var="PROVIDE_LOG_FORMAT",
|
81
|
+
description="Log output format (key_value or json)",
|
82
|
+
)
|
83
|
+
no_color: bool = config_field(
|
84
|
+
default=False,
|
85
|
+
env_var="NO_COLOR",
|
86
|
+
converter=parse_bool_strict,
|
87
|
+
description="Disable colored output",
|
88
|
+
)
|
89
|
+
no_emoji: bool = config_field(
|
90
|
+
default=False,
|
91
|
+
env_var="PROVIDE_NO_EMOJI",
|
92
|
+
converter=parse_bool_strict,
|
93
|
+
description="Disable emoji in output",
|
62
94
|
)
|
63
95
|
|
64
96
|
# Private fields - using Factory for mutable defaults
|
@@ -69,62 +101,9 @@ class Context:
|
|
69
101
|
"""Post-initialization hook."""
|
70
102
|
pass # Validation is handled by attrs validators
|
71
103
|
|
72
|
-
def _validate(self) -> None:
|
73
|
-
"""Validate context values. For attrs compatibility."""
|
74
|
-
# Validation is handled by attrs validators automatically
|
75
|
-
pass
|
76
|
-
|
77
|
-
@classmethod
|
78
|
-
def from_env(cls, prefix: str = "PROVIDE") -> "Context":
|
79
|
-
"""
|
80
|
-
Create context from environment variables using TelemetryConfig system.
|
81
|
-
|
82
|
-
Args:
|
83
|
-
prefix: Environment variable prefix (default: PROVIDE)
|
84
|
-
|
85
|
-
Returns:
|
86
|
-
New Context instance with values from environment
|
87
|
-
"""
|
88
|
-
# Use the main TelemetryConfig system for parsing
|
89
|
-
telemetry_config = TelemetryConfig.from_env(strict=False)
|
90
|
-
|
91
|
-
kwargs = {}
|
92
|
-
|
93
|
-
# Map telemetry config values to CLI context
|
94
|
-
kwargs["log_level"] = telemetry_config.logging.default_level
|
95
|
-
if telemetry_config.logging.console_formatter:
|
96
|
-
kwargs["log_format"] = telemetry_config.logging.console_formatter
|
97
|
-
if telemetry_config.logging.log_file:
|
98
|
-
kwargs["log_file"] = telemetry_config.logging.log_file
|
99
|
-
|
100
|
-
# CLI-specific environment variables that don't exist in TelemetryConfig
|
101
|
-
if profile := os.environ.get(f"{prefix}_PROFILE"):
|
102
|
-
kwargs["profile"] = profile
|
103
|
-
|
104
|
-
if debug := os.environ.get(f"{prefix}_DEBUG"):
|
105
|
-
kwargs["debug"] = debug.lower() in ("true", "1", "yes", "on")
|
106
|
-
|
107
|
-
if json_output := os.environ.get(f"{prefix}_JSON_OUTPUT"):
|
108
|
-
kwargs["json_output"] = json_output.lower() in ("true", "1", "yes", "on")
|
109
|
-
|
110
|
-
if config_file := os.environ.get(f"{prefix}_CONFIG_FILE"):
|
111
|
-
kwargs["config_file"] = Path(config_file)
|
112
|
-
|
113
|
-
# Map emoji settings to no_emoji (inverted)
|
114
|
-
kwargs["no_emoji"] = not (
|
115
|
-
telemetry_config.logging.logger_name_emoji_prefix_enabled
|
116
|
-
and telemetry_config.logging.das_emoji_prefix_enabled
|
117
|
-
)
|
118
|
-
|
119
|
-
# Check for explicit NO_COLOR override
|
120
|
-
if no_color := os.environ.get(f"{prefix}_NO_COLOR"):
|
121
|
-
kwargs["no_color"] = no_color.lower() in ("true", "1", "yes", "on")
|
122
|
-
|
123
|
-
return cls(**kwargs)
|
124
|
-
|
125
104
|
def update_from_env(self, prefix: str = "PROVIDE") -> None:
|
126
105
|
"""
|
127
|
-
Update context from environment variables
|
106
|
+
Update context from environment variables.
|
128
107
|
|
129
108
|
Args:
|
130
109
|
prefix: Environment variable prefix (default: PROVIDE)
|
@@ -132,28 +111,19 @@ class Context:
|
|
132
111
|
if self._frozen:
|
133
112
|
raise RuntimeError("Context is frozen and cannot be modified")
|
134
113
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
self.log_level = env_ctx.log_level
|
139
|
-
self.log_format = env_ctx.log_format
|
140
|
-
if env_ctx.log_file:
|
141
|
-
self.log_file = env_ctx.log_file
|
142
|
-
self.no_emoji = env_ctx.no_emoji
|
143
|
-
|
144
|
-
# Update CLI-specific values only if explicitly set in environment
|
145
|
-
if os.environ.get(f"{prefix}_PROFILE"):
|
146
|
-
self.profile = env_ctx.profile
|
147
|
-
if os.environ.get(f"{prefix}_DEBUG"):
|
148
|
-
self.debug = env_ctx.debug
|
149
|
-
if os.environ.get(f"{prefix}_JSON_OUTPUT"):
|
150
|
-
self.json_output = env_ctx.json_output
|
151
|
-
if os.environ.get(f"{prefix}_CONFIG_FILE"):
|
152
|
-
self.config_file = env_ctx.config_file
|
153
|
-
if os.environ.get(f"{prefix}_NO_COLOR"):
|
154
|
-
self.no_color = env_ctx.no_color
|
114
|
+
# Create default instance and environment instance
|
115
|
+
default_ctx = self.__class__() # All defaults
|
116
|
+
env_ctx = self.from_env(prefix=prefix) # Environment + defaults
|
155
117
|
|
156
|
-
|
118
|
+
# Only update fields where environment differs from default
|
119
|
+
for attr in fields(self.__class__):
|
120
|
+
if not attr.name.startswith("_"): # Skip private fields
|
121
|
+
default_value = getattr(default_ctx, attr.name)
|
122
|
+
env_value = getattr(env_ctx, attr.name)
|
123
|
+
|
124
|
+
# If environment value differs from default, it came from env
|
125
|
+
if env_value != default_value:
|
126
|
+
setattr(self, attr.name, env_value)
|
157
127
|
|
158
128
|
def to_dict(self) -> dict[str, Any]:
|
159
129
|
"""Convert context to dictionary."""
|
@@ -170,15 +140,18 @@ class Context:
|
|
170
140
|
}
|
171
141
|
|
172
142
|
@classmethod
|
173
|
-
def from_dict(
|
143
|
+
def from_dict(
|
144
|
+
cls, data: dict[str, Any], source: ConfigSource = ConfigSource.RUNTIME
|
145
|
+
) -> "CLIContext":
|
174
146
|
"""
|
175
147
|
Create context from dictionary.
|
176
148
|
|
177
149
|
Args:
|
178
150
|
data: Dictionary with context values
|
151
|
+
source: Source of the configuration data
|
179
152
|
|
180
153
|
Returns:
|
181
|
-
New
|
154
|
+
New CLIContext instance
|
182
155
|
"""
|
183
156
|
kwargs = {}
|
184
157
|
|
@@ -212,9 +185,7 @@ class Context:
|
|
212
185
|
Args:
|
213
186
|
path: Path to configuration file
|
214
187
|
"""
|
215
|
-
|
216
|
-
raise RuntimeError("Context is frozen and cannot be modified")
|
217
|
-
|
188
|
+
# CLIContext is not frozen, so we can modify it
|
218
189
|
path = Path(path)
|
219
190
|
if not path.exists():
|
220
191
|
raise FileNotFoundError(f"Config file not found: {path}")
|
@@ -294,16 +265,18 @@ class Context:
|
|
294
265
|
|
295
266
|
path.write_text(content)
|
296
267
|
|
297
|
-
def merge(
|
268
|
+
def merge(
|
269
|
+
self, other: "CLIContext", override_defaults: bool = False
|
270
|
+
) -> "CLIContext":
|
298
271
|
"""
|
299
272
|
Merge with another context, with other taking precedence.
|
300
273
|
|
301
274
|
Args:
|
302
|
-
other:
|
275
|
+
other: CLIContext to merge with
|
303
276
|
override_defaults: If False, only override if other's value differs from its class default
|
304
277
|
|
305
278
|
Returns:
|
306
|
-
New merged
|
279
|
+
New merged CLIContext instance
|
307
280
|
"""
|
308
281
|
merged_data = self.to_dict()
|
309
282
|
other_data = other.to_dict()
|
@@ -318,7 +291,7 @@ class Context:
|
|
318
291
|
from attrs import Factory
|
319
292
|
|
320
293
|
defaults = {}
|
321
|
-
for f in fields(
|
294
|
+
for f in fields(CLIContext):
|
322
295
|
if not f.name.startswith("_"): # Skip private fields
|
323
296
|
if isinstance(f.default, Factory):
|
324
297
|
defaults[f.name] = f.default.factory()
|
@@ -333,7 +306,7 @@ class Context:
|
|
333
306
|
continue
|
334
307
|
merged_data[key] = value
|
335
308
|
|
336
|
-
return
|
309
|
+
return CLIContext.from_dict(merged_data)
|
337
310
|
|
338
311
|
def freeze(self) -> None:
|
339
312
|
"""Freeze context to prevent further modifications."""
|
@@ -341,7 +314,7 @@ class Context:
|
|
341
314
|
# This is kept for API compatibility but does nothing
|
342
315
|
self._frozen = True
|
343
316
|
|
344
|
-
def copy(self) -> "
|
317
|
+
def copy(self) -> "CLIContext":
|
345
318
|
"""Create a deep copy of the context."""
|
346
319
|
return copy.deepcopy(self)
|
347
320
|
|
@@ -354,3 +327,8 @@ class Context:
|
|
354
327
|
profile=self.profile,
|
355
328
|
)
|
356
329
|
return self._logger
|
330
|
+
|
331
|
+
def _validate(self) -> None:
|
332
|
+
"""Validate context values. For attrs compatibility."""
|
333
|
+
# Validation is handled by attrs validators automatically
|
334
|
+
pass
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
# Import from submodules using absolute imports
|
4
4
|
from provide.foundation.crypto.certificates.base import (
|
5
|
+
_HAS_CRYPTO,
|
5
6
|
CertificateBase,
|
6
7
|
CertificateConfig,
|
7
8
|
CertificateError,
|
@@ -9,7 +10,6 @@ from provide.foundation.crypto.certificates.base import (
|
|
9
10
|
KeyPair,
|
10
11
|
KeyType,
|
11
12
|
PublicKey,
|
12
|
-
_HAS_CRYPTO,
|
13
13
|
_require_crypto,
|
14
14
|
)
|
15
15
|
from provide.foundation.crypto.certificates.certificate import Certificate
|
@@ -21,14 +21,18 @@ from provide.foundation.crypto.certificates.operations import (
|
|
21
21
|
|
22
22
|
# Re-export public types - maintaining exact same API
|
23
23
|
__all__ = [
|
24
|
+
"_HAS_CRYPTO", # For testing
|
24
25
|
"Certificate",
|
25
26
|
"CertificateBase",
|
26
27
|
"CertificateConfig",
|
27
28
|
"CertificateError",
|
28
29
|
"CurveType",
|
30
|
+
"KeyPair",
|
29
31
|
"KeyType",
|
30
|
-
"
|
31
|
-
"create_ca",
|
32
|
-
"_HAS_CRYPTO", # For testing
|
32
|
+
"PublicKey",
|
33
33
|
"_require_crypto", # For testing
|
34
|
-
|
34
|
+
"create_ca",
|
35
|
+
"create_self_signed",
|
36
|
+
"create_x509_certificate",
|
37
|
+
"validate_signature",
|
38
|
+
]
|
@@ -5,7 +5,7 @@ from enum import StrEnum, auto
|
|
5
5
|
import traceback
|
6
6
|
from typing import NotRequired, Self, TypeAlias, TypedDict
|
7
7
|
|
8
|
-
from attrs import define
|
8
|
+
from attrs import define
|
9
9
|
|
10
10
|
try:
|
11
11
|
from cryptography import x509
|
@@ -170,4 +170,4 @@ class CertificateBase:
|
|
170
170
|
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
|
171
171
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, org),
|
172
172
|
]
|
173
|
-
)
|
173
|
+
)
|