provide-foundation 0.0.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. provide/__init__.py +15 -0
  2. provide/foundation/__init__.py +155 -0
  3. provide/foundation/_version.py +58 -0
  4. provide/foundation/cli/__init__.py +67 -0
  5. provide/foundation/cli/commands/__init__.py +3 -0
  6. provide/foundation/cli/commands/deps.py +71 -0
  7. provide/foundation/cli/commands/logs/__init__.py +63 -0
  8. provide/foundation/cli/commands/logs/generate.py +357 -0
  9. provide/foundation/cli/commands/logs/generate_old.py +569 -0
  10. provide/foundation/cli/commands/logs/query.py +174 -0
  11. provide/foundation/cli/commands/logs/send.py +166 -0
  12. provide/foundation/cli/commands/logs/tail.py +112 -0
  13. provide/foundation/cli/decorators.py +262 -0
  14. provide/foundation/cli/main.py +65 -0
  15. provide/foundation/cli/testing.py +220 -0
  16. provide/foundation/cli/utils.py +210 -0
  17. provide/foundation/config/__init__.py +106 -0
  18. provide/foundation/config/base.py +295 -0
  19. provide/foundation/config/env.py +369 -0
  20. provide/foundation/config/loader.py +311 -0
  21. provide/foundation/config/manager.py +387 -0
  22. provide/foundation/config/schema.py +284 -0
  23. provide/foundation/config/sync.py +281 -0
  24. provide/foundation/config/types.py +78 -0
  25. provide/foundation/config/validators.py +80 -0
  26. provide/foundation/console/__init__.py +29 -0
  27. provide/foundation/console/input.py +364 -0
  28. provide/foundation/console/output.py +178 -0
  29. provide/foundation/context/__init__.py +12 -0
  30. provide/foundation/context/core.py +356 -0
  31. provide/foundation/core.py +20 -0
  32. provide/foundation/crypto/__init__.py +182 -0
  33. provide/foundation/crypto/algorithms.py +111 -0
  34. provide/foundation/crypto/certificates.py +896 -0
  35. provide/foundation/crypto/checksums.py +301 -0
  36. provide/foundation/crypto/constants.py +57 -0
  37. provide/foundation/crypto/hashing.py +265 -0
  38. provide/foundation/crypto/keys.py +188 -0
  39. provide/foundation/crypto/signatures.py +144 -0
  40. provide/foundation/crypto/utils.py +164 -0
  41. provide/foundation/errors/__init__.py +96 -0
  42. provide/foundation/errors/auth.py +73 -0
  43. provide/foundation/errors/base.py +81 -0
  44. provide/foundation/errors/config.py +103 -0
  45. provide/foundation/errors/context.py +299 -0
  46. provide/foundation/errors/decorators.py +484 -0
  47. provide/foundation/errors/handlers.py +360 -0
  48. provide/foundation/errors/integration.py +105 -0
  49. provide/foundation/errors/platform.py +37 -0
  50. provide/foundation/errors/process.py +140 -0
  51. provide/foundation/errors/resources.py +133 -0
  52. provide/foundation/errors/runtime.py +160 -0
  53. provide/foundation/errors/safe_decorators.py +133 -0
  54. provide/foundation/errors/types.py +276 -0
  55. provide/foundation/file/__init__.py +79 -0
  56. provide/foundation/file/atomic.py +157 -0
  57. provide/foundation/file/directory.py +134 -0
  58. provide/foundation/file/formats.py +236 -0
  59. provide/foundation/file/lock.py +175 -0
  60. provide/foundation/file/safe.py +179 -0
  61. provide/foundation/file/utils.py +170 -0
  62. provide/foundation/hub/__init__.py +88 -0
  63. provide/foundation/hub/click_builder.py +310 -0
  64. provide/foundation/hub/commands.py +42 -0
  65. provide/foundation/hub/components.py +640 -0
  66. provide/foundation/hub/decorators.py +244 -0
  67. provide/foundation/hub/info.py +32 -0
  68. provide/foundation/hub/manager.py +446 -0
  69. provide/foundation/hub/registry.py +279 -0
  70. provide/foundation/hub/type_mapping.py +54 -0
  71. provide/foundation/hub/types.py +28 -0
  72. provide/foundation/logger/__init__.py +41 -0
  73. provide/foundation/logger/base.py +22 -0
  74. provide/foundation/logger/config/__init__.py +16 -0
  75. provide/foundation/logger/config/base.py +40 -0
  76. provide/foundation/logger/config/logging.py +394 -0
  77. provide/foundation/logger/config/telemetry.py +188 -0
  78. provide/foundation/logger/core.py +239 -0
  79. provide/foundation/logger/custom_processors.py +172 -0
  80. provide/foundation/logger/emoji/__init__.py +44 -0
  81. provide/foundation/logger/emoji/matrix.py +209 -0
  82. provide/foundation/logger/emoji/sets.py +458 -0
  83. provide/foundation/logger/emoji/types.py +56 -0
  84. provide/foundation/logger/factories.py +56 -0
  85. provide/foundation/logger/processors/__init__.py +13 -0
  86. provide/foundation/logger/processors/main.py +254 -0
  87. provide/foundation/logger/processors/trace.py +113 -0
  88. provide/foundation/logger/ratelimit/__init__.py +31 -0
  89. provide/foundation/logger/ratelimit/limiters.py +294 -0
  90. provide/foundation/logger/ratelimit/processor.py +203 -0
  91. provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
  92. provide/foundation/logger/setup/__init__.py +29 -0
  93. provide/foundation/logger/setup/coordinator.py +138 -0
  94. provide/foundation/logger/setup/emoji_resolver.py +64 -0
  95. provide/foundation/logger/setup/processors.py +85 -0
  96. provide/foundation/logger/setup/testing.py +39 -0
  97. provide/foundation/logger/trace.py +38 -0
  98. provide/foundation/metrics/__init__.py +119 -0
  99. provide/foundation/metrics/otel.py +122 -0
  100. provide/foundation/metrics/simple.py +165 -0
  101. provide/foundation/observability/__init__.py +53 -0
  102. provide/foundation/observability/openobserve/__init__.py +79 -0
  103. provide/foundation/observability/openobserve/auth.py +72 -0
  104. provide/foundation/observability/openobserve/client.py +307 -0
  105. provide/foundation/observability/openobserve/commands.py +357 -0
  106. provide/foundation/observability/openobserve/exceptions.py +41 -0
  107. provide/foundation/observability/openobserve/formatters.py +298 -0
  108. provide/foundation/observability/openobserve/models.py +134 -0
  109. provide/foundation/observability/openobserve/otlp.py +320 -0
  110. provide/foundation/observability/openobserve/search.py +222 -0
  111. provide/foundation/observability/openobserve/streaming.py +235 -0
  112. provide/foundation/platform/__init__.py +44 -0
  113. provide/foundation/platform/detection.py +193 -0
  114. provide/foundation/platform/info.py +157 -0
  115. provide/foundation/process/__init__.py +39 -0
  116. provide/foundation/process/async_runner.py +373 -0
  117. provide/foundation/process/lifecycle.py +406 -0
  118. provide/foundation/process/runner.py +390 -0
  119. provide/foundation/setup/__init__.py +101 -0
  120. provide/foundation/streams/__init__.py +44 -0
  121. provide/foundation/streams/console.py +57 -0
  122. provide/foundation/streams/core.py +65 -0
  123. provide/foundation/streams/file.py +104 -0
  124. provide/foundation/testing/__init__.py +166 -0
  125. provide/foundation/testing/cli.py +227 -0
  126. provide/foundation/testing/crypto.py +163 -0
  127. provide/foundation/testing/fixtures.py +49 -0
  128. provide/foundation/testing/hub.py +23 -0
  129. provide/foundation/testing/logger.py +106 -0
  130. provide/foundation/testing/streams.py +54 -0
  131. provide/foundation/tracer/__init__.py +49 -0
  132. provide/foundation/tracer/context.py +115 -0
  133. provide/foundation/tracer/otel.py +135 -0
  134. provide/foundation/tracer/spans.py +174 -0
  135. provide/foundation/types.py +32 -0
  136. provide/foundation/utils/__init__.py +97 -0
  137. provide/foundation/utils/deps.py +195 -0
  138. provide/foundation/utils/env.py +491 -0
  139. provide/foundation/utils/formatting.py +483 -0
  140. provide/foundation/utils/parsing.py +235 -0
  141. provide/foundation/utils/rate_limiting.py +112 -0
  142. provide/foundation/utils/streams.py +67 -0
  143. provide/foundation/utils/timing.py +93 -0
  144. provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
  145. provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
  146. provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
  147. provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  148. provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
  149. provide_foundation-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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