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.

@@ -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