Skip to content

Commit 5ccd356

Browse files
authored
Upgrade Odmantic support to v1.0+ (#594)
* Upgrade Odmantic support to v1.0+ * Upgrade Odmantic support to v1.0+ * Upgrade Odmantic support to v1.0+ * Upgrade Odmantic support to v1.0+ * Upgrade Odmantic support to v1.0+ * Upgrade Odmantic support to v1.0+ * Upgrade Odmantic support to v1.0+ * Upgrade Odmantic support to v1.0+
1 parent 9e50bc9 commit 5ccd356

File tree

11 files changed

+74
-77
lines changed

11 files changed

+74
-77
lines changed

.github/workflows/test.yml

+4-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
runs-on: ubuntu-latest
1414
strategy:
1515
matrix:
16-
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ]
16+
python-version: [ "3.9", "3.10", "3.11", "3.12" ]
1717

1818
services:
1919
mongodb:
@@ -32,7 +32,7 @@ jobs:
3232
options: --health-cmd "curl http://localhost:9000/minio/health/live" --health-interval 10s --health-timeout 5s --health-retries 10
3333

3434
postgres:
35-
image: postgres:14-alpine
35+
image: postgres:17-alpine
3636
env:
3737
POSTGRES_USER: username
3838
POSTGRES_PASSWORD: password
@@ -97,6 +97,7 @@ jobs:
9797
name: coverage-${{ matrix.python-version }}
9898
path: .coverage*
9999
retention-days: 1
100+
include-hidden-files: true
100101

101102
coverage:
102103
needs:
@@ -111,7 +112,7 @@ jobs:
111112
- name: get coverage files
112113
uses: actions/download-artifact@v4
113114
with:
114-
pattern: coverage-*
115+
pattern: coverage*
115116
merge-multiple: true
116117
- name: Install Dependencies
117118
run: pip install hatch

pyproject.toml

+28-19
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,14 @@ i18n = [
4444
"babel >=2.13.0"
4545
]
4646
test = [
47-
"pytest >=7.2.0, <7.5.0",
48-
"pytest-asyncio >=0.20.2, <0.24.0",
49-
"mypy ==1.10.0",
50-
"ruff ==0.4.8",
51-
"black ==24.4.2",
52-
"httpx >=0.23.3, <0.27.0",
47+
"pytest >=8.3.0, <8.4.0",
48+
"pytest-asyncio >=0.24.0, <0.25.0",
49+
"mypy ==1.13.0",
50+
"ruff ==0.7.1",
51+
"black ==24.10.0",
52+
"httpx >=0.23.3, <0.28.0",
5353
"SQLAlchemy-Utils >=0.40.0, <0.42.0",
54-
"sqlmodel >=0.0.11, <0.15.0",
54+
"sqlmodel >=0.0.11, <0.1.0",
5555
"arrow >=1.2.3, <1.4.0",
5656
"colour >=0.1.5, <0.2.0",
5757
"phonenumbers >=8.13.3, <8.14.0",
@@ -62,27 +62,26 @@ test = [
6262
"PyMySQL[rsa] >=1.0.2, <1.2.0",
6363
"psycopg2-binary >=2.9.5, <3.0.0",
6464
"aiosqlite >=0.17.0, <0.21.0",
65-
"asyncpg >=0.27.0, <0.30.0",
65+
"asyncpg >=0.27.0, <0.31.0",
6666
"aiomysql >=0.1.1, <0.3.0",
67-
"mongoengine >=0.25.0, <0.29.0",
68-
"odmantic >=0.9.0,<0.10.0",
67+
"mongoengine >=0.25.0, <0.30.0",
6968
"tinydb >=4.7.0, <4.9.0",
70-
"Pillow >=9.4.0, <9.6.0",
69+
"Pillow >=9.4.0, <11.1.0",
7170
"itsdangerous >=2.2.0, <2.3.0",
72-
"pydantic[email] >=1.10.2, <2.8.0",
71+
"pydantic[email] >=1.10.2, <2.10.0",
7372
]
7473
cov = [
75-
"coverage[toml] >=7.0.0, <7.4.0"
74+
"coverage[toml] >=7.0.0, <7.7.0"
7675
]
7776
doc = [
7877
"mkdocs >=1.4.2, <2.0.0",
7978
"mkdocs-material >=9.0.0, <10.0.0",
80-
"mkdocstrings[python] >=0.19.0, <0.26.0",
79+
"mkdocstrings[python] >=0.19.0, <0.27.0",
8180
"mkdocs-static-i18n >=1.2.3, <1.3"
8281
]
8382
dev = [
84-
"pre-commit >=2.20.0, <4.0.0",
85-
"uvicorn >=0.20.0, <0.31.0",
83+
"pre-commit >=2.20.0, <4.1.0",
84+
"uvicorn >=0.20.0, <0.33.0",
8685
]
8786

8887
[tool.hatch.envs.default]
@@ -120,11 +119,12 @@ sqla_version = ["sqla14", "sqla2"]
120119
[tool.hatch.envs.test.overrides]
121120
matrix.sqla_version.dependencies = [
122121
{ value = "SQLAlchemy[asyncio] >=2.0, <2.1", if = ["sqla2"] },
122+
{ value = "odmantic >=1.0.0,<1.1.0", if = ["sqla2"] },
123123
{ value = "SQLAlchemy[asyncio] >=1.4, <1.5", if = ["sqla14"] },
124124
]
125125
matrix.sqla_version.scripts = [
126-
{ key = "all", value = 'coverage run -m pytest tests --ignore=tests/odmantic', if = ["sqla2"] },
127-
{ key = "sqla", value = 'coverage run -m pytest tests/sqla --ignore=tests/odmantic', if = ["sqla2"] },
126+
{ key = "all", value = 'coverage run -m pytest tests --ignore=tests/odmantic', if = ["sqla14"] },
127+
{ key = "sqla", value = 'coverage run -m pytest tests/sqla --ignore=tests/odmantic', if = ["sqla14"] },
128128
]
129129

130130
[tool.hatch.envs.cov]
@@ -180,6 +180,14 @@ parallel = true
180180
concurrency = ["thread", "greenlet"]
181181
source = ["starlette_admin", "tests"]
182182

183+
[tool.pytest]
184+
asyncio_mode = "auto"
185+
186+
[tool.pytest.ini_options]
187+
asyncio_mode="auto"
188+
asyncio_default_fixture_loop_scope="function"
189+
190+
183191
[tool.ruff]
184192
lint.select = [
185193
"B", # flake8-bugbear
@@ -217,7 +225,7 @@ known-third-party = ["starlette_admin"]
217225
[tool.ruff.lint.per-file-ignores]
218226
"__init__.py" = ["F401", "PLC0414"]
219227
"examples/**" = ["N805", "N999", "RUF012"]
220-
"tests/**" = ["RUF012"]
228+
"tests/**" = ["RUF012", "SIM115"]
221229

222230
[tool.mypy]
223231
disallow_untyped_defs = true
@@ -236,6 +244,7 @@ module = [
236244
"starlette_admin.contrib.sqla.helpers",
237245
"starlette_admin.contrib.sqla.view",
238246
"starlette_admin.contrib.odmantic.helpers",
247+
"starlette_admin.contrib.odmantic.view"
239248
]
240249
warn_unused_ignores = false
241250

starlette_admin/contrib/odmantic/converters.py

-5
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
from starlette_admin.fields import (
2424
BaseField,
2525
CollectionField,
26-
ColorField,
2726
DateTimeField,
2827
DecimalField,
2928
EmailField,
@@ -117,10 +116,6 @@ def conv_bson_datetime(self, *args: Any, **kwargs: Any) -> BaseField:
117116
def conv_pydantic_email(self, *args: Any, **kwargs: Any) -> BaseField:
118117
return EmailField(**self._standard_type_common(**kwargs))
119118

120-
@converts(pydantic.color.Color)
121-
def conv_pydantic_color(self, *args: Any, **kwargs: Any) -> BaseField:
122-
return ColorField(**self._standard_type_common(**kwargs))
123-
124119
@converts(pydantic.AnyUrl)
125120
def conv_pydantic_url(self, *args: Any, **kwargs: Any) -> BaseField:
126121
return URLField(**self._standard_type_common(**kwargs))

starlette_admin/contrib/odmantic/helpers.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33
import typing as t
44

55
import bson
6-
import pydantic as pyd
7-
import pydantic.datetime_parse
86
from odmantic import Model, query
97
from odmantic.field import (
108
FieldProxy,
119
)
1210
from odmantic.query import QueryExpression
11+
from pydantic import TypeAdapter, ValidationError # type: ignore[attr-defined]
1312

1413

1514
def normalize_list(
@@ -74,6 +73,14 @@ def _rec(value: t.Any, regex: str) -> t.Pattern:
7473
}
7574

7675

76+
def parse_datetime(value: str) -> bool:
77+
try:
78+
TypeAdapter(datetime.datetime).validate_python(value)
79+
except ValidationError:
80+
return False
81+
return True
82+
83+
7784
def resolve_proxy(model: t.Type[Model], proxy_name: str) -> t.Optional[FieldProxy]:
7885
_list = proxy_name.split(".")
7986
m = model
@@ -88,7 +95,7 @@ def _check_value(v: t.Any, proxy: t.Optional[FieldProxy]) -> t.Any:
8895
The purpose of this function is to detect datetime string, or ObjectId
8996
and convert them into the appropriate python type.
9097
"""
91-
if isinstance(v, str) and pyd.datetime_parse.datetime_re.match(v):
98+
if isinstance(v, str) and parse_datetime(v):
9299
return datetime.datetime.fromisoformat(v)
93100
if proxy is not None and +proxy == "_id" and bson.ObjectId.is_valid(v):
94101
return bson.ObjectId(v)

tests/mongoengine/test_view.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def test_api(self, client):
136136
data = response.json()
137137
assert data["total"] == 5
138138
assert len(data["items"]) == 2
139-
assert ["OPPOF19", "IPhone X"] == [x["title"] for x in data["items"]]
139+
assert [x["title"] for x in data["items"]] == ["OPPOF19", "IPhone X"]
140140
# Find by pks
141141
response = client.get(
142142
"/admin/api/product",
@@ -150,7 +150,7 @@ def test_api_fulltext(self, client):
150150
)
151151
data = response.json()
152152
assert data["total"] == 2
153-
assert ["IPhone 9", "IPhone X"] == [x["title"] for x in data["items"]]
153+
assert [x["title"] for x in data["items"]] == ["IPhone 9", "IPhone X"]
154154

155155
def test_api_query1(self, client):
156156
where = (
@@ -160,8 +160,10 @@ def test_api_query1(self, client):
160160
response = client.get(f"/admin/api/product?where={where}&order_by=price asc")
161161
data = response.json()
162162
assert data["total"] == 3
163-
assert ["OPPOF19", "Huawei P30", "IPhone 9"] == [
164-
x["title"] for x in data["items"]
163+
assert [x["title"] for x in data["items"]] == [
164+
"OPPOF19",
165+
"Huawei P30",
166+
"IPhone 9",
165167
]
166168

167169
def test_api_query2(self, client):
@@ -172,13 +174,13 @@ def test_api_query2(self, client):
172174
response = client.get(f"/admin/api/product?where={where}")
173175
data = response.json()
174176
assert data["total"] == 1
175-
assert ["IPhone X"] == [x["title"] for x in data["items"]]
177+
assert [x["title"] for x in data["items"]] == ["IPhone X"]
176178

177179
def test_api_query3(self, client):
178180
response = client.get("/admin/api/product?order_by=price desc&limit=2")
179181
data = response.json()
180182
assert data["total"] == 5
181-
assert ["Samsung Universe 9", "IPhone X"] == [x["title"] for x in data["items"]]
183+
assert [x["title"] for x in data["items"]] == ["Samsung Universe 9", "IPhone X"]
182184

183185
def test_detail(self, client):
184186
id = Product.objects(title="IPhone 9").get().id

tests/odmantic/conftest.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import asyncio
2+
13
import pytest
24
import pytest_asyncio
35
from motor.motor_asyncio import AsyncIOMotorClient
@@ -8,8 +10,9 @@
810

911

1012
@pytest_asyncio.fixture
11-
async def aio_engine(event_loop):
12-
client = AsyncIOMotorClient(MONGO_URI, io_loop=event_loop)
13+
async def aio_engine():
14+
client = AsyncIOMotorClient(MONGO_URI)
15+
client.get_io_loop = asyncio.get_event_loop
1316
sess = AIOEngine(client, MONGO_DATABASE)
1417
yield sess
1518
await client.drop_database(MONGO_DATABASE)

tests/odmantic/test_async_engine.py

+10-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import json
2-
import sys
32
from datetime import datetime
43
from typing import Any, Dict, List, Optional
54

@@ -11,13 +10,6 @@
1110
from starlette.applications import Starlette
1211
from starlette_admin.contrib.odmantic import Admin, ModelView
1312

14-
if sys.version_info < (3, 9):
15-
pytest.skip(
16-
"Skipping the test due to a segment fault error with odmantic on Python 3.8, and the library is not "
17-
"currently maintained",
18-
allow_module_level=True,
19-
)
20-
2113
pytestmark = pytest.mark.asyncio
2214

2315

@@ -113,7 +105,7 @@ async def test_api(client: AsyncClient):
113105
data = response.json()
114106
assert data["total"] == 3
115107
assert len(data["items"]) == 2
116-
assert ["Sheldon Cole", "Terry Medhurst"] == [x["name"] for x in data["items"]]
108+
assert [x["name"] for x in data["items"]] == ["Sheldon Cole", "Terry Medhurst"]
117109
# Find by pks
118110
response = await client.get(
119111
"/admin/api/user", params={"pks": [x["id"] for x in data["items"]]}
@@ -128,7 +120,7 @@ async def test_full_text_search(client: AsyncClient):
128120
assert response.status_code == 200
129121
data = response.json()
130122
assert data["total"] == 2
131-
assert ["Hills Terrill", "Terry Medhurst"] == [x["name"] for x in data["items"]]
123+
assert [x["name"] for x in data["items"]] == ["Hills Terrill", "Terry Medhurst"]
132124

133125

134126
async def test_deep_search(client: AsyncClient, aio_engine: AIOEngine):
@@ -171,7 +163,7 @@ async def test_create(client: AsyncClient, aio_engine: AIOEngine):
171163
assert response.status_code == 303
172164
users = await aio_engine.find(User, User.name == "John Doe")
173165
assert len(users) == 1
174-
assert users[0].dict(exclude={"id"}) == {
166+
assert users[0].model_dump(exclude={"id"}) == {
175167
"name": "John Doe",
176168
"birthday": datetime(1999, 1, 1),
177169
"address": {
@@ -207,9 +199,9 @@ async def test_create_validation_error(client: AsyncClient, aio_engine: AIOEngin
207199
follow_redirects=False,
208200
)
209201
assert response.text.count('<div class="invalid-feedback">') == 3
210-
assert response.text.count("ensure this value has at least 3 characters") == 1
211-
assert response.text.count("ensure this value has at least 5 characters") == 1
212-
assert response.text.count("ensure this value has at most 10 characters") == 1
202+
assert response.text.count("at least 3 characters") == 1
203+
assert response.text.count("at least 5 characters") == 1
204+
assert response.text.count("at most 10 characters") == 1
213205
assert response.status_code == 422
214206
assert (await aio_engine.find_one(User, User.name == "Jo")) is None
215207

@@ -231,7 +223,7 @@ async def test_edit(client: AsyncClient, aio_engine: AIOEngine):
231223
assert response.status_code == 303
232224
users = await aio_engine.find(User, User.id == id)
233225
assert len(users) == 1
234-
assert users[0] == {
226+
assert users[0].model_dump() == {
235227
"id": id,
236228
"name": "John Doe",
237229
"birthday": datetime(1999, 1, 1),
@@ -265,9 +257,9 @@ async def test_edit_validation_error(client: AsyncClient, aio_engine: AIOEngine)
265257
follow_redirects=False,
266258
)
267259
assert response.text.count('<div class="invalid-feedback">') == 3
268-
assert response.text.count("ensure this value has at least 3 characters") == 1
269-
assert response.text.count("ensure this value has at least 5 characters") == 1
270-
assert response.text.count("ensure this value has at most 10 characters") == 1
260+
assert response.text.count("at least 3 characters") == 1
261+
assert response.text.count("at least 5 characters") == 1
262+
assert response.text.count("at most 10 characters") == 1
271263
assert response.status_code == 422
272264
assert (await aio_engine.find_one(User, User.name == "Hills Terrill")) is not None
273265
assert (await aio_engine.find_one(User, User.name == "Jo")) is None

tests/odmantic/test_sync_engine.py

+1-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import json
2-
import sys
32

43
import pytest
54
import pytest_asyncio
@@ -8,13 +7,6 @@
87
from starlette.applications import Starlette
98
from starlette_admin.contrib.odmantic import Admin, ModelView
109

11-
if sys.version_info < (3, 9):
12-
pytest.skip(
13-
"Skipping the test due to a segment fault error with odmantic on Python 3.8, and the library is not "
14-
"currently maintained",
15-
allow_module_level=True,
16-
)
17-
1810
pytestmark = pytest.mark.asyncio
1911

2012

@@ -66,7 +58,7 @@ async def test_api(client: AsyncClient):
6658
data = response.json()
6759
assert data["total"] == 3
6860
assert len(data["items"]) == 2
69-
assert ["Jim Rohn", "Albert Einstein"] == [x["name"] for x in data["items"]]
61+
assert [x["name"] for x in data["items"]] == ["Jim Rohn", "Albert Einstein"]
7062
# Find by pks
7163
response = await client.get(
7264
"/admin/api/author", params={"pks": [x["id"] for x in data["items"]]}

0 commit comments

Comments
 (0)