|
1 | | -from enum import Enum |
2 | | -from typing import Annotated |
| 1 | +from typing import Annotated, Generic |
3 | 2 |
|
| 3 | +from common_library.basic_types import DEFAULT_FACTORY |
4 | 4 | from common_library.json_serialization import json_dumps |
5 | | -from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, field_validator |
| 5 | +from pydantic import ( |
| 6 | + BaseModel, |
| 7 | + BeforeValidator, |
| 8 | + ConfigDict, |
| 9 | + Field, |
| 10 | + field_validator, |
| 11 | +) |
6 | 12 |
|
7 | 13 | from .basic_types import IDStr |
| 14 | +from .list_operations import OrderClause, OrderDirection, TField, check_ordering_list |
8 | 15 | from .rest_base import RequestParameters |
9 | | -from .utils.common_validators import parse_json_pre_validator |
| 16 | +from .utils.common_validators import ( |
| 17 | + parse_json_pre_validator, |
| 18 | +) |
10 | 19 |
|
11 | | - |
12 | | -class OrderDirection(str, Enum): |
13 | | - ASC = "asc" |
14 | | - DESC = "desc" |
| 20 | +__all__: tuple[str, ...] = ("OrderDirection",) |
15 | 21 |
|
16 | 22 |
|
17 | 23 | class OrderBy(BaseModel): |
18 | | - # Based on https://google.aip.dev/132#ordering |
19 | | - field: IDStr = Field(..., description="field name identifier") |
20 | | - direction: OrderDirection = Field( |
21 | | - default=OrderDirection.ASC, |
22 | | - description=( |
23 | | - f"As [A,B,C,...] if `{OrderDirection.ASC.value}`" |
24 | | - f" or [Z,Y,X, ...] if `{OrderDirection.DESC.value}`" |
| 24 | + # NOTE: use instead OrderClause[TField] where TField is Literal of valid fields |
| 25 | + field: Annotated[IDStr, Field(description="field name identifier")] |
| 26 | + direction: Annotated[ |
| 27 | + OrderDirection, |
| 28 | + Field( |
| 29 | + description=( |
| 30 | + f"As [A,B,C,...] if `{OrderDirection.ASC.value}`" |
| 31 | + f" or [Z,Y,X, ...] if `{OrderDirection.DESC.value}`" |
| 32 | + ) |
25 | 33 | ), |
26 | | - ) |
| 34 | + ] = OrderDirection.ASC |
27 | 35 |
|
28 | 36 |
|
29 | 37 | class _BaseOrderQueryParams(RequestParameters): |
| 38 | + # Use OrderingQueryParams instead for more flexible ordering |
30 | 39 | order_by: OrderBy |
31 | 40 |
|
32 | 41 |
|
@@ -91,30 +100,93 @@ def _check_ordering_field_and_map(cls, v): |
91 | 100 | return _ordering_fields_api_to_column_map.get(v) or v |
92 | 101 |
|
93 | 102 | assert "json_schema_extra" in _OrderBy.model_config # nosec |
94 | | - assert isinstance(_OrderBy.model_config["json_schema_extra"], dict) # nosec |
95 | | - assert isinstance( # nosec |
96 | | - _OrderBy.model_config["json_schema_extra"]["examples"], list |
97 | | - ) |
98 | | - order_by_example = _OrderBy.model_config["json_schema_extra"]["examples"][0] |
| 103 | + |
| 104 | + order_by_example = _OrderBy.model_json_schema()["examples"][0] |
99 | 105 | order_by_example_json = json_dumps(order_by_example) |
| 106 | + |
100 | 107 | assert _OrderBy.model_validate(order_by_example), "Example is invalid" # nosec |
101 | 108 |
|
102 | 109 | converted_default = _OrderBy.model_validate( |
103 | 110 | # NOTE: enforces ordering_fields_api_to_column_map |
104 | 111 | default.model_dump() |
105 | 112 | ) |
106 | 113 |
|
107 | | - class _OrderQueryParams(_BaseOrderQueryParams): |
108 | | - order_by: Annotated[_OrderBy, BeforeValidator(parse_json_pre_validator)] = ( |
| 114 | + class _OrderJsonQueryParams(_BaseOrderQueryParams): |
| 115 | + order_by: Annotated[ |
| 116 | + _OrderBy, |
| 117 | + BeforeValidator(parse_json_pre_validator), |
109 | 118 | Field( |
110 | | - default=converted_default, |
111 | 119 | description=( |
112 | 120 | f"Order by field (`{msg_field_options}`) and direction (`{msg_direction_options}`). " |
113 | 121 | f"The default sorting order is `{json_dumps(default)}`." |
114 | 122 | ), |
115 | 123 | examples=[order_by_example], |
116 | 124 | json_schema_extra={"example_json": order_by_example_json}, |
117 | | - ) |
118 | | - ) |
| 125 | + ), |
| 126 | + ] = converted_default |
| 127 | + |
| 128 | + return _OrderJsonQueryParams |
| 129 | + |
| 130 | + |
| 131 | +def _parse_order_by(v): |
| 132 | + if not v: |
| 133 | + return [] |
| 134 | + |
| 135 | + if isinstance(v, list): |
| 136 | + v = ",".join(v) |
| 137 | + |
| 138 | + if not isinstance(v, str): |
| 139 | + msg = "order_by must be a string" |
| 140 | + raise TypeError(msg) |
119 | 141 |
|
120 | | - return _OrderQueryParams |
| 142 | + # 1. from comma-separated string to list of OrderClause |
| 143 | + clauses = [] |
| 144 | + for t in v.split(","): |
| 145 | + token = t.strip() |
| 146 | + if not token: |
| 147 | + continue |
| 148 | + if token.startswith("-"): |
| 149 | + clauses.append((token[1:], OrderDirection.DESC)) |
| 150 | + elif token.startswith("+"): |
| 151 | + clauses.append((token[1:], OrderDirection.ASC)) |
| 152 | + else: |
| 153 | + clauses.append((token, OrderDirection.ASC)) |
| 154 | + |
| 155 | + # 2. check for duplicates and conflicting directions |
| 156 | + return [ |
| 157 | + {"field": field, "direction": direction} |
| 158 | + for field, direction in check_ordering_list(clauses) |
| 159 | + ] |
| 160 | + |
| 161 | + |
| 162 | +class OrderingQueryParams(BaseModel, Generic[TField]): |
| 163 | + """ |
| 164 | + This class is designed to parse query parameters for ordering results in an API request. |
| 165 | +
|
| 166 | + It supports multiple ordering clauses and allows for flexible sorting options. |
| 167 | +
|
| 168 | + NOTE: It only parses strings and validates into list[OrderClause[TField]] |
| 169 | + where TField is a type variable representing valid field names. |
| 170 | +
|
| 171 | +
|
| 172 | + For example: |
| 173 | +
|
| 174 | + /my/path?order_by=field1,-field2,+field3 |
| 175 | +
|
| 176 | + would sort by field1 ascending, field2 descending, and field3 ascending. |
| 177 | + """ |
| 178 | + |
| 179 | + order_by: Annotated[ |
| 180 | + list[OrderClause[TField]], |
| 181 | + BeforeValidator(_parse_order_by), |
| 182 | + Field(default_factory=list), |
| 183 | + ] = DEFAULT_FACTORY |
| 184 | + |
| 185 | + model_config = ConfigDict( |
| 186 | + json_schema_extra={ |
| 187 | + "examples": [ |
| 188 | + {"order_by": "-created_at,name,+gender"}, |
| 189 | + {"order_by": ""}, |
| 190 | + ], |
| 191 | + } |
| 192 | + ) |
0 commit comments