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,352 @@
1
+ """
2
+ HTTP client logging utilities for ISO 27001 compliant audit and debug logging.
3
+
4
+ This module provides logging functionality extracted from HttpClient to keep
5
+ the main HTTP client class focused and within size limits. All sensitive data
6
+ is automatically masked using DataMasker before logging.
7
+ """
8
+
9
+ import time
10
+ from typing import Any, Dict, Optional
11
+
12
+ from .http_log_formatter import build_audit_context, build_debug_context
13
+ from .http_log_masker import (
14
+ extract_and_mask_query_params,
15
+ mask_error_message,
16
+ mask_request_data,
17
+ mask_response_data,
18
+ )
19
+
20
+
21
+ def should_skip_logging(url: str, config: Optional[Any] = None) -> bool:
22
+ """
23
+ Check if logging should be skipped for this URL.
24
+
25
+ Skips logging for /api/logs and /api/auth/token to prevent infinite loops.
26
+ Also checks audit config skipEndpoints.
27
+
28
+ Args:
29
+ url: Request URL
30
+ config: Optional MisoClientConfig to check audit.skipEndpoints
31
+
32
+ Returns:
33
+ True if logging should be skipped, False otherwise
34
+ """
35
+ # Check if audit is explicitly disabled
36
+ if config and config.audit and config.audit.enabled is False:
37
+ return True
38
+
39
+ # If no config or no audit config, default to enabled (don't skip)
40
+ # Only skip if explicitly disabled
41
+
42
+ # Check skip endpoints from config
43
+ if config and config.audit and config.audit.skipEndpoints:
44
+ for endpoint in config.audit.skipEndpoints:
45
+ if endpoint in url:
46
+ return True
47
+
48
+ # Default skip endpoints (always skip these regardless of config)
49
+ if url == "/api/v1/logs" or url.startswith("/api/v1/logs"):
50
+ return True
51
+
52
+ # Check configurable client token URI or default
53
+ client_token_uri = "/api/v1/auth/token"
54
+ if config and config.clientTokenUri:
55
+ client_token_uri = config.clientTokenUri
56
+
57
+ if url == client_token_uri or url.startswith(client_token_uri):
58
+ return True
59
+ return False
60
+
61
+
62
+ def calculate_request_metrics(
63
+ start_time: float, response: Optional[Any] = None, error: Optional[Exception] = None
64
+ ) -> tuple[int, Optional[int]]:
65
+ """
66
+ Calculate request duration and status code.
67
+
68
+ Args:
69
+ start_time: Request start time from time.perf_counter()
70
+ response: Response data (if successful)
71
+ error: Exception (if request failed)
72
+
73
+ Returns:
74
+ Tuple of (duration_ms, status_code)
75
+ """
76
+ duration_ms = int((time.perf_counter() - start_time) * 1000)
77
+
78
+ status_code: Optional[int] = None
79
+ if response is not None:
80
+ status_code = 200 # Default assumption if response exists
81
+ elif error is not None:
82
+ if hasattr(error, "status_code"):
83
+ status_code = error.status_code
84
+ else:
85
+ status_code = 500 # Default for errors
86
+
87
+ return duration_ms, status_code
88
+
89
+
90
+ def calculate_request_sizes(
91
+ request_data: Optional[Dict[str, Any]], response: Optional[Any]
92
+ ) -> tuple[Optional[int], Optional[int]]:
93
+ """
94
+ Calculate request and response sizes in bytes.
95
+
96
+ Args:
97
+ request_data: Request body data
98
+ response: Response data
99
+
100
+ Returns:
101
+ Tuple of (request_size, response_size) in bytes, None if unavailable
102
+ """
103
+ request_size: Optional[int] = None
104
+ if request_data is not None:
105
+ try:
106
+ request_str = str(request_data)
107
+ request_size = len(request_str.encode("utf-8"))
108
+ except Exception:
109
+ pass
110
+
111
+ response_size: Optional[int] = None
112
+ if response is not None:
113
+ try:
114
+ response_str = str(response)
115
+ response_size = len(response_str.encode("utf-8"))
116
+ except Exception:
117
+ pass
118
+
119
+ return request_size, response_size
120
+
121
+
122
+ def _prepare_audit_context(
123
+ method: str,
124
+ url: str,
125
+ response: Optional[Any],
126
+ error: Optional[Exception],
127
+ start_time: float,
128
+ request_data: Optional[Dict[str, Any]],
129
+ user_id: Optional[str],
130
+ log_level: str,
131
+ audit_config: Optional[Dict[str, Any]] = None,
132
+ correlation_id: Optional[str] = None,
133
+ ) -> Optional[Dict[str, Any]]:
134
+ """
135
+ Prepare audit context for logging.
136
+
137
+ Returns:
138
+ Audit context dictionary or None if logging should be skipped
139
+ """
140
+ duration_ms, status_code = calculate_request_metrics(start_time, response, error)
141
+
142
+ audit_config = audit_config or {}
143
+ audit_level = audit_config.get("level", "detailed")
144
+
145
+ request_size: Optional[int] = None
146
+ response_size: Optional[int] = None
147
+ if audit_level in ("detailed", "full"):
148
+ request_size, response_size = calculate_request_sizes(request_data, response)
149
+
150
+ error_message = mask_error_message(error) if error is not None else None
151
+ return build_audit_context(
152
+ method=method,
153
+ url=url,
154
+ status_code=status_code,
155
+ duration_ms=duration_ms,
156
+ user_id=user_id,
157
+ request_size=request_size,
158
+ response_size=response_size,
159
+ error_message=error_message,
160
+ correlation_id=correlation_id,
161
+ )
162
+
163
+
164
+ async def log_http_request_audit(
165
+ logger: Any,
166
+ method: str,
167
+ url: str,
168
+ response: Optional[Any],
169
+ error: Optional[Exception],
170
+ start_time: float,
171
+ request_data: Optional[Dict[str, Any]],
172
+ user_id: Optional[str],
173
+ log_level: str,
174
+ config: Optional[Any] = None,
175
+ ) -> None:
176
+ """
177
+ Log HTTP request audit event with ISO 27001 compliant data masking.
178
+
179
+ Supports configurable audit levels: minimal, standard, detailed, full.
180
+
181
+ Args:
182
+ logger: LoggerService instance
183
+ method: HTTP method
184
+ url: Request URL
185
+ response: Response data (if successful)
186
+ error: Exception (if request failed)
187
+ start_time: Request start time
188
+ request_data: Request body data
189
+ user_id: User ID if available
190
+ log_level: Log level configuration
191
+ config: Optional MisoClientConfig for audit configuration
192
+ """
193
+ try:
194
+ # Check if logging should be skipped
195
+ if should_skip_logging(url, config):
196
+ return
197
+
198
+ # Extract correlation ID from error if available
199
+ correlation_id: Optional[str] = None
200
+ if error:
201
+ from ..utils.error_utils import extract_correlation_id_from_error
202
+
203
+ correlation_id = extract_correlation_id_from_error(error)
204
+
205
+ if config and config.audit:
206
+ audit_config = config.audit
207
+ audit_level = audit_config.level or "detailed"
208
+ else:
209
+ audit_config = None
210
+ audit_level = "detailed"
211
+
212
+ # Minimal audit level - just metadata, no masking
213
+ if audit_level == "minimal":
214
+ duration_ms, status_code = calculate_request_metrics(start_time, response, error)
215
+ audit_context = {
216
+ "method": method,
217
+ "url": url,
218
+ "statusCode": status_code,
219
+ "duration": duration_ms,
220
+ }
221
+ if user_id:
222
+ audit_context["userId"] = user_id
223
+ if error:
224
+ audit_context["error"] = str(error)
225
+ if correlation_id:
226
+ audit_context["correlationId"] = correlation_id
227
+ action = f"http.request.{method.upper()}"
228
+ await logger.audit(action, url, audit_context)
229
+ return
230
+
231
+ # Standard, detailed, or full audit levels
232
+ # Convert AuditConfig to dict for _prepare_audit_context
233
+ audit_config_dict: Dict[str, Any] = {}
234
+ if audit_config:
235
+ if hasattr(audit_config, "model_dump"):
236
+ audit_config_dict = audit_config.model_dump()
237
+ elif hasattr(audit_config, "dict"):
238
+ audit_config_dict = audit_config.dict() # type: ignore[attr-defined]
239
+ prepared_context = _prepare_audit_context(
240
+ method,
241
+ url,
242
+ response,
243
+ error,
244
+ start_time,
245
+ request_data,
246
+ user_id,
247
+ log_level,
248
+ audit_config_dict,
249
+ correlation_id=correlation_id,
250
+ )
251
+ if prepared_context is None:
252
+ return
253
+
254
+ audit_context = prepared_context
255
+ action = f"http.request.{method.upper()}"
256
+ await logger.audit(action, url, audit_context)
257
+
258
+ except Exception:
259
+ # Silently swallow all logging errors - never break HTTP requests
260
+ pass
261
+
262
+
263
+ def _prepare_debug_context(
264
+ method: str,
265
+ url: str,
266
+ response: Optional[Any],
267
+ duration_ms: int,
268
+ status_code: Optional[int],
269
+ user_id: Optional[str],
270
+ request_data: Optional[Dict[str, Any]],
271
+ request_headers: Optional[Dict[str, Any]],
272
+ base_url: str,
273
+ max_response_size: Optional[int] = None,
274
+ ) -> Dict[str, Any]:
275
+ """
276
+ Prepare debug context for logging.
277
+
278
+ Returns:
279
+ Debug context dictionary
280
+ """
281
+ masked_headers, masked_body = mask_request_data(request_headers, request_data)
282
+ masked_response = mask_response_data(response, max_size=max_response_size)
283
+ query_params = extract_and_mask_query_params(url)
284
+
285
+ return build_debug_context(
286
+ method=method,
287
+ url=url,
288
+ status_code=status_code,
289
+ duration_ms=duration_ms,
290
+ base_url=base_url,
291
+ user_id=user_id,
292
+ masked_headers=masked_headers,
293
+ masked_body=masked_body,
294
+ masked_response=masked_response,
295
+ query_params=query_params,
296
+ )
297
+
298
+
299
+ async def log_http_request_debug(
300
+ logger: Any,
301
+ method: str,
302
+ url: str,
303
+ response: Optional[Any],
304
+ duration_ms: int,
305
+ status_code: Optional[int],
306
+ user_id: Optional[str],
307
+ request_data: Optional[Dict[str, Any]],
308
+ request_headers: Optional[Dict[str, Any]],
309
+ base_url: str,
310
+ config: Optional[Any] = None,
311
+ ) -> None:
312
+ """
313
+ Log detailed debug information for HTTP request.
314
+
315
+ All sensitive data is masked before logging.
316
+
317
+ Args:
318
+ logger: LoggerService instance
319
+ method: HTTP method
320
+ url: Request URL
321
+ response: Response data
322
+ duration_ms: Request duration in milliseconds
323
+ status_code: HTTP status code
324
+ user_id: User ID if available
325
+ request_data: Request body data
326
+ request_headers: Request headers
327
+ base_url: Base URL from config
328
+ """
329
+ try:
330
+ # Get maxResponseSize from audit config if available
331
+ max_response_size = None
332
+ if config and config.audit and hasattr(config.audit, "maxResponseSize"):
333
+ max_response_size = config.audit.maxResponseSize
334
+
335
+ debug_context = _prepare_debug_context(
336
+ method,
337
+ url,
338
+ response,
339
+ duration_ms,
340
+ status_code,
341
+ user_id,
342
+ request_data,
343
+ request_headers,
344
+ base_url,
345
+ max_response_size=max_response_size,
346
+ )
347
+ message = f"HTTP {method} {url} - Status: {status_code}, Duration: {duration_ms}ms"
348
+ await logger.debug(message, debug_context)
349
+
350
+ except Exception:
351
+ # Silently swallow all logging errors - never break HTTP requests
352
+ pass
@@ -0,0 +1,197 @@
1
+ """
2
+ HTTP client logging helper functions.
3
+
4
+ Extracted from http_client.py to reduce file size and improve maintainability.
5
+ """
6
+
7
+ import asyncio
8
+ import time
9
+ from typing import Any, Dict, Optional
10
+
11
+ from ..models.config import MisoClientConfig
12
+ from ..services.logger import LoggerService
13
+ from ..utils.jwt_tools import JwtTokenCache
14
+ from .http_client_logging import log_http_request_audit, log_http_request_debug
15
+
16
+
17
+ def handle_logging_task_error(task: asyncio.Task) -> None:
18
+ """
19
+ Handle errors in background logging tasks.
20
+
21
+ Silently swallows all exceptions to prevent logging errors from breaking requests.
22
+
23
+ Args:
24
+ task: The completed logging task
25
+ """
26
+ try:
27
+ exception = task.exception()
28
+ if exception:
29
+ # Silently swallow logging errors - never break HTTP requests
30
+ pass
31
+ except Exception:
32
+ # Task might not be done yet or other error - ignore
33
+ pass
34
+
35
+
36
+ async def wait_for_logging_tasks(logging_tasks: set[asyncio.Task], timeout: float = 0.5) -> None:
37
+ """
38
+ Wait for all pending logging tasks to complete.
39
+
40
+ Useful for tests to ensure logging has finished before assertions.
41
+
42
+ Args:
43
+ logging_tasks: Set of logging tasks
44
+ timeout: Maximum time to wait in seconds
45
+ """
46
+ if logging_tasks:
47
+ try:
48
+ await asyncio.wait_for(
49
+ asyncio.gather(*logging_tasks, return_exceptions=True),
50
+ timeout=timeout,
51
+ )
52
+ except asyncio.TimeoutError:
53
+ # Some tasks might still be running, that's okay
54
+ pass
55
+
56
+
57
+ def calculate_status_code(response: Optional[Any], error: Optional[Exception]) -> Optional[int]:
58
+ """
59
+ Calculate HTTP status code from response or error.
60
+
61
+ Args:
62
+ response: Response data (if successful)
63
+ error: Exception (if request failed)
64
+
65
+ Returns:
66
+ HTTP status code, or None if cannot determine
67
+ """
68
+ if response is not None:
69
+ return 200
70
+ if error is not None:
71
+ if hasattr(error, "status_code"):
72
+ status_code = getattr(error, "status_code", None)
73
+ if isinstance(status_code, int):
74
+ return status_code
75
+ return 500
76
+ return None
77
+
78
+
79
+ def extract_user_id_from_headers(
80
+ request_headers: Optional[Dict[str, Any]], jwt_cache: JwtTokenCache
81
+ ) -> Optional[str]:
82
+ """
83
+ Extract user ID from request headers.
84
+
85
+ Args:
86
+ request_headers: Request headers dictionary
87
+ jwt_cache: JWT token cache instance
88
+
89
+ Returns:
90
+ User ID if found, None otherwise
91
+ """
92
+ if request_headers:
93
+ return jwt_cache.extract_user_id_from_headers(request_headers)
94
+ return None
95
+
96
+
97
+ async def log_debug_if_enabled(
98
+ logger: LoggerService,
99
+ config: MisoClientConfig,
100
+ method: str,
101
+ url: str,
102
+ response: Optional[Any],
103
+ error: Optional[Exception],
104
+ start_time: float,
105
+ user_id: Optional[str],
106
+ request_data: Optional[Dict[str, Any]],
107
+ request_headers: Optional[Dict[str, Any]],
108
+ ) -> None:
109
+ """
110
+ Log debug details if debug logging is enabled.
111
+
112
+ Args:
113
+ logger: LoggerService instance
114
+ config: MisoClientConfig instance
115
+ method: HTTP method
116
+ url: Request URL
117
+ response: Response data (if successful)
118
+ error: Exception (if request failed)
119
+ start_time: Request start time
120
+ user_id: User ID if available
121
+ request_data: Request body data
122
+ request_headers: Request headers
123
+ """
124
+ if config.log_level != "debug":
125
+ return
126
+
127
+ duration_ms = int((time.perf_counter() - start_time) * 1000)
128
+ status_code = calculate_status_code(response, error)
129
+ await log_http_request_debug(
130
+ logger=logger,
131
+ method=method,
132
+ url=url,
133
+ response=response,
134
+ duration_ms=duration_ms,
135
+ status_code=status_code,
136
+ user_id=user_id,
137
+ request_data=request_data,
138
+ request_headers=request_headers,
139
+ base_url=config.controller_url,
140
+ config=config,
141
+ )
142
+
143
+
144
+ async def log_http_request(
145
+ logger: LoggerService,
146
+ config: MisoClientConfig,
147
+ jwt_cache: JwtTokenCache,
148
+ method: str,
149
+ url: str,
150
+ response: Optional[Any],
151
+ error: Optional[Exception],
152
+ start_time: float,
153
+ request_data: Optional[Dict[str, Any]],
154
+ request_headers: Optional[Dict[str, Any]],
155
+ ) -> None:
156
+ """
157
+ Log HTTP request with audit and optional debug logging.
158
+
159
+ Args:
160
+ logger: LoggerService instance
161
+ config: MisoClientConfig instance
162
+ jwt_cache: JWT token cache instance
163
+ method: HTTP method
164
+ url: Request URL
165
+ response: Response data (if successful)
166
+ error: Exception (if request failed)
167
+ start_time: Request start time
168
+ request_data: Request body data
169
+ request_headers: Request headers
170
+ """
171
+ user_id = extract_user_id_from_headers(request_headers, jwt_cache)
172
+
173
+ await log_http_request_audit(
174
+ logger=logger,
175
+ method=method,
176
+ url=url,
177
+ response=response,
178
+ error=error,
179
+ start_time=start_time,
180
+ request_data=request_data,
181
+ user_id=user_id,
182
+ log_level=config.log_level,
183
+ config=config,
184
+ )
185
+
186
+ await log_debug_if_enabled(
187
+ logger,
188
+ config,
189
+ method,
190
+ url,
191
+ response,
192
+ error,
193
+ start_time,
194
+ user_id,
195
+ request_data,
196
+ request_headers,
197
+ )
@@ -0,0 +1,138 @@
1
+ """
2
+ HTTP client query helpers for filters and pagination.
3
+
4
+ Extracted from http_client.py to reduce file size and improve maintainability.
5
+ """
6
+
7
+ from typing import Any, Dict, Optional, Union
8
+ from urllib.parse import parse_qs, urlparse
9
+
10
+ from ..models.filter import FilterBuilder, FilterQuery, JsonFilter
11
+ from ..models.pagination import PaginatedListResponse
12
+ from ..utils.filter import build_query_string
13
+
14
+
15
+ def parse_filter_query_string(query_string: str) -> Dict[str, Any]:
16
+ """
17
+ Parse filter query string into params dictionary.
18
+
19
+ Args:
20
+ query_string: Query string from FilterQuery
21
+
22
+ Returns:
23
+ Params dictionary with filters
24
+ """
25
+ query_params = parse_qs(query_string)
26
+ return {k: v[0] if len(v) == 1 else v for k, v in query_params.items()}
27
+
28
+
29
+ def merge_filter_params(kwargs: Dict[str, Any], filter_params: Dict[str, Any]) -> None:
30
+ """
31
+ Merge filter params with existing params.
32
+
33
+ Args:
34
+ kwargs: Request kwargs dictionary
35
+ filter_params: Filter params from FilterBuilder
36
+ """
37
+ existing_params = kwargs.get("params", {})
38
+ if existing_params:
39
+ merged_params = {**existing_params, **filter_params}
40
+ else:
41
+ merged_params = filter_params
42
+ kwargs["params"] = merged_params
43
+
44
+
45
+ def add_pagination_params(
46
+ kwargs: Dict[str, Any], page: Optional[int], page_size: Optional[int]
47
+ ) -> None:
48
+ """
49
+ Add pagination params to kwargs.
50
+
51
+ Args:
52
+ kwargs: Request kwargs dictionary
53
+ page: Optional page number (1-based)
54
+ page_size: Optional number of items per page
55
+ """
56
+ params = kwargs.get("params", {})
57
+ if page is not None:
58
+ params["page"] = page
59
+ if page_size is not None:
60
+ params["pageSize"] = page_size
61
+
62
+ if params:
63
+ kwargs["params"] = params
64
+
65
+
66
+ def parse_paginated_response(response_data: Any) -> Any:
67
+ """
68
+ Parse response as PaginatedListResponse if possible.
69
+
70
+ Args:
71
+ response_data: Response data from API
72
+
73
+ Returns:
74
+ PaginatedListResponse if format matches, otherwise raw response
75
+ """
76
+ try:
77
+ return PaginatedListResponse(**response_data)
78
+ except Exception:
79
+ # If response doesn't match PaginatedListResponse format, return as-is
80
+ # This allows flexibility for different response formats
81
+ return response_data
82
+
83
+
84
+ def prepare_filter_params(filter_builder: Optional[FilterBuilder]) -> Optional[Dict[str, Any]]:
85
+ """
86
+ Prepare filter parameters from FilterBuilder.
87
+
88
+ Args:
89
+ filter_builder: Optional FilterBuilder instance
90
+
91
+ Returns:
92
+ Dictionary of filter parameters, or None if no filters
93
+ """
94
+ if not filter_builder:
95
+ return None
96
+
97
+ filter_query = FilterQuery(filters=filter_builder.build())
98
+ query_string = build_query_string(filter_query)
99
+
100
+ if query_string:
101
+ # Parse query string into params dict
102
+ parsed = urlparse(f"?{query_string}")
103
+ params = parse_qs(parsed.query)
104
+ # Convert lists to single values where appropriate
105
+ return {k: v[0] if len(v) == 1 else v for k, v in params.items()}
106
+
107
+ return None
108
+
109
+
110
+ def prepare_json_filter_body(
111
+ json_filter: Optional[Union[JsonFilter, FilterQuery, Dict[str, Any]]],
112
+ json_body: Optional[Dict[str, Any]] = None,
113
+ ) -> Dict[str, Any]:
114
+ """
115
+ Prepare JSON body with filter data.
116
+
117
+ Args:
118
+ json_filter: Optional JsonFilter, FilterQuery, or dict
119
+ json_body: Optional existing JSON body
120
+
121
+ Returns:
122
+ Dictionary with merged filter and body data
123
+ """
124
+ request_body: Dict[str, Any] = {}
125
+ if json_body:
126
+ request_body.update(json_body)
127
+
128
+ if json_filter:
129
+ if isinstance(json_filter, JsonFilter):
130
+ filter_dict = json_filter.model_dump(exclude_none=True)
131
+ elif isinstance(json_filter, FilterQuery):
132
+ filter_dict = json_filter.to_json()
133
+ else:
134
+ filter_dict = json_filter
135
+
136
+ request_body.update(filter_dict)
137
+
138
+ return request_body