Skip to content

Commit e9fbfb9

Browse files
Convert ModelData from TypedDict pydantic
- Uses BaseModel instead - Adds runtime type validation at the Model level - Breaking change, requires building Models with the class Constructor instead of simply passing dictionaries around
1 parent cbece26 commit e9fbfb9

File tree

3 files changed

+54
-51
lines changed

3 files changed

+54
-51
lines changed

db_wrapper/model.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,21 @@
1313
)
1414
from uuid import UUID
1515

16+
# third party dependencies
17+
from pydantic import BaseModel
18+
1619
# internal dependency
1720
from .client import Client, sql
1821

1922

20-
class ModelData(TypedDict):
23+
class ModelData(BaseModel):
2124
"""Base interface for ModelData to be used in Model."""
2225

2326
# PENDS python 3.9 support in pylint
2427
# pylint: disable=inherit-non-class
2528
# pylint: disable=too-few-public-methods
2629

27-
_id: UUID
30+
id: UUID
2831

2932

3033
# Generic doesn't need a more descriptive name
@@ -65,7 +68,7 @@ async def one(self, item: T) -> T:
6568
columns: List[sql.Identifier] = []
6669
values: List[sql.Literal] = []
6770

68-
for column, value in item.items():
71+
for column, value in item.dict().items():
6972
values.append(sql.Literal(value))
7073

7174
columns.append(sql.Identifier(column))
@@ -100,7 +103,7 @@ async def one_by_id(self, id_value: str) -> T:
100103
query = sql.SQL(
101104
'SELECT * '
102105
'FROM {table} '
103-
'WHERE _id = {id_value};'
106+
'WHERE id = {id_value};'
104107
).format(
105108
table=self._table,
106109
id_value=sql.Literal(id_value)
@@ -152,7 +155,7 @@ def compose_changes(changes: Dict[str, Any]) -> sql.Composed:
152155
query = sql.SQL(
153156
'UPDATE {table} '
154157
'SET {changes} '
155-
'WHERE _id = {id_value} '
158+
'WHERE id = {id_value} '
156159
'RETURNING *;'
157160
).format(
158161
table=self._table,

requirements/prod.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
aiopg>=1.1.0,<2.0.0
2+
pydantic>=1.8.1,<2.0.0

tests/test_model.py

Lines changed: 45 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@
1313
TypeVar,
1414
List,
1515
Tuple,
16-
TypedDict,
1716
)
18-
from uuid import uuid4, UUID
17+
from uuid import uuid4
1918
import unittest
2019
from unittest import TestCase
2120

@@ -64,34 +63,34 @@ class TestReadOneById(TestCase):
6463

6564
@helpers.async_test
6665
async def test_it_correctly_builds_query_with_given_id(self) -> None:
67-
item: ModelData = {'_id': uuid4()}
66+
item = ModelData(id=uuid4())
6867
model, client = setup([item])
69-
await model.read.one_by_id(str(item['_id']))
68+
await model.read.one_by_id(str(item.id))
7069
query_composed = cast(
7170
helpers.AsyncMock, client.execute_and_return).call_args[0][0]
7271
query = helpers.composed_to_string(query_composed)
7372

7473
self.assertEqual(query, "SELECT * "
7574
"FROM test "
76-
f"WHERE _id = {item['_id']};")
75+
f"WHERE id = {item.id};")
7776

7877
@helpers.async_test
7978
async def test_it_returns_a_single_result(self) -> None:
80-
item: ModelData = {'_id': uuid4()}
79+
item = ModelData(id=uuid4())
8180
model, _ = setup([item])
82-
result = await model.read.one_by_id(str(item['_id']))
81+
result = await model.read.one_by_id(str(item.id))
8382

8483
self.assertEqual(result, item)
8584

86-
@helpers.async_test
85+
@ helpers.async_test
8786
async def test_it_raises_exception_if_more_than_one_result(self) -> None:
88-
item: ModelData = {'_id': uuid4()}
87+
item = ModelData(id=uuid4())
8988
model, _ = setup([item, item])
9089

9190
with self.assertRaises(UnexpectedMultipleResults):
92-
await model.read.one_by_id(str(item['_id']))
91+
await model.read.one_by_id(str(item.id))
9392

94-
@helpers.async_test
93+
@ helpers.async_test
9594
async def test_it_raises_exception_if_no_result_to_return(self) -> None:
9695
model: Model[ModelData]
9796
model, _ = setup([])
@@ -108,31 +107,31 @@ class Item(ModelData):
108107
a: str
109108
b: str
110109

111-
@helpers.async_test
110+
@ helpers.async_test
112111
async def test_it_correctly_builds_query_with_given_data(self) -> None:
113-
item: TestCreateOne.Item = {
114-
'_id': uuid4(),
112+
item = TestCreateOne.Item(**{
113+
'id': uuid4(),
115114
'a': 'a',
116115
'b': 'b',
117-
}
116+
})
118117
model, client = setup([item])
119118

120119
await model.create.one(item)
121120
query_composed = cast(
122121
helpers.AsyncMock, client.execute_and_return).call_args[0][0]
123122
query = helpers.composed_to_string(query_composed)
124123

125-
self.assertEqual(query, 'INSERT INTO test (_id,a,b) '
126-
f"VALUES ({item['_id']},a,b) "
124+
self.assertEqual(query, 'INSERT INTO test (id,a,b) '
125+
f"VALUES ({item.id},a,b) "
127126
'RETURNING *;')
128127

129-
@helpers.async_test
128+
@ helpers.async_test
130129
async def test_it_returns_the_new_record(self) -> None:
131-
item: TestCreateOne.Item = {
132-
'_id': uuid4(),
130+
item = TestCreateOne.Item(**{
131+
'id': uuid4(),
133132
'a': 'a',
134133
'b': 'b',
135-
}
134+
})
136135
model, _ = setup([item])
137136

138137
result = await model.create.one(item)
@@ -148,84 +147,84 @@ class Item(ModelData):
148147
a: str
149148
b: str
150149

151-
@helpers.async_test
150+
@ helpers.async_test
152151
async def test_it_correctly_builds_query_with_given_data(self) -> None:
153-
item: TestUpdateOne.Item = {
154-
'_id': uuid4(),
152+
item = TestUpdateOne.Item(**{
153+
'id': uuid4(),
155154
'a': 'a',
156155
'b': 'b',
157-
}
156+
})
158157
# cast required to avoid mypy error due to unpacking
159158
# TypedDict, see more on GitHub issue
160159
# https://github.com/python/mypy/issues/4122
161-
updated = cast(TestUpdateOne.Item, {**item, 'b': 'c'})
160+
updated = TestUpdateOne.Item(**{**item.dict(), 'b': 'c'})
162161
model, client = setup([updated])
163162

164-
await model.update.one_by_id(str(item['_id']), {'b': 'c'})
163+
await model.update.one_by_id(str(item.id), {'b': 'c'})
165164
query_composed = cast(
166165
helpers.AsyncMock, client.execute_and_return).call_args[0][0]
167166
query = helpers.composed_to_string(query_composed)
168167

169168
self.assertEqual(query, 'UPDATE test '
170169
'SET b = c '
171-
f"WHERE _id = {item['_id']} "
170+
f"WHERE id = {item.id} "
172171
'RETURNING *;')
173172

174-
@helpers.async_test
173+
@ helpers.async_test
175174
async def test_it_returns_the_new_record(self) -> None:
176-
item: TestUpdateOne.Item = {
177-
'_id': uuid4(),
175+
item = TestUpdateOne.Item(**{
176+
'id': uuid4(),
178177
'a': 'a',
179178
'b': 'b',
180-
}
179+
})
181180
# cast required to avoid mypy error due to unpacking
182181
# TypedDict, see more on GitHub issue
183182
# https://github.com/python/mypy/issues/4122
184-
updated = cast(TestUpdateOne.Item, {**item, 'b': 'c'})
183+
updated = TestUpdateOne.Item(**{**item.dict(), 'b': 'c'})
185184
model, _ = setup([updated])
186185

187-
result = await model.update.one_by_id(str(item['_id']), {'b': 'c'})
186+
result = await model.update.one_by_id(str(item.id), {'b': 'c'})
188187

189188
self.assertEqual(result, updated)
190189

191190

192191
class TestDeleteOneById(TestCase):
193-
"""Testing Model.delete.one_by_id"""
192+
"""Testing Model.delete.one_byid"""
194193

195194
class Item(ModelData):
196195
"""Example ModelData Item for testing."""
197196
a: str
198197
b: str
199198

200-
@helpers.async_test
199+
@ helpers.async_test
201200
async def test_it_correctly_builds_query_with_given_data(self) -> None:
202-
item: TestDeleteOneById.Item = {
203-
'_id': uuid4(),
201+
item = TestDeleteOneById.Item(**{
202+
'id': uuid4(),
204203
'a': 'a',
205204
'b': 'b',
206-
}
205+
})
207206
model, client = setup([item])
208207

209-
await model.delete.one_by_id(str(item['_id']))
208+
await model.delete.one_by_id(str(item.id))
210209

211210
query_composed = cast(
212211
helpers.AsyncMock, client.execute_and_return).call_args[0][0]
213212
query = helpers.composed_to_string(query_composed)
214213

215214
self.assertEqual(query, 'DELETE FROM test '
216-
f"WHERE id = {item['_id']} "
215+
f"WHERE id = {item.id} "
217216
'RETURNING *;')
218217

219-
@helpers.async_test
218+
@ helpers.async_test
220219
async def test_it_returns_the_deleted_record(self) -> None:
221-
item: TestDeleteOneById.Item = {
222-
'_id': uuid4(),
220+
item = TestDeleteOneById.Item(**{
221+
'id': uuid4(),
223222
'a': 'a',
224223
'b': 'b',
225-
}
224+
})
226225
model, _ = setup([item])
227226

228-
result = await model.delete.one_by_id(str(item['_id']))
227+
result = await model.delete.one_by_id(str(item.id))
229228

230229
self.assertEqual(result, item)
231230

0 commit comments

Comments
 (0)