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,188 @@
1
+ """Unified key generation for all cryptographic algorithms."""
2
+
3
+ from typing import Any, Protocol, Union
4
+
5
+ from cryptography.hazmat.primitives.asymmetric import ec, rsa
6
+
7
+ from provide.foundation import logger
8
+ from provide.foundation.crypto.constants import (
9
+ DEFAULT_ECDSA_CURVE,
10
+ DEFAULT_RSA_KEY_SIZE,
11
+ SUPPORTED_EC_CURVES,
12
+ SUPPORTED_KEY_TYPES,
13
+ SUPPORTED_RSA_SIZES,
14
+ )
15
+ from provide.foundation.crypto.signatures import generate_ed25519_keypair
16
+
17
+
18
+ class KeyPair(Protocol):
19
+ """Protocol for key pairs."""
20
+
21
+ def public_key(self) -> Any:
22
+ """Get public key."""
23
+ ...
24
+
25
+
26
+ # Type aliases for different key types
27
+ RSAKeyPair = tuple[rsa.RSAPublicKey, rsa.RSAPrivateKey]
28
+ ECKeyPair = tuple[ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey]
29
+ Ed25519KeyPair = tuple[bytes, bytes]
30
+
31
+ KeyPairType = Union[RSAKeyPair, ECKeyPair, Ed25519KeyPair]
32
+
33
+
34
+ def generate_rsa_keypair(key_size: int = DEFAULT_RSA_KEY_SIZE) -> RSAKeyPair:
35
+ """Generate RSA key pair.
36
+
37
+ Args:
38
+ key_size: RSA key size in bits (2048, 3072, or 4096)
39
+
40
+ Returns:
41
+ tuple: (public_key, private_key)
42
+
43
+ Raises:
44
+ ValueError: If key size is not supported
45
+ """
46
+ if key_size not in SUPPORTED_RSA_SIZES:
47
+ raise ValueError(
48
+ f"Unsupported RSA key size: {key_size}. "
49
+ f"Supported sizes: {sorted(SUPPORTED_RSA_SIZES)}"
50
+ )
51
+
52
+ logger.debug(f"🔑 Generating RSA key pair (size: {key_size})")
53
+
54
+ private_key = rsa.generate_private_key(
55
+ public_exponent=65537,
56
+ key_size=key_size,
57
+ )
58
+ public_key = private_key.public_key()
59
+
60
+ logger.debug(f"✅ Generated RSA key pair ({key_size} bits)")
61
+ return public_key, private_key
62
+
63
+
64
+ def generate_ec_keypair(curve: str = DEFAULT_ECDSA_CURVE) -> ECKeyPair:
65
+ """Generate ECDSA key pair.
66
+
67
+ Args:
68
+ curve: Elliptic curve name (secp256r1, secp384r1, or secp521r1)
69
+
70
+ Returns:
71
+ tuple: (public_key, private_key)
72
+
73
+ Raises:
74
+ ValueError: If curve is not supported
75
+ AttributeError: If curve name is invalid
76
+ """
77
+ if curve not in SUPPORTED_EC_CURVES:
78
+ raise ValueError(
79
+ f"Unsupported EC curve: {curve}. "
80
+ f"Supported curves: {sorted(SUPPORTED_EC_CURVES)}"
81
+ )
82
+
83
+ logger.debug(f"🔑 Generating ECDSA key pair (curve: {curve})")
84
+
85
+ # Map curve name to cryptography curve object
86
+ curve_obj = getattr(ec, curve.upper())()
87
+
88
+ private_key = ec.generate_private_key(curve_obj)
89
+ public_key = private_key.public_key()
90
+
91
+ logger.debug(f"✅ Generated ECDSA key pair ({curve})")
92
+ return public_key, private_key
93
+
94
+
95
+ def generate_keypair(
96
+ key_type: str,
97
+ key_size: int | None = None,
98
+ curve: str | None = None,
99
+ ) -> KeyPairType:
100
+ """Generate a key pair of the specified type.
101
+
102
+ Args:
103
+ key_type: Type of key to generate ("rsa", "ecdsa", or "ed25519")
104
+ key_size: Key size in bits (required for RSA)
105
+ curve: Curve name (required for ECDSA)
106
+
107
+ Returns:
108
+ KeyPair: Generated key pair
109
+
110
+ Raises:
111
+ ValueError: If key_type is not supported or required params missing
112
+ """
113
+ key_type_lower = key_type.lower()
114
+
115
+ if key_type_lower not in SUPPORTED_KEY_TYPES:
116
+ raise ValueError(
117
+ f"Unsupported key type: {key_type}. "
118
+ f"Supported types: {sorted(SUPPORTED_KEY_TYPES)}"
119
+ )
120
+
121
+ match key_type_lower:
122
+ case "rsa":
123
+ if key_size is None:
124
+ key_size = DEFAULT_RSA_KEY_SIZE
125
+ logger.debug(f"🔑 Using default RSA key size: {key_size}")
126
+ return generate_rsa_keypair(key_size)
127
+
128
+ case "ecdsa":
129
+ if curve is None:
130
+ curve = DEFAULT_ECDSA_CURVE
131
+ logger.debug(f"🔑 Using default ECDSA curve: {curve}")
132
+ return generate_ec_keypair(curve)
133
+
134
+ case "ed25519":
135
+ # Ed25519 has fixed parameters
136
+ if key_size is not None or curve is not None:
137
+ logger.warning(
138
+ "🔑 Ed25519 has fixed parameters - ignoring key_size/curve"
139
+ )
140
+ return generate_ed25519_keypair()
141
+
142
+ case _:
143
+ # This shouldn't happen due to the check above
144
+ raise ValueError(f"Internal error: unhandled key type {key_type}")
145
+
146
+
147
+ # Convenience functions for specific use cases
148
+ def generate_signing_keypair() -> Ed25519KeyPair:
149
+ """Generate Ed25519 keypair for digital signatures.
150
+
151
+ This is the recommended choice for new digital signature use cases.
152
+
153
+ Returns:
154
+ tuple: (private_key_bytes, public_key_bytes)
155
+ """
156
+ return generate_ed25519_keypair()
157
+
158
+
159
+ def generate_tls_keypair(
160
+ key_type: str = "ecdsa",
161
+ curve: str = DEFAULT_ECDSA_CURVE,
162
+ ) -> ECKeyPair | RSAKeyPair:
163
+ """Generate keypair suitable for TLS/certificates.
164
+
165
+ Args:
166
+ key_type: Either "ecdsa" (recommended) or "rsa"
167
+ curve: ECDSA curve (only used if key_type is "ecdsa")
168
+
169
+ Returns:
170
+ KeyPair: Generated key pair
171
+ """
172
+ if key_type == "ecdsa":
173
+ return generate_ec_keypair(curve)
174
+ elif key_type == "rsa":
175
+ return generate_rsa_keypair(DEFAULT_RSA_KEY_SIZE)
176
+ else:
177
+ raise ValueError(f"TLS key type must be 'ecdsa' or 'rsa', got {key_type}")
178
+
179
+
180
+ # Legacy compatibility functions (for smooth migration)
181
+ def generate_key_pair() -> Ed25519KeyPair:
182
+ """Legacy compatibility function.
183
+
184
+ This maintains compatibility with existing flavorpack code.
185
+ New code should use generate_signing_keypair() or generate_keypair().
186
+ """
187
+ logger.debug("🔑 Using legacy generate_key_pair() function")
188
+ return generate_ed25519_keypair()
@@ -0,0 +1,144 @@
1
+ """Digital signature operations using Ed25519."""
2
+
3
+ try:
4
+ from cryptography.exceptions import InvalidSignature
5
+ from cryptography.hazmat.primitives.asymmetric import ed25519
6
+
7
+ _HAS_CRYPTO = True
8
+ except ImportError:
9
+ InvalidSignature = None
10
+ ed25519 = None
11
+ _HAS_CRYPTO = False
12
+
13
+ from provide.foundation import logger
14
+ from provide.foundation.crypto.constants import (
15
+ ED25519_PRIVATE_KEY_SIZE,
16
+ ED25519_PUBLIC_KEY_SIZE,
17
+ ED25519_SIGNATURE_SIZE,
18
+ )
19
+
20
+
21
+ def _require_crypto() -> None:
22
+ """Ensure cryptography is available."""
23
+ if not _HAS_CRYPTO:
24
+ raise ImportError(
25
+ "Cryptography features require optional dependencies. "
26
+ "Install with: pip install 'provide-foundation[crypto]'"
27
+ )
28
+
29
+
30
+ def generate_ed25519_keypair() -> tuple[bytes, bytes]:
31
+ """Generate Ed25519 key pair for digital signatures.
32
+
33
+ Returns:
34
+ tuple: (private_key_bytes, public_key_bytes)
35
+ - private_key_bytes: 32-byte Ed25519 private key seed
36
+ - public_key_bytes: 32-byte Ed25519 public key
37
+ """
38
+ _require_crypto()
39
+ logger.debug("🔐 Generating Ed25519 key pair")
40
+
41
+ # Generate a new Ed25519 private key
42
+ private_key = ed25519.Ed25519PrivateKey.generate()
43
+
44
+ # Get the raw bytes for compatibility with other implementations
45
+ private_key_bytes = private_key.private_bytes_raw()
46
+ public_key_bytes = private_key.public_key().public_bytes_raw()
47
+
48
+ # Validate key sizes
49
+ assert len(private_key_bytes) == ED25519_PRIVATE_KEY_SIZE
50
+ assert len(public_key_bytes) == ED25519_PUBLIC_KEY_SIZE
51
+
52
+ logger.debug(
53
+ f"✅ Generated Ed25519 key pair (public: {len(public_key_bytes)} bytes)"
54
+ )
55
+ return private_key_bytes, public_key_bytes
56
+
57
+
58
+ def sign_data(data: bytes, private_key: bytes) -> bytes:
59
+ """Sign data with Ed25519 private key.
60
+
61
+ Args:
62
+ data: The data to sign
63
+ private_key: 32-byte Ed25519 private key seed
64
+
65
+ Returns:
66
+ bytes: 64-byte Ed25519 signature
67
+
68
+ Raises:
69
+ ValueError: If private key is wrong size
70
+ """
71
+ _require_crypto()
72
+ if len(private_key) != ED25519_PRIVATE_KEY_SIZE:
73
+ raise ValueError(
74
+ f"Private key must be {ED25519_PRIVATE_KEY_SIZE} bytes, "
75
+ f"got {len(private_key)}"
76
+ )
77
+
78
+ logger.debug(f"🔏 Signing {len(data)} bytes of data with Ed25519")
79
+
80
+ # Reconstruct the private key from the seed bytes
81
+ private_key_obj = ed25519.Ed25519PrivateKey.from_private_bytes(private_key)
82
+
83
+ # Sign the data
84
+ signature = private_key_obj.sign(data)
85
+
86
+ # Validate signature size
87
+ assert len(signature) == ED25519_SIGNATURE_SIZE
88
+
89
+ logger.debug(f"✅ Created Ed25519 signature ({len(signature)} bytes)")
90
+ return signature
91
+
92
+
93
+ def verify_signature(data: bytes, signature: bytes, public_key: bytes) -> bool:
94
+ """Verify Ed25519 signature.
95
+
96
+ Args:
97
+ data: The data that was signed
98
+ signature: 64-byte Ed25519 signature
99
+ public_key: 32-byte Ed25519 public key
100
+
101
+ Returns:
102
+ bool: True if signature is valid, False otherwise
103
+ """
104
+ _require_crypto()
105
+ if len(signature) != ED25519_SIGNATURE_SIZE:
106
+ logger.warning(
107
+ f"❌ Invalid signature size: expected {ED25519_SIGNATURE_SIZE}, "
108
+ f"got {len(signature)}"
109
+ )
110
+ return False
111
+
112
+ if len(public_key) != ED25519_PUBLIC_KEY_SIZE:
113
+ logger.warning(
114
+ f"❌ Invalid public key size: expected {ED25519_PUBLIC_KEY_SIZE}, "
115
+ f"got {len(public_key)}"
116
+ )
117
+ return False
118
+
119
+ logger.debug(f"🔍 Verifying Ed25519 signature for {len(data)} bytes of data")
120
+
121
+ try:
122
+ public_key_obj = ed25519.Ed25519PublicKey.from_public_bytes(public_key)
123
+ public_key_obj.verify(signature, data)
124
+ logger.debug("✅ Ed25519 signature verification successful")
125
+ return True
126
+ except InvalidSignature:
127
+ logger.debug("❌ Invalid Ed25519 signature")
128
+ return False
129
+ except Exception as e:
130
+ logger.error(f"❌ Ed25519 signature verification error: {e}")
131
+ return False
132
+
133
+
134
+ # Convenience function for generating signing keypairs
135
+ def generate_signing_keypair() -> tuple[bytes, bytes]:
136
+ """Generate Ed25519 keypair for signing (convenience function).
137
+
138
+ This is an alias for generate_ed25519_keypair() that makes intent clear.
139
+
140
+ Returns:
141
+ tuple: (private_key_bytes, public_key_bytes)
142
+ """
143
+ _require_crypto()
144
+ return generate_ed25519_keypair()
@@ -0,0 +1,164 @@
1
+ """Utility functions for hashing and cryptographic operations."""
2
+
3
+ import hashlib
4
+
5
+
6
+ def quick_hash(data: bytes) -> int:
7
+ """Generate a quick non-cryptographic hash for lookups.
8
+
9
+ This uses Python's built-in hash function which is fast but not
10
+ cryptographically secure. Use only for hash tables and caching.
11
+
12
+ Args:
13
+ data: Data to hash
14
+
15
+ Returns:
16
+ 32-bit hash value
17
+ """
18
+ # Use Python's built-in hash for speed, mask to 32 bits
19
+ return hash(data) & 0xFFFFFFFF
20
+
21
+
22
+ def hash_name(name: str) -> int:
23
+ """Generate a 64-bit hash of a string for fast lookup.
24
+
25
+ This is useful for creating numeric identifiers from strings.
26
+
27
+ Args:
28
+ name: String to hash
29
+
30
+ Returns:
31
+ 64-bit integer hash
32
+ """
33
+ # Use first 8 bytes of SHA256 for good distribution
34
+ hash_bytes = hashlib.sha256(name.encode("utf-8")).digest()[:8]
35
+ return int.from_bytes(hash_bytes, byteorder="little")
36
+
37
+
38
+ def compare_hash(hash1: str, hash2: str) -> bool:
39
+ """Compare two hash values in a case-insensitive manner.
40
+
41
+ Args:
42
+ hash1: First hash value
43
+ hash2: Second hash value
44
+
45
+ Returns:
46
+ True if hashes match (case-insensitive)
47
+ """
48
+ return hash1.lower() == hash2.lower()
49
+
50
+
51
+ def format_hash(
52
+ hash_value: str,
53
+ group_size: int = 8,
54
+ groups: int = 0,
55
+ separator: str = " ",
56
+ ) -> str:
57
+ """Format a hash value for display.
58
+
59
+ Args:
60
+ hash_value: Hash value to format
61
+ group_size: Number of characters per group
62
+ groups: Number of groups to show (0 for all)
63
+ separator: Separator between groups
64
+
65
+ Returns:
66
+ Formatted hash string
67
+
68
+ Examples:
69
+ >>> format_hash("abc123def456", group_size=4, separator="-")
70
+ "abc1-23de-f456"
71
+ >>> format_hash("abc123def456", group_size=4, groups=2)
72
+ "abc1 23de"
73
+ """
74
+ if group_size <= 0:
75
+ return hash_value
76
+
77
+ formatted_parts = []
78
+ for i in range(0, len(hash_value), group_size):
79
+ formatted_parts.append(hash_value[i : i + group_size])
80
+ if groups > 0 and len(formatted_parts) >= groups:
81
+ break
82
+
83
+ return separator.join(formatted_parts)
84
+
85
+
86
+ def truncate_hash(hash_value: str, length: int = 16, suffix: str = "...") -> str:
87
+ """Truncate a hash for display purposes.
88
+
89
+ Args:
90
+ hash_value: Hash value to truncate
91
+ length: Number of characters to keep
92
+ suffix: Suffix to append
93
+
94
+ Returns:
95
+ Truncated hash string
96
+
97
+ Examples:
98
+ >>> truncate_hash("abc123def456789", length=8)
99
+ "abc123de..."
100
+ """
101
+ if len(hash_value) <= length:
102
+ return hash_value
103
+ return hash_value[:length] + suffix
104
+
105
+
106
+ def hash_to_int(hash_value: str) -> int:
107
+ """Convert a hex hash string to an integer.
108
+
109
+ Args:
110
+ hash_value: Hex hash string
111
+
112
+ Returns:
113
+ Integer representation of the hash
114
+ """
115
+ return int(hash_value, 16)
116
+
117
+
118
+ def int_to_hash(value: int, length: int | None = None) -> str:
119
+ """Convert an integer to a hex hash string.
120
+
121
+ Args:
122
+ value: Integer value
123
+ length: Desired length (will pad with zeros)
124
+
125
+ Returns:
126
+ Hex string representation
127
+ """
128
+ hex_str = format(value, "x")
129
+ if length and len(hex_str) < length:
130
+ hex_str = hex_str.zfill(length)
131
+ return hex_str
132
+
133
+
134
+ def is_valid_hash(hash_value: str, algorithm: str | None = None) -> bool:
135
+ """Check if a string is a valid hash value.
136
+
137
+ Args:
138
+ hash_value: String to check
139
+ algorithm: Optional algorithm to validate length against
140
+
141
+ Returns:
142
+ True if string appears to be a valid hash
143
+ """
144
+ # Check if it's a valid hex string
145
+ try:
146
+ int(hash_value, 16)
147
+ except ValueError:
148
+ return False
149
+
150
+ # If algorithm specified, check length
151
+ if algorithm:
152
+ from provide.foundation.crypto.algorithms import (
153
+ get_digest_size,
154
+ validate_algorithm,
155
+ )
156
+
157
+ try:
158
+ validate_algorithm(algorithm)
159
+ expected_length = get_digest_size(algorithm) * 2 # hex is 2 chars per byte
160
+ return len(hash_value) == expected_length
161
+ except Exception:
162
+ return False
163
+
164
+ return True
@@ -0,0 +1,96 @@
1
+ """
2
+ Foundation error handling system.
3
+
4
+ Provides a comprehensive exception hierarchy, error context management,
5
+ and utilities for robust error handling throughout the application.
6
+ """
7
+
8
+ from provide.foundation.errors.auth import AuthenticationError, AuthorizationError
9
+ from provide.foundation.errors.base import FoundationError
10
+ from provide.foundation.errors.config import (
11
+ ConfigurationError,
12
+ ConfigValidationError,
13
+ ValidationError,
14
+ )
15
+ from provide.foundation.errors.context import (
16
+ ErrorCategory,
17
+ ErrorContext,
18
+ ErrorSeverity,
19
+ capture_error_context,
20
+ )
21
+ from provide.foundation.errors.decorators import (
22
+ fallback_on_error,
23
+ retry_on_error,
24
+ suppress_and_log,
25
+ with_error_handling,
26
+ )
27
+ from provide.foundation.errors.handlers import (
28
+ ErrorHandler,
29
+ error_boundary,
30
+ handle_error,
31
+ transactional,
32
+ )
33
+ from provide.foundation.errors.integration import (
34
+ IntegrationError,
35
+ NetworkError,
36
+ TimeoutError,
37
+ )
38
+ from provide.foundation.errors.process import (
39
+ CommandNotFoundError,
40
+ ProcessError,
41
+ ProcessTimeoutError,
42
+ )
43
+ from provide.foundation.errors.resources import (
44
+ AlreadyExistsError,
45
+ NotFoundError,
46
+ ResourceError,
47
+ )
48
+ from provide.foundation.errors.runtime import ConcurrencyError, RuntimeError, StateError
49
+ from provide.foundation.errors.safe_decorators import log_only_error_context
50
+ from provide.foundation.errors.types import (
51
+ ErrorCode,
52
+ ErrorMetadata,
53
+ RetryPolicy,
54
+ )
55
+
56
+ __all__ = [
57
+ "AlreadyExistsError",
58
+ "AuthenticationError",
59
+ "AuthorizationError",
60
+ "CommandNotFoundError",
61
+ "ConcurrencyError",
62
+ "ConfigurationError",
63
+ "ConfigValidationError",
64
+ "ErrorCategory",
65
+ # Types
66
+ "ErrorCode",
67
+ # Context
68
+ "ErrorContext",
69
+ "ErrorHandler",
70
+ "ErrorMetadata",
71
+ "ErrorSeverity",
72
+ # Base exceptions
73
+ "FoundationError",
74
+ "IntegrationError",
75
+ "NetworkError",
76
+ "NotFoundError",
77
+ "ProcessError",
78
+ "ProcessTimeoutError",
79
+ "ResourceError",
80
+ "RetryPolicy",
81
+ "RuntimeError",
82
+ "StateError",
83
+ "TimeoutError",
84
+ "ValidationError",
85
+ "capture_error_context",
86
+ # Handlers
87
+ "error_boundary",
88
+ "fallback_on_error",
89
+ "handle_error",
90
+ "log_only_error_context",
91
+ "retry_on_error",
92
+ "suppress_and_log",
93
+ "transactional",
94
+ # Decorators
95
+ "with_error_handling",
96
+ ]
@@ -0,0 +1,73 @@
1
+ """Authentication and authorization exceptions."""
2
+
3
+ from typing import Any
4
+
5
+ from provide.foundation.errors.base import FoundationError
6
+
7
+
8
+ class AuthenticationError(FoundationError):
9
+ """Raised when authentication fails.
10
+
11
+ Args:
12
+ message: Error message describing the authentication failure.
13
+ auth_method: Optional authentication method used.
14
+ realm: Optional authentication realm.
15
+ **kwargs: Additional context passed to FoundationError.
16
+
17
+ Examples:
18
+ >>> raise AuthenticationError("Invalid credentials")
19
+ >>> raise AuthenticationError("Token expired", auth_method="jwt")
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ message: str,
25
+ *,
26
+ auth_method: str | None = None,
27
+ realm: str | None = None,
28
+ **kwargs: Any,
29
+ ) -> None:
30
+ if auth_method:
31
+ kwargs.setdefault("context", {})["auth.method"] = auth_method
32
+ if realm:
33
+ kwargs.setdefault("context", {})["auth.realm"] = realm
34
+ super().__init__(message, **kwargs)
35
+
36
+ def _default_code(self) -> str:
37
+ return "AUTH_ERROR"
38
+
39
+
40
+ class AuthorizationError(FoundationError):
41
+ """Raised when authorization fails.
42
+
43
+ Args:
44
+ message: Error message describing the authorization failure.
45
+ required_permission: Optional required permission.
46
+ resource: Optional resource being accessed.
47
+ actor: Optional actor (user/service) attempting access.
48
+ **kwargs: Additional context passed to FoundationError.
49
+
50
+ Examples:
51
+ >>> raise AuthorizationError("Access denied")
52
+ >>> raise AuthorizationError("Insufficient permissions", required_permission="admin")
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ message: str,
58
+ *,
59
+ required_permission: str | None = None,
60
+ resource: str | None = None,
61
+ actor: str | None = None,
62
+ **kwargs: Any,
63
+ ) -> None:
64
+ if required_permission:
65
+ kwargs.setdefault("context", {})["authz.permission"] = required_permission
66
+ if resource:
67
+ kwargs.setdefault("context", {})["authz.resource"] = resource
68
+ if actor:
69
+ kwargs.setdefault("context", {})["authz.actor"] = actor
70
+ super().__init__(message, **kwargs)
71
+
72
+ def _default_code(self) -> str:
73
+ return "AUTHZ_ERROR"