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.
Files changed (69) hide show
  1. miso_client/__init__.py +523 -130
  2. miso_client/api/__init__.py +35 -0
  3. miso_client/api/auth_api.py +367 -0
  4. miso_client/api/logs_api.py +91 -0
  5. miso_client/api/permissions_api.py +88 -0
  6. miso_client/api/roles_api.py +88 -0
  7. miso_client/api/types/__init__.py +75 -0
  8. miso_client/api/types/auth_types.py +183 -0
  9. miso_client/api/types/logs_types.py +71 -0
  10. miso_client/api/types/permissions_types.py +31 -0
  11. miso_client/api/types/roles_types.py +31 -0
  12. miso_client/errors.py +30 -4
  13. miso_client/models/__init__.py +4 -0
  14. miso_client/models/config.py +275 -72
  15. miso_client/models/error_response.py +39 -0
  16. miso_client/models/filter.py +255 -0
  17. miso_client/models/pagination.py +44 -0
  18. miso_client/models/sort.py +25 -0
  19. miso_client/services/__init__.py +6 -5
  20. miso_client/services/auth.py +496 -87
  21. miso_client/services/cache.py +42 -41
  22. miso_client/services/encryption.py +18 -17
  23. miso_client/services/logger.py +467 -328
  24. miso_client/services/logger_chain.py +288 -0
  25. miso_client/services/permission.py +130 -67
  26. miso_client/services/redis.py +28 -23
  27. miso_client/services/role.py +145 -62
  28. miso_client/utils/__init__.py +3 -3
  29. miso_client/utils/audit_log_queue.py +222 -0
  30. miso_client/utils/auth_strategy.py +88 -0
  31. miso_client/utils/auth_utils.py +65 -0
  32. miso_client/utils/circuit_breaker.py +125 -0
  33. miso_client/utils/client_token_manager.py +244 -0
  34. miso_client/utils/config_loader.py +88 -17
  35. miso_client/utils/controller_url_resolver.py +80 -0
  36. miso_client/utils/data_masker.py +104 -33
  37. miso_client/utils/environment_token.py +126 -0
  38. miso_client/utils/error_utils.py +216 -0
  39. miso_client/utils/fastapi_endpoints.py +166 -0
  40. miso_client/utils/filter.py +364 -0
  41. miso_client/utils/filter_applier.py +143 -0
  42. miso_client/utils/filter_parser.py +110 -0
  43. miso_client/utils/flask_endpoints.py +169 -0
  44. miso_client/utils/http_client.py +494 -262
  45. miso_client/utils/http_client_logging.py +352 -0
  46. miso_client/utils/http_client_logging_helpers.py +197 -0
  47. miso_client/utils/http_client_query_helpers.py +138 -0
  48. miso_client/utils/http_error_handler.py +92 -0
  49. miso_client/utils/http_log_formatter.py +115 -0
  50. miso_client/utils/http_log_masker.py +203 -0
  51. miso_client/utils/internal_http_client.py +435 -0
  52. miso_client/utils/jwt_tools.py +125 -16
  53. miso_client/utils/logger_helpers.py +206 -0
  54. miso_client/utils/logging_helpers.py +70 -0
  55. miso_client/utils/origin_validator.py +128 -0
  56. miso_client/utils/pagination.py +275 -0
  57. miso_client/utils/request_context.py +285 -0
  58. miso_client/utils/sensitive_fields_loader.py +116 -0
  59. miso_client/utils/sort.py +116 -0
  60. miso_client/utils/token_utils.py +114 -0
  61. miso_client/utils/url_validator.py +66 -0
  62. miso_client/utils/user_token_refresh.py +245 -0
  63. miso_client-3.7.2.dist-info/METADATA +1021 -0
  64. miso_client-3.7.2.dist-info/RECORD +68 -0
  65. miso_client-0.1.0.dist-info/METADATA +0 -551
  66. miso_client-0.1.0.dist-info/RECORD +0 -23
  67. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/WHEEL +0 -0
  68. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/licenses/LICENSE +0 -0
  69. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,166 @@
1
+ """
2
+ FastAPI 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
+ from typing import Any, Callable, Optional
9
+
10
+ from ..errors import AuthenticationError
11
+ from ..models.config import (
12
+ ClientTokenEndpointOptions,
13
+ ClientTokenEndpointResponse,
14
+ DataClientConfigResponse,
15
+ MisoClientConfig,
16
+ )
17
+ from ..utils.environment_token import get_environment_token
18
+
19
+
20
+ def create_fastapi_client_token_endpoint(
21
+ miso_client: Any, options: Optional[ClientTokenEndpointOptions] = None
22
+ ) -> Callable[[Any], Any]:
23
+ """
24
+ Create FastAPI route handler for client-token endpoint.
25
+
26
+ Automatically enriches response with DataClient configuration including
27
+ controllerPublicUrl for frontend client initialization.
28
+
29
+ Args:
30
+ miso_client: MisoClient instance (must be initialized)
31
+ options: Optional configuration for endpoint
32
+
33
+ Returns:
34
+ FastAPI route handler function
35
+
36
+ Example:
37
+ >>> from fastapi import FastAPI
38
+ >>> from miso_client import MisoClient, create_fastapi_client_token_endpoint, load_config
39
+ >>>
40
+ >>> app = FastAPI()
41
+ >>> client = MisoClient(load_config())
42
+ >>> await client.initialize()
43
+ >>>
44
+ >>> app.post('/api/v1/auth/client-token')(create_fastapi_client_token_endpoint(client))
45
+ """
46
+ opts = ClientTokenEndpointOptions(
47
+ clientTokenUri=options.clientTokenUri if options else "/api/v1/auth/client-token",
48
+ expiresIn=options.expiresIn if options else 1800,
49
+ includeConfig=options.includeConfig if options else True,
50
+ )
51
+
52
+ async def handler(request: Any) -> ClientTokenEndpointResponse:
53
+ """
54
+ FastAPI route handler for client token endpoint.
55
+
56
+ Args:
57
+ request: FastAPI Request object
58
+
59
+ Returns:
60
+ ClientTokenEndpointResponse with token and optional config
61
+
62
+ Raises:
63
+ HTTPException: With appropriate status code on errors
64
+ """
65
+ try:
66
+ # Check if misoClient is initialized
67
+ if not miso_client.is_initialized():
68
+ try:
69
+ from fastapi import HTTPException
70
+
71
+ raise HTTPException(
72
+ status_code=503,
73
+ detail={
74
+ "error": "Service Unavailable",
75
+ "message": "MisoClient is not initialized",
76
+ },
77
+ )
78
+ except ImportError:
79
+ raise RuntimeError("FastAPI is not installed")
80
+
81
+ # Get token with origin validation (raises AuthenticationError if validation fails)
82
+ token = await get_environment_token(miso_client, request.headers)
83
+
84
+ # Build response
85
+ response: ClientTokenEndpointResponse = ClientTokenEndpointResponse(
86
+ token=token, expiresIn=opts.expiresIn or 1800
87
+ )
88
+
89
+ # Include config if requested
90
+ if opts.includeConfig:
91
+ config: MisoClientConfig = miso_client.config
92
+
93
+ # Derive baseUrl from request
94
+ # request.base_url is a URL object in FastAPI
95
+ base_url = str(request.base_url).rstrip("/")
96
+
97
+ # Get controller URL (prefer controllerPublicUrl for browser, fallback to controller_url)
98
+ controller_url = config.controllerPublicUrl or config.controller_url
99
+
100
+ if not controller_url:
101
+ try:
102
+ from fastapi import HTTPException
103
+
104
+ raise HTTPException(
105
+ status_code=500,
106
+ detail={
107
+ "error": "Internal Server Error",
108
+ "message": "Controller URL not configured",
109
+ },
110
+ )
111
+ except ImportError:
112
+ raise RuntimeError("FastAPI is not installed")
113
+
114
+ response.config = DataClientConfigResponse(
115
+ baseUrl=base_url,
116
+ controllerUrl=controller_url,
117
+ controllerPublicUrl=config.controllerPublicUrl,
118
+ clientId=config.client_id,
119
+ clientTokenUri=opts.clientTokenUri or "/api/v1/auth/client-token",
120
+ )
121
+
122
+ return response
123
+
124
+ except AuthenticationError as error:
125
+ # Origin validation failed (403)
126
+ error_message = str(error)
127
+ try:
128
+ from fastapi import HTTPException
129
+
130
+ if "Origin validation failed" in error_message:
131
+ raise HTTPException(
132
+ status_code=403,
133
+ detail={
134
+ "error": "Forbidden",
135
+ "message": error_message,
136
+ },
137
+ )
138
+
139
+ # Other authentication errors (500)
140
+ raise HTTPException(
141
+ status_code=500,
142
+ detail={
143
+ "error": "Internal Server Error",
144
+ "message": error_message,
145
+ },
146
+ )
147
+ except ImportError:
148
+ raise RuntimeError("FastAPI is not installed")
149
+
150
+ except Exception as error:
151
+ # Other errors (500)
152
+ error_message = str(error) if error else "Unknown error"
153
+ try:
154
+ from fastapi import HTTPException
155
+
156
+ raise HTTPException(
157
+ status_code=500,
158
+ detail={
159
+ "error": "Internal Server Error",
160
+ "message": error_message,
161
+ },
162
+ )
163
+ except ImportError:
164
+ raise RuntimeError("FastAPI is not installed")
165
+
166
+ return handler
@@ -0,0 +1,364 @@
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, Optional
9
+ from urllib.parse import parse_qs, quote, urlparse
10
+
11
+ from ..models.filter import FilterQuery, JsonFilter
12
+ from .filter_applier import apply_filters # noqa: F401
13
+ from .filter_parser import parse_filter_params # noqa: F401
14
+
15
+
16
+ def build_query_string(filter_query: FilterQuery) -> str:
17
+ """
18
+ Convert FilterQuery object to query string.
19
+
20
+ Builds query string with filter, sort, page, pageSize, and fields parameters.
21
+
22
+ Args:
23
+ filter_query: FilterQuery object with filters, sort, pagination, and fields
24
+
25
+ Returns:
26
+ Query string (e.g., '?filter=status:eq:active&page=1&pageSize=25&sort=-updated_at')
27
+
28
+ Examples:
29
+ >>> from miso_client.models.filter import FilterQuery, FilterOption
30
+ >>> query = FilterQuery(
31
+ ... filters=[FilterOption(field='status', op='eq', value='active')],
32
+ ... page=1,
33
+ ... pageSize=25
34
+ ... )
35
+ >>> build_query_string(query)
36
+ 'filter=status:eq:active&page=1&pageSize=25'
37
+ """
38
+ query_parts: List[str] = []
39
+
40
+ # Add filters
41
+ if filter_query.filters:
42
+ for filter_option in filter_query.filters:
43
+ # URL encode field
44
+ field_encoded = quote(filter_option.field)
45
+
46
+ # Handle null check operators (no value needed)
47
+ if filter_option.op in ("isNull", "isNotNull"):
48
+ query_parts.append(f"filter={field_encoded}:{filter_option.op}")
49
+ else:
50
+ # Format value for query string
51
+ if isinstance(filter_option.value, list):
52
+ # For arrays (in/nin), join with commas (don't encode the comma delimiter)
53
+ # URL encode each value individually, then join with comma
54
+ value_parts = [quote(str(v)) for v in filter_option.value]
55
+ value_str = ",".join(value_parts)
56
+ elif filter_option.value is not None:
57
+ value_str = quote(str(filter_option.value))
58
+ else:
59
+ # Value is None but operator requires value - skip or use empty string
60
+ value_str = ""
61
+
62
+ query_parts.append(f"filter={field_encoded}:{filter_option.op}:{value_str}")
63
+
64
+ # Add sort
65
+ if filter_query.sort:
66
+ for sort_field in filter_query.sort:
67
+ query_parts.append(f"sort={quote(sort_field)}")
68
+
69
+ # Add pagination
70
+ if filter_query.page is not None:
71
+ query_parts.append(f"page={filter_query.page}")
72
+
73
+ if filter_query.pageSize is not None:
74
+ query_parts.append(f"pageSize={filter_query.pageSize}")
75
+
76
+ # Add fields
77
+ if filter_query.fields:
78
+ fields_str = ",".join(quote(f) for f in filter_query.fields)
79
+ query_parts.append(f"fields={fields_str}")
80
+
81
+ return "&".join(query_parts)
82
+
83
+
84
+ def filter_query_to_json(filter_query: FilterQuery) -> Dict[str, Any]:
85
+ """
86
+ Convert FilterQuery to JSON dict (camelCase).
87
+
88
+ Args:
89
+ filter_query: FilterQuery instance
90
+
91
+ Returns:
92
+ Dictionary with filter data in camelCase format
93
+
94
+ Examples:
95
+ >>> from miso_client.models.filter import FilterQuery, FilterOption
96
+ >>> query = FilterQuery(
97
+ ... filters=[FilterOption(field='status', op='eq', value='active')],
98
+ ... page=1,
99
+ ... pageSize=25
100
+ ... )
101
+ >>> filter_query_to_json(query)
102
+ {'filters': [...], 'page': 1, 'pageSize': 25}
103
+ """
104
+ return filter_query.to_json()
105
+
106
+
107
+ def json_to_filter_query(json_data: Dict[str, Any]) -> FilterQuery:
108
+ """
109
+ Convert JSON dict to FilterQuery.
110
+
111
+ Args:
112
+ json_data: Dictionary with filter data (camelCase or snake_case)
113
+
114
+ Returns:
115
+ FilterQuery instance
116
+
117
+ Examples:
118
+ >>> json_data = {'filters': [{'field': 'status', 'op': 'eq', 'value': 'active'}]}
119
+ >>> json_to_filter_query(json_data)
120
+ FilterQuery(filters=[FilterOption(...)])
121
+ """
122
+ return FilterQuery.from_json(json_data)
123
+
124
+
125
+ def json_filter_to_query_string(json_filter: JsonFilter) -> str:
126
+ """
127
+ Convert JsonFilter to query string.
128
+
129
+ Args:
130
+ json_filter: JsonFilter instance
131
+
132
+ Returns:
133
+ Query string (e.g., 'filter=status:eq:active&page=1&pageSize=25')
134
+
135
+ Examples:
136
+ >>> from miso_client.models.filter import JsonFilter, FilterOption
137
+ >>> json_filter = JsonFilter(
138
+ ... filters=[FilterOption(field='status', op='eq', value='active')],
139
+ ... page=1,
140
+ ... pageSize=25
141
+ ... )
142
+ >>> json_filter_to_query_string(json_filter)
143
+ 'filter=status:eq:active&page=1&pageSize=25'
144
+ """
145
+ # Convert JsonFilter to FilterQuery, then use existing build_query_string
146
+ filter_query = FilterQuery(
147
+ filters=json_filter.filters,
148
+ sort=json_filter.sort,
149
+ page=json_filter.page,
150
+ pageSize=json_filter.pageSize,
151
+ fields=json_filter.fields,
152
+ )
153
+ return build_query_string(filter_query)
154
+
155
+
156
+ def query_string_to_json_filter(query_string: str) -> JsonFilter:
157
+ """
158
+ Convert query string to JsonFilter.
159
+
160
+ Args:
161
+ query_string: Query string (e.g., '?filter=status:eq:active&page=1&pageSize=25')
162
+
163
+ Returns:
164
+ JsonFilter instance
165
+
166
+ Examples:
167
+ >>> query_string = '?filter=status:eq:active&page=1&pageSize=25'
168
+ >>> json_filter = query_string_to_json_filter(query_string)
169
+ >>> json_filter.filters[0].field
170
+ 'status'
171
+ """
172
+ # Remove leading ? if present
173
+ if query_string.startswith("?"):
174
+ query_string = query_string[1:]
175
+
176
+ # Parse query string
177
+ parsed = urlparse(f"?{query_string}")
178
+ params = parse_qs(parsed.query)
179
+
180
+ # Parse filters
181
+ filters = parse_filter_params(params)
182
+
183
+ # Parse sort
184
+ sort: Optional[List[str]] = None
185
+ if "sort" in params:
186
+ sort_list = params["sort"]
187
+ if isinstance(sort_list, list):
188
+ sort = [s for s in sort_list if isinstance(s, str)]
189
+ elif isinstance(sort_list, str):
190
+ sort = [sort_list]
191
+
192
+ # Parse pagination
193
+ page: Optional[int] = None
194
+ if "page" in params:
195
+ page_str = params["page"][0] if isinstance(params["page"], list) else params["page"]
196
+ try:
197
+ page = int(page_str)
198
+ except (ValueError, TypeError):
199
+ pass
200
+
201
+ page_size: Optional[int] = None
202
+ if "pageSize" in params:
203
+ page_size_str = (
204
+ params["pageSize"][0] if isinstance(params["pageSize"], list) else params["pageSize"]
205
+ )
206
+ try:
207
+ page_size = int(page_size_str)
208
+ except (ValueError, TypeError):
209
+ pass
210
+
211
+ # Parse fields
212
+ fields: Optional[List[str]] = None
213
+ if "fields" in params:
214
+ fields_str = params["fields"][0] if isinstance(params["fields"], list) else params["fields"]
215
+ if isinstance(fields_str, str):
216
+ fields = [f.strip() for f in fields_str.split(",") if f.strip()]
217
+
218
+ return JsonFilter(
219
+ filters=filters if filters else None,
220
+ sort=sort,
221
+ page=page,
222
+ pageSize=page_size,
223
+ fields=fields,
224
+ )
225
+
226
+
227
+ def validate_filter_option(option: Dict[str, Any]) -> bool:
228
+ """
229
+ Validate single filter option structure.
230
+
231
+ Args:
232
+ option: Dictionary with filter option data
233
+
234
+ Returns:
235
+ True if valid, False otherwise
236
+
237
+ Examples:
238
+ >>> validate_filter_option({'field': 'status', 'op': 'eq', 'value': 'active'})
239
+ True
240
+ >>> validate_filter_option({'field': 'status'}) # Missing op/value
241
+ False
242
+ """
243
+ if not isinstance(option, dict):
244
+ return False
245
+
246
+ # Check required fields
247
+ if "field" not in option or "op" not in option:
248
+ return False
249
+
250
+ # Validate operator
251
+ valid_operators = [
252
+ "eq",
253
+ "neq",
254
+ "in",
255
+ "nin",
256
+ "gt",
257
+ "lt",
258
+ "gte",
259
+ "lte",
260
+ "contains",
261
+ "like",
262
+ "isNull",
263
+ "isNotNull",
264
+ ]
265
+ if option["op"] not in valid_operators:
266
+ return False
267
+
268
+ # Value is optional for null check operators
269
+ if option["op"] not in ("isNull", "isNotNull") and "value" not in option:
270
+ return False
271
+
272
+ # Validate field is string
273
+ if not isinstance(option["field"], str):
274
+ return False
275
+
276
+ return True
277
+
278
+
279
+ def validate_json_filter(json_data: Dict[str, Any]) -> bool:
280
+ """
281
+ Validate JSON filter structure.
282
+
283
+ Args:
284
+ json_data: Dictionary with filter data
285
+
286
+ Returns:
287
+ True if valid, False otherwise
288
+
289
+ Examples:
290
+ >>> json_data = {
291
+ ... 'filters': [{'field': 'status', 'op': 'eq', 'value': 'active'}],
292
+ ... 'page': 1,
293
+ ... 'pageSize': 25
294
+ ... }
295
+ >>> validate_json_filter(json_data)
296
+ True
297
+ """
298
+ if not isinstance(json_data, dict):
299
+ return False
300
+
301
+ # Validate filters if present
302
+ if "filters" in json_data and json_data["filters"] is not None:
303
+ if not isinstance(json_data["filters"], list):
304
+ return False
305
+ for filter_option in json_data["filters"]:
306
+ if not validate_filter_option(filter_option):
307
+ return False
308
+
309
+ # Validate groups if present
310
+ if "groups" in json_data and json_data["groups"] is not None:
311
+ if not isinstance(json_data["groups"], list):
312
+ return False
313
+ for group in json_data["groups"]:
314
+ if not isinstance(group, dict):
315
+ return False
316
+ if "operator" not in group:
317
+ return False
318
+ if group["operator"] not in ["and", "or"]:
319
+ return False
320
+ # Validate filters in group
321
+ if "filters" in group and group["filters"] is not None:
322
+ if not isinstance(group["filters"], list):
323
+ return False
324
+ for filter_option in group["filters"]:
325
+ if not validate_filter_option(filter_option):
326
+ return False
327
+ # Validate nested groups recursively
328
+ if "groups" in group and group["groups"] is not None:
329
+ if not isinstance(group["groups"], list):
330
+ return False
331
+ for nested_group in group["groups"]:
332
+ if not isinstance(nested_group, dict):
333
+ return False
334
+ # Recursive validation (simplified - just check structure)
335
+ if "operator" not in nested_group:
336
+ return False
337
+
338
+ # Validate sort if present
339
+ if "sort" in json_data and json_data["sort"] is not None:
340
+ if not isinstance(json_data["sort"], list):
341
+ return False
342
+ for sort_item in json_data["sort"]:
343
+ if not isinstance(sort_item, str):
344
+ return False
345
+
346
+ # Validate page if present
347
+ if "page" in json_data and json_data["page"] is not None:
348
+ if not isinstance(json_data["page"], int):
349
+ return False
350
+
351
+ # Validate pageSize if present
352
+ if "pageSize" in json_data and json_data["pageSize"] is not None:
353
+ if not isinstance(json_data["pageSize"], int):
354
+ return False
355
+
356
+ # Validate fields if present
357
+ if "fields" in json_data and json_data["fields"] is not None:
358
+ if not isinstance(json_data["fields"], list):
359
+ return False
360
+ for field in json_data["fields"]:
361
+ if not isinstance(field, str):
362
+ return False
363
+
364
+ return True
@@ -0,0 +1,143 @@
1
+ """
2
+ Filter application utilities for MisoClient SDK.
3
+
4
+ This module provides utilities for applying filters to arrays locally,
5
+ useful for testing and mocking scenarios.
6
+ """
7
+
8
+ from typing import Any, Dict, List
9
+
10
+ from ..models.filter import FilterOption
11
+
12
+
13
+ def apply_filters(items: List[Dict[str, Any]], filters: List[FilterOption]) -> List[Dict[str, Any]]:
14
+ """
15
+ Apply filters to array locally (for testing/mocks).
16
+
17
+ Args:
18
+ items: Array of dictionaries to filter
19
+ filters: List of FilterOption objects to apply
20
+
21
+ Returns:
22
+ Filtered array of items
23
+
24
+ Examples:
25
+ >>> items = [{'status': 'active', 'region': 'eu'}, {'status': 'inactive', 'region': 'us'}]
26
+ >>> filters = [FilterOption(field='status', op='eq', value='active')]
27
+ >>> apply_filters(items, filters)
28
+ [{'status': 'active', 'region': 'eu'}]
29
+ """
30
+ if not filters:
31
+ return items
32
+
33
+ filtered_items = items.copy()
34
+
35
+ for filter_option in filters:
36
+ field = filter_option.field
37
+ op = filter_option.op
38
+ value = filter_option.value
39
+
40
+ # Apply filter based on operator
41
+ if op == "eq":
42
+ filtered_items = [
43
+ item for item in filtered_items if field in item and item[field] == value
44
+ ]
45
+ elif op == "neq":
46
+ filtered_items = [
47
+ item for item in filtered_items if field not in item or item[field] != value
48
+ ]
49
+ elif op == "in":
50
+ if isinstance(value, list):
51
+ filtered_items = [
52
+ item for item in filtered_items if field in item and item[field] in value
53
+ ]
54
+ else:
55
+ filtered_items = [
56
+ item for item in filtered_items if field in item and item[field] == value
57
+ ]
58
+ elif op == "nin":
59
+ if isinstance(value, list):
60
+ filtered_items = [
61
+ item for item in filtered_items if field not in item or item[field] not in value
62
+ ]
63
+ else:
64
+ filtered_items = [
65
+ item for item in filtered_items if field not in item or item[field] != value
66
+ ]
67
+ elif op == "gt":
68
+ filtered_items = [
69
+ item
70
+ for item in filtered_items
71
+ if field in item
72
+ and isinstance(item[field], (int, float))
73
+ and isinstance(value, (int, float))
74
+ and item[field] > value
75
+ ]
76
+ elif op == "lt":
77
+ filtered_items = [
78
+ item
79
+ for item in filtered_items
80
+ if field in item
81
+ and isinstance(item[field], (int, float))
82
+ and isinstance(value, (int, float))
83
+ and item[field] < value
84
+ ]
85
+ elif op == "gte":
86
+ filtered_items = [
87
+ item
88
+ for item in filtered_items
89
+ if field in item
90
+ and isinstance(item[field], (int, float))
91
+ and isinstance(value, (int, float))
92
+ and item[field] >= value
93
+ ]
94
+ elif op == "lte":
95
+ filtered_items = [
96
+ item
97
+ for item in filtered_items
98
+ if field in item
99
+ and isinstance(item[field], (int, float))
100
+ and isinstance(value, (int, float))
101
+ and item[field] <= value
102
+ ]
103
+ elif op == "contains":
104
+ if isinstance(value, str):
105
+ # For string values, check both string fields (substring) and list fields (membership)
106
+ filtered_items = [
107
+ item
108
+ for item in filtered_items
109
+ if field in item
110
+ and (
111
+ (isinstance(item[field], str) and value in item[field])
112
+ or (isinstance(item[field], list) and value in item[field])
113
+ )
114
+ ]
115
+ else:
116
+ # For non-string values, check if value is in list/array field
117
+ filtered_items = [
118
+ item
119
+ for item in filtered_items
120
+ if field in item and isinstance(item[field], list) and value in item[field]
121
+ ]
122
+ elif op == "like":
123
+ if isinstance(value, str):
124
+ # Simple like matching (contains)
125
+ filtered_items = [
126
+ item
127
+ for item in filtered_items
128
+ if field in item
129
+ and isinstance(item[field], str)
130
+ and value.lower() in item[field].lower()
131
+ ]
132
+ elif op == "isNull":
133
+ # Field is missing or value is None
134
+ filtered_items = [
135
+ item for item in filtered_items if field not in item or item[field] is None
136
+ ]
137
+ elif op == "isNotNull":
138
+ # Field exists and value is not None
139
+ filtered_items = [
140
+ item for item in filtered_items if field in item and item[field] is not None
141
+ ]
142
+
143
+ return filtered_items