miso-client 0.4.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 +39 -1
- miso_client/models/error_response.py +10 -1
- miso_client/models/filter.py +140 -0
- miso_client/models/pagination.py +66 -0
- miso_client/models/sort.py +25 -0
- miso_client/utils/error_utils.py +104 -0
- miso_client/utils/filter.py +256 -0
- miso_client/utils/http_client.py +107 -0
- miso_client/utils/pagination.py +157 -0
- miso_client/utils/sort.py +116 -0
- {miso_client-0.4.0.dist-info → miso_client-0.5.0.dist-info}/METADATA +220 -1
- {miso_client-0.4.0.dist-info → miso_client-0.5.0.dist-info}/RECORD +15 -8
- {miso_client-0.4.0.dist-info → miso_client-0.5.0.dist-info}/WHEEL +0 -0
- {miso_client-0.4.0.dist-info → miso_client-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {miso_client-0.4.0.dist-info → miso_client-0.5.0.dist-info}/top_level.txt +0 -0
miso_client/__init__.py
CHANGED
|
@@ -27,6 +27,9 @@ from .models.config import (
|
|
|
27
27
|
UserInfo,
|
|
28
28
|
)
|
|
29
29
|
from .models.error_response import ErrorResponse
|
|
30
|
+
from .models.filter import FilterBuilder, FilterOperator, FilterOption, FilterQuery
|
|
31
|
+
from .models.pagination import Meta, PaginatedListResponse
|
|
32
|
+
from .models.sort import SortOption
|
|
30
33
|
from .services.auth import AuthService
|
|
31
34
|
from .services.cache import CacheService
|
|
32
35
|
from .services.encryption import EncryptionService
|
|
@@ -35,10 +38,19 @@ from .services.permission import PermissionService
|
|
|
35
38
|
from .services.redis import RedisService
|
|
36
39
|
from .services.role import RoleService
|
|
37
40
|
from .utils.config_loader import load_config
|
|
41
|
+
from .utils.error_utils import handle_api_error_snake_case, transform_error_to_snake_case
|
|
42
|
+
from .utils.filter import apply_filters, build_query_string, parse_filter_params
|
|
38
43
|
from .utils.http_client import HttpClient
|
|
39
44
|
from .utils.internal_http_client import InternalHttpClient
|
|
45
|
+
from .utils.pagination import (
|
|
46
|
+
apply_pagination_to_array,
|
|
47
|
+
create_meta_object,
|
|
48
|
+
create_paginated_list_response,
|
|
49
|
+
parse_pagination_params,
|
|
50
|
+
)
|
|
51
|
+
from .utils.sort import build_sort_string, parse_sort_params
|
|
40
52
|
|
|
41
|
-
__version__ = "0.
|
|
53
|
+
__version__ = "0.5.0"
|
|
42
54
|
__author__ = "AI Fabrix Team"
|
|
43
55
|
__license__ = "MIT"
|
|
44
56
|
|
|
@@ -491,6 +503,32 @@ __all__ = [
|
|
|
491
503
|
"PerformanceMetrics",
|
|
492
504
|
"ClientLoggingOptions",
|
|
493
505
|
"ErrorResponse",
|
|
506
|
+
# Pagination models
|
|
507
|
+
"Meta",
|
|
508
|
+
"PaginatedListResponse",
|
|
509
|
+
# Filter models
|
|
510
|
+
"FilterOperator",
|
|
511
|
+
"FilterOption",
|
|
512
|
+
"FilterQuery",
|
|
513
|
+
"FilterBuilder",
|
|
514
|
+
# Sort models
|
|
515
|
+
"SortOption",
|
|
516
|
+
# Pagination utilities
|
|
517
|
+
"parse_pagination_params",
|
|
518
|
+
"create_meta_object",
|
|
519
|
+
"apply_pagination_to_array",
|
|
520
|
+
"create_paginated_list_response",
|
|
521
|
+
# Filter utilities
|
|
522
|
+
"parse_filter_params",
|
|
523
|
+
"build_query_string",
|
|
524
|
+
"apply_filters",
|
|
525
|
+
# Sort utilities
|
|
526
|
+
"parse_sort_params",
|
|
527
|
+
"build_sort_string",
|
|
528
|
+
# Error utilities
|
|
529
|
+
"transform_error_to_snake_case",
|
|
530
|
+
"handle_api_error_snake_case",
|
|
531
|
+
# Services
|
|
494
532
|
"AuthService",
|
|
495
533
|
"RoleService",
|
|
496
534
|
"PermissionService",
|
|
@@ -33,9 +33,18 @@ class ErrorResponse(BaseModel):
|
|
|
33
33
|
|
|
34
34
|
errors: List[str] = Field(..., description="List of error messages")
|
|
35
35
|
type: str = Field(..., description="Error type URI (e.g., '/Errors/Bad Input')")
|
|
36
|
-
title: str = Field(
|
|
36
|
+
title: Optional[str] = Field(default=None, description="Human-readable error title")
|
|
37
37
|
statusCode: int = Field(..., alias="status_code", description="HTTP status code")
|
|
38
38
|
instance: Optional[str] = Field(default=None, description="Request instance URI")
|
|
39
|
+
request_key: Optional[str] = Field(
|
|
40
|
+
default=None, alias="requestKey", description="Request key for error tracking"
|
|
41
|
+
)
|
|
39
42
|
|
|
40
43
|
class Config:
|
|
41
44
|
populate_by_name = True # Allow both camelCase and snake_case
|
|
45
|
+
|
|
46
|
+
# Support snake_case attribute access
|
|
47
|
+
@property
|
|
48
|
+
def status_code(self) -> int:
|
|
49
|
+
"""Get statusCode as status_code (snake_case)."""
|
|
50
|
+
return self.statusCode
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filter types for MisoClient SDK.
|
|
3
|
+
|
|
4
|
+
This module contains Pydantic models and classes that define filter structures
|
|
5
|
+
for query filtering matching the Miso/Dataplane API conventions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, List, Literal, Optional, Union
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
FilterOperator = Literal[
|
|
13
|
+
"eq",
|
|
14
|
+
"neq",
|
|
15
|
+
"in",
|
|
16
|
+
"nin",
|
|
17
|
+
"gt",
|
|
18
|
+
"lt",
|
|
19
|
+
"gte",
|
|
20
|
+
"lte",
|
|
21
|
+
"contains",
|
|
22
|
+
"like",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FilterOption(BaseModel):
|
|
27
|
+
"""
|
|
28
|
+
Single filter option with field, operator, and value.
|
|
29
|
+
|
|
30
|
+
Fields:
|
|
31
|
+
field: Field name to filter on
|
|
32
|
+
op: Filter operator (eq, neq, in, nin, gt, lt, gte, lte, contains, like)
|
|
33
|
+
value: Filter value (supports single values or arrays for 'in'/'nin' operators)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
field: str = Field(..., description="Field name to filter on")
|
|
37
|
+
op: FilterOperator = Field(..., description="Filter operator")
|
|
38
|
+
value: Union[str, int, float, bool, List[Any]] = Field(
|
|
39
|
+
..., description="Filter value (supports arrays for 'in'/'nin' operators)"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FilterQuery(BaseModel):
|
|
44
|
+
"""
|
|
45
|
+
Complete filter query with filters, sort, pagination, and field selection.
|
|
46
|
+
|
|
47
|
+
Fields:
|
|
48
|
+
filters: Optional list of filter options
|
|
49
|
+
sort: Optional list of sort options (field strings with optional '-' prefix for desc)
|
|
50
|
+
page: Optional page number (1-based)
|
|
51
|
+
page_size: Optional number of items per page
|
|
52
|
+
fields: Optional list of fields to include in response
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
filters: Optional[List[FilterOption]] = Field(
|
|
56
|
+
default=None, description="List of filter options"
|
|
57
|
+
)
|
|
58
|
+
sort: Optional[List[str]] = Field(
|
|
59
|
+
default=None,
|
|
60
|
+
description="List of sort options (e.g., ['-updated_at', 'created_at'])",
|
|
61
|
+
)
|
|
62
|
+
page: Optional[int] = Field(default=None, description="Page number (1-based)")
|
|
63
|
+
page_size: Optional[int] = Field(default=None, description="Number of items per page")
|
|
64
|
+
fields: Optional[List[str]] = Field(
|
|
65
|
+
default=None, description="List of fields to include in response"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class FilterBuilder:
|
|
70
|
+
"""
|
|
71
|
+
Builder pattern for dynamic filter construction.
|
|
72
|
+
|
|
73
|
+
Allows chaining filter additions for building complex filter queries.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self):
|
|
77
|
+
"""Initialize empty filter builder."""
|
|
78
|
+
self._filters: List[FilterOption] = []
|
|
79
|
+
|
|
80
|
+
def add(self, field: str, op: FilterOperator, value: Any) -> "FilterBuilder":
|
|
81
|
+
"""
|
|
82
|
+
Add a filter option to the builder.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
field: Field name to filter on
|
|
86
|
+
op: Filter operator
|
|
87
|
+
value: Filter value
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
FilterBuilder instance for method chaining
|
|
91
|
+
"""
|
|
92
|
+
self._filters.append(FilterOption(field=field, op=op, value=value))
|
|
93
|
+
return self
|
|
94
|
+
|
|
95
|
+
def add_many(self, filters: List[FilterOption]) -> "FilterBuilder":
|
|
96
|
+
"""
|
|
97
|
+
Add multiple filter options to the builder.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
filters: List of FilterOption objects
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
FilterBuilder instance for method chaining
|
|
104
|
+
"""
|
|
105
|
+
self._filters.extend(filters)
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
def build(self) -> List[FilterOption]:
|
|
109
|
+
"""
|
|
110
|
+
Build the filter list.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of FilterOption objects
|
|
114
|
+
"""
|
|
115
|
+
return self._filters.copy()
|
|
116
|
+
|
|
117
|
+
def to_query_string(self) -> str:
|
|
118
|
+
"""
|
|
119
|
+
Convert filters to query string format.
|
|
120
|
+
|
|
121
|
+
Format: ?filter=field:op:value&filter=field:op:value
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Query string with filter parameters
|
|
125
|
+
"""
|
|
126
|
+
if not self._filters:
|
|
127
|
+
return ""
|
|
128
|
+
|
|
129
|
+
query_parts: List[str] = []
|
|
130
|
+
for filter_option in self._filters:
|
|
131
|
+
# Format value for query string
|
|
132
|
+
if isinstance(filter_option.value, list):
|
|
133
|
+
# For arrays (in/nin), join with commas
|
|
134
|
+
value_str = ",".join(str(v) for v in filter_option.value)
|
|
135
|
+
else:
|
|
136
|
+
value_str = str(filter_option.value)
|
|
137
|
+
|
|
138
|
+
query_parts.append(f"filter={filter_option.field}:{filter_option.op}:{value_str}")
|
|
139
|
+
|
|
140
|
+
return "&".join(query_parts)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pagination types for MisoClient SDK.
|
|
3
|
+
|
|
4
|
+
This module contains Pydantic models that define pagination structures
|
|
5
|
+
for paginated list responses matching the Miso/Dataplane API conventions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Generic, List, TypeVar
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Meta(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
Pagination metadata for list responses.
|
|
18
|
+
|
|
19
|
+
Fields:
|
|
20
|
+
total_items: Total number of items across all pages
|
|
21
|
+
current_page: Current page number (1-based, maps from `page` query param)
|
|
22
|
+
page_size: Number of items per page (maps from `page_size` query param)
|
|
23
|
+
type: Resource type identifier (e.g., 'item', 'user', 'group')
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
total_items: int = Field(..., alias="totalItems", description="Total number of items")
|
|
27
|
+
current_page: int = Field(..., alias="currentPage", description="Current page number (1-based)")
|
|
28
|
+
page_size: int = Field(..., alias="pageSize", description="Number of items per page")
|
|
29
|
+
type: str = Field(..., description="Resource type identifier")
|
|
30
|
+
|
|
31
|
+
class Config:
|
|
32
|
+
populate_by_name = True # Allow both snake_case and camelCase
|
|
33
|
+
|
|
34
|
+
# Support camelCase attribute access
|
|
35
|
+
@property
|
|
36
|
+
def totalItems(self) -> int:
|
|
37
|
+
"""Get total_items as totalItems (camelCase)."""
|
|
38
|
+
return self.total_items
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def currentPage(self) -> int:
|
|
42
|
+
"""Get current_page as currentPage (camelCase)."""
|
|
43
|
+
return self.current_page
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def pageSize(self) -> int:
|
|
47
|
+
"""Get page_size as pageSize (camelCase)."""
|
|
48
|
+
return self.page_size
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PaginatedListResponse(BaseModel, Generic[T]):
|
|
52
|
+
"""
|
|
53
|
+
Paginated list response structure.
|
|
54
|
+
|
|
55
|
+
Generic type parameter T represents the item type in the data array.
|
|
56
|
+
|
|
57
|
+
Fields:
|
|
58
|
+
meta: Pagination metadata
|
|
59
|
+
data: Array of items for current page
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
meta: Meta = Field(..., description="Pagination metadata")
|
|
63
|
+
data: List[T] = Field(..., description="Array of items for current page")
|
|
64
|
+
|
|
65
|
+
class Config:
|
|
66
|
+
populate_by_name = True # Allow both snake_case and camelCase
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sort types for MisoClient SDK.
|
|
3
|
+
|
|
4
|
+
This module contains Pydantic models that define sort structures
|
|
5
|
+
for query sorting matching the Miso/Dataplane API conventions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
SortOrder = Literal["asc", "desc"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SortOption(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
Sort option with field and order.
|
|
18
|
+
|
|
19
|
+
Fields:
|
|
20
|
+
field: Field name to sort by
|
|
21
|
+
order: Sort order ('asc' for ascending, 'desc' for descending)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
field: str = Field(..., description="Field name to sort by")
|
|
25
|
+
order: SortOrder = Field(..., description="Sort order ('asc' or 'desc')")
|
|
@@ -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
|
miso_client/utils/http_client.py
CHANGED
|
@@ -583,3 +583,110 @@ class HttpClient:
|
|
|
583
583
|
kwargs["headers"] = headers
|
|
584
584
|
|
|
585
585
|
return await self.request(method, url, data, **kwargs)
|
|
586
|
+
|
|
587
|
+
async def get_with_filters(
|
|
588
|
+
self,
|
|
589
|
+
url: str,
|
|
590
|
+
filter_builder: Optional[Any] = None,
|
|
591
|
+
**kwargs,
|
|
592
|
+
) -> Any:
|
|
593
|
+
"""
|
|
594
|
+
Make GET request with filter builder support.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
url: Request URL
|
|
598
|
+
filter_builder: Optional FilterBuilder instance with filters
|
|
599
|
+
**kwargs: Additional httpx request parameters
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
Response data (JSON parsed)
|
|
603
|
+
|
|
604
|
+
Raises:
|
|
605
|
+
MisoClientError: If request fails
|
|
606
|
+
|
|
607
|
+
Examples:
|
|
608
|
+
>>> from miso_client.models.filter import FilterBuilder
|
|
609
|
+
>>> filter_builder = FilterBuilder().add('status', 'eq', 'active')
|
|
610
|
+
>>> response = await client.http_client.get_with_filters('/api/items', filter_builder)
|
|
611
|
+
"""
|
|
612
|
+
from ..models.filter import FilterQuery
|
|
613
|
+
from ..utils.filter import build_query_string
|
|
614
|
+
|
|
615
|
+
# Build query string from filter builder
|
|
616
|
+
if filter_builder:
|
|
617
|
+
# Create FilterQuery from FilterBuilder
|
|
618
|
+
filter_query = FilterQuery(filters=filter_builder.build())
|
|
619
|
+
query_string = build_query_string(filter_query)
|
|
620
|
+
|
|
621
|
+
# Parse query string into params
|
|
622
|
+
if query_string:
|
|
623
|
+
from urllib.parse import parse_qs
|
|
624
|
+
|
|
625
|
+
# Parse query string into dict
|
|
626
|
+
query_params = parse_qs(query_string)
|
|
627
|
+
# Convert single-item lists to values
|
|
628
|
+
params = {k: v[0] if len(v) == 1 else v for k, v in query_params.items()}
|
|
629
|
+
|
|
630
|
+
# Merge with existing params
|
|
631
|
+
existing_params = kwargs.get("params", {})
|
|
632
|
+
if existing_params:
|
|
633
|
+
# Merge params (filter builder takes precedence)
|
|
634
|
+
merged_params = {**existing_params, **params}
|
|
635
|
+
else:
|
|
636
|
+
merged_params = params
|
|
637
|
+
|
|
638
|
+
kwargs["params"] = merged_params
|
|
639
|
+
|
|
640
|
+
return await self.get(url, **kwargs)
|
|
641
|
+
|
|
642
|
+
async def get_paginated(
|
|
643
|
+
self,
|
|
644
|
+
url: str,
|
|
645
|
+
page: Optional[int] = None,
|
|
646
|
+
page_size: Optional[int] = None,
|
|
647
|
+
**kwargs,
|
|
648
|
+
) -> Any:
|
|
649
|
+
"""
|
|
650
|
+
Make GET request with pagination support.
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
url: Request URL
|
|
654
|
+
page: Optional page number (1-based)
|
|
655
|
+
page_size: Optional number of items per page
|
|
656
|
+
**kwargs: Additional httpx request parameters
|
|
657
|
+
|
|
658
|
+
Returns:
|
|
659
|
+
PaginatedListResponse with meta and data (or raw response if format doesn't match)
|
|
660
|
+
|
|
661
|
+
Raises:
|
|
662
|
+
MisoClientError: If request fails
|
|
663
|
+
|
|
664
|
+
Examples:
|
|
665
|
+
>>> response = await client.http_client.get_paginated('/api/items', page=1, page_size=25)
|
|
666
|
+
>>> response.meta.total_items
|
|
667
|
+
120
|
|
668
|
+
>>> len(response.data)
|
|
669
|
+
25
|
|
670
|
+
"""
|
|
671
|
+
from ..models.pagination import PaginatedListResponse
|
|
672
|
+
|
|
673
|
+
# Add pagination params
|
|
674
|
+
params = kwargs.get("params", {})
|
|
675
|
+
if page is not None:
|
|
676
|
+
params["page"] = page
|
|
677
|
+
if page_size is not None:
|
|
678
|
+
params["page_size"] = page_size
|
|
679
|
+
|
|
680
|
+
if params:
|
|
681
|
+
kwargs["params"] = params
|
|
682
|
+
|
|
683
|
+
# Make request
|
|
684
|
+
response_data = await self.get(url, **kwargs)
|
|
685
|
+
|
|
686
|
+
# Try to parse as PaginatedListResponse
|
|
687
|
+
try:
|
|
688
|
+
return PaginatedListResponse(**response_data)
|
|
689
|
+
except Exception:
|
|
690
|
+
# If response doesn't match PaginatedListResponse format, return as-is
|
|
691
|
+
# This allows flexibility for different response formats
|
|
692
|
+
return response_data
|
|
@@ -0,0 +1,157 @@
|
|
|
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 List, Tuple, TypeVar
|
|
9
|
+
|
|
10
|
+
from ..models.pagination import Meta, PaginatedListResponse
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_pagination_params(params: dict) -> Tuple[int, int]:
|
|
16
|
+
"""
|
|
17
|
+
Parse query parameters to pagination values.
|
|
18
|
+
|
|
19
|
+
Parses `page` and `page_size` query parameters into `current_page` and `page_size`.
|
|
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
|
+
Tuple of (current_page, page_size) as integers
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
>>> parse_pagination_params({'page': '1', 'page_size': '25'})
|
|
30
|
+
(1, 25)
|
|
31
|
+
>>> parse_pagination_params({'page': '2'})
|
|
32
|
+
(2, 25) # Default page_size is 25
|
|
33
|
+
"""
|
|
34
|
+
# Default values
|
|
35
|
+
default_page = 1
|
|
36
|
+
default_page_size = 25
|
|
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 (current_page, page_size)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def create_meta_object(total_items: int, current_page: int, page_size: int, type: str) -> Meta:
|
|
66
|
+
"""
|
|
67
|
+
Construct Meta object from pagination parameters.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
total_items: Total number of items across all pages
|
|
71
|
+
current_page: Current page number (1-based)
|
|
72
|
+
page_size: Number of items per page
|
|
73
|
+
type: Resource type identifier (e.g., 'item', 'user', 'group')
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Meta object with pagination metadata
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
>>> meta = create_meta_object(120, 1, 25, 'item')
|
|
80
|
+
>>> meta.total_items
|
|
81
|
+
120
|
|
82
|
+
>>> meta.current_page
|
|
83
|
+
1
|
|
84
|
+
"""
|
|
85
|
+
return Meta(
|
|
86
|
+
total_items=total_items,
|
|
87
|
+
current_page=current_page,
|
|
88
|
+
page_size=page_size,
|
|
89
|
+
type=type,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def apply_pagination_to_array(items: List[T], current_page: int, page_size: int) -> List[T]:
|
|
94
|
+
"""
|
|
95
|
+
Apply pagination to array (for testing/mocks).
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
items: Array of items to paginate
|
|
99
|
+
current_page: Current page number (1-based)
|
|
100
|
+
page_size: Number of items per page
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Paginated subset of items for the specified page
|
|
104
|
+
|
|
105
|
+
Examples:
|
|
106
|
+
>>> items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
107
|
+
>>> apply_pagination_to_array(items, 1, 3)
|
|
108
|
+
[1, 2, 3]
|
|
109
|
+
>>> apply_pagination_to_array(items, 2, 3)
|
|
110
|
+
[4, 5, 6]
|
|
111
|
+
"""
|
|
112
|
+
if not items:
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
if current_page < 1:
|
|
116
|
+
current_page = 1
|
|
117
|
+
if page_size < 1:
|
|
118
|
+
page_size = 25
|
|
119
|
+
|
|
120
|
+
# Calculate start and end indices
|
|
121
|
+
start_index = (current_page - 1) * page_size
|
|
122
|
+
end_index = start_index + page_size
|
|
123
|
+
|
|
124
|
+
# Return paginated subset
|
|
125
|
+
return items[start_index:end_index]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def create_paginated_list_response(
|
|
129
|
+
items: List[T],
|
|
130
|
+
total_items: int,
|
|
131
|
+
current_page: int,
|
|
132
|
+
page_size: int,
|
|
133
|
+
type: str,
|
|
134
|
+
) -> PaginatedListResponse[T]:
|
|
135
|
+
"""
|
|
136
|
+
Wrap array + meta into standard paginated response.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
items: Array of items for current page
|
|
140
|
+
total_items: Total number of items across all pages
|
|
141
|
+
current_page: Current page number (1-based)
|
|
142
|
+
page_size: Number of items per page
|
|
143
|
+
type: Resource type identifier (e.g., 'item', 'user', 'group')
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
PaginatedListResponse with meta and data
|
|
147
|
+
|
|
148
|
+
Examples:
|
|
149
|
+
>>> items = [{'id': 1}, {'id': 2}]
|
|
150
|
+
>>> response = create_paginated_list_response(items, 10, 1, 2, 'item')
|
|
151
|
+
>>> response.meta.total_items
|
|
152
|
+
10
|
|
153
|
+
>>> len(response.data)
|
|
154
|
+
2
|
|
155
|
+
"""
|
|
156
|
+
meta = create_meta_object(total_items, current_page, page_size, type)
|
|
157
|
+
return PaginatedListResponse(meta=meta, data=items)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sort utilities for MisoClient SDK.
|
|
3
|
+
|
|
4
|
+
This module provides reusable sort utilities for parsing sort parameters
|
|
5
|
+
and building sort query strings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import List
|
|
9
|
+
from urllib.parse import quote
|
|
10
|
+
|
|
11
|
+
from ..models.sort import SortOption
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_sort_params(params: dict) -> List[SortOption]:
|
|
15
|
+
"""
|
|
16
|
+
Parse sort query parameters into SortOption list.
|
|
17
|
+
|
|
18
|
+
Parses `?sort=-field` format into SortOption objects.
|
|
19
|
+
Supports multiple sort parameters (array of sort strings).
|
|
20
|
+
Prefix with '-' for descending order, otherwise ascending.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
params: Dictionary with query parameters (e.g., {'sort': '-updated_at'} or {'sort': ['-updated_at', 'created_at']})
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
List of SortOption objects
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
>>> parse_sort_params({'sort': '-updated_at'})
|
|
30
|
+
[SortOption(field='updated_at', order='desc')]
|
|
31
|
+
>>> parse_sort_params({'sort': ['-updated_at', 'created_at']})
|
|
32
|
+
[SortOption(field='updated_at', order='desc'), SortOption(field='created_at', order='asc')]
|
|
33
|
+
"""
|
|
34
|
+
sort_options: List[SortOption] = []
|
|
35
|
+
|
|
36
|
+
# Get sort parameter (can be string or list)
|
|
37
|
+
sort_param = params.get("sort")
|
|
38
|
+
if not sort_param:
|
|
39
|
+
return sort_options
|
|
40
|
+
|
|
41
|
+
# Normalize to list
|
|
42
|
+
if isinstance(sort_param, str):
|
|
43
|
+
sort_strings = [sort_param]
|
|
44
|
+
elif isinstance(sort_param, list):
|
|
45
|
+
sort_strings = sort_param
|
|
46
|
+
else:
|
|
47
|
+
return sort_options
|
|
48
|
+
|
|
49
|
+
# Parse each sort string
|
|
50
|
+
for sort_str in sort_strings:
|
|
51
|
+
if not isinstance(sort_str, str):
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
sort_str = sort_str.strip()
|
|
55
|
+
if not sort_str:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
# Check for descending order (prefix with '-')
|
|
59
|
+
if sort_str.startswith("-"):
|
|
60
|
+
field = sort_str[1:].strip()
|
|
61
|
+
order = "desc"
|
|
62
|
+
else:
|
|
63
|
+
field = sort_str.strip()
|
|
64
|
+
order = "asc"
|
|
65
|
+
|
|
66
|
+
if field:
|
|
67
|
+
sort_options.append(SortOption(field=field, order=order))
|
|
68
|
+
|
|
69
|
+
return sort_options
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def build_sort_string(sort_options: List[SortOption]) -> str:
|
|
73
|
+
"""
|
|
74
|
+
Convert SortOption list to query string format.
|
|
75
|
+
|
|
76
|
+
Converts SortOption objects to sort query string format.
|
|
77
|
+
Descending order fields are prefixed with '-'.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
sort_options: List of SortOption objects
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Sort query string (e.g., '-updated_at,created_at' or single value '-updated_at')
|
|
84
|
+
|
|
85
|
+
Examples:
|
|
86
|
+
>>> from miso_client.models.sort import SortOption
|
|
87
|
+
>>> sort_options = [SortOption(field='updated_at', order='desc')]
|
|
88
|
+
>>> build_sort_string(sort_options)
|
|
89
|
+
'-updated_at'
|
|
90
|
+
>>> sort_options = [
|
|
91
|
+
... SortOption(field='updated_at', order='desc'),
|
|
92
|
+
... SortOption(field='created_at', order='asc')
|
|
93
|
+
... ]
|
|
94
|
+
>>> build_sort_string(sort_options)
|
|
95
|
+
'-updated_at,created_at'
|
|
96
|
+
"""
|
|
97
|
+
if not sort_options:
|
|
98
|
+
return ""
|
|
99
|
+
|
|
100
|
+
sort_strings: List[str] = []
|
|
101
|
+
for sort_option in sort_options:
|
|
102
|
+
field = sort_option.field
|
|
103
|
+
order = sort_option.order
|
|
104
|
+
|
|
105
|
+
# URL encode field name
|
|
106
|
+
field_encoded = quote(field)
|
|
107
|
+
|
|
108
|
+
# Add '-' prefix for descending order
|
|
109
|
+
if order == "desc":
|
|
110
|
+
sort_strings.append(f"-{field_encoded}")
|
|
111
|
+
else:
|
|
112
|
+
sort_strings.append(field_encoded)
|
|
113
|
+
|
|
114
|
+
# Join multiple sorts with comma (if needed for single sort param)
|
|
115
|
+
# Or return as comma-separated string
|
|
116
|
+
return ",".join(sort_strings)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: miso-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Python client SDK for AI Fabrix authentication, authorization, and logging
|
|
5
5
|
Home-page: https://github.com/aifabrix/miso-client-python
|
|
6
6
|
Author: AI Fabrix Team
|
|
@@ -611,6 +611,225 @@ print(error_response.statusCode) # 422
|
|
|
611
611
|
print(error_response.instance) # "/api/endpoint"
|
|
612
612
|
```
|
|
613
613
|
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
### Pagination, Filtering, and Sorting Utilities
|
|
617
|
+
|
|
618
|
+
**What happens:** The SDK provides reusable utilities for pagination, filtering, sorting, and error handling that work with any API endpoint.
|
|
619
|
+
|
|
620
|
+
#### Pagination
|
|
621
|
+
|
|
622
|
+
**Pagination Parameters:**
|
|
623
|
+
- `page`: Page number (1-based, defaults to 1)
|
|
624
|
+
- `page_size`: Number of items per page (defaults to 25)
|
|
625
|
+
|
|
626
|
+
```python
|
|
627
|
+
from miso_client import (
|
|
628
|
+
parse_pagination_params,
|
|
629
|
+
create_paginated_list_response,
|
|
630
|
+
PaginatedListResponse,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# Parse pagination from query parameters
|
|
634
|
+
params = {"page": "1", "page_size": "25"}
|
|
635
|
+
current_page, page_size = parse_pagination_params(params)
|
|
636
|
+
|
|
637
|
+
# Create paginated response
|
|
638
|
+
items = [{"id": 1}, {"id": 2}]
|
|
639
|
+
response = create_paginated_list_response(
|
|
640
|
+
items,
|
|
641
|
+
total_items=120,
|
|
642
|
+
current_page=1,
|
|
643
|
+
page_size=25,
|
|
644
|
+
type="item"
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
# Response structure:
|
|
648
|
+
# {
|
|
649
|
+
# "meta": {
|
|
650
|
+
# "total_items": 120,
|
|
651
|
+
# "current_page": 1,
|
|
652
|
+
# "page_size": 25,
|
|
653
|
+
# "type": "item"
|
|
654
|
+
# },
|
|
655
|
+
# "data": [{"id": 1}, {"id": 2}]
|
|
656
|
+
# }
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
#### Filtering
|
|
660
|
+
|
|
661
|
+
**Filter Operators:** `eq`, `neq`, `in`, `nin`, `gt`, `lt`, `gte`, `lte`, `contains`, `like`
|
|
662
|
+
|
|
663
|
+
**Filter Format:** `field:op:value` (e.g., `status:eq:active`)
|
|
664
|
+
|
|
665
|
+
```python
|
|
666
|
+
from miso_client import FilterBuilder, parse_filter_params, build_query_string
|
|
667
|
+
|
|
668
|
+
# Dynamic filter building with FilterBuilder
|
|
669
|
+
filter_builder = FilterBuilder() \
|
|
670
|
+
.add('status', 'eq', 'active') \
|
|
671
|
+
.add('region', 'in', ['eu', 'us']) \
|
|
672
|
+
.add('created_at', 'gte', '2024-01-01')
|
|
673
|
+
|
|
674
|
+
# Get query string
|
|
675
|
+
query_string = filter_builder.to_query_string()
|
|
676
|
+
# Returns: "filter=status:eq:active&filter=region:in:eu,us&filter=created_at:gte:2024-01-01"
|
|
677
|
+
|
|
678
|
+
# Parse existing filter parameters
|
|
679
|
+
params = {'filter': ['status:eq:active', 'region:in:eu,us']}
|
|
680
|
+
filters = parse_filter_params(params)
|
|
681
|
+
# Returns: [FilterOption(field='status', op='eq', value='active'), ...]
|
|
682
|
+
|
|
683
|
+
# Use with HTTP client
|
|
684
|
+
response = await client.http_client.get_with_filters(
|
|
685
|
+
'/api/items',
|
|
686
|
+
filter_builder=filter_builder
|
|
687
|
+
)
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
**Building Complete Filter Queries:**
|
|
691
|
+
|
|
692
|
+
```python
|
|
693
|
+
from miso_client import FilterQuery, FilterOption, build_query_string
|
|
694
|
+
|
|
695
|
+
# Create filter query with filters, sort, pagination, and fields
|
|
696
|
+
filter_query = FilterQuery(
|
|
697
|
+
filters=[
|
|
698
|
+
FilterOption(field='status', op='eq', value='active'),
|
|
699
|
+
FilterOption(field='region', op='in', value=['eu', 'us'])
|
|
700
|
+
],
|
|
701
|
+
sort=['-updated_at', 'created_at'],
|
|
702
|
+
page=1,
|
|
703
|
+
page_size=25,
|
|
704
|
+
fields=['id', 'name', 'status']
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
# Build query string
|
|
708
|
+
query_string = build_query_string(filter_query)
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
#### Sorting
|
|
712
|
+
|
|
713
|
+
**Sort Format:** `-field` for descending, `field` for ascending (e.g., `-updated_at`, `created_at`)
|
|
714
|
+
|
|
715
|
+
```python
|
|
716
|
+
from miso_client import parse_sort_params, build_sort_string, SortOption
|
|
717
|
+
|
|
718
|
+
# Parse sort parameters
|
|
719
|
+
params = {'sort': '-updated_at'}
|
|
720
|
+
sort_options = parse_sort_params(params)
|
|
721
|
+
# Returns: [SortOption(field='updated_at', order='desc')]
|
|
722
|
+
|
|
723
|
+
# Parse multiple sorts
|
|
724
|
+
params = {'sort': ['-updated_at', 'created_at']}
|
|
725
|
+
sort_options = parse_sort_params(params)
|
|
726
|
+
# Returns: [
|
|
727
|
+
# SortOption(field='updated_at', order='desc'),
|
|
728
|
+
# SortOption(field='created_at', order='asc')
|
|
729
|
+
# ]
|
|
730
|
+
|
|
731
|
+
# Build sort string
|
|
732
|
+
sort_options = [
|
|
733
|
+
SortOption(field='updated_at', order='desc'),
|
|
734
|
+
SortOption(field='created_at', order='asc')
|
|
735
|
+
]
|
|
736
|
+
sort_string = build_sort_string(sort_options)
|
|
737
|
+
# Returns: "-updated_at,created_at"
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
#### Combined Usage
|
|
741
|
+
|
|
742
|
+
**Pagination + Filter + Sort:**
|
|
743
|
+
|
|
744
|
+
```python
|
|
745
|
+
from miso_client import (
|
|
746
|
+
FilterBuilder,
|
|
747
|
+
FilterQuery,
|
|
748
|
+
build_query_string,
|
|
749
|
+
parse_pagination_params,
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
# Build filters
|
|
753
|
+
filter_builder = FilterBuilder() \
|
|
754
|
+
.add('status', 'eq', 'active') \
|
|
755
|
+
.add('region', 'in', ['eu', 'us'])
|
|
756
|
+
|
|
757
|
+
# Parse pagination
|
|
758
|
+
params = {'page': '1', 'page_size': '25'}
|
|
759
|
+
current_page, page_size = parse_pagination_params(params)
|
|
760
|
+
|
|
761
|
+
# Create complete query
|
|
762
|
+
filter_query = FilterQuery(
|
|
763
|
+
filters=filter_builder.build(),
|
|
764
|
+
sort=['-updated_at'],
|
|
765
|
+
page=current_page,
|
|
766
|
+
page_size=page_size
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
# Build query string
|
|
770
|
+
query_string = build_query_string(filter_query)
|
|
771
|
+
|
|
772
|
+
# Use with HTTP client
|
|
773
|
+
response = await client.http_client.get_with_filters(
|
|
774
|
+
'/api/items',
|
|
775
|
+
filter_builder=filter_builder,
|
|
776
|
+
params={'page': current_page, 'page_size': page_size}
|
|
777
|
+
)
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
**Or use pagination helper:**
|
|
781
|
+
|
|
782
|
+
```python
|
|
783
|
+
# Get paginated response
|
|
784
|
+
response = await client.http_client.get_paginated(
|
|
785
|
+
'/api/items',
|
|
786
|
+
page=1,
|
|
787
|
+
page_size=25
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
# Response is automatically parsed as PaginatedListResponse
|
|
791
|
+
print(response.meta.total_items) # 120
|
|
792
|
+
print(response.meta.current_page) # 1
|
|
793
|
+
print(len(response.data)) # 25
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
#### Metadata Filter Integration
|
|
797
|
+
|
|
798
|
+
**Working with `/metadata/filter` endpoint:**
|
|
799
|
+
|
|
800
|
+
```python
|
|
801
|
+
# Get metadata filters from endpoint
|
|
802
|
+
metadata_response = await client.http_client.post(
|
|
803
|
+
"/api/v1/metadata/filter",
|
|
804
|
+
{"documentStorageKey": "my-doc-storage"}
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
# Convert AccessFieldFilter to FilterBuilder
|
|
808
|
+
filter_builder = FilterBuilder()
|
|
809
|
+
for access_filter in metadata_response.mandatoryFilters:
|
|
810
|
+
filter_builder.add(access_filter.field, 'in', access_filter.values)
|
|
811
|
+
|
|
812
|
+
# Use with query utilities
|
|
813
|
+
query_string = filter_builder.to_query_string()
|
|
814
|
+
|
|
815
|
+
# Apply to API requests
|
|
816
|
+
response = await client.http_client.get_with_filters(
|
|
817
|
+
'/api/items',
|
|
818
|
+
filter_builder=filter_builder
|
|
819
|
+
)
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
**Features:**
|
|
823
|
+
|
|
824
|
+
- **Snake_case Convention**: All utilities use snake_case to match Miso/Dataplane API
|
|
825
|
+
- **Type Safety**: Full type hints with Pydantic models
|
|
826
|
+
- **Dynamic Filtering**: FilterBuilder supports method chaining for complex filters
|
|
827
|
+
- **Local Testing**: `apply_filters()` and `apply_pagination_to_array()` for local filtering/pagination in tests
|
|
828
|
+
- **URL Encoding**: Automatic URL encoding for field names and values
|
|
829
|
+
- **Backward Compatible**: Works alongside existing HTTP client methods
|
|
830
|
+
|
|
831
|
+
---
|
|
832
|
+
|
|
614
833
|
### Common Tasks
|
|
615
834
|
|
|
616
835
|
**Add authentication middleware (FastAPI):**
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
miso_client/__init__.py,sha256=
|
|
1
|
+
miso_client/__init__.py,sha256=iDpRsYBbBDrLQFNJdtDlv4yrg1pvwNZpD2C3TYAyOSo,15595
|
|
2
2
|
miso_client/errors.py,sha256=uyS5j-_bUCA5gbINPYQd0wMpGsaEH0tJRK0obQTq2oo,1976
|
|
3
3
|
miso_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
miso_client/models/__init__.py,sha256=lMnzU7j2Z5_UehvOeIrbJIo3MH4j5KINGfU1vJTzTyU,139
|
|
5
5
|
miso_client/models/config.py,sha256=TLckmwajrdnJpMTsjCtaFkz8wFAXcEQJfKjXQ8ww6vM,8024
|
|
6
|
-
miso_client/models/error_response.py,sha256=
|
|
6
|
+
miso_client/models/error_response.py,sha256=YBQkchdWOo6NjdNpLlOvGaIhOwA1MJzkTS2oeU0e2Mk,1694
|
|
7
|
+
miso_client/models/filter.py,sha256=3XWVxAjtUP78KPocd4-Ko9SijeRXm3jWVVBCXDPnXuI,4111
|
|
8
|
+
miso_client/models/pagination.py,sha256=f-YbspLrQH3ZhDYzbMCTloeKF2EyaSi7zjFLgh6dhBQ,2078
|
|
9
|
+
miso_client/models/sort.py,sha256=CutVQnYC4uRgesJ8zgd4OmPGZIop1KzvtgiTjNproSM,621
|
|
7
10
|
miso_client/services/__init__.py,sha256=2ok62Z9kaS0Zze-OxRkxEJ4JidzN9jL_pzGMOxpZppQ,489
|
|
8
11
|
miso_client/services/auth.py,sha256=hYnHEoTNgeH_g0ItoVDq99fBZCZX0o-0o-9gRxqZYmw,5465
|
|
9
12
|
miso_client/services/cache.py,sha256=lXcLcRm56snOX3AQiEKi-j7FIikCLYnEK2HXxBkxm2M,6510
|
|
@@ -15,12 +18,16 @@ miso_client/services/role.py,sha256=qmxhk54QUGCjCuCWm_ruDlTq7iT9yOicfUvdRVkFKUI,
|
|
|
15
18
|
miso_client/utils/__init__.py,sha256=HArSxVKrmCqFkqFOPwe1i3B2IBHJ1vRqYu98c_KASV0,366
|
|
16
19
|
miso_client/utils/config_loader.py,sha256=yZk4pXNIBu3i61KqxM8QwsjraM0xhqUcH2THl8-DMu0,3027
|
|
17
20
|
miso_client/utils/data_masker.py,sha256=D7AEyViGxoShLa5UUZHYhRCPQMPKqX7qNilTK9h87OM,7035
|
|
18
|
-
miso_client/utils/
|
|
21
|
+
miso_client/utils/error_utils.py,sha256=B-BXmDTTj6II1HwjC_DE6o9QKOMs6B3bzFXkOiPhf20,3274
|
|
22
|
+
miso_client/utils/filter.py,sha256=obkdQ3FFD69Qg0VviFMoM8y6zAP5JYBaIujmOT9diU8,8984
|
|
23
|
+
miso_client/utils/http_client.py,sha256=H386RsKjiw-JG7XhrTn8gCRDMWMvp8s7bC1jqa7qvGk,23807
|
|
19
24
|
miso_client/utils/internal_http_client.py,sha256=jgiaO94EiIUbMQWUKN4FhYqOQ9r0BZea0_grRcOepL4,16078
|
|
20
25
|
miso_client/utils/jwt_tools.py,sha256=-pvz5nk5BztEnhFnL-dtOv8Q5E0G2oh4RwFrVk2rVpg,1981
|
|
26
|
+
miso_client/utils/pagination.py,sha256=_sUzAGSCiUujVa9W3nedVRaCx18_9MuT_9bBlWmVc9o,4449
|
|
21
27
|
miso_client/utils/sensitive_fields_loader.py,sha256=EHODxyM1Gw7hgKXCvJ1B4Hf4LZqcEqWEXu4q5CPFaic,3667
|
|
22
|
-
miso_client
|
|
23
|
-
miso_client-0.
|
|
24
|
-
miso_client-0.
|
|
25
|
-
miso_client-0.
|
|
26
|
-
miso_client-0.
|
|
28
|
+
miso_client/utils/sort.py,sha256=_bCLRCGevgNkXYqsgXasc3vaDQwkXV76kgHIyw-dato,3438
|
|
29
|
+
miso_client-0.5.0.dist-info/licenses/LICENSE,sha256=3hoU8LdT9_EIFIx6FjMk5sQnVCBMX3FRIOzqqy5im4c,1076
|
|
30
|
+
miso_client-0.5.0.dist-info/METADATA,sha256=ZzmZZNHxnv4n3P95ZJgMAPFKRE3gLTgYUhCG9sh20C4,28396
|
|
31
|
+
miso_client-0.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
32
|
+
miso_client-0.5.0.dist-info/top_level.txt,sha256=8i_FNeRn8PRy6scnXOpVr-IJYsArkqIvxRMTZPtik9E,12
|
|
33
|
+
miso_client-0.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|