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,356 @@
|
|
1
|
+
"""Unified context for configuration and CLI state management."""
|
2
|
+
|
3
|
+
import copy
|
4
|
+
import json
|
5
|
+
import os
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import Any
|
8
|
+
|
9
|
+
from attrs import define, field, fields, validators
|
10
|
+
|
11
|
+
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
|
+
|
15
|
+
try:
|
16
|
+
import tomli as tomllib
|
17
|
+
except ImportError:
|
18
|
+
try:
|
19
|
+
import tomllib
|
20
|
+
except ImportError:
|
21
|
+
tomllib = None
|
22
|
+
|
23
|
+
try:
|
24
|
+
import yaml
|
25
|
+
except ImportError:
|
26
|
+
yaml = None
|
27
|
+
|
28
|
+
|
29
|
+
VALID_LOG_LEVELS = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
30
|
+
|
31
|
+
|
32
|
+
@define(slots=True, frozen=False)
|
33
|
+
class Context:
|
34
|
+
"""
|
35
|
+
Unified context for configuration and CLI state.
|
36
|
+
|
37
|
+
Combines configuration management with runtime state for CLI tools
|
38
|
+
and services. Supports loading from files, environment variables,
|
39
|
+
and programmatic updates.
|
40
|
+
"""
|
41
|
+
|
42
|
+
log_level: str = field(
|
43
|
+
default="INFO", validator=validators.in_(VALID_LOG_LEVELS), converter=str.upper
|
44
|
+
)
|
45
|
+
profile: str = field(default="default")
|
46
|
+
debug: bool = field(default=False, converter=lambda x: parse_bool(x, strict=True))
|
47
|
+
json_output: bool = field(
|
48
|
+
default=False, converter=lambda x: parse_bool(x, strict=True)
|
49
|
+
)
|
50
|
+
config_file: Path | None = field(
|
51
|
+
default=None, converter=lambda x: Path(x) if x else None
|
52
|
+
)
|
53
|
+
log_file: Path | None = field(
|
54
|
+
default=None, converter=lambda x: Path(x) if x else None
|
55
|
+
)
|
56
|
+
log_format: str = field(default="key_value")
|
57
|
+
no_color: bool = field(
|
58
|
+
default=False, converter=lambda x: parse_bool(x, strict=True)
|
59
|
+
)
|
60
|
+
no_emoji: bool = field(
|
61
|
+
default=False, converter=lambda x: parse_bool(x, strict=True)
|
62
|
+
)
|
63
|
+
|
64
|
+
# Private fields - using Factory for mutable defaults
|
65
|
+
_logger: Any = field(init=False, factory=lambda: None, repr=False)
|
66
|
+
_frozen: bool = field(init=False, default=False, repr=False)
|
67
|
+
|
68
|
+
def __attrs_post_init__(self) -> None:
|
69
|
+
"""Post-initialization hook."""
|
70
|
+
pass # Validation is handled by attrs validators
|
71
|
+
|
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
|
+
def update_from_env(self, prefix: str = "PROVIDE") -> None:
|
126
|
+
"""
|
127
|
+
Update context from environment variables using TelemetryConfig system.
|
128
|
+
|
129
|
+
Args:
|
130
|
+
prefix: Environment variable prefix (default: PROVIDE)
|
131
|
+
"""
|
132
|
+
if self._frozen:
|
133
|
+
raise RuntimeError("Context is frozen and cannot be modified")
|
134
|
+
|
135
|
+
env_ctx = self.from_env(prefix)
|
136
|
+
|
137
|
+
# Update values from TelemetryConfig (these are always updated since they're the primary source)
|
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
|
155
|
+
|
156
|
+
self._validate()
|
157
|
+
|
158
|
+
def to_dict(self) -> dict[str, Any]:
|
159
|
+
"""Convert context to dictionary."""
|
160
|
+
return {
|
161
|
+
"log_level": self.log_level,
|
162
|
+
"profile": self.profile,
|
163
|
+
"debug": self.debug,
|
164
|
+
"json_output": self.json_output,
|
165
|
+
"config_file": str(self.config_file) if self.config_file else None,
|
166
|
+
"log_file": str(self.log_file) if self.log_file else None,
|
167
|
+
"log_format": self.log_format,
|
168
|
+
"no_color": self.no_color,
|
169
|
+
"no_emoji": self.no_emoji,
|
170
|
+
}
|
171
|
+
|
172
|
+
@classmethod
|
173
|
+
def from_dict(cls, data: dict[str, Any]) -> "Context":
|
174
|
+
"""
|
175
|
+
Create context from dictionary.
|
176
|
+
|
177
|
+
Args:
|
178
|
+
data: Dictionary with context values
|
179
|
+
|
180
|
+
Returns:
|
181
|
+
New Context instance
|
182
|
+
"""
|
183
|
+
kwargs = {}
|
184
|
+
|
185
|
+
if "log_level" in data:
|
186
|
+
kwargs["log_level"] = data["log_level"]
|
187
|
+
if "profile" in data:
|
188
|
+
kwargs["profile"] = data["profile"]
|
189
|
+
if "debug" in data:
|
190
|
+
kwargs["debug"] = data["debug"]
|
191
|
+
if "json_output" in data:
|
192
|
+
kwargs["json_output"] = data["json_output"]
|
193
|
+
if data.get("config_file"):
|
194
|
+
kwargs["config_file"] = Path(data["config_file"])
|
195
|
+
if data.get("log_file"):
|
196
|
+
kwargs["log_file"] = Path(data["log_file"])
|
197
|
+
if "log_format" in data:
|
198
|
+
kwargs["log_format"] = data["log_format"]
|
199
|
+
if "no_color" in data:
|
200
|
+
kwargs["no_color"] = data["no_color"]
|
201
|
+
if "no_emoji" in data:
|
202
|
+
kwargs["no_emoji"] = data["no_emoji"]
|
203
|
+
|
204
|
+
return cls(**kwargs)
|
205
|
+
|
206
|
+
def load_config(self, path: str | Path) -> None:
|
207
|
+
"""
|
208
|
+
Load configuration from file.
|
209
|
+
|
210
|
+
Supports TOML, JSON, and YAML formats based on file extension.
|
211
|
+
|
212
|
+
Args:
|
213
|
+
path: Path to configuration file
|
214
|
+
"""
|
215
|
+
if self._frozen:
|
216
|
+
raise RuntimeError("Context is frozen and cannot be modified")
|
217
|
+
|
218
|
+
path = Path(path)
|
219
|
+
if not path.exists():
|
220
|
+
raise FileNotFoundError(f"Config file not found: {path}")
|
221
|
+
|
222
|
+
content = path.read_text()
|
223
|
+
|
224
|
+
if path.suffix in (".toml", ".tml"):
|
225
|
+
if tomllib is None:
|
226
|
+
raise ImportError("tomli/tomllib required for TOML support")
|
227
|
+
data = tomllib.loads(content)
|
228
|
+
elif path.suffix == ".json":
|
229
|
+
data = json.loads(content)
|
230
|
+
elif path.suffix in (".yaml", ".yml"):
|
231
|
+
if yaml is None:
|
232
|
+
raise ImportError("PyYAML required for YAML support")
|
233
|
+
data = yaml.safe_load(content)
|
234
|
+
else:
|
235
|
+
raise ValueError(f"Unsupported config format: {path.suffix}")
|
236
|
+
|
237
|
+
# Update context from loaded data
|
238
|
+
if "log_level" in data:
|
239
|
+
self.log_level = data["log_level"]
|
240
|
+
if "profile" in data:
|
241
|
+
self.profile = data["profile"]
|
242
|
+
if "debug" in data:
|
243
|
+
self.debug = data["debug"]
|
244
|
+
if "json_output" in data:
|
245
|
+
self.json_output = data["json_output"]
|
246
|
+
if data.get("config_file"):
|
247
|
+
self.config_file = Path(data["config_file"])
|
248
|
+
if data.get("log_file"):
|
249
|
+
self.log_file = Path(data["log_file"])
|
250
|
+
if "log_format" in data:
|
251
|
+
self.log_format = data["log_format"]
|
252
|
+
if "no_color" in data:
|
253
|
+
self.no_color = data["no_color"]
|
254
|
+
if "no_emoji" in data:
|
255
|
+
self.no_emoji = data["no_emoji"]
|
256
|
+
|
257
|
+
self._validate()
|
258
|
+
|
259
|
+
def save_config(self, path: str | Path) -> None:
|
260
|
+
"""
|
261
|
+
Save configuration to file.
|
262
|
+
|
263
|
+
Format is determined by file extension.
|
264
|
+
|
265
|
+
Args:
|
266
|
+
path: Path to save configuration
|
267
|
+
"""
|
268
|
+
path = Path(path)
|
269
|
+
data = self.to_dict()
|
270
|
+
|
271
|
+
# Remove None values for cleaner output
|
272
|
+
data = {k: v for k, v in data.items() if v is not None}
|
273
|
+
|
274
|
+
if path.suffix in (".toml", ".tml"):
|
275
|
+
try:
|
276
|
+
import tomli_w
|
277
|
+
|
278
|
+
content = tomli_w.dumps(data)
|
279
|
+
except ImportError:
|
280
|
+
raise ImportError("tomli_w required for TOML writing")
|
281
|
+
elif path.suffix == ".json":
|
282
|
+
content = json.dumps(data, indent=2)
|
283
|
+
elif path.suffix in (".yaml", ".yml"):
|
284
|
+
if yaml is None:
|
285
|
+
raise ImportError("PyYAML required for YAML support")
|
286
|
+
content = yaml.safe_dump(data, default_flow_style=False)
|
287
|
+
else:
|
288
|
+
if not path.suffix:
|
289
|
+
raise ValueError(
|
290
|
+
f"Unsupported config format: no file extension for {path}"
|
291
|
+
)
|
292
|
+
else:
|
293
|
+
raise ValueError(f"Unsupported config format: {path.suffix}")
|
294
|
+
|
295
|
+
path.write_text(content)
|
296
|
+
|
297
|
+
def merge(self, other: "Context", override_defaults: bool = False) -> "Context":
|
298
|
+
"""
|
299
|
+
Merge with another context, with other taking precedence.
|
300
|
+
|
301
|
+
Args:
|
302
|
+
other: Context to merge with
|
303
|
+
override_defaults: If False, only override if other's value differs from its class default
|
304
|
+
|
305
|
+
Returns:
|
306
|
+
New merged Context instance
|
307
|
+
"""
|
308
|
+
merged_data = self.to_dict()
|
309
|
+
other_data = other.to_dict()
|
310
|
+
|
311
|
+
if override_defaults:
|
312
|
+
# Update with non-None values from other
|
313
|
+
for key, value in other_data.items():
|
314
|
+
if value is not None:
|
315
|
+
merged_data[key] = value
|
316
|
+
else:
|
317
|
+
# Only override if the value differs from the default
|
318
|
+
from attrs import Factory
|
319
|
+
|
320
|
+
defaults = {}
|
321
|
+
for f in fields(Context):
|
322
|
+
if not f.name.startswith("_"): # Skip private fields
|
323
|
+
if isinstance(f.default, Factory):
|
324
|
+
defaults[f.name] = f.default.factory()
|
325
|
+
elif f.default is not None:
|
326
|
+
defaults[f.name] = f.default
|
327
|
+
|
328
|
+
for key, value in other_data.items():
|
329
|
+
if value is not None:
|
330
|
+
# Check if this is different from the default
|
331
|
+
if key in defaults and value == defaults[key]:
|
332
|
+
# Skip default values
|
333
|
+
continue
|
334
|
+
merged_data[key] = value
|
335
|
+
|
336
|
+
return Context.from_dict(merged_data)
|
337
|
+
|
338
|
+
def freeze(self) -> None:
|
339
|
+
"""Freeze context to prevent further modifications."""
|
340
|
+
# Note: With attrs, we can't dynamically freeze an instance
|
341
|
+
# This is kept for API compatibility but does nothing
|
342
|
+
self._frozen = True
|
343
|
+
|
344
|
+
def copy(self) -> "Context":
|
345
|
+
"""Create a deep copy of the context."""
|
346
|
+
return copy.deepcopy(self)
|
347
|
+
|
348
|
+
@property
|
349
|
+
def logger(self) -> Any:
|
350
|
+
"""Get or create a logger for this context."""
|
351
|
+
if self._logger is None:
|
352
|
+
self._logger = get_logger("context").bind(
|
353
|
+
log_level=self.log_level,
|
354
|
+
profile=self.profile,
|
355
|
+
)
|
356
|
+
return self._logger
|
@@ -0,0 +1,20 @@
|
|
1
|
+
#
|
2
|
+
# core.py
|
3
|
+
#
|
4
|
+
"""
|
5
|
+
Foundation Telemetry Core Setup Functions.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from provide.foundation.logger.setup.emoji_resolver import ResolvedEmojiConfig
|
9
|
+
from provide.foundation.setup import (
|
10
|
+
reset_foundation_setup_for_testing,
|
11
|
+
setup_telemetry,
|
12
|
+
shutdown_foundation_telemetry,
|
13
|
+
)
|
14
|
+
|
15
|
+
__all__ = [
|
16
|
+
"ResolvedEmojiConfig",
|
17
|
+
"reset_foundation_setup_for_testing",
|
18
|
+
"setup_telemetry",
|
19
|
+
"shutdown_foundation_telemetry",
|
20
|
+
]
|
@@ -0,0 +1,182 @@
|
|
1
|
+
"""Cryptographic utilities for Foundation.
|
2
|
+
|
3
|
+
Provides hashing, checksum verification, digital signatures, key generation,
|
4
|
+
and X.509 certificate management.
|
5
|
+
"""
|
6
|
+
|
7
|
+
# Standard crypto imports (always available - use hashlib)
|
8
|
+
from provide.foundation.crypto.algorithms import (
|
9
|
+
DEFAULT_ALGORITHM,
|
10
|
+
SUPPORTED_ALGORITHMS,
|
11
|
+
get_hasher,
|
12
|
+
is_secure_algorithm,
|
13
|
+
validate_algorithm,
|
14
|
+
)
|
15
|
+
from provide.foundation.crypto.checksums import (
|
16
|
+
calculate_checksums,
|
17
|
+
parse_checksum_file,
|
18
|
+
verify_data,
|
19
|
+
verify_file,
|
20
|
+
write_checksum_file,
|
21
|
+
)
|
22
|
+
|
23
|
+
# Cryptography-dependent imports (require optional dependency)
|
24
|
+
try:
|
25
|
+
from provide.foundation.crypto.certificates import (
|
26
|
+
Certificate,
|
27
|
+
CertificateBase,
|
28
|
+
CertificateConfig,
|
29
|
+
CertificateError,
|
30
|
+
CurveType,
|
31
|
+
KeyType,
|
32
|
+
create_ca,
|
33
|
+
create_self_signed,
|
34
|
+
)
|
35
|
+
|
36
|
+
_HAS_CRYPTO = True
|
37
|
+
except ImportError:
|
38
|
+
Certificate = None
|
39
|
+
CertificateBase = None
|
40
|
+
CertificateConfig = None
|
41
|
+
CertificateError = None
|
42
|
+
CurveType = None
|
43
|
+
KeyType = None
|
44
|
+
create_ca = None
|
45
|
+
create_self_signed = None
|
46
|
+
_HAS_CRYPTO = False
|
47
|
+
|
48
|
+
# Standard imports (always available)
|
49
|
+
from provide.foundation.crypto.hashing import (
|
50
|
+
hash_data,
|
51
|
+
hash_file,
|
52
|
+
hash_stream,
|
53
|
+
hash_string,
|
54
|
+
)
|
55
|
+
from provide.foundation.crypto.utils import (
|
56
|
+
compare_hash,
|
57
|
+
format_hash,
|
58
|
+
hash_name,
|
59
|
+
quick_hash,
|
60
|
+
)
|
61
|
+
|
62
|
+
# More cryptography-dependent imports
|
63
|
+
try:
|
64
|
+
from provide.foundation.crypto.constants import (
|
65
|
+
DEFAULT_CERTIFICATE_KEY_TYPE,
|
66
|
+
DEFAULT_CERTIFICATE_VALIDITY_DAYS,
|
67
|
+
DEFAULT_ECDSA_CURVE,
|
68
|
+
DEFAULT_RSA_KEY_SIZE,
|
69
|
+
DEFAULT_SIGNATURE_ALGORITHM,
|
70
|
+
ED25519_PRIVATE_KEY_SIZE,
|
71
|
+
ED25519_PUBLIC_KEY_SIZE,
|
72
|
+
ED25519_SIGNATURE_SIZE,
|
73
|
+
SUPPORTED_EC_CURVES,
|
74
|
+
SUPPORTED_KEY_TYPES,
|
75
|
+
SUPPORTED_RSA_SIZES,
|
76
|
+
get_default_hash_algorithm,
|
77
|
+
get_default_signature_algorithm,
|
78
|
+
)
|
79
|
+
from provide.foundation.crypto.keys import (
|
80
|
+
generate_ec_keypair,
|
81
|
+
generate_key_pair,
|
82
|
+
generate_keypair,
|
83
|
+
generate_rsa_keypair,
|
84
|
+
generate_tls_keypair,
|
85
|
+
)
|
86
|
+
from provide.foundation.crypto.signatures import (
|
87
|
+
generate_ed25519_keypair,
|
88
|
+
generate_signing_keypair,
|
89
|
+
sign_data,
|
90
|
+
verify_signature,
|
91
|
+
)
|
92
|
+
|
93
|
+
if not _HAS_CRYPTO:
|
94
|
+
_HAS_CRYPTO = True
|
95
|
+
except ImportError:
|
96
|
+
# Constants stubs
|
97
|
+
DEFAULT_CERTIFICATE_KEY_TYPE = None
|
98
|
+
DEFAULT_CERTIFICATE_VALIDITY_DAYS = None
|
99
|
+
DEFAULT_ECDSA_CURVE = None
|
100
|
+
DEFAULT_RSA_KEY_SIZE = None
|
101
|
+
DEFAULT_SIGNATURE_ALGORITHM = None
|
102
|
+
ED25519_PRIVATE_KEY_SIZE = None
|
103
|
+
ED25519_PUBLIC_KEY_SIZE = None
|
104
|
+
ED25519_SIGNATURE_SIZE = None
|
105
|
+
SUPPORTED_EC_CURVES = None
|
106
|
+
SUPPORTED_KEY_TYPES = None
|
107
|
+
SUPPORTED_RSA_SIZES = None
|
108
|
+
get_default_hash_algorithm = None
|
109
|
+
get_default_signature_algorithm = None
|
110
|
+
# Key generation stubs
|
111
|
+
generate_ec_keypair = None
|
112
|
+
generate_key_pair = None
|
113
|
+
generate_keypair = None
|
114
|
+
generate_rsa_keypair = None
|
115
|
+
generate_tls_keypair = None
|
116
|
+
# Signature stubs
|
117
|
+
generate_ed25519_keypair = None
|
118
|
+
generate_signing_keypair = None
|
119
|
+
sign_data = None
|
120
|
+
verify_signature = None
|
121
|
+
|
122
|
+
# Public API organized by use case frequency
|
123
|
+
__all__ = [
|
124
|
+
# Most common operations (90% of usage)
|
125
|
+
"hash_file",
|
126
|
+
"hash_data",
|
127
|
+
"verify_file",
|
128
|
+
"verify_data",
|
129
|
+
# Digital signatures (5% of usage)
|
130
|
+
"sign_data",
|
131
|
+
"verify_signature",
|
132
|
+
"generate_signing_keypair",
|
133
|
+
# X.509 certificates (5% of usage)
|
134
|
+
"Certificate",
|
135
|
+
"create_self_signed",
|
136
|
+
"create_ca",
|
137
|
+
# Key generation
|
138
|
+
"generate_keypair",
|
139
|
+
"generate_rsa_keypair",
|
140
|
+
"generate_ec_keypair",
|
141
|
+
"generate_ed25519_keypair",
|
142
|
+
"generate_tls_keypair",
|
143
|
+
# Existing hashing & checksum functions
|
144
|
+
"hash_stream",
|
145
|
+
"hash_string",
|
146
|
+
"calculate_checksums",
|
147
|
+
"parse_checksum_file",
|
148
|
+
"write_checksum_file",
|
149
|
+
# Algorithm management
|
150
|
+
"DEFAULT_ALGORITHM",
|
151
|
+
"SUPPORTED_ALGORITHMS",
|
152
|
+
"get_hasher",
|
153
|
+
"is_secure_algorithm",
|
154
|
+
"validate_algorithm",
|
155
|
+
"get_default_hash_algorithm",
|
156
|
+
"get_default_signature_algorithm",
|
157
|
+
# Utility functions
|
158
|
+
"compare_hash",
|
159
|
+
"format_hash",
|
160
|
+
"hash_name",
|
161
|
+
"quick_hash",
|
162
|
+
# Constants
|
163
|
+
"DEFAULT_SIGNATURE_ALGORITHM",
|
164
|
+
"DEFAULT_CERTIFICATE_KEY_TYPE",
|
165
|
+
"DEFAULT_CERTIFICATE_VALIDITY_DAYS",
|
166
|
+
"DEFAULT_RSA_KEY_SIZE",
|
167
|
+
"DEFAULT_ECDSA_CURVE",
|
168
|
+
"SUPPORTED_KEY_TYPES",
|
169
|
+
"SUPPORTED_RSA_SIZES",
|
170
|
+
"SUPPORTED_EC_CURVES",
|
171
|
+
"ED25519_PRIVATE_KEY_SIZE",
|
172
|
+
"ED25519_PUBLIC_KEY_SIZE",
|
173
|
+
"ED25519_SIGNATURE_SIZE",
|
174
|
+
# Advanced certificate classes
|
175
|
+
"CertificateBase",
|
176
|
+
"CertificateConfig",
|
177
|
+
"CertificateError",
|
178
|
+
"KeyType",
|
179
|
+
"CurveType",
|
180
|
+
# Legacy compatibility
|
181
|
+
"generate_key_pair",
|
182
|
+
]
|
@@ -0,0 +1,111 @@
|
|
1
|
+
"""Hash algorithm management and validation."""
|
2
|
+
|
3
|
+
import hashlib
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from provide.foundation.errors.config import ValidationError
|
7
|
+
|
8
|
+
# Supported hash algorithms
|
9
|
+
SUPPORTED_ALGORITHMS = {
|
10
|
+
"md5",
|
11
|
+
"sha1",
|
12
|
+
"sha224",
|
13
|
+
"sha256",
|
14
|
+
"sha384",
|
15
|
+
"sha512",
|
16
|
+
"sha3_224",
|
17
|
+
"sha3_256",
|
18
|
+
"sha3_384",
|
19
|
+
"sha3_512",
|
20
|
+
"blake2b",
|
21
|
+
"blake2s",
|
22
|
+
}
|
23
|
+
|
24
|
+
# Default algorithm for general use
|
25
|
+
DEFAULT_ALGORITHM = "sha256"
|
26
|
+
|
27
|
+
# Algorithms considered cryptographically secure
|
28
|
+
SECURE_ALGORITHMS = {
|
29
|
+
"sha256",
|
30
|
+
"sha384",
|
31
|
+
"sha512",
|
32
|
+
"sha3_256",
|
33
|
+
"sha3_384",
|
34
|
+
"sha3_512",
|
35
|
+
"blake2b",
|
36
|
+
"blake2s",
|
37
|
+
}
|
38
|
+
|
39
|
+
|
40
|
+
def validate_algorithm(algorithm: str) -> None:
|
41
|
+
"""Validate that a hash algorithm is supported.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
algorithm: Hash algorithm name
|
45
|
+
|
46
|
+
Raises:
|
47
|
+
ValidationError: If algorithm is not supported
|
48
|
+
"""
|
49
|
+
if algorithm.lower() not in SUPPORTED_ALGORITHMS:
|
50
|
+
raise ValidationError(
|
51
|
+
f"Unsupported hash algorithm: {algorithm}",
|
52
|
+
field="algorithm",
|
53
|
+
value=algorithm,
|
54
|
+
rule="must be one of: " + ", ".join(sorted(SUPPORTED_ALGORITHMS)),
|
55
|
+
)
|
56
|
+
|
57
|
+
|
58
|
+
def get_hasher(algorithm: str) -> Any:
|
59
|
+
"""Get a hash object for the specified algorithm.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
algorithm: Hash algorithm name
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
Hash object from hashlib
|
66
|
+
|
67
|
+
Raises:
|
68
|
+
ValidationError: If algorithm is not supported
|
69
|
+
"""
|
70
|
+
validate_algorithm(algorithm)
|
71
|
+
|
72
|
+
algorithm_lower = algorithm.lower()
|
73
|
+
|
74
|
+
# Handle special cases
|
75
|
+
if algorithm_lower.startswith("sha3_"):
|
76
|
+
# sha3_256 -> sha3_256 (hashlib uses underscores)
|
77
|
+
return hashlib.new(algorithm_lower)
|
78
|
+
elif algorithm_lower.startswith("blake2"):
|
79
|
+
# blake2b, blake2s
|
80
|
+
return hashlib.new(algorithm_lower)
|
81
|
+
else:
|
82
|
+
# Standard algorithms (md5, sha1, sha256, etc.)
|
83
|
+
return hashlib.new(algorithm_lower)
|
84
|
+
|
85
|
+
|
86
|
+
def is_secure_algorithm(algorithm: str) -> bool:
|
87
|
+
"""Check if an algorithm is considered cryptographically secure.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
algorithm: Hash algorithm name
|
91
|
+
|
92
|
+
Returns:
|
93
|
+
True if algorithm is secure, False otherwise
|
94
|
+
"""
|
95
|
+
return algorithm.lower() in SECURE_ALGORITHMS
|
96
|
+
|
97
|
+
|
98
|
+
def get_digest_size(algorithm: str) -> int:
|
99
|
+
"""Get the digest size in bytes for an algorithm.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
algorithm: Hash algorithm name
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
Digest size in bytes
|
106
|
+
|
107
|
+
Raises:
|
108
|
+
ValidationError: If algorithm is not supported
|
109
|
+
"""
|
110
|
+
hasher = get_hasher(algorithm)
|
111
|
+
return hasher.digest_size
|