miso-client 0.2.0__py3-none-any.whl → 0.5.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,10 @@ from .models.config import (
26
26
  RoleResult,
27
27
  UserInfo,
28
28
  )
29
+ from .models.error_response import ErrorResponse
30
+ from .models.filter import FilterBuilder, FilterOperator, FilterOption, FilterQuery
31
+ from .models.pagination import Meta, PaginatedListResponse
32
+ from .models.sort import SortOption
29
33
  from .services.auth import AuthService
30
34
  from .services.cache import CacheService
31
35
  from .services.encryption import EncryptionService
@@ -34,9 +38,19 @@ from .services.permission import PermissionService
34
38
  from .services.redis import RedisService
35
39
  from .services.role import RoleService
36
40
  from .utils.config_loader import load_config
41
+ from .utils.error_utils import handle_api_error_snake_case, transform_error_to_snake_case
42
+ from .utils.filter import apply_filters, build_query_string, parse_filter_params
37
43
  from .utils.http_client import HttpClient
44
+ from .utils.internal_http_client import InternalHttpClient
45
+ from .utils.pagination import (
46
+ apply_pagination_to_array,
47
+ create_meta_object,
48
+ create_paginated_list_response,
49
+ parse_pagination_params,
50
+ )
51
+ from .utils.sort import build_sort_string, parse_sort_params
38
52
 
39
- __version__ = "0.1.0"
53
+ __version__ = "0.5.0"
40
54
  __author__ = "AI Fabrix Team"
41
55
  __license__ = "MIT"
42
56
 
@@ -60,14 +74,29 @@ class MisoClient:
60
74
  config: MisoClient configuration including controller URL, client credentials, etc.
61
75
  """
62
76
  self.config = config
63
- self.http_client = HttpClient(config)
77
+
78
+ # Create InternalHttpClient first (pure HTTP functionality, no logging)
79
+ self._internal_http_client = InternalHttpClient(config)
80
+
81
+ # Create Redis service
64
82
  self.redis = RedisService(config.redis)
83
+
84
+ # Create LoggerService with InternalHttpClient (to avoid circular dependency)
85
+ # LoggerService uses InternalHttpClient for sending logs to prevent audit loops
86
+ self.logger = LoggerService(self._internal_http_client, self.redis)
87
+
88
+ # Create public HttpClient wrapping InternalHttpClient with logger
89
+ # This HttpClient adds automatic ISO 27001 compliant audit and debug logging
90
+ self.http_client = HttpClient(config, self.logger)
91
+
65
92
  # Cache service (uses Redis if available, falls back to in-memory)
66
93
  self.cache = CacheService(self.redis)
94
+
95
+ # Services use public HttpClient (with audit logging)
67
96
  self.auth = AuthService(self.http_client, self.redis)
68
97
  self.roles = RoleService(self.http_client, self.cache)
69
98
  self.permissions = PermissionService(self.http_client, self.cache)
70
- self.logger = LoggerService(self.http_client, self.redis)
99
+
71
100
  # Encryption service (reads ENCRYPTION_KEY from environment by default)
72
101
  self.encryption = EncryptionService()
73
102
  self.initialized = False
@@ -473,6 +502,33 @@ __all__ = [
473
502
  "ClientTokenResponse",
474
503
  "PerformanceMetrics",
475
504
  "ClientLoggingOptions",
505
+ "ErrorResponse",
506
+ # Pagination models
507
+ "Meta",
508
+ "PaginatedListResponse",
509
+ # Filter models
510
+ "FilterOperator",
511
+ "FilterOption",
512
+ "FilterQuery",
513
+ "FilterBuilder",
514
+ # Sort models
515
+ "SortOption",
516
+ # Pagination utilities
517
+ "parse_pagination_params",
518
+ "create_meta_object",
519
+ "apply_pagination_to_array",
520
+ "create_paginated_list_response",
521
+ # Filter utilities
522
+ "parse_filter_params",
523
+ "build_query_string",
524
+ "apply_filters",
525
+ # Sort utilities
526
+ "parse_sort_params",
527
+ "build_sort_string",
528
+ # Error utilities
529
+ "transform_error_to_snake_case",
530
+ "handle_api_error_snake_case",
531
+ # Services
476
532
  "AuthService",
477
533
  "RoleService",
478
534
  "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,50 @@
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: Optional[str] = Field(default=None, 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
+ request_key: Optional[str] = Field(
40
+ default=None, alias="requestKey", description="Request key for error tracking"
41
+ )
42
+
43
+ class Config:
44
+ populate_by_name = True # Allow both camelCase and snake_case
45
+
46
+ # Support snake_case attribute access
47
+ @property
48
+ def status_code(self) -> int:
49
+ """Get statusCode as status_code (snake_case)."""
50
+ return self.statusCode
@@ -0,0 +1,140 @@
1
+ """
2
+ Filter types for MisoClient SDK.
3
+
4
+ This module contains Pydantic models and classes that define filter structures
5
+ for query filtering matching the Miso/Dataplane API conventions.
6
+ """
7
+
8
+ from typing import Any, List, Literal, Optional, Union
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+ FilterOperator = Literal[
13
+ "eq",
14
+ "neq",
15
+ "in",
16
+ "nin",
17
+ "gt",
18
+ "lt",
19
+ "gte",
20
+ "lte",
21
+ "contains",
22
+ "like",
23
+ ]
24
+
25
+
26
+ class FilterOption(BaseModel):
27
+ """
28
+ Single filter option with field, operator, and value.
29
+
30
+ Fields:
31
+ field: Field name to filter on
32
+ op: Filter operator (eq, neq, in, nin, gt, lt, gte, lte, contains, like)
33
+ value: Filter value (supports single values or arrays for 'in'/'nin' operators)
34
+ """
35
+
36
+ field: str = Field(..., description="Field name to filter on")
37
+ op: FilterOperator = Field(..., description="Filter operator")
38
+ value: Union[str, int, float, bool, List[Any]] = Field(
39
+ ..., description="Filter value (supports arrays for 'in'/'nin' operators)"
40
+ )
41
+
42
+
43
+ class FilterQuery(BaseModel):
44
+ """
45
+ Complete filter query with filters, sort, pagination, and field selection.
46
+
47
+ Fields:
48
+ filters: Optional list of filter options
49
+ sort: Optional list of sort options (field strings with optional '-' prefix for desc)
50
+ page: Optional page number (1-based)
51
+ page_size: Optional number of items per page
52
+ fields: Optional list of fields to include in response
53
+ """
54
+
55
+ filters: Optional[List[FilterOption]] = Field(
56
+ default=None, description="List of filter options"
57
+ )
58
+ sort: Optional[List[str]] = Field(
59
+ default=None,
60
+ description="List of sort options (e.g., ['-updated_at', 'created_at'])",
61
+ )
62
+ page: Optional[int] = Field(default=None, description="Page number (1-based)")
63
+ page_size: Optional[int] = Field(default=None, description="Number of items per page")
64
+ fields: Optional[List[str]] = Field(
65
+ default=None, description="List of fields to include in response"
66
+ )
67
+
68
+
69
+ class FilterBuilder:
70
+ """
71
+ Builder pattern for dynamic filter construction.
72
+
73
+ Allows chaining filter additions for building complex filter queries.
74
+ """
75
+
76
+ def __init__(self):
77
+ """Initialize empty filter builder."""
78
+ self._filters: List[FilterOption] = []
79
+
80
+ def add(self, field: str, op: FilterOperator, value: Any) -> "FilterBuilder":
81
+ """
82
+ Add a filter option to the builder.
83
+
84
+ Args:
85
+ field: Field name to filter on
86
+ op: Filter operator
87
+ value: Filter value
88
+
89
+ Returns:
90
+ FilterBuilder instance for method chaining
91
+ """
92
+ self._filters.append(FilterOption(field=field, op=op, value=value))
93
+ return self
94
+
95
+ def add_many(self, filters: List[FilterOption]) -> "FilterBuilder":
96
+ """
97
+ Add multiple filter options to the builder.
98
+
99
+ Args:
100
+ filters: List of FilterOption objects
101
+
102
+ Returns:
103
+ FilterBuilder instance for method chaining
104
+ """
105
+ self._filters.extend(filters)
106
+ return self
107
+
108
+ def build(self) -> List[FilterOption]:
109
+ """
110
+ Build the filter list.
111
+
112
+ Returns:
113
+ List of FilterOption objects
114
+ """
115
+ return self._filters.copy()
116
+
117
+ def to_query_string(self) -> str:
118
+ """
119
+ Convert filters to query string format.
120
+
121
+ Format: ?filter=field:op:value&filter=field:op:value
122
+
123
+ Returns:
124
+ Query string with filter parameters
125
+ """
126
+ if not self._filters:
127
+ return ""
128
+
129
+ query_parts: List[str] = []
130
+ for filter_option in self._filters:
131
+ # Format value for query string
132
+ if isinstance(filter_option.value, list):
133
+ # For arrays (in/nin), join with commas
134
+ value_str = ",".join(str(v) for v in filter_option.value)
135
+ else:
136
+ value_str = str(filter_option.value)
137
+
138
+ query_parts.append(f"filter={filter_option.field}:{filter_option.op}:{value_str}")
139
+
140
+ return "&".join(query_parts)
@@ -0,0 +1,66 @@
1
+ """
2
+ Pagination types for MisoClient SDK.
3
+
4
+ This module contains Pydantic models that define pagination structures
5
+ for paginated list responses matching the Miso/Dataplane API conventions.
6
+ """
7
+
8
+ from typing import Generic, List, TypeVar
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ class Meta(BaseModel):
16
+ """
17
+ Pagination metadata for list responses.
18
+
19
+ Fields:
20
+ total_items: Total number of items across all pages
21
+ current_page: Current page number (1-based, maps from `page` query param)
22
+ page_size: Number of items per page (maps from `page_size` query param)
23
+ type: Resource type identifier (e.g., 'item', 'user', 'group')
24
+ """
25
+
26
+ total_items: int = Field(..., alias="totalItems", description="Total number of items")
27
+ current_page: int = Field(..., alias="currentPage", description="Current page number (1-based)")
28
+ page_size: int = Field(..., alias="pageSize", description="Number of items per page")
29
+ type: str = Field(..., description="Resource type identifier")
30
+
31
+ class Config:
32
+ populate_by_name = True # Allow both snake_case and camelCase
33
+
34
+ # Support camelCase attribute access
35
+ @property
36
+ def totalItems(self) -> int:
37
+ """Get total_items as totalItems (camelCase)."""
38
+ return self.total_items
39
+
40
+ @property
41
+ def currentPage(self) -> int:
42
+ """Get current_page as currentPage (camelCase)."""
43
+ return self.current_page
44
+
45
+ @property
46
+ def pageSize(self) -> int:
47
+ """Get page_size as pageSize (camelCase)."""
48
+ return self.page_size
49
+
50
+
51
+ class PaginatedListResponse(BaseModel, Generic[T]):
52
+ """
53
+ Paginated list response structure.
54
+
55
+ Generic type parameter T represents the item type in the data array.
56
+
57
+ Fields:
58
+ meta: Pagination metadata
59
+ data: Array of items for current page
60
+ """
61
+
62
+ meta: Meta = Field(..., description="Pagination metadata")
63
+ data: List[T] = Field(..., description="Array of items for current page")
64
+
65
+ class Config:
66
+ populate_by_name = True # Allow both snake_case and camelCase
@@ -0,0 +1,25 @@
1
+ """
2
+ Sort types for MisoClient SDK.
3
+
4
+ This module contains Pydantic models that define sort structures
5
+ for query sorting matching the Miso/Dataplane API conventions.
6
+ """
7
+
8
+ from typing import Literal
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+ SortOrder = Literal["asc", "desc"]
13
+
14
+
15
+ class SortOption(BaseModel):
16
+ """
17
+ Sort option with field and order.
18
+
19
+ Fields:
20
+ field: Field name to sort by
21
+ order: Sort order ('asc' for ascending, 'desc' for descending)
22
+ """
23
+
24
+ field: str = Field(..., description="Field name to sort by")
25
+ order: SortOrder = Field(..., description="Sort order ('asc' or 'desc')")
@@ -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