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.
@@ -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."""