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,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filter parsing utilities for MisoClient SDK.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for parsing filter parameters from query strings
|
|
5
|
+
and converting them into FilterOption objects.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, List, Optional, Union, cast
|
|
9
|
+
from urllib.parse import unquote
|
|
10
|
+
|
|
11
|
+
from ..models.filter import FilterOperator, FilterOption
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_filter_params(params: dict) -> List[FilterOption]:
|
|
15
|
+
"""
|
|
16
|
+
Parse filter query parameters into FilterOption list.
|
|
17
|
+
|
|
18
|
+
Parses `?filter=field:op:value` format into FilterOption objects.
|
|
19
|
+
Supports multiple filter parameters (array of filter strings).
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
params: Dictionary with query parameters (e.g., {'filter': ['status:eq:active', 'region:in:eu,us']})
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
List of FilterOption objects
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
>>> parse_filter_params({'filter': ['status:eq:active']})
|
|
29
|
+
[FilterOption(field='status', op='eq', value='active')]
|
|
30
|
+
>>> parse_filter_params({'filter': ['region:in:eu,us']})
|
|
31
|
+
[FilterOption(field='region', op='in', value=['eu', 'us'])]
|
|
32
|
+
"""
|
|
33
|
+
filters: List[FilterOption] = []
|
|
34
|
+
|
|
35
|
+
# Get filter parameter (can be string or list)
|
|
36
|
+
filter_param = params.get("filter") or params.get("filters")
|
|
37
|
+
if not filter_param:
|
|
38
|
+
return filters
|
|
39
|
+
|
|
40
|
+
# Normalize to list
|
|
41
|
+
if isinstance(filter_param, str):
|
|
42
|
+
filter_strings = [filter_param]
|
|
43
|
+
elif isinstance(filter_param, list):
|
|
44
|
+
filter_strings = filter_param
|
|
45
|
+
else:
|
|
46
|
+
return filters
|
|
47
|
+
|
|
48
|
+
# Parse each filter string
|
|
49
|
+
for filter_str in filter_strings:
|
|
50
|
+
if not isinstance(filter_str, str):
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
# Split by colon (field:op:value)
|
|
54
|
+
# For isNull/isNotNull, value part may be empty or missing
|
|
55
|
+
parts = filter_str.split(":", 2)
|
|
56
|
+
if len(parts) < 2:
|
|
57
|
+
continue # Skip invalid filter format
|
|
58
|
+
|
|
59
|
+
field = unquote(parts[0].strip())
|
|
60
|
+
op = parts[1].strip()
|
|
61
|
+
value_str = unquote(parts[2].strip()) if len(parts) > 2 else ""
|
|
62
|
+
|
|
63
|
+
# Validate operator
|
|
64
|
+
valid_operators = [
|
|
65
|
+
"eq",
|
|
66
|
+
"neq",
|
|
67
|
+
"in",
|
|
68
|
+
"nin",
|
|
69
|
+
"gt",
|
|
70
|
+
"lt",
|
|
71
|
+
"gte",
|
|
72
|
+
"lte",
|
|
73
|
+
"contains",
|
|
74
|
+
"like",
|
|
75
|
+
"isNull",
|
|
76
|
+
"isNotNull",
|
|
77
|
+
]
|
|
78
|
+
if op not in valid_operators:
|
|
79
|
+
continue # Skip invalid operator
|
|
80
|
+
|
|
81
|
+
# Parse value based on operator
|
|
82
|
+
parsed_value: Optional[Union[str, int, float, bool, List[Any]]] = None
|
|
83
|
+
if op in ("isNull", "isNotNull"):
|
|
84
|
+
# Null check operators don't need values
|
|
85
|
+
parsed_value = None
|
|
86
|
+
elif op in ("in", "nin"):
|
|
87
|
+
# Array values: comma-separated
|
|
88
|
+
parsed_value = [v.strip() for v in value_str.split(",") if v.strip()]
|
|
89
|
+
else:
|
|
90
|
+
# Single value: try to parse as number/boolean, fallback to string
|
|
91
|
+
single_value: Union[str, int, float, bool] = value_str
|
|
92
|
+
# Try to parse as integer
|
|
93
|
+
try:
|
|
94
|
+
if "." not in value_str:
|
|
95
|
+
single_value = int(value_str)
|
|
96
|
+
else:
|
|
97
|
+
single_value = float(value_str)
|
|
98
|
+
except (ValueError, TypeError):
|
|
99
|
+
# Try boolean
|
|
100
|
+
if value_str.lower() in ("true", "false"):
|
|
101
|
+
single_value = value_str.lower() == "true"
|
|
102
|
+
else:
|
|
103
|
+
single_value = value_str
|
|
104
|
+
parsed_value = single_value
|
|
105
|
+
|
|
106
|
+
value = parsed_value
|
|
107
|
+
|
|
108
|
+
filters.append(FilterOption(field=field, op=cast(FilterOperator, op), value=value))
|
|
109
|
+
|
|
110
|
+
return filters
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Flask endpoint utilities for client token endpoint.
|
|
3
|
+
|
|
4
|
+
Provides server-side route handlers for creating client token endpoints
|
|
5
|
+
that return client token + DataClient configuration to frontend clients.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from typing import Any, Callable, Optional
|
|
10
|
+
|
|
11
|
+
from ..errors import AuthenticationError
|
|
12
|
+
from ..models.config import (
|
|
13
|
+
ClientTokenEndpointOptions,
|
|
14
|
+
ClientTokenEndpointResponse,
|
|
15
|
+
DataClientConfigResponse,
|
|
16
|
+
MisoClientConfig,
|
|
17
|
+
)
|
|
18
|
+
from ..utils.environment_token import get_environment_token
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def create_flask_client_token_endpoint(
|
|
22
|
+
miso_client: Any, options: Optional[ClientTokenEndpointOptions] = None
|
|
23
|
+
) -> Callable[[], Any]:
|
|
24
|
+
"""
|
|
25
|
+
Create Flask route handler for client-token endpoint.
|
|
26
|
+
|
|
27
|
+
Automatically enriches response with DataClient configuration including
|
|
28
|
+
controllerPublicUrl for frontend client initialization.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
miso_client: MisoClient instance (must be initialized)
|
|
32
|
+
options: Optional configuration for endpoint
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Flask route handler function
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> from flask import Flask
|
|
39
|
+
>>> from miso_client import MisoClient, create_flask_client_token_endpoint, load_config
|
|
40
|
+
>>>
|
|
41
|
+
>>> app = Flask(__name__)
|
|
42
|
+
>>> client = MisoClient(load_config())
|
|
43
|
+
>>> await client.initialize()
|
|
44
|
+
>>>
|
|
45
|
+
>>> app.post('/api/v1/auth/client-token')(create_flask_client_token_endpoint(client))
|
|
46
|
+
"""
|
|
47
|
+
opts = ClientTokenEndpointOptions(
|
|
48
|
+
clientTokenUri=options.clientTokenUri if options else "/api/v1/auth/client-token",
|
|
49
|
+
expiresIn=options.expiresIn if options else 1800,
|
|
50
|
+
includeConfig=options.includeConfig if options else True,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def handler() -> tuple[dict[str, Any], int]:
|
|
54
|
+
"""
|
|
55
|
+
Flask route handler for client token endpoint.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Tuple of (response_dict, status_code)
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
# Check if misoClient is initialized
|
|
62
|
+
if not miso_client.is_initialized():
|
|
63
|
+
return (
|
|
64
|
+
{
|
|
65
|
+
"error": "Service Unavailable",
|
|
66
|
+
"message": "MisoClient is not initialized",
|
|
67
|
+
},
|
|
68
|
+
503,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Get Flask request object
|
|
72
|
+
try:
|
|
73
|
+
from flask import request
|
|
74
|
+
except ImportError:
|
|
75
|
+
return (
|
|
76
|
+
{
|
|
77
|
+
"error": "Internal Server Error",
|
|
78
|
+
"message": "Flask is not installed",
|
|
79
|
+
},
|
|
80
|
+
500,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Get token with origin validation (raises AuthenticationError if validation fails)
|
|
84
|
+
# Run async function in sync context
|
|
85
|
+
# Handle both sync and async Flask contexts
|
|
86
|
+
try:
|
|
87
|
+
# Try to get existing event loop
|
|
88
|
+
_ = asyncio.get_running_loop()
|
|
89
|
+
# If we get here, we're in an async context (Flask 2.0+ async handler)
|
|
90
|
+
# In this case, we need to await, but Flask async handlers handle this
|
|
91
|
+
# For now, create a new event loop in a thread
|
|
92
|
+
import concurrent.futures
|
|
93
|
+
|
|
94
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
95
|
+
future = executor.submit(
|
|
96
|
+
asyncio.run, get_environment_token(miso_client, request.headers)
|
|
97
|
+
)
|
|
98
|
+
token = future.result()
|
|
99
|
+
except RuntimeError:
|
|
100
|
+
# No running loop, safe to use asyncio.run()
|
|
101
|
+
token = asyncio.run(get_environment_token(miso_client, request.headers))
|
|
102
|
+
|
|
103
|
+
# Build response
|
|
104
|
+
response: ClientTokenEndpointResponse = ClientTokenEndpointResponse(
|
|
105
|
+
token=token, expiresIn=opts.expiresIn or 1800
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Include config if requested
|
|
109
|
+
if opts.includeConfig:
|
|
110
|
+
config: MisoClientConfig = miso_client.config
|
|
111
|
+
|
|
112
|
+
# Derive baseUrl from request
|
|
113
|
+
base_url = f"{request.scheme}://{request.host or 'localhost'}"
|
|
114
|
+
|
|
115
|
+
# Get controller URL (prefer controllerPublicUrl for browser, fallback to controller_url)
|
|
116
|
+
controller_url = config.controllerPublicUrl or config.controller_url
|
|
117
|
+
|
|
118
|
+
if not controller_url:
|
|
119
|
+
return (
|
|
120
|
+
{
|
|
121
|
+
"error": "Internal Server Error",
|
|
122
|
+
"message": "Controller URL not configured",
|
|
123
|
+
},
|
|
124
|
+
500,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
response.config = DataClientConfigResponse(
|
|
128
|
+
baseUrl=base_url,
|
|
129
|
+
controllerUrl=controller_url,
|
|
130
|
+
controllerPublicUrl=config.controllerPublicUrl,
|
|
131
|
+
clientId=config.client_id,
|
|
132
|
+
clientTokenUri=opts.clientTokenUri or "/api/v1/auth/client-token",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return response.model_dump(exclude_none=True), 200
|
|
136
|
+
|
|
137
|
+
except AuthenticationError as error:
|
|
138
|
+
# Origin validation failed (403)
|
|
139
|
+
error_message = str(error)
|
|
140
|
+
if "Origin validation failed" in error_message:
|
|
141
|
+
return (
|
|
142
|
+
{
|
|
143
|
+
"error": "Forbidden",
|
|
144
|
+
"message": error_message,
|
|
145
|
+
},
|
|
146
|
+
403,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Other authentication errors (500)
|
|
150
|
+
return (
|
|
151
|
+
{
|
|
152
|
+
"error": "Internal Server Error",
|
|
153
|
+
"message": error_message,
|
|
154
|
+
},
|
|
155
|
+
500,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
except Exception as error:
|
|
159
|
+
# Other errors (500)
|
|
160
|
+
error_message = str(error) if error else "Unknown error"
|
|
161
|
+
return (
|
|
162
|
+
{
|
|
163
|
+
"error": "Internal Server Error",
|
|
164
|
+
"message": error_message,
|
|
165
|
+
},
|
|
166
|
+
500,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return handler
|