miso-client 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.
- miso_client/__init__.py +489 -0
- miso_client/errors.py +44 -0
- miso_client/models/__init__.py +1 -0
- miso_client/models/config.py +174 -0
- miso_client/py.typed +0 -0
- miso_client/services/__init__.py +20 -0
- miso_client/services/auth.py +160 -0
- miso_client/services/cache.py +204 -0
- miso_client/services/encryption.py +93 -0
- miso_client/services/logger.py +457 -0
- miso_client/services/permission.py +208 -0
- miso_client/services/redis.py +179 -0
- miso_client/services/role.py +180 -0
- miso_client/utils/__init__.py +15 -0
- miso_client/utils/config_loader.py +87 -0
- miso_client/utils/data_masker.py +156 -0
- miso_client/utils/http_client.py +377 -0
- miso_client/utils/jwt_tools.py +78 -0
- miso_client-0.1.0.dist-info/METADATA +551 -0
- miso_client-0.1.0.dist-info/RECORD +23 -0
- miso_client-0.1.0.dist-info/WHEEL +5 -0
- miso_client-0.1.0.dist-info/licenses/LICENSE +21 -0
- miso_client-0.1.0.dist-info/top_level.txt +1 -0
miso_client/__init__.py
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MisoClient SDK - Python client for AI Fabrix authentication, authorization, and logging.
|
|
3
|
+
|
|
4
|
+
This package provides a reusable client SDK for integrating with the Miso Controller
|
|
5
|
+
for authentication, role-based access control, permission management, and logging.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from .models.config import (
|
|
11
|
+
RedisConfig,
|
|
12
|
+
MisoClientConfig,
|
|
13
|
+
UserInfo,
|
|
14
|
+
AuthResult,
|
|
15
|
+
LogEntry,
|
|
16
|
+
RoleResult,
|
|
17
|
+
PermissionResult,
|
|
18
|
+
ClientTokenResponse,
|
|
19
|
+
PerformanceMetrics,
|
|
20
|
+
ClientLoggingOptions,
|
|
21
|
+
)
|
|
22
|
+
from .services.auth import AuthService
|
|
23
|
+
from .services.role import RoleService
|
|
24
|
+
from .services.permission import PermissionService
|
|
25
|
+
from .services.logger import LoggerService, LoggerChain
|
|
26
|
+
from .services.redis import RedisService
|
|
27
|
+
from .services.encryption import EncryptionService
|
|
28
|
+
from .services.cache import CacheService
|
|
29
|
+
from .utils.http_client import HttpClient
|
|
30
|
+
from .utils.config_loader import load_config
|
|
31
|
+
from .errors import (
|
|
32
|
+
MisoClientError,
|
|
33
|
+
AuthenticationError,
|
|
34
|
+
AuthorizationError,
|
|
35
|
+
ConnectionError,
|
|
36
|
+
ConfigurationError,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__version__ = "0.1.0"
|
|
40
|
+
__author__ = "AI Fabrix Team"
|
|
41
|
+
__license__ = "MIT"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MisoClient:
|
|
45
|
+
"""
|
|
46
|
+
Main MisoClient SDK class for authentication, authorization, and logging.
|
|
47
|
+
|
|
48
|
+
This client provides a unified interface for:
|
|
49
|
+
- Token validation and user management
|
|
50
|
+
- Role-based access control
|
|
51
|
+
- Permission management
|
|
52
|
+
- Application logging with Redis caching
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, config: MisoClientConfig):
|
|
56
|
+
"""
|
|
57
|
+
Initialize MisoClient with configuration.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
config: MisoClient configuration including controller URL, client credentials, etc.
|
|
61
|
+
"""
|
|
62
|
+
self.config = config
|
|
63
|
+
self.http_client = HttpClient(config)
|
|
64
|
+
self.redis = RedisService(config.redis)
|
|
65
|
+
# Cache service (uses Redis if available, falls back to in-memory)
|
|
66
|
+
self.cache = CacheService(self.redis)
|
|
67
|
+
self.auth = AuthService(self.http_client, self.redis)
|
|
68
|
+
self.roles = RoleService(self.http_client, self.cache)
|
|
69
|
+
self.permissions = PermissionService(self.http_client, self.cache)
|
|
70
|
+
self.logger = LoggerService(self.http_client, self.redis)
|
|
71
|
+
# Encryption service (reads ENCRYPTION_KEY from environment by default)
|
|
72
|
+
self.encryption = EncryptionService()
|
|
73
|
+
self.initialized = False
|
|
74
|
+
|
|
75
|
+
async def initialize(self) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Initialize the client (connect to Redis if configured).
|
|
78
|
+
|
|
79
|
+
This method should be called before using the client. It will attempt
|
|
80
|
+
to connect to Redis if configured, but will gracefully fall back to
|
|
81
|
+
controller-only mode if Redis is unavailable.
|
|
82
|
+
"""
|
|
83
|
+
if self.initialized:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
await self.redis.connect()
|
|
88
|
+
self.initialized = True
|
|
89
|
+
except Exception:
|
|
90
|
+
# Redis connection failed, continue with controller fallback mode
|
|
91
|
+
self.initialized = True # Still mark as initialized for fallback mode
|
|
92
|
+
|
|
93
|
+
async def disconnect(self) -> None:
|
|
94
|
+
"""Disconnect from Redis."""
|
|
95
|
+
await self.redis.disconnect()
|
|
96
|
+
await self.http_client.close()
|
|
97
|
+
self.initialized = False
|
|
98
|
+
|
|
99
|
+
def is_initialized(self) -> bool:
|
|
100
|
+
"""Check if client is initialized."""
|
|
101
|
+
return self.initialized
|
|
102
|
+
|
|
103
|
+
# ==================== AUTHENTICATION METHODS ====================
|
|
104
|
+
|
|
105
|
+
def get_token(self, req: dict) -> str | None:
|
|
106
|
+
"""
|
|
107
|
+
Extract Bearer token from request headers.
|
|
108
|
+
|
|
109
|
+
Supports common request object patterns (dict with headers).
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
req: Request object with headers dict containing 'authorization' key
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Bearer token string or None if not found
|
|
116
|
+
"""
|
|
117
|
+
headers_obj = req.get("headers", {}) if isinstance(req, dict) else getattr(req, "headers", {})
|
|
118
|
+
headers: dict[str, Any] = headers_obj if isinstance(headers_obj, dict) else {}
|
|
119
|
+
auth_value = headers.get("authorization") or headers.get("Authorization")
|
|
120
|
+
if not isinstance(auth_value, str):
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
# Support "Bearer <token>" format
|
|
124
|
+
if auth_value.startswith("Bearer "):
|
|
125
|
+
return auth_value[7:]
|
|
126
|
+
|
|
127
|
+
# If no Bearer prefix, assume the whole header is the token
|
|
128
|
+
return auth_value
|
|
129
|
+
|
|
130
|
+
async def get_environment_token(self) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Get environment token using client credentials.
|
|
133
|
+
|
|
134
|
+
This is called automatically by HttpClient but can be called manually.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Client token string
|
|
138
|
+
"""
|
|
139
|
+
return await self.auth.get_environment_token()
|
|
140
|
+
|
|
141
|
+
def login(self, redirect_uri: str) -> str:
|
|
142
|
+
"""
|
|
143
|
+
Initiate login flow by redirecting to controller.
|
|
144
|
+
|
|
145
|
+
Returns the login URL for browser redirect or manual navigation.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
redirect_uri: URI to redirect to after successful login
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Login URL string
|
|
152
|
+
"""
|
|
153
|
+
return self.auth.login(redirect_uri)
|
|
154
|
+
|
|
155
|
+
async def validate_token(self, token: str) -> bool:
|
|
156
|
+
"""
|
|
157
|
+
Validate token with controller.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
token: JWT token to validate
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
True if token is valid, False otherwise
|
|
164
|
+
"""
|
|
165
|
+
return await self.auth.validate_token(token)
|
|
166
|
+
|
|
167
|
+
async def get_user(self, token: str) -> UserInfo | None:
|
|
168
|
+
"""
|
|
169
|
+
Get user information from token.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
token: JWT token
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
UserInfo if token is valid, None otherwise
|
|
176
|
+
"""
|
|
177
|
+
return await self.auth.get_user(token)
|
|
178
|
+
|
|
179
|
+
async def get_user_info(self, token: str) -> UserInfo | None:
|
|
180
|
+
"""
|
|
181
|
+
Get user information from GET /api/auth/user endpoint.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
token: JWT token
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
UserInfo if token is valid, None otherwise
|
|
188
|
+
"""
|
|
189
|
+
return await self.auth.get_user_info(token)
|
|
190
|
+
|
|
191
|
+
async def is_authenticated(self, token: str) -> bool:
|
|
192
|
+
"""
|
|
193
|
+
Check if user is authenticated.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
token: JWT token
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
True if user is authenticated, False otherwise
|
|
200
|
+
"""
|
|
201
|
+
return await self.auth.is_authenticated(token)
|
|
202
|
+
|
|
203
|
+
async def logout(self) -> None:
|
|
204
|
+
"""Logout user."""
|
|
205
|
+
return await self.auth.logout()
|
|
206
|
+
|
|
207
|
+
# ==================== AUTHORIZATION METHODS ====================
|
|
208
|
+
|
|
209
|
+
async def get_roles(self, token: str) -> list[str]:
|
|
210
|
+
"""
|
|
211
|
+
Get user roles (cached in Redis if available).
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
token: JWT token
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
List of user roles
|
|
218
|
+
"""
|
|
219
|
+
return await self.roles.get_roles(token)
|
|
220
|
+
|
|
221
|
+
async def has_role(self, token: str, role: str) -> bool:
|
|
222
|
+
"""
|
|
223
|
+
Check if user has specific role.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
token: JWT token
|
|
227
|
+
role: Role to check
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
True if user has the role, False otherwise
|
|
231
|
+
"""
|
|
232
|
+
return await self.roles.has_role(token, role)
|
|
233
|
+
|
|
234
|
+
async def has_any_role(self, token: str, roles: list[str]) -> bool:
|
|
235
|
+
"""
|
|
236
|
+
Check if user has any of the specified roles.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
token: JWT token
|
|
240
|
+
roles: List of roles to check
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
True if user has any of the roles, False otherwise
|
|
244
|
+
"""
|
|
245
|
+
return await self.roles.has_any_role(token, roles)
|
|
246
|
+
|
|
247
|
+
async def has_all_roles(self, token: str, roles: list[str]) -> bool:
|
|
248
|
+
"""
|
|
249
|
+
Check if user has all of the specified roles.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
token: JWT token
|
|
253
|
+
roles: List of roles to check
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
True if user has all roles, False otherwise
|
|
257
|
+
"""
|
|
258
|
+
return await self.roles.has_all_roles(token, roles)
|
|
259
|
+
|
|
260
|
+
async def refresh_roles(self, token: str) -> list[str]:
|
|
261
|
+
"""
|
|
262
|
+
Force refresh roles from controller (bypass cache).
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
token: JWT token
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Fresh list of user roles
|
|
269
|
+
"""
|
|
270
|
+
return await self.roles.refresh_roles(token)
|
|
271
|
+
|
|
272
|
+
async def get_permissions(self, token: str) -> list[str]:
|
|
273
|
+
"""
|
|
274
|
+
Get user permissions (cached in Redis if available).
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
token: JWT token
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
List of user permissions
|
|
281
|
+
"""
|
|
282
|
+
return await self.permissions.get_permissions(token)
|
|
283
|
+
|
|
284
|
+
async def has_permission(self, token: str, permission: str) -> bool:
|
|
285
|
+
"""
|
|
286
|
+
Check if user has specific permission.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
token: JWT token
|
|
290
|
+
permission: Permission to check
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
True if user has the permission, False otherwise
|
|
294
|
+
"""
|
|
295
|
+
return await self.permissions.has_permission(token, permission)
|
|
296
|
+
|
|
297
|
+
async def has_any_permission(self, token: str, permissions: list[str]) -> bool:
|
|
298
|
+
"""
|
|
299
|
+
Check if user has any of the specified permissions.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
token: JWT token
|
|
303
|
+
permissions: List of permissions to check
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
True if user has any of the permissions, False otherwise
|
|
307
|
+
"""
|
|
308
|
+
return await self.permissions.has_any_permission(token, permissions)
|
|
309
|
+
|
|
310
|
+
async def has_all_permissions(self, token: str, permissions: list[str]) -> bool:
|
|
311
|
+
"""
|
|
312
|
+
Check if user has all of the specified permissions.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
token: JWT token
|
|
316
|
+
permissions: List of permissions to check
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
True if user has all permissions, False otherwise
|
|
320
|
+
"""
|
|
321
|
+
return await self.permissions.has_all_permissions(token, permissions)
|
|
322
|
+
|
|
323
|
+
async def refresh_permissions(self, token: str) -> list[str]:
|
|
324
|
+
"""
|
|
325
|
+
Force refresh permissions from controller (bypass cache).
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
token: JWT token
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Fresh list of user permissions
|
|
332
|
+
"""
|
|
333
|
+
return await self.permissions.refresh_permissions(token)
|
|
334
|
+
|
|
335
|
+
async def clear_permissions_cache(self, token: str) -> None:
|
|
336
|
+
"""
|
|
337
|
+
Clear cached permissions for a user.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
token: JWT token
|
|
341
|
+
"""
|
|
342
|
+
return await self.permissions.clear_permissions_cache(token)
|
|
343
|
+
|
|
344
|
+
# ==================== LOGGING METHODS ====================
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def log(self) -> LoggerService:
|
|
348
|
+
"""
|
|
349
|
+
Get logger service for application logging.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
LoggerService instance
|
|
353
|
+
"""
|
|
354
|
+
return self.logger
|
|
355
|
+
|
|
356
|
+
# ==================== ENCRYPTION METHODS ====================
|
|
357
|
+
|
|
358
|
+
def encrypt(self, plaintext: str) -> str:
|
|
359
|
+
"""
|
|
360
|
+
Encrypt sensitive data.
|
|
361
|
+
|
|
362
|
+
Convenience method that delegates to encryption service.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
plaintext: Plain text string to encrypt
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Base64-encoded encrypted string
|
|
369
|
+
"""
|
|
370
|
+
return self.encryption.encrypt(plaintext)
|
|
371
|
+
|
|
372
|
+
def decrypt(self, encrypted_text: str) -> str:
|
|
373
|
+
"""
|
|
374
|
+
Decrypt sensitive data.
|
|
375
|
+
|
|
376
|
+
Convenience method that delegates to encryption service.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
encrypted_text: Base64-encoded encrypted string
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Decrypted plain text string
|
|
383
|
+
"""
|
|
384
|
+
return self.encryption.decrypt(encrypted_text)
|
|
385
|
+
|
|
386
|
+
# ==================== CACHING METHODS ====================
|
|
387
|
+
|
|
388
|
+
async def cache_get(self, key: str) -> Optional[Any]:
|
|
389
|
+
"""
|
|
390
|
+
Get cached value.
|
|
391
|
+
|
|
392
|
+
Convenience method that delegates to cache service.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
key: Cache key
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Cached value if found, None otherwise
|
|
399
|
+
"""
|
|
400
|
+
return await self.cache.get(key)
|
|
401
|
+
|
|
402
|
+
async def cache_set(self, key: str, value: Any, ttl: int) -> bool:
|
|
403
|
+
"""
|
|
404
|
+
Set cached value with TTL.
|
|
405
|
+
|
|
406
|
+
Convenience method that delegates to cache service.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
key: Cache key
|
|
410
|
+
value: Value to cache
|
|
411
|
+
ttl: Time to live in seconds
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
True if successful, False otherwise
|
|
415
|
+
"""
|
|
416
|
+
return await self.cache.set(key, value, ttl)
|
|
417
|
+
|
|
418
|
+
async def cache_delete(self, key: str) -> bool:
|
|
419
|
+
"""
|
|
420
|
+
Delete cached value.
|
|
421
|
+
|
|
422
|
+
Convenience method that delegates to cache service.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
key: Cache key
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
True if deleted, False otherwise
|
|
429
|
+
"""
|
|
430
|
+
return await self.cache.delete(key)
|
|
431
|
+
|
|
432
|
+
async def cache_clear(self) -> None:
|
|
433
|
+
"""
|
|
434
|
+
Clear all cached values.
|
|
435
|
+
|
|
436
|
+
Convenience method that delegates to cache service.
|
|
437
|
+
"""
|
|
438
|
+
await self.cache.clear()
|
|
439
|
+
|
|
440
|
+
# ==================== UTILITY METHODS ====================
|
|
441
|
+
|
|
442
|
+
def get_config(self) -> MisoClientConfig:
|
|
443
|
+
"""
|
|
444
|
+
Get current configuration.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
Copy of current configuration
|
|
448
|
+
"""
|
|
449
|
+
return self.config.model_copy()
|
|
450
|
+
|
|
451
|
+
def is_redis_connected(self) -> bool:
|
|
452
|
+
"""
|
|
453
|
+
Check if Redis is connected.
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
True if Redis is connected, False otherwise
|
|
457
|
+
"""
|
|
458
|
+
return self.redis.is_connected()
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
# Export types
|
|
462
|
+
__all__ = [
|
|
463
|
+
"MisoClient",
|
|
464
|
+
"RedisConfig",
|
|
465
|
+
"MisoClientConfig",
|
|
466
|
+
"UserInfo",
|
|
467
|
+
"AuthResult",
|
|
468
|
+
"LogEntry",
|
|
469
|
+
"RoleResult",
|
|
470
|
+
"PermissionResult",
|
|
471
|
+
"ClientTokenResponse",
|
|
472
|
+
"PerformanceMetrics",
|
|
473
|
+
"ClientLoggingOptions",
|
|
474
|
+
"AuthService",
|
|
475
|
+
"RoleService",
|
|
476
|
+
"PermissionService",
|
|
477
|
+
"LoggerService",
|
|
478
|
+
"LoggerChain",
|
|
479
|
+
"RedisService",
|
|
480
|
+
"EncryptionService",
|
|
481
|
+
"CacheService",
|
|
482
|
+
"HttpClient",
|
|
483
|
+
"load_config",
|
|
484
|
+
"MisoClientError",
|
|
485
|
+
"AuthenticationError",
|
|
486
|
+
"AuthorizationError",
|
|
487
|
+
"ConnectionError",
|
|
488
|
+
"ConfigurationError",
|
|
489
|
+
]
|
miso_client/errors.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SDK exceptions and error handling.
|
|
3
|
+
|
|
4
|
+
This module defines custom exceptions for the MisoClient SDK.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MisoClientError(Exception):
|
|
9
|
+
"""Base exception for MisoClient SDK errors."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str, status_code: int | None = None, error_body: dict | None = None):
|
|
12
|
+
"""
|
|
13
|
+
Initialize MisoClient error.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
message: Error message
|
|
17
|
+
status_code: HTTP status code if applicable
|
|
18
|
+
error_body: Sanitized error response body (secrets masked)
|
|
19
|
+
"""
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.message = message
|
|
22
|
+
self.status_code = status_code
|
|
23
|
+
self.error_body = error_body if error_body is not None else None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AuthenticationError(MisoClientError):
|
|
27
|
+
"""Raised when authentication fails."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AuthorizationError(MisoClientError):
|
|
32
|
+
"""Raised when authorization check fails."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ConnectionError(MisoClientError):
|
|
37
|
+
"""Raised when connection to controller or Redis fails."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ConfigurationError(MisoClientError):
|
|
42
|
+
"""Raised when configuration is invalid."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Pydantic models for MisoClient configuration and data types."""
|