Skip to content

Commit 1594d84

Browse files
authored
Adding support for Pydantic V2
1 parent 5dd305d commit 1594d84

File tree

10 files changed

+432
-379
lines changed

10 files changed

+432
-379
lines changed

.github/workflows/pull_request.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ jobs:
88
strategy:
99
matrix:
1010
python_version: [
11-
"3.7",
1211
"3.8",
1312
"3.9",
1413
"3.10",
14+
"3.11",
15+
"3.12",
1516
]
1617
runs-on: ubuntu-latest
1718
env:

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
# Change Log
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](http://keepachangelog.com/)
6+
and this project adheres to [Semantic Versioning](http://semver.org/).
7+
8+
## [1.0.0] - 2024-01-15
9+
10+
Added support for Pydantic V2 (version - ^2.5).
11+
12+
### Added
13+
14+
- Test coverage for Python versions `3.11` and `3.12`
15+
16+
### Changed
17+
- Python base version from `3.7` to `3.8`.
18+
- Changed Pydantic version from `^1.8.2` to `^2.5`.
19+
- Updated Model validation function from `parse_obj` to `model_validate`.
20+
- Renamed backend initialization class from `Config` to `db_config` to follow pydantic's naming convention.
21+
- Updated method for generation of dictionary from `dict` to `model_dump`.
22+
23+
## [0.4.2] - 2023-11-16
24+
25+
Added count() for dynamo backend that returns integer count as total.

poetry.lock

Lines changed: 358 additions & 327 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pydanticrud/backends/dynamodb.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def _to_epoch_decimal(dt: datetime) -> Decimal:
102102

103103
def chunk_list(lst, size):
104104
for i in range(0, len(lst), size):
105-
yield lst[i : i + size]
105+
yield lst[i: i + size]
106106

107107

108108
def index_definition(index_name, keys, gsi=False):
@@ -123,7 +123,7 @@ def index_definition(index_name, keys, gsi=False):
123123
class DynamoSerializer:
124124
def __init__(self, schema, ttl_field=None):
125125
self.properties = schema.get("properties")
126-
self.definitions = schema.get("definitions")
126+
self.definitions = schema.get("$defs")
127127
self.ttl_field = ttl_field
128128

129129
def _get_type_possibilities(self, field_name) -> Set[tuple]:
@@ -143,7 +143,6 @@ def type_from_definition(definition_signature: Union[str, dict]) -> dict:
143143
t = definition_signature.split("/")[-1]
144144
return self.definitions[t]
145145
return definition_signature
146-
147146
type_dicts = [type_from_definition(t) for t in possible_types]
148147

149148
return set([(t["type"], t.get("format", "")) for t in type_dicts])
@@ -213,7 +212,7 @@ def __init__(self, cls, result, serialized_items):
213212

214213
class Backend:
215214
def __init__(self, cls):
216-
cfg = cls.Config
215+
cfg = cls.db_config
217216
self.cls = cls
218217
self.schema = cls.schema()
219218
self.hash_key = cfg.hash_key

pydanticrud/backends/sqlite.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def get_column_data(field_type):
4545

4646
class Backend:
4747
def __init__(self, cls):
48-
cfg = cls.Config
48+
cfg = cls.db_config
4949
self.hash_key = cfg.hash_key
5050
self.table_name = cls.get_table_name()
5151

@@ -162,7 +162,7 @@ def get(self, item_key):
162162

163163
def save(self, item, condition: Optional[Rule] = None) -> bool:
164164
table_name = item.get_table_name()
165-
hash_key = item.Config.hash_key
165+
hash_key = item.db_config.hash_key
166166
key = getattr(item, hash_key)
167167
fields = tuple(self._columns.keys())
168168

pydanticrud/main.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
import pydantic.error_wrappers
21
from pydantic import BaseModel as PydanticBaseModel
3-
from pydantic.main import ModelMetaclass
4-
from rule_engine import Rule
2+
from pydantic._internal._model_construction import ModelMetaclass
53

64

75
class CrudMetaClass(ModelMetaclass):
86
def __new__(mcs, name, bases, namespace, **kwargs):
97
cls = super().__new__(mcs, name, bases, namespace, **kwargs)
10-
if hasattr(cls.__config__, "backend"):
11-
cls.__backend__ = cls.__config__.backend(cls)
12-
8+
if hasattr(cls, "db_config") and hasattr(cls.db_config, "backend"):
9+
cls.__backend__ = cls.db_config.backend(cls)
1310
return cls
1411

1512

@@ -46,7 +43,7 @@ def initialize(cls):
4643

4744
@classmethod
4845
def get_table_name(cls) -> str:
49-
return cls.Config.title.lower()
46+
return cls.db_config.title.lower()
5047

5148
@classmethod
5249
def exists(cls) -> bool:
@@ -65,11 +62,11 @@ def count(cls, *args, **kwargs):
6562

6663
@classmethod
6764
def get(cls, *args, **kwargs):
68-
return cls.parse_obj(cls.__backend__.get(*args, **kwargs))
65+
return cls.model_validate(cls.__backend__.get(*args, **kwargs))
6966

7067
def save(self) -> bool:
7168
# Parse the new obj to trigger validation
72-
self.__class__.parse_obj(self.dict(by_alias=True))
69+
self.__class__.model_validate(self.model_dump(by_alias=True))
7370

7471
# Maybe we should pass a conditional to the backend but for now the only place that uses it doesn't need it.
7572
return self.__class__.__backend__.save(self)

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
[tool.poetry]
22
name = "pydanticrud"
3-
version = "0.4.2"
3+
version = "1.0.0"
44
description = "Supercharge your Pydantic models with CRUD methods and a pluggable backend"
55
authors = ["Timothy Farrell <[email protected]>"]
66
license = "MIT"
77

88
[tool.poetry.dependencies]
9-
python = ">=3.6"
9+
python = ">=3.8"
1010
boto3 = "^1.17.112"
1111
rule-engine = "^3.2.0"
12-
pydantic = "^1.8.2"
12+
pydantic = "^2.5"
1313
dataclasses = {version = "^0.8", python = "3.6"}
1414

1515
[tool.poetry.dev-dependencies]

tests/test_dynamodb.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import docker
88
from botocore.exceptions import ClientError
9-
from pydantic import BaseModel as PydanticBaseModel, Field, root_validator, ValidationError
9+
from pydantic import model_validator, BaseModel as PydanticBaseModel, Field, ValidationError
1010
from pydanticrud import BaseModel, DynamoDbBackend, ConditionCheckFailed
1111
import pytest
1212
from pydanticrud.exceptions import DoesNotExist
@@ -30,11 +30,11 @@ class SimpleKeyModel(BaseModel):
3030
expires: datetime
3131
sigfig: Decimal
3232
enabled: bool
33-
data: Dict[int, int] = None
33+
data: Dict[int, int] = {}
3434
items: List[int]
3535
hash: UUID
3636

37-
class Config:
37+
class db_config:
3838
title = "ModelTitle123"
3939
hash_key = "name"
4040
ttl = "expires"
@@ -49,13 +49,14 @@ class AliasKeyModel(BaseModel):
4949
name: str
5050
type_: str = Field(alias="type")
5151

52-
@root_validator(pre=True)
52+
@model_validator(mode="before")
53+
@classmethod
5354
def type_from_typ(cls, values):
5455
if 'typ' in values:
5556
values['type'] = values.pop('typ')
5657
return values
5758

58-
class Config:
59+
class db_config:
5960
title = "AliasTitle123"
6061
hash_key = "name"
6162
backend = DynamoDbBackend
@@ -71,7 +72,7 @@ class ComplexKeyModel(BaseModel):
7172
thread_id: str
7273
body: str = "some random string"
7374

74-
class Config:
75+
class db_config:
7576
title = "ComplexModelTitle123"
7677
hash_key = "account"
7778
range_key = "sort_date_key"
@@ -101,14 +102,13 @@ class NestedModel(BaseModel):
101102
ticket: Optional[Ticket]
102103
other: Union[Ticket, SomethingElse]
103104

104-
class Config:
105+
class db_config:
105106
title = "NestedModelTitle123"
106107
hash_key = "account"
107108
range_key = "sort_date_key"
108109
backend = DynamoDbBackend
109110
endpoint = "http://localhost:18002"
110111

111-
112112
def alias_model_data_generator(**kwargs):
113113
data = dict(
114114
id=random.randint(0, 100000),
@@ -231,7 +231,7 @@ def simple_query_data(simple_table):
231231
data = [datum for datum in [simple_model_data_generator(**i) for i in presets]]
232232
del data[0]["data"] # We need to have no data to ensure that default values work
233233
for datum in data:
234-
SimpleKeyModel.parse_obj(datum).save()
234+
SimpleKeyModel.model_validate(datum).save()
235235
try:
236236
yield data
237237
finally:
@@ -250,20 +250,20 @@ def complex_query_data(complex_table):
250250
for i, p in enumerate(presets)
251251
]
252252
for datum in data:
253-
ComplexKeyModel.parse_obj(datum).save()
253+
ComplexKeyModel.model_validate(datum).save()
254254
try:
255255
yield data
256256
finally:
257257
for datum in data:
258-
ComplexKeyModel.delete((datum[ComplexKeyModel.Config.hash_key], datum[ComplexKeyModel.Config.range_key]))
258+
ComplexKeyModel.delete((datum[ComplexKeyModel.db_config.hash_key], datum[getattr(ComplexKeyModel.db_config, "range_key")]))
259259

260260

261261
@pytest.fixture(scope="module")
262262
def alias_query_data(alias_table):
263263
presets = [dict(name="Jerry"), dict(name="Hermione"), dict(), dict(), dict()]
264264
data = [datum for datum in [alias_model_data_generator(**i) for i in presets]]
265265
for datum in data:
266-
AliasKeyModel.parse_obj(datum).save()
266+
AliasKeyModel.model_validate(datum).save()
267267
try:
268268
yield data
269269
finally:
@@ -276,35 +276,35 @@ def nested_query_data(nested_table):
276276
presets = [dict()] * 5
277277
data = [datum for datum in [nested_model_data_generator(**i) for i in presets]]
278278
for datum in data:
279-
nested_datum = NestedModel.parse_obj(datum)
279+
nested_datum = NestedModel.model_validate(datum)
280280
nested_datum.save()
281281
try:
282282
yield data
283283
finally:
284284
for datum in data:
285-
NestedModel.delete((datum[NestedModel.Config.hash_key], datum[NestedModel.Config.range_key]))
285+
NestedModel.delete((datum[NestedModel.db_config.hash_key], datum[NestedModel.db_config.range_key]))
286286

287287

288288
@pytest.fixture
289289
def nested_query_data_empty_ticket(nested_table):
290290
presets = [dict()] * 5
291291
data = [datum for datum in [nested_model_data_generator(include_ticket=False, **i) for i in presets]]
292292
for datum in data:
293-
NestedModel.parse_obj(datum).save()
293+
NestedModel.model_validate(datum).save()
294294
try:
295295
yield data
296296
finally:
297297
for datum in data:
298-
NestedModel.delete((datum[NestedModel.Config.hash_key], datum[NestedModel.Config.range_key]))
298+
NestedModel.delete((datum[NestedModel.db_config.hash_key], datum[NestedModel.db_config.range_key]))
299299

300300

301301
def test_save_get_delete_simple(dynamo, simple_table):
302302
data = simple_model_data_generator()
303-
a = SimpleKeyModel.parse_obj(data)
303+
a = SimpleKeyModel.model_validate(data)
304304
a.save()
305305
try:
306306
b = SimpleKeyModel.get(data["name"])
307-
assert b.dict() == a.dict()
307+
assert b.dict() == a.model_dump()
308308
finally:
309309
SimpleKeyModel.delete(data["name"])
310310

@@ -330,7 +330,7 @@ def test_save_ttl_field_is_float(dynamo, simple_query_data):
330330
def test_query_with_hash_key_simple(dynamo, simple_query_data):
331331
res = SimpleKeyModel.query(Rule(f"name == '{simple_query_data[0]['name']}'"))
332332
res_data = {m.name: m.dict() for m in res}
333-
simple_query_data[0]["data"] = None # This is a default value and should be populated as such
333+
simple_query_data[0]["data"] = {} # This is a default value and should be populated as such
334334
assert res_data == {simple_query_data[0]["name"]: simple_query_data[0]}
335335

336336

@@ -383,11 +383,11 @@ def test_query_scan_contains_simple(dynamo, simple_query_data):
383383

384384
def test_save_get_delete_complex(dynamo, complex_table):
385385
data = complex_model_data_generator()
386-
a = ComplexKeyModel.parse_obj(data)
386+
a = ComplexKeyModel.model_validate(data)
387387
a.save()
388388
try:
389389
b = ComplexKeyModel.get((data["account"], data["sort_date_key"]))
390-
assert b.dict() == a.dict()
390+
assert b.dict() == a.model_dump()
391391
finally:
392392
ComplexKeyModel.delete((data["account"], data["sort_date_key"]))
393393

@@ -528,7 +528,7 @@ def test_query_alias_save(dynamo):
528528
AliasKeyModel.initialize()
529529
try:
530530
for datum in data:
531-
AliasKeyModel.parse_obj(datum).save()
531+
AliasKeyModel.model_validate(datum).save()
532532
except Exception as e:
533533
raise pytest.fail("Failed to save Alias model!")
534534

@@ -567,7 +567,7 @@ def test_alias_model_validator_ingest(dynamo):
567567

568568
def test_batch_write(dynamo, complex_table):
569569
response = {"UnprocessedItems": {}}
570-
data = [ComplexKeyModel.parse_obj(complex_model_data_generator()) for x in range(0, 10)]
570+
data = [ComplexKeyModel.model_validate(complex_model_data_generator()) for x in range(0, 10)]
571571
un_proc = ComplexKeyModel.batch_save(data)
572572
assert un_proc == response["UnprocessedItems"]
573573
res_get = ComplexKeyModel.get((data[0].account, data[0].sort_date_key))
@@ -581,7 +581,7 @@ def test_batch_write(dynamo, complex_table):
581581

582582
def test_message_batch_write_client_exception(dynamo, complex_table):
583583
data = [
584-
ComplexKeyModel.parse_obj(complex_model_data_generator(body="some big string" * 10000))
584+
ComplexKeyModel.model_validate(complex_model_data_generator(body="some big string" * 10000))
585585
for x in range(0, 2)
586586
]
587587
with pytest.raises(ClientError) as exc:

tests/test_model.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class Model(BaseModel):
2121
name: str
2222
total: float
2323

24-
class Config:
24+
class db_config:
2525
title = "ModelTitle123"
2626
backend = FalseBackend
2727

@@ -57,4 +57,4 @@ def test_model_backend_query():
5757

5858

5959
def test_model_table_name_from_title():
60-
assert Model.get_table_name() == Model.Config.title.lower()
60+
assert Model.get_table_name() == Model.db_config.title.lower()

tests/test_sqlite.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class Model(BaseModel):
1919
data: Dict[str, str]
2020
items: List[int]
2121

22-
class Config:
22+
class db_config:
2323
title = "ModelTitle123"
2424
hash_key = "id"
2525
backend = SqliteBackend
@@ -69,8 +69,8 @@ def test_initialize_creates_table():
6969

7070
def test_save_and_get(model_in_db):
7171
data = model_data_generator()
72-
a = Model.parse_obj(data)
73-
assert a.dict() == data
72+
a = Model.model_validate(data)
73+
assert a.model_dump() == data
7474
a.save()
7575
b = Model.get(data["id"])
7676
assert b.dict() == data
@@ -108,13 +108,13 @@ def test_query(model_in_db):
108108
data2["id"] = 2
109109
data3 = model_data_generator()
110110
data3["id"] = 1234
111-
Model.parse_obj(data1).save()
112-
Model.parse_obj(data2).save()
113-
Model.parse_obj(data3).save()
111+
Model.model_validate(data1).save()
112+
Model.model_validate(data2).save()
113+
Model.model_validate(data3).save()
114114
for r in range(0, 10):
115115
_data = model_data_generator()
116116
_data["id"] += 3
117-
Model.parse_obj(_data).save()
117+
Model.model_validate(_data).save()
118118
res = Model.query(Rule(f"id < 3"))
119119
data = {m.id: m.dict() for m in res}
120120
assert data == {1: data1, 2: data2}

0 commit comments

Comments
 (0)