miso-client 0.1.0__py3-none-any.whl → 3.7.2__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.
Files changed (69) hide show
  1. miso_client/__init__.py +523 -130
  2. miso_client/api/__init__.py +35 -0
  3. miso_client/api/auth_api.py +367 -0
  4. miso_client/api/logs_api.py +91 -0
  5. miso_client/api/permissions_api.py +88 -0
  6. miso_client/api/roles_api.py +88 -0
  7. miso_client/api/types/__init__.py +75 -0
  8. miso_client/api/types/auth_types.py +183 -0
  9. miso_client/api/types/logs_types.py +71 -0
  10. miso_client/api/types/permissions_types.py +31 -0
  11. miso_client/api/types/roles_types.py +31 -0
  12. miso_client/errors.py +30 -4
  13. miso_client/models/__init__.py +4 -0
  14. miso_client/models/config.py +275 -72
  15. miso_client/models/error_response.py +39 -0
  16. miso_client/models/filter.py +255 -0
  17. miso_client/models/pagination.py +44 -0
  18. miso_client/models/sort.py +25 -0
  19. miso_client/services/__init__.py +6 -5
  20. miso_client/services/auth.py +496 -87
  21. miso_client/services/cache.py +42 -41
  22. miso_client/services/encryption.py +18 -17
  23. miso_client/services/logger.py +467 -328
  24. miso_client/services/logger_chain.py +288 -0
  25. miso_client/services/permission.py +130 -67
  26. miso_client/services/redis.py +28 -23
  27. miso_client/services/role.py +145 -62
  28. miso_client/utils/__init__.py +3 -3
  29. miso_client/utils/audit_log_queue.py +222 -0
  30. miso_client/utils/auth_strategy.py +88 -0
  31. miso_client/utils/auth_utils.py +65 -0
  32. miso_client/utils/circuit_breaker.py +125 -0
  33. miso_client/utils/client_token_manager.py +244 -0
  34. miso_client/utils/config_loader.py +88 -17
  35. miso_client/utils/controller_url_resolver.py +80 -0
  36. miso_client/utils/data_masker.py +104 -33
  37. miso_client/utils/environment_token.py +126 -0
  38. miso_client/utils/error_utils.py +216 -0
  39. miso_client/utils/fastapi_endpoints.py +166 -0
  40. miso_client/utils/filter.py +364 -0
  41. miso_client/utils/filter_applier.py +143 -0
  42. miso_client/utils/filter_parser.py +110 -0
  43. miso_client/utils/flask_endpoints.py +169 -0
  44. miso_client/utils/http_client.py +494 -262
  45. miso_client/utils/http_client_logging.py +352 -0
  46. miso_client/utils/http_client_logging_helpers.py +197 -0
  47. miso_client/utils/http_client_query_helpers.py +138 -0
  48. miso_client/utils/http_error_handler.py +92 -0
  49. miso_client/utils/http_log_formatter.py +115 -0
  50. miso_client/utils/http_log_masker.py +203 -0
  51. miso_client/utils/internal_http_client.py +435 -0
  52. miso_client/utils/jwt_tools.py +125 -16
  53. miso_client/utils/logger_helpers.py +206 -0
  54. miso_client/utils/logging_helpers.py +70 -0
  55. miso_client/utils/origin_validator.py +128 -0
  56. miso_client/utils/pagination.py +275 -0
  57. miso_client/utils/request_context.py +285 -0
  58. miso_client/utils/sensitive_fields_loader.py +116 -0
  59. miso_client/utils/sort.py +116 -0
  60. miso_client/utils/token_utils.py +114 -0
  61. miso_client/utils/url_validator.py +66 -0
  62. miso_client/utils/user_token_refresh.py +245 -0
  63. miso_client-3.7.2.dist-info/METADATA +1021 -0
  64. miso_client-3.7.2.dist-info/RECORD +68 -0
  65. miso_client-0.1.0.dist-info/METADATA +0 -551
  66. miso_client-0.1.0.dist-info/RECORD +0 -23
  67. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/WHEEL +0 -0
  68. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/licenses/LICENSE +0 -0
  69. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,92 @@
1
+ """
2
+ HTTP error handler utilities for InternalHttpClient.
3
+
4
+ This module provides error parsing and handling functionality for HTTP responses.
5
+ """
6
+
7
+ from typing import Optional
8
+
9
+ import httpx
10
+
11
+ from ..models.error_response import ErrorResponse
12
+
13
+
14
+ def extract_correlation_id_from_response(
15
+ response: Optional[httpx.Response] = None,
16
+ ) -> Optional[str]:
17
+ """
18
+ Extract correlation ID from response headers.
19
+
20
+ Checks common correlation ID header names.
21
+
22
+ Args:
23
+ response: HTTP response object (optional)
24
+
25
+ Returns:
26
+ Correlation ID string if found, None otherwise
27
+ """
28
+ if not response:
29
+ return None
30
+
31
+ # Check common correlation ID header names (case-insensitive)
32
+ correlation_headers = [
33
+ "x-correlation-id",
34
+ "x-request-id",
35
+ "correlation-id",
36
+ "correlationId",
37
+ "x-correlationid",
38
+ "request-id",
39
+ ]
40
+
41
+ for header_name in correlation_headers:
42
+ correlation_id = response.headers.get(header_name) or response.headers.get(
43
+ header_name.lower()
44
+ )
45
+ if correlation_id:
46
+ return str(correlation_id)
47
+
48
+ return None
49
+
50
+
51
+ def parse_error_response(response: httpx.Response, url: str) -> Optional[ErrorResponse]:
52
+ """
53
+ Parse structured error response from HTTP response.
54
+
55
+ Extracts correlation ID from response headers if not present in response body.
56
+
57
+ Args:
58
+ response: HTTP response object
59
+ url: Request URL (used for instance URI if not in response)
60
+
61
+ Returns:
62
+ ErrorResponse if response matches structure, None otherwise
63
+ """
64
+ if not response.headers.get("content-type", "").startswith("application/json"):
65
+ return None
66
+
67
+ try:
68
+ response_data = response.json()
69
+ # Check if response matches ErrorResponse structure
70
+ if (
71
+ isinstance(response_data, dict)
72
+ and "errors" in response_data
73
+ and "type" in response_data
74
+ and "title" in response_data
75
+ and "statusCode" in response_data
76
+ ):
77
+ # Set instance from URL if not provided
78
+ if "instance" not in response_data or not response_data["instance"]:
79
+ response_data["instance"] = url
80
+
81
+ # Extract correlation ID from headers if not present in response body
82
+ if "correlationId" not in response_data or not response_data["correlationId"]:
83
+ correlation_id = extract_correlation_id_from_response(response)
84
+ if correlation_id:
85
+ response_data["correlationId"] = correlation_id
86
+
87
+ return ErrorResponse(**response_data)
88
+ except (ValueError, TypeError, KeyError):
89
+ # JSON parsing failed or structure doesn't match
90
+ pass
91
+
92
+ return None
@@ -0,0 +1,115 @@
1
+ """
2
+ HTTP log formatting utilities for ISO 27001 compliant audit and debug logging.
3
+
4
+ This module provides formatting functions for building audit and debug log contexts.
5
+ All sensitive data should be masked before passing to these formatters.
6
+ """
7
+
8
+ from typing import Any, Dict, Optional
9
+
10
+
11
+ def _add_optional_fields(context: Dict[str, Any], **fields: Any) -> None:
12
+ """
13
+ Add optional fields to context dictionary if they are not None.
14
+
15
+ Args:
16
+ context: Context dictionary to add fields to
17
+ **fields: Optional fields to add (value is None if field should be skipped)
18
+ """
19
+ for key, value in fields.items():
20
+ if value is not None:
21
+ context[key] = value
22
+
23
+
24
+ def build_audit_context(
25
+ method: str,
26
+ url: str,
27
+ status_code: Optional[int],
28
+ duration_ms: int,
29
+ user_id: Optional[str],
30
+ request_size: Optional[int],
31
+ response_size: Optional[int],
32
+ error_message: Optional[str],
33
+ correlation_id: Optional[str] = None,
34
+ ) -> Dict[str, Any]:
35
+ """
36
+ Build audit context dictionary for logging.
37
+
38
+ Args:
39
+ method: HTTP method
40
+ url: Request URL
41
+ status_code: HTTP status code
42
+ duration_ms: Request duration in milliseconds
43
+ user_id: User ID if available
44
+ request_size: Request size in bytes (optional)
45
+ response_size: Response size in bytes (optional)
46
+ error_message: Error message if request failed (optional)
47
+ correlation_id: Correlation ID if available (optional)
48
+
49
+ Returns:
50
+ Audit context dictionary
51
+ """
52
+ audit_context: Dict[str, Any] = {
53
+ "method": method,
54
+ "url": url,
55
+ "statusCode": status_code,
56
+ "duration": duration_ms,
57
+ }
58
+ _add_optional_fields(
59
+ audit_context,
60
+ userId=user_id,
61
+ requestSize=request_size,
62
+ responseSize=response_size,
63
+ error=error_message,
64
+ correlationId=correlation_id,
65
+ )
66
+ return audit_context
67
+
68
+
69
+ def build_debug_context(
70
+ method: str,
71
+ url: str,
72
+ status_code: Optional[int],
73
+ duration_ms: int,
74
+ base_url: str,
75
+ user_id: Optional[str],
76
+ masked_headers: Optional[Dict[str, Any]],
77
+ masked_body: Optional[Any],
78
+ masked_response: Optional[str],
79
+ query_params: Optional[Dict[str, Any]],
80
+ ) -> Dict[str, Any]:
81
+ """
82
+ Build debug context dictionary for detailed logging.
83
+
84
+ Args:
85
+ method: HTTP method
86
+ url: Request URL
87
+ status_code: HTTP status code
88
+ duration_ms: Request duration in milliseconds
89
+ base_url: Base URL from config
90
+ user_id: User ID if available
91
+ masked_headers: Masked request headers
92
+ masked_body: Masked request body
93
+ masked_response: Masked response body
94
+ query_params: Masked query parameters
95
+
96
+ Returns:
97
+ Debug context dictionary
98
+ """
99
+ debug_context: Dict[str, Any] = {
100
+ "method": method,
101
+ "url": url,
102
+ "statusCode": status_code,
103
+ "duration": duration_ms,
104
+ "baseURL": base_url,
105
+ "timeout": 30.0, # Default timeout
106
+ }
107
+ _add_optional_fields(
108
+ debug_context,
109
+ userId=user_id,
110
+ requestHeaders=masked_headers,
111
+ requestBody=masked_body,
112
+ responseBody=masked_response,
113
+ queryParams=query_params,
114
+ )
115
+ return debug_context
@@ -0,0 +1,203 @@
1
+ """
2
+ HTTP log data masking utilities for ISO 27001 compliant logging.
3
+
4
+ This module provides data masking functions specifically for HTTP request/response
5
+ logging. All sensitive data is masked using DataMasker before logging.
6
+ """
7
+
8
+ from typing import Any, Dict, Optional
9
+ from urllib.parse import parse_qs, urlparse
10
+
11
+ from .data_masker import DataMasker
12
+
13
+
14
+ def mask_error_message(error: Exception) -> Optional[str]:
15
+ """
16
+ Mask sensitive data in error message.
17
+
18
+ Args:
19
+ error: Exception object
20
+
21
+ Returns:
22
+ Masked error message string, or None if no error
23
+ """
24
+ if error is None:
25
+ return None
26
+
27
+ try:
28
+ error_message = str(error)
29
+ # Mask if error message contains sensitive keywords
30
+ if isinstance(error_message, str) and any(
31
+ keyword in error_message.lower() for keyword in ["password", "token", "secret", "key"]
32
+ ):
33
+ return DataMasker.MASKED_VALUE
34
+ return error_message
35
+ except Exception:
36
+ return None
37
+
38
+
39
+ def mask_request_data(
40
+ request_headers: Optional[Dict[str, Any]], request_data: Optional[Dict[str, Any]]
41
+ ) -> tuple[Optional[Dict[str, Any]], Optional[Any]]:
42
+ """
43
+ Mask sensitive data in request headers and body.
44
+
45
+ Args:
46
+ request_headers: Request headers dictionary
47
+ request_data: Request body data
48
+
49
+ Returns:
50
+ Tuple of (masked_headers, masked_body)
51
+ """
52
+ masked_headers: Optional[Dict[str, Any]] = None
53
+ if request_headers:
54
+ masked_headers = DataMasker.mask_sensitive_data(request_headers)
55
+
56
+ masked_body: Optional[Any] = None
57
+ if request_data is not None:
58
+ masked_body = DataMasker.mask_sensitive_data(request_data)
59
+
60
+ return masked_headers, masked_body
61
+
62
+
63
+ def extract_and_mask_query_params(url: str) -> Optional[Dict[str, Any]]:
64
+ """
65
+ Extract query parameters from URL and mask sensitive data.
66
+
67
+ Args:
68
+ url: Request URL with query string
69
+
70
+ Returns:
71
+ Masked query parameters dictionary, or None if no query params
72
+ """
73
+ try:
74
+ parsed_url = urlparse(url)
75
+ if not parsed_url.query:
76
+ return None
77
+
78
+ query_dict = parse_qs(parsed_url.query)
79
+ # Convert lists to single values for simplicity
80
+ query_simple: Dict[str, Any] = {
81
+ k: v[0] if len(v) == 1 else v for k, v in query_dict.items()
82
+ }
83
+ masked = DataMasker.mask_sensitive_data(query_simple)
84
+ return masked if isinstance(masked, dict) else None
85
+ except Exception:
86
+ return None
87
+
88
+
89
+ def estimate_object_size(obj: Any) -> int:
90
+ """
91
+ Quick size estimation without full JSON serialization.
92
+
93
+ Args:
94
+ obj: Object to estimate size for
95
+
96
+ Returns:
97
+ Estimated size in bytes
98
+ """
99
+ if obj is None:
100
+ return 0
101
+
102
+ if isinstance(obj, str):
103
+ return len(obj.encode("utf-8"))
104
+
105
+ if not isinstance(obj, (dict, list)):
106
+ return 10 # Estimate for primitives
107
+
108
+ if isinstance(obj, list):
109
+ if len(obj) == 0:
110
+ return 10
111
+ # Sample first few items for estimation
112
+ sample_size = min(3, len(obj))
113
+ estimated_item_size = sum(estimate_object_size(item) for item in obj[:sample_size])
114
+ avg_item_size = estimated_item_size / sample_size if sample_size > 0 else 100
115
+ return int(len(obj) * avg_item_size)
116
+
117
+ # Object: estimate based on property count and values
118
+ size = 0
119
+ for key, value in obj.items():
120
+ size += len(str(key).encode("utf-8")) + estimate_object_size(value)
121
+ return size
122
+
123
+
124
+ def truncate_response_body(body: Any, max_size: int = 10000) -> tuple[Any, bool]:
125
+ """
126
+ Truncate response body to reduce processing cost.
127
+
128
+ Args:
129
+ body: Response body to truncate
130
+ max_size: Maximum size in bytes
131
+
132
+ Returns:
133
+ Tuple of (truncated_data, was_truncated)
134
+ """
135
+ if body is None:
136
+ return body, False
137
+
138
+ # For strings, truncate directly
139
+ if isinstance(body, str):
140
+ body_bytes = body.encode("utf-8")
141
+ if len(body_bytes) <= max_size:
142
+ return body, False
143
+ truncated = body_bytes[:max_size].decode("utf-8", errors="ignore") + "..."
144
+ return truncated, True
145
+
146
+ # For objects/arrays, estimate size first
147
+ estimated_size = estimate_object_size(body)
148
+ if estimated_size <= max_size:
149
+ return body, False
150
+
151
+ # If estimated size is too large, return placeholder
152
+ return {
153
+ "_message": "Response body too large, truncated for performance",
154
+ "_estimatedSize": estimated_size,
155
+ }, True
156
+
157
+
158
+ def mask_response_data(
159
+ response: Optional[Any], max_size: Optional[int] = None, max_masking_size: Optional[int] = None
160
+ ) -> Optional[str]:
161
+ """
162
+ Mask sensitive data in response body and limit size.
163
+
164
+ Args:
165
+ response: Response data
166
+ max_size: Maximum size before truncation (default: 10000)
167
+ max_masking_size: Maximum size before skipping masking (default: 50000)
168
+
169
+ Returns:
170
+ Masked response body as string, or None
171
+ """
172
+ if response is None:
173
+ return None
174
+
175
+ max_size = max_size or 10000
176
+ max_masking_size = max_masking_size or 50000
177
+
178
+ try:
179
+ # Check if we should skip masking due to size
180
+ estimated_size = estimate_object_size(response)
181
+ if estimated_size > max_masking_size:
182
+ return str({" _message": "Response body too large, masking skipped"})
183
+
184
+ # Truncate if needed
185
+ truncated_body, was_truncated = truncate_response_body(response, max_size)
186
+
187
+ # Mask sensitive data
188
+ try:
189
+ if isinstance(truncated_body, dict):
190
+ masked_dict = DataMasker.mask_sensitive_data(truncated_body)
191
+ result = str(masked_dict)
192
+ if was_truncated and len(result) > 1000:
193
+ result = result[:1000] + "..."
194
+ return result
195
+ elif isinstance(truncated_body, str):
196
+ # Already truncated string
197
+ return truncated_body
198
+ else:
199
+ return str(truncated_body)
200
+ except Exception:
201
+ return str(truncated_body) if was_truncated else str(response)
202
+ except Exception:
203
+ return None