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,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logger helper functions for building log entries.
|
|
3
|
+
|
|
4
|
+
Extracted from logger.py to reduce file size and improve maintainability.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any, Dict, Literal, Optional, Union
|
|
11
|
+
|
|
12
|
+
from ..models.config import ClientLoggingOptions, ForeignKeyReference, LogEntry
|
|
13
|
+
from ..utils.data_masker import DataMasker
|
|
14
|
+
from ..utils.jwt_tools import decode_token
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def extract_jwt_context(token: Optional[str]) -> Dict[str, Any]:
|
|
18
|
+
"""
|
|
19
|
+
Extract JWT token information.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
token: JWT token string
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Dictionary with userId, applicationId, sessionId, roles, permissions
|
|
26
|
+
"""
|
|
27
|
+
if not token:
|
|
28
|
+
return {}
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
decoded = decode_token(token)
|
|
32
|
+
if not decoded:
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
# Extract roles - handle different formats
|
|
36
|
+
roles = []
|
|
37
|
+
if "roles" in decoded:
|
|
38
|
+
roles = decoded["roles"] if isinstance(decoded["roles"], list) else []
|
|
39
|
+
elif "realm_access" in decoded and isinstance(decoded["realm_access"], dict):
|
|
40
|
+
roles = decoded["realm_access"].get("roles", [])
|
|
41
|
+
|
|
42
|
+
# Extract permissions - handle different formats
|
|
43
|
+
permissions = []
|
|
44
|
+
if "permissions" in decoded:
|
|
45
|
+
permissions = decoded["permissions"] if isinstance(decoded["permissions"], list) else []
|
|
46
|
+
elif "scope" in decoded and isinstance(decoded["scope"], str):
|
|
47
|
+
permissions = decoded["scope"].split()
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
"userId": decoded.get("sub") or decoded.get("userId") or decoded.get("user_id"),
|
|
51
|
+
"applicationId": decoded.get("applicationId") or decoded.get("app_id"),
|
|
52
|
+
"sessionId": decoded.get("sessionId") or decoded.get("sid"),
|
|
53
|
+
"roles": roles,
|
|
54
|
+
"permissions": permissions,
|
|
55
|
+
}
|
|
56
|
+
except Exception:
|
|
57
|
+
# JWT parsing failed, return empty context
|
|
58
|
+
return {}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def extract_metadata() -> Dict[str, Any]:
|
|
62
|
+
"""
|
|
63
|
+
Extract metadata from environment (browser or Node.js).
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Dictionary with hostname, userAgent, etc.
|
|
67
|
+
"""
|
|
68
|
+
metadata: Dict[str, Any] = {}
|
|
69
|
+
|
|
70
|
+
# Try to extract Node.js/Python metadata
|
|
71
|
+
if hasattr(os, "environ"):
|
|
72
|
+
metadata["hostname"] = os.environ.get("HOSTNAME", "unknown")
|
|
73
|
+
|
|
74
|
+
# In Python, we don't have browser metadata like in TypeScript
|
|
75
|
+
# But we can capture some environment info
|
|
76
|
+
metadata["platform"] = sys.platform
|
|
77
|
+
metadata["python_version"] = sys.version
|
|
78
|
+
|
|
79
|
+
return metadata
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _convert_to_foreign_key_reference(
|
|
83
|
+
value: Optional[Union[str, ForeignKeyReference]], entity_type: str
|
|
84
|
+
) -> Optional[ForeignKeyReference]:
|
|
85
|
+
"""
|
|
86
|
+
Convert string ID or ForeignKeyReference to ForeignKeyReference object.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
value: String ID or ForeignKeyReference object
|
|
90
|
+
entity_type: Entity type (e.g., 'User', 'Application')
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
ForeignKeyReference object or None
|
|
94
|
+
"""
|
|
95
|
+
if value is None:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
# If already a ForeignKeyReference, return as-is
|
|
99
|
+
if isinstance(value, ForeignKeyReference):
|
|
100
|
+
return value
|
|
101
|
+
|
|
102
|
+
# If string, create minimal ForeignKeyReference
|
|
103
|
+
# Note: This is a minimal conversion - full ForeignKeyReference should come from API responses
|
|
104
|
+
if isinstance(value, str):
|
|
105
|
+
return ForeignKeyReference(
|
|
106
|
+
id=value,
|
|
107
|
+
key=value, # Use id as key when key is not available
|
|
108
|
+
name=value, # Use id as name when name is not available
|
|
109
|
+
type=entity_type,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def build_log_entry(
|
|
116
|
+
level: Literal["error", "audit", "info", "debug"],
|
|
117
|
+
message: str,
|
|
118
|
+
context: Optional[Dict[str, Any]],
|
|
119
|
+
config_client_id: str,
|
|
120
|
+
correlation_id: Optional[str] = None,
|
|
121
|
+
jwt_token: Optional[str] = None,
|
|
122
|
+
stack_trace: Optional[str] = None,
|
|
123
|
+
options: Optional[ClientLoggingOptions] = None,
|
|
124
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
125
|
+
mask_sensitive: bool = True,
|
|
126
|
+
) -> LogEntry:
|
|
127
|
+
"""
|
|
128
|
+
Build LogEntry object from parameters.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
level: Log level
|
|
132
|
+
message: Log message
|
|
133
|
+
context: Additional context data
|
|
134
|
+
config_client_id: Client ID from config
|
|
135
|
+
correlation_id: Optional correlation ID
|
|
136
|
+
jwt_token: Optional JWT token for context extraction
|
|
137
|
+
stack_trace: Stack trace for errors
|
|
138
|
+
options: Logging options
|
|
139
|
+
metadata: Environment metadata
|
|
140
|
+
mask_sensitive: Whether to mask sensitive data
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
LogEntry object
|
|
144
|
+
"""
|
|
145
|
+
# Extract JWT context if token provided
|
|
146
|
+
jwt_context = extract_jwt_context(jwt_token or (options.token if options else None))
|
|
147
|
+
|
|
148
|
+
# Extract environment metadata
|
|
149
|
+
env_metadata = metadata or extract_metadata()
|
|
150
|
+
|
|
151
|
+
# Generate correlation ID if not provided
|
|
152
|
+
final_correlation_id = correlation_id or (options.correlationId if options else None)
|
|
153
|
+
|
|
154
|
+
# Mask sensitive data in context if enabled
|
|
155
|
+
should_mask = (options.maskSensitiveData if options else None) is not False and mask_sensitive
|
|
156
|
+
masked_context = DataMasker.mask_sensitive_data(context) if should_mask and context else context
|
|
157
|
+
|
|
158
|
+
# Convert applicationId and userId to ForeignKeyReference if needed
|
|
159
|
+
application_id_value = options.applicationId if options else None
|
|
160
|
+
user_id_value = (options.userId if options else None) or jwt_context.get("userId")
|
|
161
|
+
|
|
162
|
+
application_id_ref = _convert_to_foreign_key_reference(application_id_value, "Application")
|
|
163
|
+
user_id_ref = _convert_to_foreign_key_reference(user_id_value, "User")
|
|
164
|
+
|
|
165
|
+
log_entry_data = {
|
|
166
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
167
|
+
"level": level,
|
|
168
|
+
"environment": "unknown", # Backend extracts from client credentials
|
|
169
|
+
"application": config_client_id, # Use clientId as application identifier
|
|
170
|
+
"applicationId": application_id_ref,
|
|
171
|
+
"message": message,
|
|
172
|
+
"context": masked_context,
|
|
173
|
+
"stackTrace": stack_trace,
|
|
174
|
+
"correlationId": final_correlation_id,
|
|
175
|
+
"userId": user_id_ref,
|
|
176
|
+
"sessionId": (options.sessionId if options else None) or jwt_context.get("sessionId"),
|
|
177
|
+
"requestId": options.requestId if options else None,
|
|
178
|
+
"ipAddress": options.ipAddress if options else None,
|
|
179
|
+
"userAgent": options.userAgent if options else None,
|
|
180
|
+
**env_metadata,
|
|
181
|
+
# Indexed context fields from options
|
|
182
|
+
"sourceKey": options.sourceKey if options else None,
|
|
183
|
+
"sourceDisplayName": options.sourceDisplayName if options else None,
|
|
184
|
+
"externalSystemKey": options.externalSystemKey if options else None,
|
|
185
|
+
"externalSystemDisplayName": options.externalSystemDisplayName if options else None,
|
|
186
|
+
"recordKey": options.recordKey if options else None,
|
|
187
|
+
"recordDisplayName": options.recordDisplayName if options else None,
|
|
188
|
+
# Credential context
|
|
189
|
+
"credentialId": options.credentialId if options else None,
|
|
190
|
+
"credentialType": options.credentialType if options else None,
|
|
191
|
+
# Request metrics
|
|
192
|
+
"requestSize": options.requestSize if options else None,
|
|
193
|
+
"responseSize": options.responseSize if options else None,
|
|
194
|
+
"durationMs": options.durationMs if options else None,
|
|
195
|
+
"durationSeconds": options.durationSeconds if options else None,
|
|
196
|
+
"timeout": options.timeout if options else None,
|
|
197
|
+
"retryCount": options.retryCount if options else None,
|
|
198
|
+
# Error classification
|
|
199
|
+
"errorCategory": options.errorCategory if options else None,
|
|
200
|
+
"httpStatusCategory": options.httpStatusCategory if options else None,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# Remove None values
|
|
204
|
+
log_entry_data = {k: v for k, v in log_entry_data.items() if v is not None}
|
|
205
|
+
|
|
206
|
+
return LogEntry(**log_entry_data)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Logging context helpers for extracting indexed fields."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@runtime_checkable
|
|
7
|
+
class HasKey(Protocol):
|
|
8
|
+
"""Protocol for objects with key and displayName."""
|
|
9
|
+
|
|
10
|
+
key: str
|
|
11
|
+
displayName: Optional[str]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class HasExternalSystem(Protocol):
|
|
16
|
+
"""Protocol for objects with key, displayName, and optional externalSystem."""
|
|
17
|
+
|
|
18
|
+
key: str
|
|
19
|
+
displayName: Optional[str]
|
|
20
|
+
externalSystem: Optional[HasKey]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def extract_logging_context(
|
|
24
|
+
source: Optional[HasExternalSystem] = None,
|
|
25
|
+
record: Optional[HasKey] = None,
|
|
26
|
+
external_system: Optional[HasKey] = None,
|
|
27
|
+
) -> Dict[str, Any]:
|
|
28
|
+
"""
|
|
29
|
+
Extract indexed fields for logging.
|
|
30
|
+
|
|
31
|
+
Indexed fields:
|
|
32
|
+
- sourceKey, sourceDisplayName
|
|
33
|
+
- externalSystemKey, externalSystemDisplayName
|
|
34
|
+
- recordKey, recordDisplayName
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
source: ExternalDataSource object (optional)
|
|
38
|
+
record: ExternalRecord object (optional)
|
|
39
|
+
external_system: ExternalSystem object (optional)
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Dictionary with indexed context fields (only non-None values)
|
|
43
|
+
|
|
44
|
+
Design principles:
|
|
45
|
+
- No DB access
|
|
46
|
+
- Explicit context passing
|
|
47
|
+
- Safe to use in hot paths
|
|
48
|
+
"""
|
|
49
|
+
context: Dict[str, Any] = {}
|
|
50
|
+
|
|
51
|
+
if source:
|
|
52
|
+
context["sourceKey"] = source.key
|
|
53
|
+
if source.displayName:
|
|
54
|
+
context["sourceDisplayName"] = source.displayName
|
|
55
|
+
if source.externalSystem:
|
|
56
|
+
context["externalSystemKey"] = source.externalSystem.key
|
|
57
|
+
if source.externalSystem.displayName:
|
|
58
|
+
context["externalSystemDisplayName"] = source.externalSystem.displayName
|
|
59
|
+
|
|
60
|
+
if external_system:
|
|
61
|
+
context["externalSystemKey"] = external_system.key
|
|
62
|
+
if external_system.displayName:
|
|
63
|
+
context["externalSystemDisplayName"] = external_system.displayName
|
|
64
|
+
|
|
65
|
+
if record:
|
|
66
|
+
context["recordKey"] = record.key
|
|
67
|
+
if record.displayName:
|
|
68
|
+
context["recordDisplayName"] = record.displayName
|
|
69
|
+
|
|
70
|
+
return context
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Origin validation utility for CORS security.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for validating request origins against
|
|
5
|
+
a list of allowed origins, with support for wildcard port matching.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validate_origin(headers: Any, allowed_origins: List[str]) -> Dict[str, Any]:
|
|
13
|
+
"""
|
|
14
|
+
Validate request origin against allowed origins list.
|
|
15
|
+
|
|
16
|
+
Checks the 'origin' header first, then falls back to 'referer' header.
|
|
17
|
+
Supports wildcard ports (e.g., 'http://localhost:*' matches any port).
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
headers: Request headers dict or object with headers attribute
|
|
21
|
+
allowed_origins: List of allowed origin URLs (supports wildcard ports)
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Dictionary with:
|
|
25
|
+
- valid: bool - Whether origin is valid
|
|
26
|
+
- error: Optional[str] - Error message if invalid, None if valid
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> headers = {"origin": "http://localhost:3000"}
|
|
30
|
+
>>> result = validate_origin(headers, ["http://localhost:*"])
|
|
31
|
+
>>> result["valid"]
|
|
32
|
+
True
|
|
33
|
+
"""
|
|
34
|
+
if not allowed_origins:
|
|
35
|
+
# If no allowed origins configured, allow all (backward compatibility)
|
|
36
|
+
return {"valid": True, "error": None}
|
|
37
|
+
|
|
38
|
+
# Extract headers dict from various request object types
|
|
39
|
+
headers_dict: Optional[Dict[str, Any]] = None
|
|
40
|
+
if isinstance(headers, dict):
|
|
41
|
+
headers_dict = headers
|
|
42
|
+
elif hasattr(headers, "headers"):
|
|
43
|
+
# FastAPI, Flask style: request.headers
|
|
44
|
+
headers_obj = getattr(headers, "headers")
|
|
45
|
+
if isinstance(headers_obj, dict):
|
|
46
|
+
headers_dict = headers_obj
|
|
47
|
+
elif hasattr(headers_obj, "get"):
|
|
48
|
+
# Headers object with get method (like Starlette headers)
|
|
49
|
+
headers_dict = dict(headers_obj)
|
|
50
|
+
elif hasattr(headers, "get"):
|
|
51
|
+
# Already a dict-like object
|
|
52
|
+
headers_dict = dict(headers)
|
|
53
|
+
|
|
54
|
+
if headers_dict is None:
|
|
55
|
+
return {"valid": False, "error": "Unable to extract headers from request"}
|
|
56
|
+
|
|
57
|
+
# Extract origin from headers (case-insensitive)
|
|
58
|
+
origin = None
|
|
59
|
+
for key in ["origin", "Origin", "ORIGIN"]:
|
|
60
|
+
if key in headers_dict:
|
|
61
|
+
origin_value = headers_dict[key]
|
|
62
|
+
if isinstance(origin_value, str) and origin_value.strip():
|
|
63
|
+
origin = origin_value.strip()
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
# Fallback to referer header if origin not found
|
|
67
|
+
if not origin:
|
|
68
|
+
for key in ["referer", "Referer", "REFERER", "referrer", "Referrer", "REFERRER"]:
|
|
69
|
+
if key in headers_dict:
|
|
70
|
+
referer_value = headers_dict[key]
|
|
71
|
+
if isinstance(referer_value, str) and referer_value.strip():
|
|
72
|
+
# Extract origin from referer URL
|
|
73
|
+
try:
|
|
74
|
+
parsed = urlparse(referer_value.strip())
|
|
75
|
+
if parsed.scheme and parsed.netloc:
|
|
76
|
+
origin = f"{parsed.scheme}://{parsed.netloc}"
|
|
77
|
+
break
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
if not origin:
|
|
82
|
+
return {"valid": False, "error": "No origin or referer header found"}
|
|
83
|
+
|
|
84
|
+
# Normalize origin (remove trailing slash, lowercase scheme/host)
|
|
85
|
+
try:
|
|
86
|
+
parsed_origin = urlparse(origin)
|
|
87
|
+
if not parsed_origin.scheme or not parsed_origin.netloc:
|
|
88
|
+
return {"valid": False, "error": f"Invalid origin format: {origin}"}
|
|
89
|
+
origin_scheme = parsed_origin.scheme.lower()
|
|
90
|
+
origin_netloc = parsed_origin.netloc.lower()
|
|
91
|
+
origin_normalized = f"{origin_scheme}://{origin_netloc}"
|
|
92
|
+
except Exception:
|
|
93
|
+
return {"valid": False, "error": f"Invalid origin format: {origin}"}
|
|
94
|
+
|
|
95
|
+
# Check against allowed origins
|
|
96
|
+
for allowed in allowed_origins:
|
|
97
|
+
if not allowed or not isinstance(allowed, str):
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
parsed_allowed = urlparse(allowed)
|
|
102
|
+
allowed_scheme = parsed_allowed.scheme.lower()
|
|
103
|
+
allowed_netloc = parsed_allowed.netloc.lower()
|
|
104
|
+
allowed_normalized = f"{allowed_scheme}://{allowed_netloc}"
|
|
105
|
+
|
|
106
|
+
# Check for exact match
|
|
107
|
+
if origin_normalized == allowed_normalized:
|
|
108
|
+
return {"valid": True, "error": None}
|
|
109
|
+
|
|
110
|
+
# Check for wildcard port match (e.g., localhost:* matches localhost:3000)
|
|
111
|
+
if "*" in allowed_netloc:
|
|
112
|
+
# Extract host from origin
|
|
113
|
+
origin_host = origin_netloc.split(":")[0]
|
|
114
|
+
|
|
115
|
+
# Extract host from allowed (may have wildcard port)
|
|
116
|
+
allowed_host = allowed_netloc.split(":")[0]
|
|
117
|
+
allowed_port = allowed_netloc.split(":")[1] if ":" in allowed_netloc else None
|
|
118
|
+
|
|
119
|
+
# Match if host matches and allowed has wildcard port
|
|
120
|
+
if origin_host == allowed_host and allowed_port == "*":
|
|
121
|
+
if origin_scheme == allowed_scheme:
|
|
122
|
+
return {"valid": True, "error": None}
|
|
123
|
+
|
|
124
|
+
except Exception:
|
|
125
|
+
# Skip invalid allowed origin format
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
return {"valid": False, "error": f"Origin '{origin}' is not in allowed origins list"}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pagination utilities for MisoClient SDK.
|
|
3
|
+
|
|
4
|
+
This module provides reusable pagination utilities for parsing pagination parameters,
|
|
5
|
+
creating meta objects, and working with paginated responses.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, List, Tuple, TypeVar
|
|
9
|
+
|
|
10
|
+
from ..models.pagination import Meta, PaginatedListResponse
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parsePaginationParams(params: dict) -> Dict[str, int]:
|
|
16
|
+
"""
|
|
17
|
+
Parse query parameters into pagination values.
|
|
18
|
+
|
|
19
|
+
Parses `page` and `page_size` query parameters into `currentPage` and `pageSize`.
|
|
20
|
+
Both are 1-based (page starts at 1).
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
params: Dictionary with query parameters (e.g., {'page': '1', 'page_size': '25'})
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Dictionary with 'currentPage' and 'pageSize' keys (camelCase)
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
>>> parsePaginationParams({'page': '1', 'page_size': '25'})
|
|
30
|
+
{'currentPage': 1, 'pageSize': 25}
|
|
31
|
+
>>> parsePaginationParams({'page': '2'})
|
|
32
|
+
{'currentPage': 2, 'pageSize': 20} # Default pageSize is 20
|
|
33
|
+
"""
|
|
34
|
+
# Default values (matching TypeScript default of 20)
|
|
35
|
+
default_page = 1
|
|
36
|
+
default_page_size = 20
|
|
37
|
+
|
|
38
|
+
# Parse page (must be >= 1)
|
|
39
|
+
page_str = params.get("page") or params.get("current_page")
|
|
40
|
+
if page_str is None:
|
|
41
|
+
current_page = default_page
|
|
42
|
+
else:
|
|
43
|
+
try:
|
|
44
|
+
current_page = int(page_str)
|
|
45
|
+
if current_page < 1:
|
|
46
|
+
current_page = default_page
|
|
47
|
+
except (ValueError, TypeError):
|
|
48
|
+
current_page = default_page
|
|
49
|
+
|
|
50
|
+
# Parse page_size (must be >= 1)
|
|
51
|
+
page_size_str = params.get("page_size") or params.get("pageSize")
|
|
52
|
+
if page_size_str is None:
|
|
53
|
+
page_size = default_page_size
|
|
54
|
+
else:
|
|
55
|
+
try:
|
|
56
|
+
page_size = int(page_size_str)
|
|
57
|
+
if page_size < 1:
|
|
58
|
+
page_size = default_page_size
|
|
59
|
+
except (ValueError, TypeError):
|
|
60
|
+
page_size = default_page_size
|
|
61
|
+
|
|
62
|
+
return {"currentPage": current_page, "pageSize": page_size}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Alias for backward compatibility
|
|
66
|
+
def parse_pagination_params(params: dict) -> Tuple[int, int]:
|
|
67
|
+
"""
|
|
68
|
+
Parse query parameters to pagination values (legacy function).
|
|
69
|
+
|
|
70
|
+
Parses `page` and `page_size` query parameters into `current_page` and `page_size`.
|
|
71
|
+
Both are 1-based (page starts at 1).
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
params: Dictionary with query parameters (e.g., {'page': '1', 'page_size': '25'})
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Tuple of (current_page, page_size) as integers
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
>>> parse_pagination_params({'page': '1', 'page_size': '25'})
|
|
81
|
+
(1, 25)
|
|
82
|
+
>>> parse_pagination_params({'page': '2'})
|
|
83
|
+
(2, 20) # Default page_size is 20
|
|
84
|
+
"""
|
|
85
|
+
result = parsePaginationParams(params)
|
|
86
|
+
return (result["currentPage"], result["pageSize"])
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def createMetaObject(totalItems: int, currentPage: int, pageSize: int, type: str) -> Meta:
|
|
90
|
+
"""
|
|
91
|
+
Construct meta object for API response.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
totalItems: Total number of items available in full dataset
|
|
95
|
+
currentPage: Current page index (1-based)
|
|
96
|
+
pageSize: Number of items per page
|
|
97
|
+
type: Logical resource type (e.g., "application", "environment")
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Meta object
|
|
101
|
+
|
|
102
|
+
Examples:
|
|
103
|
+
>>> meta = createMetaObject(120, 1, 25, 'item')
|
|
104
|
+
>>> meta.totalItems
|
|
105
|
+
120
|
|
106
|
+
>>> meta.currentPage
|
|
107
|
+
1
|
|
108
|
+
"""
|
|
109
|
+
return Meta(
|
|
110
|
+
totalItems=totalItems,
|
|
111
|
+
currentPage=currentPage,
|
|
112
|
+
pageSize=pageSize,
|
|
113
|
+
type=type,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def create_meta_object(total_items: int, current_page: int, page_size: int, type: str) -> Meta:
|
|
118
|
+
"""
|
|
119
|
+
Construct Meta object from pagination parameters.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
total_items: Total number of items across all pages
|
|
123
|
+
current_page: Current page number (1-based)
|
|
124
|
+
page_size: Number of items per page
|
|
125
|
+
type: Resource type identifier (e.g., 'item', 'user', 'group')
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Meta object with pagination metadata
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
>>> meta = create_meta_object(120, 1, 25, 'item')
|
|
132
|
+
>>> meta.totalItems
|
|
133
|
+
120
|
|
134
|
+
>>> meta.currentPage
|
|
135
|
+
1
|
|
136
|
+
"""
|
|
137
|
+
return Meta(
|
|
138
|
+
totalItems=total_items,
|
|
139
|
+
currentPage=current_page,
|
|
140
|
+
pageSize=page_size,
|
|
141
|
+
type=type,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def applyPaginationToArray(items: List[T], currentPage: int, pageSize: int) -> List[T]:
|
|
146
|
+
"""
|
|
147
|
+
Apply pagination to an array (for mock/testing).
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
items: Array of items to paginate
|
|
151
|
+
currentPage: Current page index (1-based)
|
|
152
|
+
pageSize: Number of items per page
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Paginated slice of the array
|
|
156
|
+
|
|
157
|
+
Examples:
|
|
158
|
+
>>> items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
159
|
+
>>> applyPaginationToArray(items, 1, 3)
|
|
160
|
+
[1, 2, 3]
|
|
161
|
+
>>> applyPaginationToArray(items, 2, 3)
|
|
162
|
+
[4, 5, 6]
|
|
163
|
+
"""
|
|
164
|
+
if not items:
|
|
165
|
+
return []
|
|
166
|
+
|
|
167
|
+
if currentPage < 1:
|
|
168
|
+
currentPage = 1
|
|
169
|
+
if pageSize < 1:
|
|
170
|
+
pageSize = 25
|
|
171
|
+
|
|
172
|
+
# Calculate start and end indices
|
|
173
|
+
start_index = (currentPage - 1) * pageSize
|
|
174
|
+
end_index = start_index + pageSize
|
|
175
|
+
|
|
176
|
+
# Return paginated subset
|
|
177
|
+
return items[start_index:end_index]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def apply_pagination_to_array(items: List[T], current_page: int, page_size: int) -> List[T]:
|
|
181
|
+
"""
|
|
182
|
+
Apply pagination to array (for testing/mocks).
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
items: Array of items to paginate
|
|
186
|
+
current_page: Current page number (1-based)
|
|
187
|
+
page_size: Number of items per page
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Paginated subset of items for the specified page
|
|
191
|
+
|
|
192
|
+
Examples:
|
|
193
|
+
>>> items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
194
|
+
>>> apply_pagination_to_array(items, 1, 3)
|
|
195
|
+
[1, 2, 3]
|
|
196
|
+
>>> apply_pagination_to_array(items, 2, 3)
|
|
197
|
+
[4, 5, 6]
|
|
198
|
+
"""
|
|
199
|
+
if not items:
|
|
200
|
+
return []
|
|
201
|
+
|
|
202
|
+
if current_page < 1:
|
|
203
|
+
current_page = 1
|
|
204
|
+
if page_size < 1:
|
|
205
|
+
page_size = 25
|
|
206
|
+
|
|
207
|
+
# Calculate start and end indices
|
|
208
|
+
start_index = (current_page - 1) * page_size
|
|
209
|
+
end_index = start_index + page_size
|
|
210
|
+
|
|
211
|
+
# Return paginated subset
|
|
212
|
+
return items[start_index:end_index]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def createPaginatedListResponse(
|
|
216
|
+
items: List[T],
|
|
217
|
+
totalItems: int,
|
|
218
|
+
currentPage: int,
|
|
219
|
+
pageSize: int,
|
|
220
|
+
type: str,
|
|
221
|
+
) -> PaginatedListResponse[T]:
|
|
222
|
+
"""
|
|
223
|
+
Wrap array + meta into a standard paginated response.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
items: Array of items for the current page
|
|
227
|
+
totalItems: Total number of items available in full dataset
|
|
228
|
+
currentPage: Current page index (1-based)
|
|
229
|
+
pageSize: Number of items per page
|
|
230
|
+
type: Logical resource type (e.g., "application", "environment")
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
PaginatedListResponse object
|
|
234
|
+
|
|
235
|
+
Examples:
|
|
236
|
+
>>> items = [{'id': 1}, {'id': 2}]
|
|
237
|
+
>>> response = createPaginatedListResponse(items, 10, 1, 2, 'item')
|
|
238
|
+
>>> response.meta.totalItems
|
|
239
|
+
10
|
|
240
|
+
>>> len(response.data)
|
|
241
|
+
2
|
|
242
|
+
"""
|
|
243
|
+
meta = createMetaObject(totalItems, currentPage, pageSize, type)
|
|
244
|
+
return PaginatedListResponse(meta=meta, data=items)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def create_paginated_list_response(
|
|
248
|
+
items: List[T],
|
|
249
|
+
total_items: int,
|
|
250
|
+
current_page: int,
|
|
251
|
+
page_size: int,
|
|
252
|
+
type: str,
|
|
253
|
+
) -> PaginatedListResponse[T]:
|
|
254
|
+
"""
|
|
255
|
+
Wrap array + meta into standard paginated response (legacy function).
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
items: Array of items for current page
|
|
259
|
+
total_items: Total number of items across all pages
|
|
260
|
+
current_page: Current page number (1-based)
|
|
261
|
+
page_size: Number of items per page
|
|
262
|
+
type: Resource type identifier (e.g., 'item', 'user', 'group')
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
PaginatedListResponse with meta and data
|
|
266
|
+
|
|
267
|
+
Examples:
|
|
268
|
+
>>> items = [{'id': 1}, {'id': 2}]
|
|
269
|
+
>>> response = create_paginated_list_response(items, 10, 1, 2, 'item')
|
|
270
|
+
>>> response.meta.totalItems
|
|
271
|
+
10
|
|
272
|
+
>>> len(response.data)
|
|
273
|
+
2
|
|
274
|
+
"""
|
|
275
|
+
return createPaginatedListResponse(items, total_items, current_page, page_size, type)
|