Skip to content

Commit afd66d4

Browse files
authored
🐛 Fixes OrderBy serialization error in pydanticv2 (#6828)
1 parent 1f18a84 commit afd66d4

File tree

11 files changed

+97
-29
lines changed

11 files changed

+97
-29
lines changed

packages/models-library/src/models_library/rest_ordering.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ class OrderBy(BaseModel):
2727

2828

2929
class _BaseOrderQueryParams(RequestParameters):
30-
order_by: OrderBy | None = None
30+
order_by: OrderBy
3131

3232

33-
def create_ordering_query_model_classes(
33+
def create_ordering_query_model_class(
3434
*,
3535
ordering_fields: set[str],
3636
default: OrderBy,

packages/models-library/tests/test_rest_ordering.py

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1+
import pickle
2+
13
import pytest
24
from common_library.json_serialization import json_dumps
35
from models_library.basic_types import IDStr
46
from models_library.rest_ordering import (
57
OrderBy,
68
OrderDirection,
7-
create_ordering_query_model_classes,
9+
create_ordering_query_model_class,
810
)
911
from pydantic import (
1012
BaseModel,
1113
ConfigDict,
1214
Field,
1315
Json,
16+
TypeAdapter,
1417
ValidationError,
1518
field_validator,
1619
)
@@ -47,8 +50,72 @@ def _validate_order_by_field(cls, v):
4750
)
4851

4952

53+
@pytest.mark.xfail(
54+
reason="create_ordering_query_model_class.<locals>._OrderBy is still not pickable"
55+
)
56+
def test_pickle_ordering_query_model_class():
57+
OrderQueryParamsModel = create_ordering_query_model_class(
58+
ordering_fields={"name", "description"},
59+
default=OrderBy(field=IDStr("name"), direction=OrderDirection.DESC),
60+
)
61+
62+
data = {"order_by": {"field": "name", "direction": "asc"}}
63+
query_model = OrderQueryParamsModel.model_validate(data)
64+
65+
# https://docs.pydantic.dev/latest/concepts/serialization/#pickledumpsmodel
66+
expected = query_model.order_by
67+
68+
# see https://github.com/ITISFoundation/osparc-simcore/pull/6828
69+
# FAILURE: raises `AttributeError: Can't pickle local object 'create_ordering_query_model_class.<locals>._OrderBy'`
70+
data = pickle.dumps(expected)
71+
72+
loaded = pickle.loads(data)
73+
assert loaded == expected
74+
75+
76+
def test_conversion_order_by_from_query_to_domain_model():
77+
OrderQueryParamsModel = create_ordering_query_model_class(
78+
ordering_fields={"modified_at", "name", "description"},
79+
default=OrderBy(field=IDStr("modified_at"), direction=OrderDirection.DESC),
80+
)
81+
82+
# normal
83+
data = {"order_by": {"field": "modified_at", "direction": "asc"}}
84+
query_model = OrderQueryParamsModel.model_validate(data)
85+
86+
expected_data = data["order_by"]
87+
88+
assert type(query_model.order_by) is not OrderBy
89+
assert isinstance(query_model.order_by, OrderBy)
90+
91+
# NOTE: This does NOT convert to OrderBy but has correct data
92+
order_by = TypeAdapter(OrderBy).validate_python(
93+
query_model.order_by, from_attributes=True
94+
)
95+
assert type(order_by) is not OrderBy
96+
assert order_by.model_dump(mode="json") == expected_data
97+
98+
order_by = OrderBy.model_validate(query_model.order_by.model_dump())
99+
assert type(order_by) is OrderBy
100+
assert order_by.model_dump(mode="json") == expected_data
101+
102+
# NOTE: This does NOT convert to OrderBy but has correct data
103+
order_by = OrderBy.model_validate(query_model.order_by, from_attributes=True)
104+
assert type(order_by) is not OrderBy
105+
assert order_by.model_dump(mode="json") == expected_data
106+
107+
order_by = OrderBy(**query_model.order_by.model_dump())
108+
assert type(order_by) is OrderBy
109+
assert order_by.model_dump(mode="json") == expected_data
110+
111+
# we should use this !!!
112+
order_by = OrderBy.model_construct(**query_model.order_by.model_dump())
113+
assert type(order_by) is OrderBy
114+
assert order_by.model_dump(mode="json") == expected_data
115+
116+
50117
def test_ordering_query_model_class_factory():
51-
BaseOrderingQueryModel = create_ordering_query_model_classes(
118+
BaseOrderingQueryModel = create_ordering_query_model_class(
52119
ordering_fields={"modified_at", "name", "description"},
53120
default=OrderBy(field=IDStr("modified_at"), direction=OrderDirection.DESC),
54121
ordering_fields_api_to_column_map={"modified_at": "modified_column"},
@@ -77,7 +144,7 @@ class OrderQueryParamsModel(BaseOrderingQueryModel):
77144

78145
def test_ordering_query_model_class__fails_with_invalid_fields():
79146

80-
OrderQueryParamsModel = create_ordering_query_model_classes(
147+
OrderQueryParamsModel = create_ordering_query_model_class(
81148
ordering_fields={"modified", "name", "description"},
82149
default=OrderBy(field=IDStr("modified"), direction=OrderDirection.DESC),
83150
)
@@ -94,7 +161,7 @@ def test_ordering_query_model_class__fails_with_invalid_fields():
94161

95162

96163
def test_ordering_query_model_class__fails_with_invalid_direction():
97-
OrderQueryParamsModel = create_ordering_query_model_classes(
164+
OrderQueryParamsModel = create_ordering_query_model_class(
98165
ordering_fields={"modified", "name", "description"},
99166
default=OrderBy(field=IDStr("modified"), direction=OrderDirection.DESC),
100167
)
@@ -112,7 +179,7 @@ def test_ordering_query_model_class__fails_with_invalid_direction():
112179

113180
def test_ordering_query_model_class__defaults():
114181

115-
OrderQueryParamsModel = create_ordering_query_model_classes(
182+
OrderQueryParamsModel = create_ordering_query_model_class(
116183
ordering_fields={"modified", "name", "description"},
117184
default=OrderBy(field=IDStr("modified"), direction=OrderDirection.DESC),
118185
ordering_fields_api_to_column_map={"modified": "modified_at"},
@@ -142,7 +209,7 @@ def test_ordering_query_model_class__defaults():
142209

143210

144211
def test_ordering_query_model_with_map():
145-
OrderQueryParamsModel = create_ordering_query_model_classes(
212+
OrderQueryParamsModel = create_ordering_query_model_class(
146213
ordering_fields={"modified", "name", "description"},
147214
default=OrderBy(field=IDStr("modified"), direction=OrderDirection.DESC),
148215
ordering_fields_api_to_column_map={"modified": "some_db_column_name"},
@@ -155,7 +222,7 @@ def test_ordering_query_model_with_map():
155222

156223
def test_ordering_query_parse_json_pre_validator():
157224

158-
OrderQueryParamsModel = create_ordering_query_model_classes(
225+
OrderQueryParamsModel = create_ordering_query_model_class(
159226
ordering_fields={"modified", "name"},
160227
default=OrderBy(field=IDStr("modified"), direction=OrderDirection.DESC),
161228
)

packages/service-library/tests/aiohttp/test_requests_validation.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from models_library.rest_ordering import (
1616
OrderBy,
1717
OrderDirection,
18-
create_ordering_query_model_classes,
18+
create_ordering_query_model_class,
1919
)
2020
from pydantic import BaseModel, ConfigDict, Field
2121
from servicelib.aiohttp import status
@@ -365,7 +365,7 @@ async def test_parse_request_with_invalid_headers_params(
365365

366366
def test_parse_request_query_parameters_as_with_order_by_query_models():
367367

368-
OrderQueryModel = create_ordering_query_model_classes(
368+
OrderQueryModel = create_ordering_query_model_class(
369369
ordering_fields={"modified", "name"}, default=OrderBy(field="name")
370370
)
371371

@@ -376,4 +376,5 @@ def test_parse_request_query_parameters_as_with_order_by_query_models():
376376
request = make_mocked_request("GET", path=f"{url}")
377377

378378
query_params = parse_request_query_parameters_as(OrderQueryModel, request)
379-
assert query_params.order_by.model_dump() == expected.model_dump()
379+
380+
assert OrderBy.model_construct(**query_params.order_by.model_dump()) == expected

services/web/server/src/simcore_service_webserver/folders/_folders_handlers.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from models_library.rest_ordering import OrderBy
1111
from models_library.rest_pagination import Page
1212
from models_library.rest_pagination_utils import paginate_data
13-
from pydantic import TypeAdapter
1413
from servicelib.aiohttp import status
1514
from servicelib.aiohttp.requests_validation import (
1615
parse_request_body_as,
@@ -82,7 +81,7 @@ async def list_folders(request: web.Request):
8281
trashed=query_params.filters.trashed,
8382
offset=query_params.offset,
8483
limit=query_params.limit,
85-
order_by=OrderBy.model_validate(query_params.order_by),
84+
order_by=OrderBy.model_construct(**query_params.order_by.model_dump()),
8685
)
8786

8887
page = Page[FolderGet].model_validate(
@@ -121,7 +120,7 @@ async def list_folders_full_search(request: web.Request):
121120
trashed=query_params.filters.trashed,
122121
offset=query_params.offset,
123122
limit=query_params.limit,
124-
order_by=TypeAdapter(OrderBy).validate_python(query_params.order_by),
123+
order_by=OrderBy.model_construct(**query_params.order_by.model_dump()),
125124
)
126125

127126
page = Page[FolderGet].model_validate(

services/web/server/src/simcore_service_webserver/folders/_models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from models_library.rest_ordering import (
99
OrderBy,
1010
OrderDirection,
11-
create_ordering_query_model_classes,
11+
create_ordering_query_model_class,
1212
)
1313
from models_library.rest_pagination import PageQueryParameters
1414
from models_library.trash import RemoveQueryParams
@@ -42,7 +42,7 @@ class FolderFilters(Filters):
4242
)
4343

4444

45-
_FolderOrderQueryParams: type[RequestParameters] = create_ordering_query_model_classes(
45+
_FolderOrderQueryParams: type[RequestParameters] = create_ordering_query_model_class(
4646
ordering_fields={
4747
"modified_at",
4848
"name",

services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ async def list_projects(request: web.Request):
203203
limit=query_params.limit,
204204
offset=query_params.offset,
205205
search=query_params.search,
206-
order_by=OrderBy.model_validate(query_params.order_by),
206+
order_by=OrderBy.model_construct(**query_params.order_by.model_dump()),
207207
folder_id=query_params.folder_id,
208208
workspace_id=query_params.workspace_id,
209209
)
@@ -241,7 +241,7 @@ async def list_projects_full_search(request: web.Request):
241241
limit=query_params.limit,
242242
offset=query_params.offset,
243243
text=query_params.text,
244-
order_by=query_params.order_by,
244+
order_by=OrderBy.model_construct(**query_params.order_by.model_dump()),
245245
tag_ids_list=tag_ids_list,
246246
)
247247

services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from models_library.rest_ordering import (
1616
OrderBy,
1717
OrderDirection,
18-
create_ordering_query_model_classes,
18+
create_ordering_query_model_class,
1919
)
2020
from models_library.rest_pagination import PageQueryParameters
2121
from models_library.utils.common_validators import (
@@ -100,7 +100,7 @@ class ProjectFilters(Filters):
100100
)
101101

102102

103-
ProjectsListOrderParams = create_ordering_query_model_classes(
103+
ProjectsListOrderParams = create_ordering_query_model_class(
104104
ordering_fields={
105105
"type",
106106
"uuid",

services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_handlers.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from models_library.rest_ordering import (
1717
OrderBy,
1818
OrderDirection,
19-
create_ordering_query_model_classes,
19+
create_ordering_query_model_class,
2020
)
2121
from models_library.rest_pagination import Page, PageQueryParameters
2222
from models_library.rest_pagination_utils import paginate_data
@@ -53,7 +53,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse:
5353

5454
_ResorceUsagesListOrderQueryParams: type[
5555
RequestParameters
56-
] = create_ordering_query_model_classes(
56+
] = create_ordering_query_model_class(
5757
ordering_fields={
5858
"wallet_id",
5959
"wallet_name",
@@ -136,7 +136,7 @@ async def list_resource_usage_services(request: web.Request):
136136
wallet_id=query_params.wallet_id,
137137
offset=query_params.offset,
138138
limit=query_params.limit,
139-
order_by=OrderBy.model_validate(query_params.order_by),
139+
order_by=OrderBy.model_construct(**query_params.order_by.model_dump()),
140140
filters=TypeAdapter(ServiceResourceUsagesFilters | None).validate_python(
141141
query_params.filters
142142
),
@@ -216,7 +216,7 @@ async def export_resource_usage_services(request: web.Request):
216216
user_id=req_ctx.user_id,
217217
product_name=req_ctx.product_name,
218218
wallet_id=query_params.wallet_id,
219-
order_by=TypeAdapter(OrderBy | None).validate_python(query_params.order_by),
219+
order_by=OrderBy.model_construct(**query_params.order_by.model_dump()),
220220
filters=TypeAdapter(ServiceResourceUsagesFilters | None).validate_python(
221221
query_params.filters
222222
),

services/web/server/src/simcore_service_webserver/workspaces/_models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from models_library.rest_ordering import (
77
OrderBy,
88
OrderDirection,
9-
create_ordering_query_model_classes,
9+
create_ordering_query_model_class,
1010
)
1111
from models_library.rest_pagination import PageQueryParameters
1212
from models_library.trash import RemoveQueryParams
@@ -31,7 +31,7 @@ class WorkspacesPathParams(StrictRequestParameters):
3131

3232
_WorkspacesListOrderQueryParams: type[
3333
RequestParameters
34-
] = create_ordering_query_model_classes(
34+
] = create_ordering_query_model_class(
3535
ordering_fields={
3636
"modified_at",
3737
"name",

services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ async def list_workspaces(request: web.Request):
7979
filter_trashed=query_params.filters.trashed,
8080
offset=query_params.offset,
8181
limit=query_params.limit,
82-
order_by=OrderBy.model_validate(query_params.order_by),
82+
order_by=OrderBy.model_construct(**query_params.order_by.model_dump()),
8383
)
8484

8585
page = Page[WorkspaceGet].model_validate(

services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__list_with_query_params.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
import json
88
import random
99
from collections import UserDict
10+
from collections.abc import Iterator
1011
from copy import deepcopy
1112
from pathlib import Path
12-
from typing import Any, Iterator
13+
from typing import Any
1314

1415
import pytest
1516
import sqlalchemy as sa

0 commit comments

Comments
 (0)