completed pagination functionality with filter option
This commit is contained in:
parent
b603399690
commit
1810a85c99
4 changed files with 482 additions and 11 deletions
|
|
@ -26,7 +26,16 @@ class PaginationParams(BaseModel):
|
||||||
page: int = Field(ge=1, description="Current page number (1-based)")
|
page: int = Field(ge=1, description="Current page number (1-based)")
|
||||||
pageSize: int = Field(ge=1, le=1000, description="Number of items per page")
|
pageSize: int = Field(ge=1, le=1000, description="Number of items per page")
|
||||||
sort: List[SortField] = Field(default_factory=list, description="List of sort fields in priority order")
|
sort: List[SortField] = Field(default_factory=list, description="List of sort fields in priority order")
|
||||||
filters: Optional[Dict[str, Any]] = Field(default=None, description="Filter criteria (structure TBD for future implementation)")
|
filters: Optional[Dict[str, Any]] = Field(
|
||||||
|
default=None,
|
||||||
|
description="""Filter criteria dictionary. Supports:
|
||||||
|
- General search: {"search": "text"} - searches across all text fields (case-insensitive)
|
||||||
|
- Field-specific filters:
|
||||||
|
- Simple equals: {"status": "running"}
|
||||||
|
- With operator: {"status": {"operator": "equals", "value": "running"}}
|
||||||
|
- Supported operators: equals/eq, contains, startsWith, endsWith, gt, gte, lt, lte, in, notIn
|
||||||
|
- Multiple filters are combined with AND logic"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PaginationRequest(BaseModel):
|
class PaginationRequest(BaseModel):
|
||||||
|
|
@ -57,7 +66,7 @@ class PaginationMetadata(BaseModel):
|
||||||
totalItems: int = Field(..., description="Total number of items across all pages (after filters)")
|
totalItems: int = Field(..., description="Total number of items across all pages (after filters)")
|
||||||
totalPages: int = Field(..., description="Total number of pages (calculated from totalItems / pageSize)")
|
totalPages: int = Field(..., description="Total number of pages (calculated from totalItems / pageSize)")
|
||||||
sort: List[SortField] = Field(..., description="Current sort configuration applied")
|
sort: List[SortField] = Field(..., description="Current sort configuration applied")
|
||||||
filters: Optional[Dict[str, Any]] = Field(default=None, description="Current filters applied (for future use)")
|
filters: Optional[Dict[str, Any]] = Field(default=None, description="Current filters applied (mirrors request filters)")
|
||||||
|
|
||||||
|
|
||||||
class PaginatedResponse(BaseModel, Generic[T]):
|
class PaginatedResponse(BaseModel, Generic[T]):
|
||||||
|
|
|
||||||
|
|
@ -236,9 +236,163 @@ class AppObjects:
|
||||||
return self.access.canModify(model_class, recordId)
|
return self.access.canModify(model_class, recordId)
|
||||||
|
|
||||||
def _applyFilters(self, records: List[Dict[str, Any]], filters: Dict[str, Any]) -> List[Dict[str, Any]]:
|
def _applyFilters(self, records: List[Dict[str, Any]], filters: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
"""Apply filter criteria to records (implementation for future filtering)."""
|
"""
|
||||||
# TODO: Implement filtering logic when needed
|
Apply filter criteria to records.
|
||||||
return records
|
|
||||||
|
Supports:
|
||||||
|
- General search: {"search": "text"} - searches across all text fields
|
||||||
|
- Field-specific filters:
|
||||||
|
- Simple: {"status": "running"} - equals match
|
||||||
|
- With operator: {"status": {"operator": "equals", "value": "running"}}
|
||||||
|
- Operators: equals, contains, gt, gte, lt, lte, in, notIn, startsWith, endsWith
|
||||||
|
|
||||||
|
Args:
|
||||||
|
records: List of record dictionaries to filter
|
||||||
|
filters: Filter criteria dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Filtered list of records
|
||||||
|
"""
|
||||||
|
if not filters or not records:
|
||||||
|
return records
|
||||||
|
|
||||||
|
filtered = []
|
||||||
|
|
||||||
|
for record in records:
|
||||||
|
matches = True
|
||||||
|
|
||||||
|
# Handle general search across text fields
|
||||||
|
if "search" in filters:
|
||||||
|
search_term = str(filters["search"]).lower()
|
||||||
|
if search_term:
|
||||||
|
# Search in all string fields
|
||||||
|
found = False
|
||||||
|
for key, value in record.items():
|
||||||
|
if isinstance(value, str) and search_term in value.lower():
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
elif isinstance(value, (int, float)) and search_term in str(value):
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
matches = False
|
||||||
|
|
||||||
|
# Handle field-specific filters
|
||||||
|
for field_name, filter_value in filters.items():
|
||||||
|
if field_name == "search":
|
||||||
|
continue # Already handled above
|
||||||
|
|
||||||
|
if field_name not in record:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
record_value = record.get(field_name)
|
||||||
|
|
||||||
|
# Handle simple value (equals operator)
|
||||||
|
if not isinstance(filter_value, dict):
|
||||||
|
if record_value != filter_value:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Handle filter with operator
|
||||||
|
operator = filter_value.get("operator", "equals")
|
||||||
|
filter_val = filter_value.get("value")
|
||||||
|
|
||||||
|
if operator in ["equals", "eq"]:
|
||||||
|
if record_value != filter_val:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "contains":
|
||||||
|
record_str = str(record_value).lower() if record_value is not None else ""
|
||||||
|
filter_str = str(filter_val).lower() if filter_val is not None else ""
|
||||||
|
if filter_str not in record_str:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "startsWith":
|
||||||
|
record_str = str(record_value).lower() if record_value is not None else ""
|
||||||
|
filter_str = str(filter_val).lower() if filter_val is not None else ""
|
||||||
|
if not record_str.startswith(filter_str):
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "endsWith":
|
||||||
|
record_str = str(record_value).lower() if record_value is not None else ""
|
||||||
|
filter_str = str(filter_val).lower() if filter_val is not None else ""
|
||||||
|
if not record_str.endswith(filter_str):
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "gt":
|
||||||
|
try:
|
||||||
|
record_num = float(record_value) if record_value is not None else float('-inf')
|
||||||
|
filter_num = float(filter_val) if filter_val is not None else float('-inf')
|
||||||
|
if record_num <= filter_num:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "gte":
|
||||||
|
try:
|
||||||
|
record_num = float(record_value) if record_value is not None else float('-inf')
|
||||||
|
filter_num = float(filter_val) if filter_val is not None else float('-inf')
|
||||||
|
if record_num < filter_num:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "lt":
|
||||||
|
try:
|
||||||
|
record_num = float(record_value) if record_value is not None else float('inf')
|
||||||
|
filter_num = float(filter_val) if filter_val is not None else float('inf')
|
||||||
|
if record_num >= filter_num:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "lte":
|
||||||
|
try:
|
||||||
|
record_num = float(record_value) if record_value is not None else float('inf')
|
||||||
|
filter_num = float(filter_val) if filter_val is not None else float('inf')
|
||||||
|
if record_num > filter_num:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "in":
|
||||||
|
if not isinstance(filter_val, list):
|
||||||
|
filter_val = [filter_val]
|
||||||
|
if record_value not in filter_val:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "notIn":
|
||||||
|
if not isinstance(filter_val, list):
|
||||||
|
filter_val = [filter_val]
|
||||||
|
if record_value in filter_val:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Unknown operator - default to equals
|
||||||
|
if record_value != filter_val:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if matches:
|
||||||
|
filtered.append(record)
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
|
||||||
def _applySorting(self, records: List[Dict[str, Any]], sortFields: List[Any]) -> List[Dict[str, Any]]:
|
def _applySorting(self, records: List[Dict[str, Any]], sortFields: List[Any]) -> List[Dict[str, Any]]:
|
||||||
"""Apply multi-level sorting to records using stable sort (sorts from least to most significant field)."""
|
"""Apply multi-level sorting to records using stable sort (sorts from least to most significant field)."""
|
||||||
|
|
|
||||||
|
|
@ -211,9 +211,163 @@ class ChatObjects:
|
||||||
return self.access.canModify(model_class, recordId)
|
return self.access.canModify(model_class, recordId)
|
||||||
|
|
||||||
def _applyFilters(self, records: List[Dict[str, Any]], filters: Dict[str, Any]) -> List[Dict[str, Any]]:
|
def _applyFilters(self, records: List[Dict[str, Any]], filters: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
"""Apply filter criteria to records (implementation for future filtering)."""
|
"""
|
||||||
# TODO: Implement filtering logic when needed
|
Apply filter criteria to records.
|
||||||
return records
|
|
||||||
|
Supports:
|
||||||
|
- General search: {"search": "text"} - searches across all text fields
|
||||||
|
- Field-specific filters:
|
||||||
|
- Simple: {"status": "running"} - equals match
|
||||||
|
- With operator: {"status": {"operator": "equals", "value": "running"}}
|
||||||
|
- Operators: equals, contains, gt, gte, lt, lte, in, notIn, startsWith, endsWith
|
||||||
|
|
||||||
|
Args:
|
||||||
|
records: List of record dictionaries to filter
|
||||||
|
filters: Filter criteria dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Filtered list of records
|
||||||
|
"""
|
||||||
|
if not filters or not records:
|
||||||
|
return records
|
||||||
|
|
||||||
|
filtered = []
|
||||||
|
|
||||||
|
for record in records:
|
||||||
|
matches = True
|
||||||
|
|
||||||
|
# Handle general search across text fields
|
||||||
|
if "search" in filters:
|
||||||
|
search_term = str(filters["search"]).lower()
|
||||||
|
if search_term:
|
||||||
|
# Search in all string fields
|
||||||
|
found = False
|
||||||
|
for key, value in record.items():
|
||||||
|
if isinstance(value, str) and search_term in value.lower():
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
elif isinstance(value, (int, float)) and search_term in str(value):
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
matches = False
|
||||||
|
|
||||||
|
# Handle field-specific filters
|
||||||
|
for field_name, filter_value in filters.items():
|
||||||
|
if field_name == "search":
|
||||||
|
continue # Already handled above
|
||||||
|
|
||||||
|
if field_name not in record:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
record_value = record.get(field_name)
|
||||||
|
|
||||||
|
# Handle simple value (equals operator)
|
||||||
|
if not isinstance(filter_value, dict):
|
||||||
|
if record_value != filter_value:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Handle filter with operator
|
||||||
|
operator = filter_value.get("operator", "equals")
|
||||||
|
filter_val = filter_value.get("value")
|
||||||
|
|
||||||
|
if operator in ["equals", "eq"]:
|
||||||
|
if record_value != filter_val:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "contains":
|
||||||
|
record_str = str(record_value).lower() if record_value is not None else ""
|
||||||
|
filter_str = str(filter_val).lower() if filter_val is not None else ""
|
||||||
|
if filter_str not in record_str:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "startsWith":
|
||||||
|
record_str = str(record_value).lower() if record_value is not None else ""
|
||||||
|
filter_str = str(filter_val).lower() if filter_val is not None else ""
|
||||||
|
if not record_str.startswith(filter_str):
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "endsWith":
|
||||||
|
record_str = str(record_value).lower() if record_value is not None else ""
|
||||||
|
filter_str = str(filter_val).lower() if filter_val is not None else ""
|
||||||
|
if not record_str.endswith(filter_str):
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "gt":
|
||||||
|
try:
|
||||||
|
record_num = float(record_value) if record_value is not None else float('-inf')
|
||||||
|
filter_num = float(filter_val) if filter_val is not None else float('-inf')
|
||||||
|
if record_num <= filter_num:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "gte":
|
||||||
|
try:
|
||||||
|
record_num = float(record_value) if record_value is not None else float('-inf')
|
||||||
|
filter_num = float(filter_val) if filter_val is not None else float('-inf')
|
||||||
|
if record_num < filter_num:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "lt":
|
||||||
|
try:
|
||||||
|
record_num = float(record_value) if record_value is not None else float('inf')
|
||||||
|
filter_num = float(filter_val) if filter_val is not None else float('inf')
|
||||||
|
if record_num >= filter_num:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "lte":
|
||||||
|
try:
|
||||||
|
record_num = float(record_value) if record_value is not None else float('inf')
|
||||||
|
filter_num = float(filter_val) if filter_val is not None else float('inf')
|
||||||
|
if record_num > filter_num:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "in":
|
||||||
|
if not isinstance(filter_val, list):
|
||||||
|
filter_val = [filter_val]
|
||||||
|
if record_value not in filter_val:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "notIn":
|
||||||
|
if not isinstance(filter_val, list):
|
||||||
|
filter_val = [filter_val]
|
||||||
|
if record_value in filter_val:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Unknown operator - default to equals
|
||||||
|
if record_value != filter_val:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if matches:
|
||||||
|
filtered.append(record)
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
|
||||||
def _applySorting(self, records: List[Dict[str, Any]], sortFields: List[Any]) -> List[Dict[str, Any]]:
|
def _applySorting(self, records: List[Dict[str, Any]], sortFields: List[Any]) -> List[Dict[str, Any]]:
|
||||||
"""Apply multi-level sorting to records using stable sort (sorts from least to most significant field)."""
|
"""Apply multi-level sorting to records using stable sort (sorts from least to most significant field)."""
|
||||||
|
|
|
||||||
|
|
@ -247,9 +247,163 @@ class ComponentObjects:
|
||||||
return self.access.canModify(model_class, recordId)
|
return self.access.canModify(model_class, recordId)
|
||||||
|
|
||||||
def _applyFilters(self, records: List[Dict[str, Any]], filters: Dict[str, Any]) -> List[Dict[str, Any]]:
|
def _applyFilters(self, records: List[Dict[str, Any]], filters: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
"""Apply filter criteria to records (implementation for future filtering)."""
|
"""
|
||||||
# TODO: Implement filtering logic when needed
|
Apply filter criteria to records.
|
||||||
return records
|
|
||||||
|
Supports:
|
||||||
|
- General search: {"search": "text"} - searches across all text fields
|
||||||
|
- Field-specific filters:
|
||||||
|
- Simple: {"status": "running"} - equals match
|
||||||
|
- With operator: {"status": {"operator": "equals", "value": "running"}}
|
||||||
|
- Operators: equals, contains, gt, gte, lt, lte, in, notIn, startsWith, endsWith
|
||||||
|
|
||||||
|
Args:
|
||||||
|
records: List of record dictionaries to filter
|
||||||
|
filters: Filter criteria dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Filtered list of records
|
||||||
|
"""
|
||||||
|
if not filters or not records:
|
||||||
|
return records
|
||||||
|
|
||||||
|
filtered = []
|
||||||
|
|
||||||
|
for record in records:
|
||||||
|
matches = True
|
||||||
|
|
||||||
|
# Handle general search across text fields
|
||||||
|
if "search" in filters:
|
||||||
|
search_term = str(filters["search"]).lower()
|
||||||
|
if search_term:
|
||||||
|
# Search in all string fields
|
||||||
|
found = False
|
||||||
|
for key, value in record.items():
|
||||||
|
if isinstance(value, str) and search_term in value.lower():
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
elif isinstance(value, (int, float)) and search_term in str(value):
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
matches = False
|
||||||
|
|
||||||
|
# Handle field-specific filters
|
||||||
|
for field_name, filter_value in filters.items():
|
||||||
|
if field_name == "search":
|
||||||
|
continue # Already handled above
|
||||||
|
|
||||||
|
if field_name not in record:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
record_value = record.get(field_name)
|
||||||
|
|
||||||
|
# Handle simple value (equals operator)
|
||||||
|
if not isinstance(filter_value, dict):
|
||||||
|
if record_value != filter_value:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Handle filter with operator
|
||||||
|
operator = filter_value.get("operator", "equals")
|
||||||
|
filter_val = filter_value.get("value")
|
||||||
|
|
||||||
|
if operator in ["equals", "eq"]:
|
||||||
|
if record_value != filter_val:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "contains":
|
||||||
|
record_str = str(record_value).lower() if record_value is not None else ""
|
||||||
|
filter_str = str(filter_val).lower() if filter_val is not None else ""
|
||||||
|
if filter_str not in record_str:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "startsWith":
|
||||||
|
record_str = str(record_value).lower() if record_value is not None else ""
|
||||||
|
filter_str = str(filter_val).lower() if filter_val is not None else ""
|
||||||
|
if not record_str.startswith(filter_str):
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "endsWith":
|
||||||
|
record_str = str(record_value).lower() if record_value is not None else ""
|
||||||
|
filter_str = str(filter_val).lower() if filter_val is not None else ""
|
||||||
|
if not record_str.endswith(filter_str):
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "gt":
|
||||||
|
try:
|
||||||
|
record_num = float(record_value) if record_value is not None else float('-inf')
|
||||||
|
filter_num = float(filter_val) if filter_val is not None else float('-inf')
|
||||||
|
if record_num <= filter_num:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "gte":
|
||||||
|
try:
|
||||||
|
record_num = float(record_value) if record_value is not None else float('-inf')
|
||||||
|
filter_num = float(filter_val) if filter_val is not None else float('-inf')
|
||||||
|
if record_num < filter_num:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "lt":
|
||||||
|
try:
|
||||||
|
record_num = float(record_value) if record_value is not None else float('inf')
|
||||||
|
filter_num = float(filter_val) if filter_val is not None else float('inf')
|
||||||
|
if record_num >= filter_num:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "lte":
|
||||||
|
try:
|
||||||
|
record_num = float(record_value) if record_value is not None else float('inf')
|
||||||
|
filter_num = float(filter_val) if filter_val is not None else float('inf')
|
||||||
|
if record_num > filter_num:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "in":
|
||||||
|
if not isinstance(filter_val, list):
|
||||||
|
filter_val = [filter_val]
|
||||||
|
if record_value not in filter_val:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
elif operator == "notIn":
|
||||||
|
if not isinstance(filter_val, list):
|
||||||
|
filter_val = [filter_val]
|
||||||
|
if record_value in filter_val:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Unknown operator - default to equals
|
||||||
|
if record_value != filter_val:
|
||||||
|
matches = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if matches:
|
||||||
|
filtered.append(record)
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
|
||||||
def _applySorting(self, records: List[Dict[str, Any]], sortFields: List[Any]) -> List[Dict[str, Any]]:
|
def _applySorting(self, records: List[Dict[str, Any]], sortFields: List[Any]) -> List[Dict[str, Any]]:
|
||||||
"""Apply multi-level sorting to records using stable sort (sorts from least to most significant field)."""
|
"""Apply multi-level sorting to records using stable sort (sorts from least to most significant field)."""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue