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.
- miso_client/__init__.py +523 -130
- miso_client/api/__init__.py +35 -0
- miso_client/api/auth_api.py +367 -0
- miso_client/api/logs_api.py +91 -0
- miso_client/api/permissions_api.py +88 -0
- miso_client/api/roles_api.py +88 -0
- miso_client/api/types/__init__.py +75 -0
- miso_client/api/types/auth_types.py +183 -0
- miso_client/api/types/logs_types.py +71 -0
- miso_client/api/types/permissions_types.py +31 -0
- miso_client/api/types/roles_types.py +31 -0
- miso_client/errors.py +30 -4
- miso_client/models/__init__.py +4 -0
- miso_client/models/config.py +275 -72
- miso_client/models/error_response.py +39 -0
- miso_client/models/filter.py +255 -0
- miso_client/models/pagination.py +44 -0
- miso_client/models/sort.py +25 -0
- miso_client/services/__init__.py +6 -5
- miso_client/services/auth.py +496 -87
- miso_client/services/cache.py +42 -41
- miso_client/services/encryption.py +18 -17
- miso_client/services/logger.py +467 -328
- miso_client/services/logger_chain.py +288 -0
- miso_client/services/permission.py +130 -67
- miso_client/services/redis.py +28 -23
- miso_client/services/role.py +145 -62
- miso_client/utils/__init__.py +3 -3
- miso_client/utils/audit_log_queue.py +222 -0
- miso_client/utils/auth_strategy.py +88 -0
- miso_client/utils/auth_utils.py +65 -0
- miso_client/utils/circuit_breaker.py +125 -0
- miso_client/utils/client_token_manager.py +244 -0
- miso_client/utils/config_loader.py +88 -17
- miso_client/utils/controller_url_resolver.py +80 -0
- miso_client/utils/data_masker.py +104 -33
- miso_client/utils/environment_token.py +126 -0
- miso_client/utils/error_utils.py +216 -0
- miso_client/utils/fastapi_endpoints.py +166 -0
- miso_client/utils/filter.py +364 -0
- miso_client/utils/filter_applier.py +143 -0
- miso_client/utils/filter_parser.py +110 -0
- miso_client/utils/flask_endpoints.py +169 -0
- miso_client/utils/http_client.py +494 -262
- miso_client/utils/http_client_logging.py +352 -0
- miso_client/utils/http_client_logging_helpers.py +197 -0
- miso_client/utils/http_client_query_helpers.py +138 -0
- miso_client/utils/http_error_handler.py +92 -0
- miso_client/utils/http_log_formatter.py +115 -0
- miso_client/utils/http_log_masker.py +203 -0
- miso_client/utils/internal_http_client.py +435 -0
- miso_client/utils/jwt_tools.py +125 -16
- miso_client/utils/logger_helpers.py +206 -0
- miso_client/utils/logging_helpers.py +70 -0
- miso_client/utils/origin_validator.py +128 -0
- miso_client/utils/pagination.py +275 -0
- miso_client/utils/request_context.py +285 -0
- miso_client/utils/sensitive_fields_loader.py +116 -0
- miso_client/utils/sort.py +116 -0
- miso_client/utils/token_utils.py +114 -0
- miso_client/utils/url_validator.py +66 -0
- miso_client/utils/user_token_refresh.py +245 -0
- miso_client-3.7.2.dist-info/METADATA +1021 -0
- miso_client-3.7.2.dist-info/RECORD +68 -0
- miso_client-0.1.0.dist-info/METADATA +0 -551
- miso_client-0.1.0.dist-info/RECORD +0 -23
- {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/WHEEL +0 -0
- {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/licenses/LICENSE +0 -0
- {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
|