miso-client 0.2.0__py3-none-any.whl → 0.5.0__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.
Potentially problematic release.
This version of miso-client might be problematic. Click here for more details.
- miso_client/__init__.py +59 -3
- miso_client/errors.py +22 -1
- miso_client/models/__init__.py +4 -0
- miso_client/models/error_response.py +50 -0
- miso_client/models/filter.py +140 -0
- miso_client/models/pagination.py +66 -0
- miso_client/models/sort.py +25 -0
- miso_client/services/logger.py +7 -6
- miso_client/utils/data_masker.py +77 -5
- miso_client/utils/error_utils.py +104 -0
- miso_client/utils/filter.py +256 -0
- miso_client/utils/http_client.py +517 -212
- miso_client/utils/internal_http_client.py +471 -0
- miso_client/utils/pagination.py +157 -0
- miso_client/utils/sensitive_fields_loader.py +116 -0
- miso_client/utils/sort.py +116 -0
- {miso_client-0.2.0.dist-info → miso_client-0.5.0.dist-info}/METADATA +348 -3
- miso_client-0.5.0.dist-info/RECORD +33 -0
- miso_client-0.2.0.dist-info/RECORD +0 -23
- {miso_client-0.2.0.dist-info → miso_client-0.5.0.dist-info}/WHEEL +0 -0
- {miso_client-0.2.0.dist-info → miso_client-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {miso_client-0.2.0.dist-info → miso_client-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Error utilities for MisoClient SDK.
|
|
3
|
+
|
|
4
|
+
This module provides error transformation utilities for handling
|
|
5
|
+
snake_case error responses from the API.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from ..errors import MisoClientError
|
|
11
|
+
from ..models.error_response import ErrorResponse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def transform_error_to_snake_case(error_data: dict) -> ErrorResponse:
|
|
15
|
+
"""
|
|
16
|
+
Transform errors to snake_case format.
|
|
17
|
+
|
|
18
|
+
Converts error data dictionary to ErrorResponse object,
|
|
19
|
+
supporting both camelCase and snake_case field names.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
error_data: Dictionary with error data (can be camelCase or snake_case)
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
ErrorResponse object with standardized format
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
>>> error_data = {
|
|
29
|
+
... 'errors': ['Error message'],
|
|
30
|
+
... 'type': '/Errors/Bad Input',
|
|
31
|
+
... 'title': 'Bad Request',
|
|
32
|
+
... 'status_code': 400,
|
|
33
|
+
... 'instance': '/api/endpoint'
|
|
34
|
+
... }
|
|
35
|
+
>>> error_response = transform_error_to_snake_case(error_data)
|
|
36
|
+
>>> error_response.statusCode
|
|
37
|
+
400
|
|
38
|
+
"""
|
|
39
|
+
# ErrorResponse already supports both formats via populate_by_name=True
|
|
40
|
+
return ErrorResponse(**error_data)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def handle_api_error_snake_case(
|
|
44
|
+
response_data: dict, status_code: int, instance: Optional[str] = None
|
|
45
|
+
) -> MisoClientError:
|
|
46
|
+
"""
|
|
47
|
+
Handle errors with snake_case response format.
|
|
48
|
+
|
|
49
|
+
Creates MisoClientError with ErrorResponse from snake_case API response.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
response_data: Error response data from API (can be camelCase or snake_case)
|
|
53
|
+
status_code: HTTP status code (overrides status_code in response_data)
|
|
54
|
+
instance: Optional request instance URI (overrides instance in response_data)
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
MisoClientError with structured ErrorResponse
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
>>> response_data = {
|
|
61
|
+
... 'errors': ['Validation failed'],
|
|
62
|
+
... 'type': '/Errors/Validation',
|
|
63
|
+
... 'title': 'Validation Error',
|
|
64
|
+
... 'status_code': 422
|
|
65
|
+
... }
|
|
66
|
+
>>> error = handle_api_error_snake_case(response_data, 422, '/api/endpoint')
|
|
67
|
+
>>> error.error_response.statusCode
|
|
68
|
+
422
|
|
69
|
+
"""
|
|
70
|
+
# Create a copy to avoid mutating the original
|
|
71
|
+
data = response_data.copy()
|
|
72
|
+
|
|
73
|
+
# Override instance if provided
|
|
74
|
+
if instance:
|
|
75
|
+
data["instance"] = instance
|
|
76
|
+
|
|
77
|
+
# Override status_code if provided
|
|
78
|
+
data["status_code"] = status_code
|
|
79
|
+
# Also set camelCase version for consistency
|
|
80
|
+
data["statusCode"] = status_code
|
|
81
|
+
|
|
82
|
+
# Ensure title has a default if missing
|
|
83
|
+
if "title" not in data:
|
|
84
|
+
data["title"] = None
|
|
85
|
+
|
|
86
|
+
# Transform to ErrorResponse
|
|
87
|
+
error_response = transform_error_to_snake_case(data)
|
|
88
|
+
|
|
89
|
+
# Create error message from errors list
|
|
90
|
+
if error_response.errors:
|
|
91
|
+
if len(error_response.errors) == 1:
|
|
92
|
+
message = error_response.errors[0]
|
|
93
|
+
else:
|
|
94
|
+
title_prefix = f"{error_response.title}: " if error_response.title else ""
|
|
95
|
+
message = f"{title_prefix}{'; '.join(error_response.errors)}"
|
|
96
|
+
else:
|
|
97
|
+
message = error_response.title or "API Error"
|
|
98
|
+
|
|
99
|
+
# Create MisoClientError with ErrorResponse
|
|
100
|
+
return MisoClientError(
|
|
101
|
+
message=message,
|
|
102
|
+
status_code=status_code,
|
|
103
|
+
error_response=error_response,
|
|
104
|
+
)
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filter utilities for MisoClient SDK.
|
|
3
|
+
|
|
4
|
+
This module provides reusable filter utilities for parsing filter parameters,
|
|
5
|
+
building query strings, and applying filters to arrays.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, List
|
|
9
|
+
from urllib.parse import quote, unquote
|
|
10
|
+
|
|
11
|
+
from ..models.filter import FilterOption, FilterQuery
|
|
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
|
+
parts = filter_str.split(":", 2)
|
|
55
|
+
if len(parts) != 3:
|
|
56
|
+
continue # Skip invalid filter format
|
|
57
|
+
|
|
58
|
+
field = unquote(parts[0].strip())
|
|
59
|
+
op = parts[1].strip()
|
|
60
|
+
value_str = unquote(parts[2].strip())
|
|
61
|
+
|
|
62
|
+
# Validate operator
|
|
63
|
+
valid_operators = ["eq", "neq", "in", "nin", "gt", "lt", "gte", "lte", "contains", "like"]
|
|
64
|
+
if op not in valid_operators:
|
|
65
|
+
continue # Skip invalid operator
|
|
66
|
+
|
|
67
|
+
# Parse value based on operator
|
|
68
|
+
if op in ("in", "nin"):
|
|
69
|
+
# Array values: comma-separated
|
|
70
|
+
value = [v.strip() for v in value_str.split(",") if v.strip()]
|
|
71
|
+
else:
|
|
72
|
+
# Single value: try to parse as number/boolean, fallback to string
|
|
73
|
+
value = value_str
|
|
74
|
+
# Try to parse as integer
|
|
75
|
+
try:
|
|
76
|
+
if "." not in value_str:
|
|
77
|
+
value = int(value_str)
|
|
78
|
+
else:
|
|
79
|
+
value = float(value_str)
|
|
80
|
+
except (ValueError, TypeError):
|
|
81
|
+
# Try boolean
|
|
82
|
+
if value_str.lower() in ("true", "false"):
|
|
83
|
+
value = value_str.lower() == "true"
|
|
84
|
+
else:
|
|
85
|
+
value = value_str
|
|
86
|
+
|
|
87
|
+
filters.append(FilterOption(field=field, op=op, value=value))
|
|
88
|
+
|
|
89
|
+
return filters
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def build_query_string(filter_query: FilterQuery) -> str:
|
|
93
|
+
"""
|
|
94
|
+
Convert FilterQuery object to query string.
|
|
95
|
+
|
|
96
|
+
Builds query string with filter, sort, page, page_size, and fields parameters.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
filter_query: FilterQuery object with filters, sort, pagination, and fields
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Query string (e.g., '?filter=status:eq:active&page=1&page_size=25&sort=-updated_at')
|
|
103
|
+
|
|
104
|
+
Examples:
|
|
105
|
+
>>> from miso_client.models.filter import FilterQuery, FilterOption
|
|
106
|
+
>>> query = FilterQuery(
|
|
107
|
+
... filters=[FilterOption(field='status', op='eq', value='active')],
|
|
108
|
+
... page=1,
|
|
109
|
+
... page_size=25
|
|
110
|
+
... )
|
|
111
|
+
>>> build_query_string(query)
|
|
112
|
+
'filter=status:eq:active&page=1&page_size=25'
|
|
113
|
+
"""
|
|
114
|
+
query_parts: List[str] = []
|
|
115
|
+
|
|
116
|
+
# Add filters
|
|
117
|
+
if filter_query.filters:
|
|
118
|
+
for filter_option in filter_query.filters:
|
|
119
|
+
# Format value for query string
|
|
120
|
+
if isinstance(filter_option.value, list):
|
|
121
|
+
# For arrays (in/nin), join with commas (don't encode the comma delimiter)
|
|
122
|
+
# URL encode each value individually, then join with comma
|
|
123
|
+
value_parts = [quote(str(v)) for v in filter_option.value]
|
|
124
|
+
value_str = ",".join(value_parts)
|
|
125
|
+
else:
|
|
126
|
+
value_str = quote(str(filter_option.value))
|
|
127
|
+
|
|
128
|
+
# URL encode field
|
|
129
|
+
field_encoded = quote(filter_option.field)
|
|
130
|
+
|
|
131
|
+
query_parts.append(f"filter={field_encoded}:{filter_option.op}:{value_str}")
|
|
132
|
+
|
|
133
|
+
# Add sort
|
|
134
|
+
if filter_query.sort:
|
|
135
|
+
for sort_field in filter_query.sort:
|
|
136
|
+
query_parts.append(f"sort={quote(sort_field)}")
|
|
137
|
+
|
|
138
|
+
# Add pagination
|
|
139
|
+
if filter_query.page is not None:
|
|
140
|
+
query_parts.append(f"page={filter_query.page}")
|
|
141
|
+
|
|
142
|
+
if filter_query.page_size is not None:
|
|
143
|
+
query_parts.append(f"page_size={filter_query.page_size}")
|
|
144
|
+
|
|
145
|
+
# Add fields
|
|
146
|
+
if filter_query.fields:
|
|
147
|
+
fields_str = ",".join(quote(f) for f in filter_query.fields)
|
|
148
|
+
query_parts.append(f"fields={fields_str}")
|
|
149
|
+
|
|
150
|
+
return "&".join(query_parts)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def apply_filters(items: List[Dict[str, Any]], filters: List[FilterOption]) -> List[Dict[str, Any]]:
|
|
154
|
+
"""
|
|
155
|
+
Apply filters to array locally (for testing/mocks).
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
items: Array of dictionaries to filter
|
|
159
|
+
filters: List of FilterOption objects to apply
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Filtered array of items
|
|
163
|
+
|
|
164
|
+
Examples:
|
|
165
|
+
>>> items = [{'status': 'active', 'region': 'eu'}, {'status': 'inactive', 'region': 'us'}]
|
|
166
|
+
>>> filters = [FilterOption(field='status', op='eq', value='active')]
|
|
167
|
+
>>> apply_filters(items, filters)
|
|
168
|
+
[{'status': 'active', 'region': 'eu'}]
|
|
169
|
+
"""
|
|
170
|
+
if not filters:
|
|
171
|
+
return items
|
|
172
|
+
|
|
173
|
+
filtered_items = items.copy()
|
|
174
|
+
|
|
175
|
+
for filter_option in filters:
|
|
176
|
+
field = filter_option.field
|
|
177
|
+
op = filter_option.op
|
|
178
|
+
value = filter_option.value
|
|
179
|
+
|
|
180
|
+
# Apply filter based on operator
|
|
181
|
+
if op == "eq":
|
|
182
|
+
filtered_items = [
|
|
183
|
+
item for item in filtered_items if field in item and item[field] == value
|
|
184
|
+
]
|
|
185
|
+
elif op == "neq":
|
|
186
|
+
filtered_items = [
|
|
187
|
+
item for item in filtered_items if field not in item or item[field] != value
|
|
188
|
+
]
|
|
189
|
+
elif op == "in":
|
|
190
|
+
if isinstance(value, list):
|
|
191
|
+
filtered_items = [
|
|
192
|
+
item for item in filtered_items if field in item and item[field] in value
|
|
193
|
+
]
|
|
194
|
+
else:
|
|
195
|
+
filtered_items = [
|
|
196
|
+
item for item in filtered_items if field in item and item[field] == value
|
|
197
|
+
]
|
|
198
|
+
elif op == "nin":
|
|
199
|
+
if isinstance(value, list):
|
|
200
|
+
filtered_items = [
|
|
201
|
+
item for item in filtered_items if field not in item or item[field] not in value
|
|
202
|
+
]
|
|
203
|
+
else:
|
|
204
|
+
filtered_items = [
|
|
205
|
+
item for item in filtered_items if field not in item or item[field] != value
|
|
206
|
+
]
|
|
207
|
+
elif op == "gt":
|
|
208
|
+
filtered_items = [
|
|
209
|
+
item
|
|
210
|
+
for item in filtered_items
|
|
211
|
+
if field in item and isinstance(item[field], (int, float)) and item[field] > value
|
|
212
|
+
]
|
|
213
|
+
elif op == "lt":
|
|
214
|
+
filtered_items = [
|
|
215
|
+
item
|
|
216
|
+
for item in filtered_items
|
|
217
|
+
if field in item and isinstance(item[field], (int, float)) and item[field] < value
|
|
218
|
+
]
|
|
219
|
+
elif op == "gte":
|
|
220
|
+
filtered_items = [
|
|
221
|
+
item
|
|
222
|
+
for item in filtered_items
|
|
223
|
+
if field in item and isinstance(item[field], (int, float)) and item[field] >= value
|
|
224
|
+
]
|
|
225
|
+
elif op == "lte":
|
|
226
|
+
filtered_items = [
|
|
227
|
+
item
|
|
228
|
+
for item in filtered_items
|
|
229
|
+
if field in item and isinstance(item[field], (int, float)) and item[field] <= value
|
|
230
|
+
]
|
|
231
|
+
elif op == "contains":
|
|
232
|
+
if isinstance(value, str):
|
|
233
|
+
filtered_items = [
|
|
234
|
+
item
|
|
235
|
+
for item in filtered_items
|
|
236
|
+
if field in item and isinstance(item[field], str) and value in item[field]
|
|
237
|
+
]
|
|
238
|
+
else:
|
|
239
|
+
# For non-string values, check if value is in list/array field
|
|
240
|
+
filtered_items = [
|
|
241
|
+
item
|
|
242
|
+
for item in filtered_items
|
|
243
|
+
if field in item and isinstance(item[field], list) and value in item[field]
|
|
244
|
+
]
|
|
245
|
+
elif op == "like":
|
|
246
|
+
if isinstance(value, str):
|
|
247
|
+
# Simple like matching (contains)
|
|
248
|
+
filtered_items = [
|
|
249
|
+
item
|
|
250
|
+
for item in filtered_items
|
|
251
|
+
if field in item
|
|
252
|
+
and isinstance(item[field], str)
|
|
253
|
+
and value.lower() in item[field].lower()
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
return filtered_items
|