miso-client 0.1.0__py3-none-any.whl → 0.4.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.

Potentially problematic release.


This version of miso-client might be problematic. Click here for more details.

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