memoryintelligence 0.1.0__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.
- memoryintelligence/__init__.py +71 -0
- memoryintelligence/_auth.py +316 -0
- memoryintelligence/_errors.py +232 -0
- memoryintelligence/_http.py +419 -0
- memoryintelligence/_simple.py +301 -0
- memoryintelligence/_version.py +3 -0
- memoryintelligence/py.typed +2 -0
- memoryintelligence-0.1.0.dist-info/METADATA +220 -0
- memoryintelligence-0.1.0.dist-info/RECORD +12 -0
- memoryintelligence-0.1.0.dist-info/WHEEL +5 -0
- memoryintelligence-0.1.0.dist-info/licenses/LICENSE +31 -0
- memoryintelligence-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""memoryintelligence: the official Python client for Memory Intelligence.
|
|
2
|
+
|
|
3
|
+
Ask once. Never explain again.
|
|
4
|
+
|
|
5
|
+
from memoryintelligence import MemoryIntelligence
|
|
6
|
+
|
|
7
|
+
mi = MemoryIntelligence.from_env() # reads MI_API_KEY
|
|
8
|
+
note = mi.capture("Deploy is Friday at 3pm.", source="standup")
|
|
9
|
+
|
|
10
|
+
for hit in mi.ask("when is the deploy?"):
|
|
11
|
+
print(hit.score, hit.summary)
|
|
12
|
+
|
|
13
|
+
assert mi.verify(note.id).valid # every memory has a receipt
|
|
14
|
+
|
|
15
|
+
Eight verbs on the canonical API: capture, ask, get, list, verify, explain,
|
|
16
|
+
forget, batch, upload. Results come back as small typed objects, not envelopes.
|
|
17
|
+
"""
|
|
18
|
+
from ._version import __version__
|
|
19
|
+
|
|
20
|
+
from ._simple import (
|
|
21
|
+
MemoryIntelligence,
|
|
22
|
+
MI,
|
|
23
|
+
Memory,
|
|
24
|
+
Match,
|
|
25
|
+
Proof,
|
|
26
|
+
Explanation,
|
|
27
|
+
Receipt,
|
|
28
|
+
)
|
|
29
|
+
from ._errors import (
|
|
30
|
+
MIError,
|
|
31
|
+
ConfigurationError,
|
|
32
|
+
AuthenticationError,
|
|
33
|
+
RateLimitError,
|
|
34
|
+
NotFoundError,
|
|
35
|
+
ValidationError,
|
|
36
|
+
ServerError,
|
|
37
|
+
ConnectionError,
|
|
38
|
+
TimeoutError,
|
|
39
|
+
ConflictError,
|
|
40
|
+
PaymentRequiredError,
|
|
41
|
+
PermissionError,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
__author__ = "Memory Intelligence"
|
|
45
|
+
__license__ = "MIT"
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"__version__",
|
|
49
|
+
# Client
|
|
50
|
+
"MemoryIntelligence",
|
|
51
|
+
"MI",
|
|
52
|
+
# Result types
|
|
53
|
+
"Memory",
|
|
54
|
+
"Match",
|
|
55
|
+
"Proof",
|
|
56
|
+
"Explanation",
|
|
57
|
+
"Receipt",
|
|
58
|
+
# Exceptions
|
|
59
|
+
"MIError",
|
|
60
|
+
"ConfigurationError",
|
|
61
|
+
"AuthenticationError",
|
|
62
|
+
"RateLimitError",
|
|
63
|
+
"NotFoundError",
|
|
64
|
+
"ValidationError",
|
|
65
|
+
"ServerError",
|
|
66
|
+
"ConnectionError",
|
|
67
|
+
"TimeoutError",
|
|
68
|
+
"ConflictError",
|
|
69
|
+
"PaymentRequiredError",
|
|
70
|
+
"PermissionError",
|
|
71
|
+
]
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Memory Intelligence SDK - Authentication.
|
|
2
|
+
|
|
3
|
+
API key resolution and validation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
from ._errors import ConfigurationError
|
|
13
|
+
|
|
14
|
+
# Try to load python-dotenv if available (optional dependency)
|
|
15
|
+
_dotenv_loaded = False
|
|
16
|
+
try:
|
|
17
|
+
from dotenv import load_dotenv
|
|
18
|
+
# Load .env file from current directory (won't override existing env vars)
|
|
19
|
+
load_dotenv(override=False)
|
|
20
|
+
_dotenv_loaded = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
pass # python-dotenv not installed, skip .env loading
|
|
23
|
+
|
|
24
|
+
# Production API URL
|
|
25
|
+
DEFAULT_API_URL = "https://api.memoryintelligence.io"
|
|
26
|
+
|
|
27
|
+
# Valid API key prefixes
|
|
28
|
+
LIVE_KEY_PREFIX = "mi_sk_live_"
|
|
29
|
+
TEST_KEY_PREFIX = "mi_sk_test_"
|
|
30
|
+
BETA_KEY_PREFIX = "mi_sk_beta_" # beta programme keys
|
|
31
|
+
VALID_KEY_PREFIXES = (LIVE_KEY_PREFIX, TEST_KEY_PREFIX, BETA_KEY_PREFIX)
|
|
32
|
+
|
|
33
|
+
# Minimum key length (prefix + some randomness)
|
|
34
|
+
MIN_KEY_LENGTH = 32
|
|
35
|
+
|
|
36
|
+
# URL patterns that indicate localhost/local development
|
|
37
|
+
LOCALHOST_PATTERNS = [
|
|
38
|
+
r"localhost",
|
|
39
|
+
r"127\.\d+\.\d+\.\d+",
|
|
40
|
+
r"0\.0\.0\.0",
|
|
41
|
+
r"::1",
|
|
42
|
+
r"\.local$",
|
|
43
|
+
r"\.internal$",
|
|
44
|
+
r":\d+/(?!/)" # Any port specifier (rough heuristic)
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def resolve_api_key(explicit_key: str | None) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Resolve API key from explicit argument or environment.
|
|
51
|
+
|
|
52
|
+
Priority:
|
|
53
|
+
1. Explicit key passed to constructor
|
|
54
|
+
2. MI_API_KEY environment variable
|
|
55
|
+
3. ConfigurationError (key is required)
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
explicit_key: API key passed directly to constructor
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Resolved API key string
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
ConfigurationError: If no API key is found
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
>>> resolve_api_key("mi_sk_test_abc123...")
|
|
68
|
+
'mi_sk_test_abc123...'
|
|
69
|
+
>>> resolve_api_key(None) # With MI_API_KEY set
|
|
70
|
+
'mi_sk_live_xyz789...'
|
|
71
|
+
"""
|
|
72
|
+
if explicit_key is not None:
|
|
73
|
+
key = explicit_key.strip()
|
|
74
|
+
if key:
|
|
75
|
+
return key
|
|
76
|
+
|
|
77
|
+
env_key = os.environ.get("MI_API_KEY")
|
|
78
|
+
if env_key:
|
|
79
|
+
key = env_key.strip()
|
|
80
|
+
if key:
|
|
81
|
+
return key
|
|
82
|
+
|
|
83
|
+
raise ConfigurationError(
|
|
84
|
+
"API key is required. Pass it directly, set MI_API_KEY environment variable, "
|
|
85
|
+
"or add MI_API_KEY=your_key to a .env file (requires: pip install memoryintelligence[dotenv]). "
|
|
86
|
+
"Get your API key at https://memoryintelligence.io/portal"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def validate_api_key(api_key: str, base_url: str) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Validate API key format and environment compatibility.
|
|
93
|
+
|
|
94
|
+
Combines format and environment checks into one call.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
api_key: API key to validate
|
|
98
|
+
base_url: Base URL being used (to check live key + localhost)
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
ConfigurationError: If key format or environment is invalid
|
|
102
|
+
"""
|
|
103
|
+
validate_key_format(api_key)
|
|
104
|
+
validate_key_environment(api_key, base_url)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def resolve_base_url(explicit_url: str | None) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Resolve base URL from explicit argument or environment.
|
|
110
|
+
|
|
111
|
+
Priority:
|
|
112
|
+
1. Explicit URL passed to constructor
|
|
113
|
+
2. MI_BASE_URL environment variable
|
|
114
|
+
3. Production default (https://api.memoryintelligence.io)
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
explicit_url: Base URL passed directly to constructor
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Resolved base URL string (normalized, no trailing slash)
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
ConfigurationError: If URL is malformed
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
>>> resolve_base_url("https://custom.example.com")
|
|
127
|
+
'https://custom.example.com'
|
|
128
|
+
>>> resolve_base_url("https://custom.example.com/") # Trailing slash removed
|
|
129
|
+
'https://custom.example.com'
|
|
130
|
+
"""
|
|
131
|
+
url = None
|
|
132
|
+
|
|
133
|
+
if explicit_url is not None:
|
|
134
|
+
url = explicit_url.strip()
|
|
135
|
+
else:
|
|
136
|
+
env_url = os.environ.get("MI_BASE_URL")
|
|
137
|
+
if env_url:
|
|
138
|
+
url = env_url.strip()
|
|
139
|
+
|
|
140
|
+
if not url:
|
|
141
|
+
return DEFAULT_API_URL
|
|
142
|
+
|
|
143
|
+
# Basic URL validation
|
|
144
|
+
if not url.startswith(("http://", "https://")):
|
|
145
|
+
raise ConfigurationError(
|
|
146
|
+
f"Invalid base_url: must start with http:// or https://. Got: {url[:50]}..."
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Validate URL structure
|
|
150
|
+
try:
|
|
151
|
+
parsed = urlparse(url)
|
|
152
|
+
if not parsed.netloc:
|
|
153
|
+
raise ConfigurationError(f"Invalid base_url: missing host. Got: {url}")
|
|
154
|
+
except Exception as e:
|
|
155
|
+
raise ConfigurationError(f"Invalid base_url: {e}") from e
|
|
156
|
+
|
|
157
|
+
# Remove trailing slashes for consistency
|
|
158
|
+
return url.rstrip("/")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def validate_key_format(api_key: str) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Validate API key format.
|
|
164
|
+
|
|
165
|
+
Valid keys must:
|
|
166
|
+
- Start with 'mi_sk_live_' or 'mi_sk_test_'
|
|
167
|
+
- Be at least 32 characters long
|
|
168
|
+
- Not contain whitespace
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
api_key: API key to validate
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
ConfigurationError: If key format is invalid
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
>>> validate_key_format("mi_sk_test_abc123xyz...")
|
|
178
|
+
# passes silently
|
|
179
|
+
>>> validate_key_format("invalid_key")
|
|
180
|
+
ConfigurationError: Invalid API key format...
|
|
181
|
+
"""
|
|
182
|
+
if not api_key:
|
|
183
|
+
raise ConfigurationError("API key cannot be empty.")
|
|
184
|
+
|
|
185
|
+
# Check for whitespace
|
|
186
|
+
if api_key != api_key.strip():
|
|
187
|
+
raise ConfigurationError(
|
|
188
|
+
"API key contains leading or trailing whitespace. Please check your key."
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Check prefix
|
|
192
|
+
if not any(api_key.startswith(prefix) for prefix in VALID_KEY_PREFIXES):
|
|
193
|
+
# Show what we received (safely truncated)
|
|
194
|
+
displayed_key = api_key[:20] + "..." if len(api_key) > 20 else api_key
|
|
195
|
+
raise ConfigurationError(
|
|
196
|
+
f"Invalid API key format. Key must start with 'mi_sk_live_', 'mi_sk_test_', or 'mi_sk_beta_'. "
|
|
197
|
+
f"Got: '{displayed_key}'. Get your API key at https://memoryintelligence.io/dashboard"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Check minimum length
|
|
201
|
+
if len(api_key) < MIN_KEY_LENGTH:
|
|
202
|
+
raise ConfigurationError(
|
|
203
|
+
f"API key too short. Expected at least {MIN_KEY_LENGTH} characters, "
|
|
204
|
+
f"got {len(api_key)}. Please provide a complete key."
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def validate_key_environment(api_key: str, base_url: str) -> None:
|
|
209
|
+
"""
|
|
210
|
+
Validate that live keys are not used with localhost/development URLs.
|
|
211
|
+
|
|
212
|
+
This is a safety measure to prevent accidentally using production keys
|
|
213
|
+
in development environments.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
api_key: The API key being used
|
|
217
|
+
base_url: The base URL being configured
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
ConfigurationError: If live key is used with localhost URL
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
>>> validate_key_environment(
|
|
224
|
+
... "mi_sk_live_abc123...",
|
|
225
|
+
... "http://localhost:8000"
|
|
226
|
+
... )
|
|
227
|
+
ConfigurationError: Live key detected with local base_url...
|
|
228
|
+
"""
|
|
229
|
+
is_live_key = api_key.startswith(LIVE_KEY_PREFIX)
|
|
230
|
+
|
|
231
|
+
# Check for local/development URLs
|
|
232
|
+
is_localhost = _is_localhost_url(base_url)
|
|
233
|
+
|
|
234
|
+
if is_live_key and is_localhost:
|
|
235
|
+
raise ConfigurationError(
|
|
236
|
+
"Live key detected with local base_url. "
|
|
237
|
+
"Use a test key for local development. "
|
|
238
|
+
"Test keys start with 'mi_sk_test_'. "
|
|
239
|
+
"Get test keys at https://memoryintelligence.io/dashboard"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _is_localhost_url(url: str) -> bool:
|
|
244
|
+
"""
|
|
245
|
+
Check if URL points to localhost/development environment.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
url: URL string to check
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
True if URL appears to be localhost/local development
|
|
252
|
+
"""
|
|
253
|
+
url_lower = url.lower()
|
|
254
|
+
|
|
255
|
+
for pattern in LOCALHOST_PATTERNS:
|
|
256
|
+
if re.search(pattern, url_lower):
|
|
257
|
+
return True
|
|
258
|
+
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def is_live_key(api_key: str) -> bool:
|
|
263
|
+
"""
|
|
264
|
+
Check if API key is a live/production key.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
api_key: API key to check
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
True if key starts with 'mi_sk_live_'
|
|
271
|
+
|
|
272
|
+
Example:
|
|
273
|
+
>>> is_live_key("mi_sk_live_abc123")
|
|
274
|
+
True
|
|
275
|
+
>>> is_live_key("mi_sk_test_xyz789")
|
|
276
|
+
False
|
|
277
|
+
"""
|
|
278
|
+
return api_key.startswith(LIVE_KEY_PREFIX)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def is_test_key(api_key: str) -> bool:
|
|
282
|
+
"""
|
|
283
|
+
Check if API key is a test/development key.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
api_key: API key to check
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
True if key starts with 'mi_sk_test_'
|
|
290
|
+
|
|
291
|
+
Example:
|
|
292
|
+
>>> is_test_key("mi_sk_test_xyz789")
|
|
293
|
+
True
|
|
294
|
+
>>> is_test_key("mi_sk_live_abc123")
|
|
295
|
+
False
|
|
296
|
+
"""
|
|
297
|
+
return api_key.startswith(TEST_KEY_PREFIX)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def mask_key(api_key: str) -> str:
|
|
301
|
+
"""
|
|
302
|
+
Mask API key for safe logging/display.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
api_key: API key to mask
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Masked key showing only first 12 and last 4 characters
|
|
309
|
+
|
|
310
|
+
Example:
|
|
311
|
+
>>> mask_key("mi_sk_test_abc123xyz789")
|
|
312
|
+
'mi_sk_test_abc...yz89'
|
|
313
|
+
"""
|
|
314
|
+
if len(api_key) <= 16:
|
|
315
|
+
return "***"
|
|
316
|
+
return f"{api_key[:12]}...{api_key[-4:]}"
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Memory Intelligence SDK - Error types.
|
|
2
|
+
|
|
3
|
+
Complete error hierarchy for programmatic exception handling.
|
|
4
|
+
Every error has a `code` string attribute that matches the API error envelope.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MIError(Exception):
|
|
11
|
+
"""Base. All SDK errors inherit from this."""
|
|
12
|
+
code: str = "mi_error"
|
|
13
|
+
|
|
14
|
+
def __init__(self, message: str = "Memory Intelligence error"):
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConfigurationError(MIError):
|
|
19
|
+
"""Invalid SDK configuration (wrong key format, live key + localhost, etc.)"""
|
|
20
|
+
code = "configuration_error"
|
|
21
|
+
|
|
22
|
+
def __init__(self, message: str = "Invalid configuration"):
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LicenseError(MIError):
|
|
27
|
+
"""License invalid, expired beyond grace period, or revoked."""
|
|
28
|
+
code = "license_error"
|
|
29
|
+
days_expired: int = 0 # 0 if not expired, N if in warning period
|
|
30
|
+
renew_url: str = "https://memoryintelligence.io/billing"
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
message: str = "License error",
|
|
35
|
+
days_expired: int = 0,
|
|
36
|
+
renew_url: str = "https://memoryintelligence.io/billing",
|
|
37
|
+
):
|
|
38
|
+
super().__init__(message)
|
|
39
|
+
self.days_expired = days_expired
|
|
40
|
+
self.renew_url = renew_url
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AuthenticationError(MIError):
|
|
44
|
+
"""API key rejected by server."""
|
|
45
|
+
code = "authentication_error"
|
|
46
|
+
|
|
47
|
+
def __init__(self, message: str = "Authentication failed"):
|
|
48
|
+
super().__init__(message)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RateLimitError(MIError):
|
|
52
|
+
"""Rate limit exceeded."""
|
|
53
|
+
code = "rate_limit"
|
|
54
|
+
retry_after: int = 60 # seconds, from Retry-After header
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
message: str = "Rate limit exceeded",
|
|
59
|
+
retry_after: int = 60,
|
|
60
|
+
):
|
|
61
|
+
super().__init__(message)
|
|
62
|
+
self.retry_after = retry_after
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ScopeViolationError(MIError):
|
|
66
|
+
"""Cross-scope access attempt or missing scope_id."""
|
|
67
|
+
code = "scope_violation"
|
|
68
|
+
|
|
69
|
+
def __init__(self, message: str = "Scope violation"):
|
|
70
|
+
super().__init__(message)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class PIIViolationError(MIError):
|
|
74
|
+
"""Content rejected due to PII policy."""
|
|
75
|
+
code = "pii_violation"
|
|
76
|
+
detected_types: list = field(default_factory=list)
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
message: str = "PII violation detected",
|
|
81
|
+
detected_types: list | None = None,
|
|
82
|
+
):
|
|
83
|
+
super().__init__(message)
|
|
84
|
+
self.detected_types = detected_types or []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class GovernanceError(MIError):
|
|
88
|
+
"""Operation violates governance policy."""
|
|
89
|
+
code = "governance_error"
|
|
90
|
+
|
|
91
|
+
def __init__(self, message: str = "Governance policy violation"):
|
|
92
|
+
super().__init__(message)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ConflictError(MIError):
|
|
96
|
+
"""Resource conflict (e.g., duplicate UMO)."""
|
|
97
|
+
code = "conflict"
|
|
98
|
+
|
|
99
|
+
def __init__(self, message: str = "Resource conflict"):
|
|
100
|
+
super().__init__(message)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class PaymentRequiredError(MIError):
|
|
104
|
+
"""Payment required - license upgrade needed."""
|
|
105
|
+
code = "payment_required"
|
|
106
|
+
|
|
107
|
+
def __init__(self, message: str = "License upgrade required"):
|
|
108
|
+
super().__init__(message)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class EncryptionError(MIError):
|
|
112
|
+
"""Encryption/decryption failure."""
|
|
113
|
+
code = "encryption_error"
|
|
114
|
+
|
|
115
|
+
def __init__(self, message: str = "Encryption error"):
|
|
116
|
+
super().__init__(message)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class PermissionError(MIError):
|
|
120
|
+
"""Permission denied."""
|
|
121
|
+
code = "permission_error"
|
|
122
|
+
|
|
123
|
+
def __init__(self, message: str = "Permission denied"):
|
|
124
|
+
super().__init__(message)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ProvenanceError(MIError):
|
|
128
|
+
"""Provenance verification failed."""
|
|
129
|
+
code = "provenance_error"
|
|
130
|
+
|
|
131
|
+
def __init__(self, message: str = "Provenance verification failed"):
|
|
132
|
+
super().__init__(message)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class ValidationError(MIError):
|
|
136
|
+
"""Request payload failed server-side validation."""
|
|
137
|
+
code = "validation_error"
|
|
138
|
+
field: str = "" # which field failed
|
|
139
|
+
|
|
140
|
+
def __init__(
|
|
141
|
+
self,
|
|
142
|
+
message: str = "Validation error",
|
|
143
|
+
field: str = "",
|
|
144
|
+
):
|
|
145
|
+
super().__init__(message)
|
|
146
|
+
self.field = field
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class PaymentRequiredError(MIError):
|
|
150
|
+
"""Payment required - license upgrade needed."""
|
|
151
|
+
code = "payment_required"
|
|
152
|
+
|
|
153
|
+
def __init__(self, message: str = "License upgrade required"):
|
|
154
|
+
super().__init__(message)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class PermissionError(MIError):
|
|
158
|
+
"""Permission denied for this operation."""
|
|
159
|
+
code = "permission_error"
|
|
160
|
+
|
|
161
|
+
def __init__(self, message: str = "Permission denied"):
|
|
162
|
+
super().__init__(message)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class NotFoundError(MIError):
|
|
166
|
+
"""Resource not found."""
|
|
167
|
+
code = "not_found"
|
|
168
|
+
|
|
169
|
+
def __init__(self, message: str = "Resource not found"):
|
|
170
|
+
super().__init__(message)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ConflictError(MIError):
|
|
174
|
+
"""Resource conflict (e.g., duplicate UMO)."""
|
|
175
|
+
code = "conflict"
|
|
176
|
+
|
|
177
|
+
def __init__(self, message: str = "Resource conflict"):
|
|
178
|
+
super().__init__(message)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class ServerError(MIError):
|
|
182
|
+
"""Unrecoverable server-side error."""
|
|
183
|
+
code = "server_error"
|
|
184
|
+
request_id: str = "" # from X-MI-Request-ID response header
|
|
185
|
+
|
|
186
|
+
def __init__(
|
|
187
|
+
self,
|
|
188
|
+
message: str = "Server error",
|
|
189
|
+
request_id: str = "",
|
|
190
|
+
):
|
|
191
|
+
super().__init__(message)
|
|
192
|
+
self.request_id = request_id
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ConnectionError(MIError):
|
|
196
|
+
"""Could not reach the API."""
|
|
197
|
+
code = "connection_error"
|
|
198
|
+
|
|
199
|
+
def __init__(self, message: str = "Could not connect to API"):
|
|
200
|
+
super().__init__(message)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class TimeoutError(MIError):
|
|
204
|
+
"""Request timed out."""
|
|
205
|
+
code = "timeout"
|
|
206
|
+
|
|
207
|
+
def __init__(self, message: str = "Request timed out"):
|
|
208
|
+
super().__init__(message)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# HTTP status code to exception mapping
|
|
212
|
+
# Used by _http.py to raise appropriate exceptions
|
|
213
|
+
HTTP_STATUS_EXCEPTIONS: dict[int, type[MIError]] = {
|
|
214
|
+
400: ValidationError,
|
|
215
|
+
401: AuthenticationError,
|
|
216
|
+
402: PaymentRequiredError,
|
|
217
|
+
403: PermissionError,
|
|
218
|
+
404: NotFoundError,
|
|
219
|
+
409: ConflictError,
|
|
220
|
+
422: ValidationError,
|
|
221
|
+
429: RateLimitError,
|
|
222
|
+
451: PIIViolationError,
|
|
223
|
+
500: ServerError,
|
|
224
|
+
502: ServerError,
|
|
225
|
+
503: ServerError,
|
|
226
|
+
504: ServerError,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def get_exception_for_status(status_code: int) -> type[MIError]:
|
|
231
|
+
"""Get the appropriate exception class for an HTTP status code."""
|
|
232
|
+
return HTTP_STATUS_EXCEPTIONS.get(status_code, ServerError)
|