provide-foundation 0.0.0.dev0__py3-none-any.whl → 0.0.0.dev2__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 +41 -23
- provide/foundation/archive/__init__.py +23 -0
- provide/foundation/archive/base.py +70 -0
- provide/foundation/archive/bzip2.py +157 -0
- provide/foundation/archive/gzip.py +159 -0
- provide/foundation/archive/operations.py +334 -0
- provide/foundation/archive/tar.py +164 -0
- provide/foundation/archive/zip.py +203 -0
- provide/foundation/cli/__init__.py +2 -2
- provide/foundation/cli/commands/deps.py +13 -7
- provide/foundation/cli/commands/logs/__init__.py +1 -1
- provide/foundation/cli/commands/logs/query.py +1 -1
- provide/foundation/cli/commands/logs/send.py +1 -1
- provide/foundation/cli/commands/logs/tail.py +1 -1
- provide/foundation/cli/decorators.py +11 -10
- provide/foundation/cli/main.py +1 -1
- provide/foundation/cli/testing.py +2 -35
- provide/foundation/cli/utils.py +21 -17
- provide/foundation/config/__init__.py +35 -2
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +479 -0
- provide/foundation/config/defaults.py +67 -0
- provide/foundation/config/env.py +4 -19
- provide/foundation/config/loader.py +9 -3
- provide/foundation/config/sync.py +19 -4
- provide/foundation/console/input.py +5 -5
- provide/foundation/console/output.py +35 -13
- provide/foundation/context/__init__.py +8 -4
- provide/foundation/context/core.py +85 -109
- provide/foundation/core.py +1 -2
- provide/foundation/crypto/__init__.py +2 -0
- provide/foundation/crypto/certificates/__init__.py +34 -0
- provide/foundation/crypto/certificates/base.py +173 -0
- provide/foundation/crypto/certificates/certificate.py +290 -0
- provide/foundation/crypto/certificates/factory.py +213 -0
- provide/foundation/crypto/certificates/generator.py +138 -0
- provide/foundation/crypto/certificates/loader.py +130 -0
- provide/foundation/crypto/certificates/operations.py +198 -0
- provide/foundation/crypto/certificates/trust.py +107 -0
- provide/foundation/errors/__init__.py +2 -3
- provide/foundation/errors/decorators.py +0 -231
- provide/foundation/errors/types.py +0 -97
- provide/foundation/eventsets/__init__.py +0 -0
- provide/foundation/eventsets/display.py +84 -0
- provide/foundation/eventsets/registry.py +160 -0
- provide/foundation/eventsets/resolver.py +192 -0
- provide/foundation/eventsets/sets/das.py +128 -0
- provide/foundation/eventsets/sets/database.py +125 -0
- provide/foundation/eventsets/sets/http.py +153 -0
- provide/foundation/eventsets/sets/llm.py +139 -0
- provide/foundation/eventsets/sets/task_queue.py +107 -0
- provide/foundation/eventsets/types.py +70 -0
- provide/foundation/file/directory.py +13 -22
- provide/foundation/file/lock.py +3 -1
- provide/foundation/hub/components.py +77 -515
- provide/foundation/hub/config.py +151 -0
- provide/foundation/hub/discovery.py +62 -0
- provide/foundation/hub/handlers.py +81 -0
- provide/foundation/hub/lifecycle.py +194 -0
- provide/foundation/hub/manager.py +4 -4
- provide/foundation/hub/processors.py +44 -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 +12 -12
- provide/foundation/{observability → integrations}/openobserve/commands.py +3 -3
- provide/foundation/integrations/openobserve/config.py +37 -0
- provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/otlp.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/search.py +2 -2
- provide/foundation/{observability → integrations}/openobserve/streaming.py +4 -4
- provide/foundation/logger/__init__.py +3 -10
- provide/foundation/logger/config/logging.py +68 -298
- provide/foundation/logger/config/telemetry.py +41 -121
- provide/foundation/logger/core.py +0 -2
- provide/foundation/logger/custom_processors.py +1 -0
- provide/foundation/logger/factories.py +11 -2
- provide/foundation/logger/processors/main.py +20 -84
- provide/foundation/logger/setup/__init__.py +5 -1
- provide/foundation/logger/setup/coordinator.py +76 -24
- provide/foundation/logger/setup/processors.py +2 -9
- provide/foundation/logger/trace.py +27 -0
- provide/foundation/metrics/otel.py +10 -10
- provide/foundation/observability/__init__.py +2 -2
- provide/foundation/process/__init__.py +9 -0
- provide/foundation/process/exit.py +47 -0
- provide/foundation/process/lifecycle.py +115 -59
- provide/foundation/resilience/__init__.py +35 -0
- provide/foundation/resilience/circuit.py +164 -0
- provide/foundation/resilience/decorators.py +220 -0
- provide/foundation/resilience/fallback.py +193 -0
- provide/foundation/resilience/retry.py +325 -0
- provide/foundation/streams/config.py +79 -0
- provide/foundation/streams/console.py +7 -8
- provide/foundation/streams/core.py +6 -3
- provide/foundation/streams/file.py +12 -2
- provide/foundation/testing/__init__.py +84 -2
- provide/foundation/testing/archive/__init__.py +24 -0
- provide/foundation/testing/archive/fixtures.py +217 -0
- provide/foundation/testing/cli.py +30 -17
- provide/foundation/testing/common/__init__.py +32 -0
- provide/foundation/testing/common/fixtures.py +236 -0
- provide/foundation/testing/file/__init__.py +40 -0
- provide/foundation/testing/file/content_fixtures.py +316 -0
- provide/foundation/testing/file/directory_fixtures.py +107 -0
- provide/foundation/testing/file/fixtures.py +52 -0
- provide/foundation/testing/file/special_fixtures.py +153 -0
- provide/foundation/testing/logger.py +117 -11
- provide/foundation/testing/mocking/__init__.py +46 -0
- provide/foundation/testing/mocking/fixtures.py +331 -0
- provide/foundation/testing/process/__init__.py +48 -0
- provide/foundation/testing/process/async_fixtures.py +405 -0
- provide/foundation/testing/process/fixtures.py +56 -0
- provide/foundation/testing/process/subprocess_fixtures.py +209 -0
- provide/foundation/testing/threading/__init__.py +38 -0
- provide/foundation/testing/threading/basic_fixtures.py +101 -0
- provide/foundation/testing/threading/data_fixtures.py +99 -0
- provide/foundation/testing/threading/execution_fixtures.py +263 -0
- provide/foundation/testing/threading/fixtures.py +54 -0
- provide/foundation/testing/threading/sync_fixtures.py +97 -0
- provide/foundation/testing/time/__init__.py +32 -0
- provide/foundation/testing/time/fixtures.py +409 -0
- provide/foundation/testing/transport/__init__.py +30 -0
- provide/foundation/testing/transport/fixtures.py +280 -0
- provide/foundation/tools/__init__.py +58 -0
- provide/foundation/tools/base.py +348 -0
- provide/foundation/tools/cache.py +268 -0
- provide/foundation/tools/downloader.py +224 -0
- provide/foundation/tools/installer.py +254 -0
- provide/foundation/tools/registry.py +223 -0
- provide/foundation/tools/resolver.py +321 -0
- provide/foundation/tools/verifier.py +186 -0
- provide/foundation/tracer/otel.py +7 -11
- provide/foundation/tracer/spans.py +2 -2
- provide/foundation/transport/__init__.py +155 -0
- provide/foundation/transport/base.py +171 -0
- provide/foundation/transport/client.py +266 -0
- provide/foundation/transport/config.py +140 -0
- provide/foundation/transport/errors.py +79 -0
- provide/foundation/transport/http.py +232 -0
- provide/foundation/transport/middleware.py +360 -0
- provide/foundation/transport/registry.py +167 -0
- provide/foundation/transport/types.py +45 -0
- provide/foundation/utils/deps.py +14 -12
- provide/foundation/utils/parsing.py +49 -4
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/METADATA +5 -28
- provide_foundation-0.0.0.dev2.dist-info/RECORD +225 -0
- provide/foundation/cli/commands/logs/generate_old.py +0 -569
- provide/foundation/crypto/certificates.py +0 -896
- provide/foundation/logger/emoji/__init__.py +0 -44
- provide/foundation/logger/emoji/matrix.py +0 -209
- provide/foundation/logger/emoji/sets.py +0 -458
- provide/foundation/logger/emoji/types.py +0 -56
- provide/foundation/logger/setup/emoji_resolver.py +0 -64
- provide_foundation-0.0.0.dev0.dist-info/RECORD +0 -149
- /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
- /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/top_level.txt +0 -0
@@ -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=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
|
|
70
|
+
@with_error_handling(fallback=None, suppress=(TypeError, ValueError, AttributeError))
|
55
71
|
def _output_json(data: Any, stream=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
|
]
|
@@ -8,9 +8,11 @@ from typing import Any
|
|
8
8
|
|
9
9
|
from attrs import define, field, fields, validators
|
10
10
|
|
11
|
+
from provide.foundation.config.env import RuntimeConfig
|
12
|
+
from provide.foundation.config.base import field as config_field, ConfigSource
|
13
|
+
from provide.foundation.config.converters import parse_bool_strict
|
11
14
|
from provide.foundation.logger import get_logger
|
12
15
|
from provide.foundation.logger.config import TelemetryConfig
|
13
|
-
from provide.foundation.utils.parsing import parse_bool
|
14
16
|
|
15
17
|
try:
|
16
18
|
import tomli as tomllib
|
@@ -29,36 +31,68 @@ except ImportError:
|
|
29
31
|
VALID_LOG_LEVELS = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
30
32
|
|
31
33
|
|
32
|
-
@define(slots=True,
|
33
|
-
class
|
34
|
+
@define(slots=True, repr=False)
|
35
|
+
class CLIContext(RuntimeConfig):
|
34
36
|
"""
|
35
|
-
|
37
|
+
Runtime context for CLI execution and state management.
|
36
38
|
|
37
|
-
|
38
|
-
|
39
|
-
and programmatic updates.
|
39
|
+
Manages CLI-specific settings, output formatting, and runtime state
|
40
|
+
during command execution. Supports loading from files, environment variables,
|
41
|
+
and programmatic updates during CLI command execution.
|
40
42
|
"""
|
41
43
|
|
42
|
-
log_level: str =
|
43
|
-
default="INFO",
|
44
|
+
log_level: str = config_field(
|
45
|
+
default="INFO",
|
46
|
+
env_var="PROVIDE_LOG_LEVEL",
|
47
|
+
converter=str.upper,
|
48
|
+
validator=validators.in_(VALID_LOG_LEVELS),
|
49
|
+
description="Logging level for CLI output"
|
44
50
|
)
|
45
|
-
profile: str =
|
46
|
-
|
47
|
-
|
48
|
-
|
51
|
+
profile: str = config_field(
|
52
|
+
default="default",
|
53
|
+
env_var="PROVIDE_PROFILE",
|
54
|
+
description="Configuration profile to use"
|
49
55
|
)
|
50
|
-
|
51
|
-
default=
|
56
|
+
debug: bool = config_field(
|
57
|
+
default=False,
|
58
|
+
env_var="PROVIDE_DEBUG",
|
59
|
+
converter=parse_bool_strict,
|
60
|
+
description="Enable debug mode"
|
52
61
|
)
|
53
|
-
|
54
|
-
default=
|
62
|
+
json_output: bool = config_field(
|
63
|
+
default=False,
|
64
|
+
env_var="PROVIDE_JSON_OUTPUT",
|
65
|
+
converter=parse_bool_strict,
|
66
|
+
description="Output in JSON format"
|
55
67
|
)
|
56
|
-
|
57
|
-
|
58
|
-
|
68
|
+
config_file: Path | None = config_field(
|
69
|
+
default=None,
|
70
|
+
env_var="PROVIDE_CONFIG_FILE",
|
71
|
+
converter=lambda x: Path(x) if x else None,
|
72
|
+
description="Path to configuration file"
|
59
73
|
)
|
60
|
-
|
61
|
-
default=
|
74
|
+
log_file: Path | None = config_field(
|
75
|
+
default=None,
|
76
|
+
env_var="PROVIDE_LOG_FILE",
|
77
|
+
converter=lambda x: Path(x) if x else None,
|
78
|
+
description="Path to log file"
|
79
|
+
)
|
80
|
+
log_format: str = config_field(
|
81
|
+
default="key_value",
|
82
|
+
env_var="PROVIDE_LOG_FORMAT",
|
83
|
+
description="Log output format (key_value or json)"
|
84
|
+
)
|
85
|
+
no_color: bool = config_field(
|
86
|
+
default=False,
|
87
|
+
env_var="NO_COLOR",
|
88
|
+
converter=parse_bool_strict,
|
89
|
+
description="Disable colored output"
|
90
|
+
)
|
91
|
+
no_emoji: bool = config_field(
|
92
|
+
default=False,
|
93
|
+
env_var="PROVIDE_NO_EMOJI",
|
94
|
+
converter=parse_bool_strict,
|
95
|
+
description="Disable emoji in output"
|
62
96
|
)
|
63
97
|
|
64
98
|
# Private fields - using Factory for mutable defaults
|
@@ -69,62 +103,9 @@ class Context:
|
|
69
103
|
"""Post-initialization hook."""
|
70
104
|
pass # Validation is handled by attrs validators
|
71
105
|
|
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
106
|
def update_from_env(self, prefix: str = "PROVIDE") -> None:
|
126
107
|
"""
|
127
|
-
Update context from environment variables
|
108
|
+
Update context from environment variables.
|
128
109
|
|
129
110
|
Args:
|
130
111
|
prefix: Environment variable prefix (default: PROVIDE)
|
@@ -132,28 +113,19 @@ class Context:
|
|
132
113
|
if self._frozen:
|
133
114
|
raise RuntimeError("Context is frozen and cannot be modified")
|
134
115
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
155
|
-
|
156
|
-
self._validate()
|
116
|
+
# Create default instance and environment instance
|
117
|
+
default_ctx = self.__class__() # All defaults
|
118
|
+
env_ctx = self.from_env(prefix=prefix) # Environment + defaults
|
119
|
+
|
120
|
+
# Only update fields where environment differs from default
|
121
|
+
for attr in fields(self.__class__):
|
122
|
+
if not attr.name.startswith("_"): # Skip private fields
|
123
|
+
default_value = getattr(default_ctx, attr.name)
|
124
|
+
env_value = getattr(env_ctx, attr.name)
|
125
|
+
|
126
|
+
# If environment value differs from default, it came from env
|
127
|
+
if env_value != default_value:
|
128
|
+
setattr(self, attr.name, env_value)
|
157
129
|
|
158
130
|
def to_dict(self) -> dict[str, Any]:
|
159
131
|
"""Convert context to dictionary."""
|
@@ -170,15 +142,16 @@ class Context:
|
|
170
142
|
}
|
171
143
|
|
172
144
|
@classmethod
|
173
|
-
def from_dict(cls, data: dict[str, Any]) -> "
|
145
|
+
def from_dict(cls, data: dict[str, Any], source: ConfigSource = ConfigSource.RUNTIME) -> "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,16 @@ class Context:
|
|
294
265
|
|
295
266
|
path.write_text(content)
|
296
267
|
|
297
|
-
def merge(self, other: "
|
268
|
+
def merge(self, other: "CLIContext", override_defaults: bool = False) -> "CLIContext":
|
298
269
|
"""
|
299
270
|
Merge with another context, with other taking precedence.
|
300
271
|
|
301
272
|
Args:
|
302
|
-
other:
|
273
|
+
other: CLIContext to merge with
|
303
274
|
override_defaults: If False, only override if other's value differs from its class default
|
304
275
|
|
305
276
|
Returns:
|
306
|
-
New merged
|
277
|
+
New merged CLIContext instance
|
307
278
|
"""
|
308
279
|
merged_data = self.to_dict()
|
309
280
|
other_data = other.to_dict()
|
@@ -318,7 +289,7 @@ class Context:
|
|
318
289
|
from attrs import Factory
|
319
290
|
|
320
291
|
defaults = {}
|
321
|
-
for f in fields(
|
292
|
+
for f in fields(CLIContext):
|
322
293
|
if not f.name.startswith("_"): # Skip private fields
|
323
294
|
if isinstance(f.default, Factory):
|
324
295
|
defaults[f.name] = f.default.factory()
|
@@ -333,7 +304,7 @@ class Context:
|
|
333
304
|
continue
|
334
305
|
merged_data[key] = value
|
335
306
|
|
336
|
-
return
|
307
|
+
return CLIContext.from_dict(merged_data)
|
337
308
|
|
338
309
|
def freeze(self) -> None:
|
339
310
|
"""Freeze context to prevent further modifications."""
|
@@ -341,7 +312,7 @@ class Context:
|
|
341
312
|
# This is kept for API compatibility but does nothing
|
342
313
|
self._frozen = True
|
343
314
|
|
344
|
-
def copy(self) -> "
|
315
|
+
def copy(self) -> "CLIContext":
|
345
316
|
"""Create a deep copy of the context."""
|
346
317
|
return copy.deepcopy(self)
|
347
318
|
|
@@ -354,3 +325,8 @@ class Context:
|
|
354
325
|
profile=self.profile,
|
355
326
|
)
|
356
327
|
return self._logger
|
328
|
+
|
329
|
+
def _validate(self) -> None:
|
330
|
+
"""Validate context values. For attrs compatibility."""
|
331
|
+
# Validation is handled by attrs validators automatically
|
332
|
+
pass
|
provide/foundation/core.py
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
Foundation Telemetry Core Setup Functions.
|
6
6
|
"""
|
7
7
|
|
8
|
-
|
8
|
+
# Emoji resolver removed - using event sets now
|
9
9
|
from provide.foundation.setup import (
|
10
10
|
reset_foundation_setup_for_testing,
|
11
11
|
setup_telemetry,
|
@@ -13,7 +13,6 @@ from provide.foundation.setup import (
|
|
13
13
|
)
|
14
14
|
|
15
15
|
__all__ = [
|
16
|
-
"ResolvedEmojiConfig",
|
17
16
|
"reset_foundation_setup_for_testing",
|
18
17
|
"setup_telemetry",
|
19
18
|
"shutdown_foundation_telemetry",
|
@@ -0,0 +1,34 @@
|
|
1
|
+
"""X.509 certificate generation and management."""
|
2
|
+
|
3
|
+
# Import from submodules using absolute imports
|
4
|
+
from provide.foundation.crypto.certificates.base import (
|
5
|
+
CertificateBase,
|
6
|
+
CertificateConfig,
|
7
|
+
CertificateError,
|
8
|
+
CurveType,
|
9
|
+
KeyPair,
|
10
|
+
KeyType,
|
11
|
+
PublicKey,
|
12
|
+
_HAS_CRYPTO,
|
13
|
+
_require_crypto,
|
14
|
+
)
|
15
|
+
from provide.foundation.crypto.certificates.certificate import Certificate
|
16
|
+
from provide.foundation.crypto.certificates.factory import create_ca, create_self_signed
|
17
|
+
from provide.foundation.crypto.certificates.operations import (
|
18
|
+
create_x509_certificate,
|
19
|
+
validate_signature,
|
20
|
+
)
|
21
|
+
|
22
|
+
# Re-export public types - maintaining exact same API
|
23
|
+
__all__ = [
|
24
|
+
"Certificate",
|
25
|
+
"CertificateBase",
|
26
|
+
"CertificateConfig",
|
27
|
+
"CertificateError",
|
28
|
+
"CurveType",
|
29
|
+
"KeyType",
|
30
|
+
"create_self_signed",
|
31
|
+
"create_ca",
|
32
|
+
"_HAS_CRYPTO", # For testing
|
33
|
+
"_require_crypto", # For testing
|
34
|
+
]
|
@@ -0,0 +1,173 @@
|
|
1
|
+
"""Certificate base classes, types, and utilities."""
|
2
|
+
|
3
|
+
from datetime import UTC, datetime
|
4
|
+
from enum import StrEnum, auto
|
5
|
+
import traceback
|
6
|
+
from typing import NotRequired, Self, TypeAlias, TypedDict
|
7
|
+
|
8
|
+
from attrs import define, field
|
9
|
+
|
10
|
+
try:
|
11
|
+
from cryptography import x509
|
12
|
+
from cryptography.hazmat.backends import default_backend
|
13
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
14
|
+
from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
15
|
+
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
16
|
+
from cryptography.x509 import Certificate as X509Certificate
|
17
|
+
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
|
18
|
+
|
19
|
+
_HAS_CRYPTO = True
|
20
|
+
except ImportError:
|
21
|
+
# Stub out cryptography types for type hints
|
22
|
+
x509 = None
|
23
|
+
default_backend = None
|
24
|
+
hashes = None
|
25
|
+
serialization = None
|
26
|
+
ec = None
|
27
|
+
rsa = None
|
28
|
+
load_pem_private_key = None
|
29
|
+
X509Certificate = None
|
30
|
+
ExtendedKeyUsageOID = None
|
31
|
+
NameOID = None
|
32
|
+
_HAS_CRYPTO = False
|
33
|
+
|
34
|
+
from provide.foundation import logger
|
35
|
+
from provide.foundation.crypto.constants import (
|
36
|
+
DEFAULT_RSA_KEY_SIZE,
|
37
|
+
)
|
38
|
+
from provide.foundation.errors.config import ValidationError
|
39
|
+
|
40
|
+
|
41
|
+
def _require_crypto():
|
42
|
+
"""Ensure cryptography is available for crypto operations."""
|
43
|
+
if not _HAS_CRYPTO:
|
44
|
+
raise ImportError(
|
45
|
+
"Cryptography features require optional dependencies. Install with: "
|
46
|
+
"pip install 'provide-foundation[crypto]'"
|
47
|
+
)
|
48
|
+
|
49
|
+
|
50
|
+
class CertificateError(ValidationError):
|
51
|
+
"""Certificate-related errors."""
|
52
|
+
|
53
|
+
def __init__(self, message: str, hint: str | None = None) -> None:
|
54
|
+
super().__init__(
|
55
|
+
message=message,
|
56
|
+
field="certificate",
|
57
|
+
value=None,
|
58
|
+
rule=hint or "Certificate operation failed",
|
59
|
+
)
|
60
|
+
|
61
|
+
|
62
|
+
class KeyType(StrEnum):
|
63
|
+
RSA = auto()
|
64
|
+
ECDSA = auto()
|
65
|
+
|
66
|
+
|
67
|
+
class CurveType(StrEnum):
|
68
|
+
SECP256R1 = auto()
|
69
|
+
SECP384R1 = auto()
|
70
|
+
SECP521R1 = auto()
|
71
|
+
|
72
|
+
|
73
|
+
class CertificateConfig(TypedDict):
|
74
|
+
common_name: str
|
75
|
+
organization: str
|
76
|
+
alt_names: list[str]
|
77
|
+
key_type: KeyType
|
78
|
+
not_valid_before: datetime
|
79
|
+
not_valid_after: datetime
|
80
|
+
# Optional key generation parameters
|
81
|
+
key_size: NotRequired[int]
|
82
|
+
curve: NotRequired[CurveType]
|
83
|
+
|
84
|
+
|
85
|
+
if _HAS_CRYPTO:
|
86
|
+
KeyPair: TypeAlias = rsa.RSAPrivateKey | ec.EllipticCurvePrivateKey
|
87
|
+
PublicKey: TypeAlias = rsa.RSAPublicKey | ec.EllipticCurvePublicKey
|
88
|
+
else:
|
89
|
+
KeyPair: TypeAlias = None
|
90
|
+
PublicKey: TypeAlias = None
|
91
|
+
|
92
|
+
|
93
|
+
@define(slots=True, frozen=True)
|
94
|
+
class CertificateBase:
|
95
|
+
"""Immutable base certificate data."""
|
96
|
+
|
97
|
+
subject: "x509.Name"
|
98
|
+
issuer: "x509.Name"
|
99
|
+
public_key: "PublicKey"
|
100
|
+
not_valid_before: datetime
|
101
|
+
not_valid_after: datetime
|
102
|
+
serial_number: int
|
103
|
+
|
104
|
+
@classmethod
|
105
|
+
def create(cls, config: CertificateConfig) -> tuple[Self, "KeyPair"]:
|
106
|
+
"""Create a new certificate base and private key."""
|
107
|
+
_require_crypto()
|
108
|
+
try:
|
109
|
+
logger.debug("📜📝🚀 CertificateBase.create: Starting base creation")
|
110
|
+
not_valid_before = config["not_valid_before"]
|
111
|
+
not_valid_after = config["not_valid_after"]
|
112
|
+
|
113
|
+
if not_valid_before.tzinfo is None:
|
114
|
+
not_valid_before = not_valid_before.replace(tzinfo=UTC)
|
115
|
+
if not_valid_after.tzinfo is None:
|
116
|
+
not_valid_after = not_valid_after.replace(tzinfo=UTC)
|
117
|
+
|
118
|
+
logger.debug(
|
119
|
+
f"📜⏳✅ CertificateBase.create: Using validity: "
|
120
|
+
f"{not_valid_before} to {not_valid_after}"
|
121
|
+
)
|
122
|
+
|
123
|
+
private_key: KeyPair
|
124
|
+
match config["key_type"]:
|
125
|
+
case KeyType.RSA:
|
126
|
+
key_size = config.get("key_size", DEFAULT_RSA_KEY_SIZE)
|
127
|
+
logger.debug(f"📜🔑🚀 Generating RSA key (size: {key_size})")
|
128
|
+
private_key = rsa.generate_private_key(
|
129
|
+
public_exponent=65537, key_size=key_size
|
130
|
+
)
|
131
|
+
case KeyType.ECDSA:
|
132
|
+
curve_choice = config.get("curve", CurveType.SECP384R1)
|
133
|
+
logger.debug(f"📜🔑🚀 Generating ECDSA key (curve: {curve_choice})")
|
134
|
+
curve = getattr(ec, curve_choice.name)()
|
135
|
+
private_key = ec.generate_private_key(curve)
|
136
|
+
case _:
|
137
|
+
raise ValueError(
|
138
|
+
f"Internal Error: Unsupported key type: {config['key_type']}"
|
139
|
+
)
|
140
|
+
|
141
|
+
subject = cls._create_name(config["common_name"], config["organization"])
|
142
|
+
issuer = cls._create_name(config["common_name"], config["organization"])
|
143
|
+
|
144
|
+
serial_number = x509.random_serial_number()
|
145
|
+
logger.debug(f"📜🔑✅ Generated serial number: {serial_number}")
|
146
|
+
|
147
|
+
base = cls(
|
148
|
+
subject=subject,
|
149
|
+
issuer=issuer,
|
150
|
+
public_key=private_key.public_key(),
|
151
|
+
not_valid_before=not_valid_before,
|
152
|
+
not_valid_after=not_valid_after,
|
153
|
+
serial_number=serial_number,
|
154
|
+
)
|
155
|
+
logger.debug("📜📝✅ CertificateBase.create: Base creation complete")
|
156
|
+
return base, private_key
|
157
|
+
|
158
|
+
except Exception as e:
|
159
|
+
logger.error(
|
160
|
+
f"📜❌ CertificateBase.create: Failed: {e}",
|
161
|
+
extra={"error": str(e), "trace": traceback.format_exc()},
|
162
|
+
)
|
163
|
+
raise CertificateError(f"Failed to generate certificate base: {e}") from e
|
164
|
+
|
165
|
+
@staticmethod
|
166
|
+
def _create_name(common_name: str, org: str) -> "x509.Name":
|
167
|
+
"""Helper method to construct an X.509 name."""
|
168
|
+
return x509.Name(
|
169
|
+
[
|
170
|
+
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
|
171
|
+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, org),
|
172
|
+
]
|
173
|
+
)
|