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 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.4.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(..., description="Human-readable error title")
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
@@ -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.4.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=MjiF-VJCkY6_s5_Oy8U43ZRa-XRVgrTpco-PnGPBTC4,14395
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=Dd-Rm2ylxU9ssYUsGBifjT-Let2MMxn28LJszQCO5bY,1362
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/http_client.py,sha256=AwlP0h17SdJ2lqf5j19HVztjvvMrCPTybkbWdvGzM38,20252
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-0.4.0.dist-info/licenses/LICENSE,sha256=3hoU8LdT9_EIFIx6FjMk5sQnVCBMX3FRIOzqqy5im4c,1076
23
- miso_client-0.4.0.dist-info/METADATA,sha256=Ek0HsqCnGba1PhmwPWre-KSrxVcOthJ1XtDBuObGyUw,22690
24
- miso_client-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
- miso_client-0.4.0.dist-info/top_level.txt,sha256=8i_FNeRn8PRy6scnXOpVr-IJYsArkqIvxRMTZPtik9E,12
26
- miso_client-0.4.0.dist-info/RECORD,,
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,,