miso-client 0.2.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
@@ -26,6 +26,7 @@ from .models.config import (
26
26
  RoleResult,
27
27
  UserInfo,
28
28
  )
29
+ from .models.error_response import ErrorResponse
29
30
  from .services.auth import AuthService
30
31
  from .services.cache import CacheService
31
32
  from .services.encryption import EncryptionService
@@ -35,8 +36,9 @@ from .services.redis import RedisService
35
36
  from .services.role import RoleService
36
37
  from .utils.config_loader import load_config
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
 
@@ -60,14 +62,29 @@ class MisoClient:
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
@@ -473,6 +490,7 @@ __all__ = [
473
490
  "ClientTokenResponse",
474
491
  "PerformanceMetrics",
475
492
  "ClientLoggingOptions",
493
+ "ErrorResponse",
476
494
  "AuthService",
477
495
  "RoleService",
478
496
  "PermissionService",
miso_client/errors.py CHANGED
@@ -4,12 +4,21 @@ 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
15
 
11
16
  def __init__(
12
- self, message: str, status_code: int | None = None, error_body: dict | None = None
17
+ self,
18
+ message: str,
19
+ status_code: int | None = None,
20
+ error_body: dict | None = None,
21
+ error_response: "ErrorResponse | None" = None,
13
22
  ):
14
23
  """
15
24
  Initialize MisoClient error.
@@ -18,11 +27,23 @@ class MisoClientError(Exception):
18
27
  message: Error message
19
28
  status_code: HTTP status code if applicable
20
29
  error_body: Sanitized error response body (secrets masked)
30
+ error_response: Structured error response object (RFC 7807-style)
21
31
  """
22
32
  super().__init__(message)
23
33
  self.message = message
24
34
  self.status_code = status_code
25
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
26
47
 
27
48
 
28
49
  class AuthenticationError(MisoClientError):
@@ -1 +1,5 @@
1
1
  """Pydantic models for MisoClient configuration and data types."""
2
+
3
+ from .error_response import ErrorResponse
4
+
5
+ __all__ = ["ErrorResponse"]
@@ -0,0 +1,41 @@
1
+ """
2
+ Structured error response model following RFC 7807-style format.
3
+
4
+ This module provides a generic error response interface that can be used
5
+ across different applications for consistent error handling.
6
+ """
7
+
8
+ from typing import List, Optional
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+
13
+ class ErrorResponse(BaseModel):
14
+ """
15
+ Structured error response following RFC 7807-style format.
16
+
17
+ This model represents a standardized error response structure that includes:
18
+ - Multiple error messages
19
+ - Error type identifier
20
+ - Human-readable title
21
+ - HTTP status code
22
+ - Request instance URI (optional)
23
+
24
+ Example:
25
+ {
26
+ "errors": ["Error message 1", "Error message 2"],
27
+ "type": "/Errors/Bad Input",
28
+ "title": "Bad Request",
29
+ "statusCode": 400,
30
+ "instance": "/OpenApi/rest/Xzy"
31
+ }
32
+ """
33
+
34
+ errors: List[str] = Field(..., description="List of error messages")
35
+ type: str = Field(..., description="Error type URI (e.g., '/Errors/Bad Input')")
36
+ title: str = Field(..., description="Human-readable error title")
37
+ statusCode: int = Field(..., alias="status_code", description="HTTP status code")
38
+ instance: Optional[str] = Field(default=None, description="Request instance URI")
39
+
40
+ class Config:
41
+ populate_by_name = True # Allow both camelCase and snake_case
@@ -14,23 +14,23 @@ from typing import Any, Dict, Literal, Optional
14
14
  from ..models.config import ClientLoggingOptions, LogEntry
15
15
  from ..services.redis import RedisService
16
16
  from ..utils.data_masker import DataMasker
17
- from ..utils.http_client import HttpClient
17
+ from ..utils.internal_http_client import InternalHttpClient
18
18
  from ..utils.jwt_tools import decode_token
19
19
 
20
20
 
21
21
  class LoggerService:
22
22
  """Logger service for application logging and audit events."""
23
23
 
24
- def __init__(self, http_client: HttpClient, redis: RedisService):
24
+ def __init__(self, internal_http_client: InternalHttpClient, redis: RedisService):
25
25
  """
26
26
  Initialize logger service.
27
27
 
28
28
  Args:
29
- http_client: HTTP client instance
29
+ internal_http_client: Internal HTTP client instance (used for log sending)
30
30
  redis: Redis service instance
31
31
  """
32
- self.config = http_client.config
33
- self.http_client = http_client
32
+ self.config = internal_http_client.config
33
+ self.internal_http_client = internal_http_client
34
34
  self.redis = redis
35
35
  self.mask_sensitive_data = True # Default: mask sensitive data
36
36
  self.correlation_counter = 0
@@ -357,12 +357,13 @@ class LoggerService:
357
357
  return # Successfully queued in Redis
358
358
 
359
359
  # Fallback to unified logging endpoint with client credentials
360
+ # Use InternalHttpClient to avoid circular dependency with HttpClient
360
361
  try:
361
362
  # Backend extracts environment and application from client credentials
362
363
  log_payload = log_entry.model_dump(
363
364
  exclude={"environment", "application"}, exclude_none=True
364
365
  )
365
- await self.http_client.request("POST", "/api/logs", log_payload)
366
+ await self.internal_http_client.request("POST", "/api/logs", log_payload)
366
367
  except Exception:
367
368
  # Failed to send log to controller
368
369
  # Silently fail to avoid infinite logging loops
@@ -5,7 +5,9 @@ Implements ISO 27001 data protection controls by masking sensitive fields
5
5
  in log entries and context data.
6
6
  """
7
7
 
8
- from typing import Any, Set
8
+ from typing import Any, Optional, Set
9
+
10
+ from .sensitive_fields_loader import get_sensitive_fields_array
9
11
 
10
12
 
11
13
  class DataMasker:
@@ -13,8 +15,8 @@ class DataMasker:
13
15
 
14
16
  MASKED_VALUE = "***MASKED***"
15
17
 
16
- # Set of sensitive field names (normalized)
17
- _sensitive_fields: Set[str] = {
18
+ # Hardcoded set of sensitive field names (normalized) - fallback if JSON cannot be loaded
19
+ _hardcoded_sensitive_fields: Set[str] = {
18
20
  "password",
19
21
  "passwd",
20
22
  "pwd",
@@ -38,6 +40,73 @@ class DataMasker:
38
40
  "secretkey",
39
41
  }
40
42
 
43
+ # Cached merged sensitive fields (loaded on first use)
44
+ _sensitive_fields: Optional[Set[str]] = None
45
+ _config_loaded: bool = False
46
+
47
+ @classmethod
48
+ def _load_config(cls, config_path: Optional[str] = None) -> None:
49
+ """
50
+ Load sensitive fields configuration from JSON and merge with hardcoded defaults.
51
+
52
+ This method is called automatically on first use. It loads JSON configuration
53
+ and merges it with hardcoded defaults, ensuring backward compatibility.
54
+
55
+ Args:
56
+ config_path: Optional custom path to JSON config file
57
+ """
58
+ if cls._config_loaded:
59
+ return
60
+
61
+ # Start with hardcoded fields as base
62
+ merged_fields = set(cls._hardcoded_sensitive_fields)
63
+
64
+ try:
65
+ # Try to load fields from JSON configuration
66
+ json_fields = get_sensitive_fields_array(config_path)
67
+ if json_fields:
68
+ # Normalize and add JSON fields (same normalization as hardcoded fields)
69
+ for field in json_fields:
70
+ if isinstance(field, str):
71
+ # Normalize: lowercase and remove underscores/hyphens
72
+ normalized = field.lower().replace("_", "").replace("-", "")
73
+ merged_fields.add(normalized)
74
+ except Exception:
75
+ # If JSON loading fails, fall back to hardcoded fields only
76
+ pass
77
+
78
+ cls._sensitive_fields = merged_fields
79
+ cls._config_loaded = True
80
+
81
+ @classmethod
82
+ def _get_sensitive_fields(cls) -> Set[str]:
83
+ """
84
+ Get the set of sensitive fields (loads config on first call).
85
+
86
+ Returns:
87
+ Set of normalized sensitive field names
88
+ """
89
+ if not cls._config_loaded:
90
+ cls._load_config()
91
+ assert cls._sensitive_fields is not None
92
+ return cls._sensitive_fields
93
+
94
+ @classmethod
95
+ def set_config_path(cls, config_path: str) -> None:
96
+ """
97
+ Set custom path for sensitive fields configuration.
98
+
99
+ Must be called before first use of DataMasker methods if custom path is needed.
100
+ Otherwise, default path or environment variable will be used.
101
+
102
+ Args:
103
+ config_path: Path to JSON configuration file
104
+ """
105
+ # Reset cache to force reload with new path
106
+ cls._config_loaded = False
107
+ cls._sensitive_fields = None
108
+ cls._load_config(config_path)
109
+
41
110
  @classmethod
42
111
  def is_sensitive_field(cls, key: str) -> bool:
43
112
  """
@@ -52,12 +121,15 @@ class DataMasker:
52
121
  # Normalize key: lowercase and remove underscores/hyphens
53
122
  normalized_key = key.lower().replace("_", "").replace("-", "")
54
123
 
124
+ # Get sensitive fields (loads config on first use)
125
+ sensitive_fields = cls._get_sensitive_fields()
126
+
55
127
  # Check exact match
56
- if normalized_key in cls._sensitive_fields:
128
+ if normalized_key in sensitive_fields:
57
129
  return True
58
130
 
59
131
  # Check if field contains sensitive keywords
60
- for sensitive_field in cls._sensitive_fields:
132
+ for sensitive_field in sensitive_fields:
61
133
  if sensitive_field in normalized_key:
62
134
  return True
63
135