objectdb 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- objectdb-0.7.0/.gitignore +13 -0
- objectdb-0.7.0/PKG-INFO +8 -0
- objectdb-0.7.0/README.md +0 -0
- objectdb-0.7.0/pyproject.toml +17 -0
- objectdb-0.7.0/src/objectdb/__init__.py +0 -0
- objectdb-0.7.0/src/objectdb/database.py +200 -0
- objectdb-0.7.0/src/objectdb/database_mongodb.py +52 -0
- objectdb-0.7.0/src/objectdb/py.typed +0 -0
- objectdb-0.7.0/tests/__init__.py +0 -0
- objectdb-0.7.0/tests/conftest.py +15 -0
- objectdb-0.7.0/tests/test_database.py +66 -0
- objectdb-0.7.0/tests/test_database_mongodb.py +109 -0
objectdb-0.7.0/PKG-INFO
ADDED
objectdb-0.7.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "objectdb"
|
|
3
|
+
version = "0.7.0"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = [{name = "Felix Trautwein <mail@felixtrautwein.de>"}]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
"motor>=3.7.1",
|
|
10
|
+
"mongomock_motor>=0.0.36",
|
|
11
|
+
"pytest-asyncio>=1.2.0",
|
|
12
|
+
"bson>=0.5.10"
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["hatchling"]
|
|
17
|
+
build-backend = "hatchling.build"
|
|
File without changes
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Database abstraction layer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Any, Dict, Generic, Optional, Type, TypeVar
|
|
8
|
+
|
|
9
|
+
import pydantic
|
|
10
|
+
from bson import ObjectId
|
|
11
|
+
from pydantic import GetCoreSchemaHandler
|
|
12
|
+
from pydantic_core import core_schema
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T", bound="DatabaseItem")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ForeignKey(Generic[T]):
|
|
18
|
+
"""A reference to another DatabaseItem."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, target_type: type[T], identifier: str):
|
|
21
|
+
self.target_type = target_type
|
|
22
|
+
self.identifier = identifier
|
|
23
|
+
|
|
24
|
+
def __eq__(self, other: object) -> bool:
|
|
25
|
+
return (
|
|
26
|
+
isinstance(other, ForeignKey)
|
|
27
|
+
and self.target_type == other.target_type
|
|
28
|
+
and self.identifier == other.identifier
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def __hash__(self) -> int:
|
|
32
|
+
return hash((self.target_type, self.identifier))
|
|
33
|
+
|
|
34
|
+
def __repr__(self) -> str:
|
|
35
|
+
return f"ForeignKey({self.target_type.__name__}:{self.identifier})"
|
|
36
|
+
|
|
37
|
+
#
|
|
38
|
+
# --- Pydantic integration ---
|
|
39
|
+
#
|
|
40
|
+
@classmethod
|
|
41
|
+
def __class_getitem__(cls, item: type[T]):
|
|
42
|
+
target_type = item
|
|
43
|
+
|
|
44
|
+
class _ForeignKey(cls): # type: ignore
|
|
45
|
+
__origin__ = cls
|
|
46
|
+
__args__ = (item,)
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def __get_pydantic_core_schema__(cls, source_type, handler: GetCoreSchemaHandler):
|
|
50
|
+
def validator(v):
|
|
51
|
+
if isinstance(v, ForeignKey):
|
|
52
|
+
return v
|
|
53
|
+
if isinstance(v, target_type):
|
|
54
|
+
return ForeignKey(target_type, v.identifier)
|
|
55
|
+
if isinstance(v, str):
|
|
56
|
+
return ForeignKey(target_type, v)
|
|
57
|
+
raise TypeError(f"Cannot convert {v!r} to ForeignKey[{target_type.__name__}]")
|
|
58
|
+
|
|
59
|
+
return core_schema.no_info_after_validator_function(
|
|
60
|
+
validator,
|
|
61
|
+
core_schema.union_schema(
|
|
62
|
+
[
|
|
63
|
+
core_schema.is_instance_schema(target_type),
|
|
64
|
+
core_schema.str_schema(),
|
|
65
|
+
core_schema.is_instance_schema(ForeignKey),
|
|
66
|
+
]
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def __get_pydantic_json_schema__(cls, _core_schema, handler):
|
|
72
|
+
# Expose as string in OpenAPI
|
|
73
|
+
return handler(core_schema.str_schema())
|
|
74
|
+
|
|
75
|
+
return _ForeignKey
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class PyObjectId(ObjectId):
|
|
79
|
+
"""Custom ObjectId type for Pydantic."""
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def __get_pydantic_core_schema__(cls, _source, _handler) -> core_schema.PlainValidatorFunctionSchema:
|
|
83
|
+
return core_schema.no_info_plain_validator_function(cls.validate)
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def validate(cls, v: Any) -> PyObjectId:
|
|
87
|
+
"""Validate and convert to ObjectId."""
|
|
88
|
+
if isinstance(v, ObjectId):
|
|
89
|
+
return cls(v)
|
|
90
|
+
if not ObjectId.is_valid(v):
|
|
91
|
+
raise ValueError(f"Invalid ObjectId: {v}")
|
|
92
|
+
return cls(v)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class DatabaseItem(ABC, pydantic.BaseModel):
|
|
96
|
+
"""Base class for database items."""
|
|
97
|
+
|
|
98
|
+
model_config = pydantic.ConfigDict(
|
|
99
|
+
revalidate_instances="always", json_encoders={ObjectId: str}, populate_by_name=True
|
|
100
|
+
)
|
|
101
|
+
identifier: PyObjectId = pydantic.Field(default_factory=PyObjectId, alias="_id")
|
|
102
|
+
|
|
103
|
+
def __eq__(self, other: object) -> bool:
|
|
104
|
+
if not isinstance(other, DatabaseItem):
|
|
105
|
+
return NotImplemented
|
|
106
|
+
return self.identifier == other.identifier
|
|
107
|
+
|
|
108
|
+
def __hash__(self) -> int:
|
|
109
|
+
return hash(self.identifier)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class DatabaseError(Exception):
|
|
113
|
+
"""Errors related to database operations."""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class UnknownEntityError(DatabaseError):
|
|
117
|
+
"""Requested entity does not exist."""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Database(ABC):
|
|
121
|
+
"""Database abstraction."""
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
async def update(self, item: DatabaseItem) -> None:
|
|
125
|
+
"""Update entity."""
|
|
126
|
+
|
|
127
|
+
@abstractmethod
|
|
128
|
+
async def get(self, schema: Type[T], identifier: PyObjectId) -> T:
|
|
129
|
+
"""Return entity, raise UnknownEntityError if entity does not exist."""
|
|
130
|
+
|
|
131
|
+
@abstractmethod
|
|
132
|
+
async def get_all(self, schema: Type[T]) -> Dict[str, T]:
|
|
133
|
+
"""Return all entities of schema."""
|
|
134
|
+
|
|
135
|
+
@abstractmethod
|
|
136
|
+
async def delete(self, schema: Type[T], identifier: PyObjectId, cascade: bool = False) -> None:
|
|
137
|
+
"""Delete entity."""
|
|
138
|
+
|
|
139
|
+
@abstractmethod
|
|
140
|
+
async def find(self, schema: Type[T], **kwargs: str) -> Optional[Dict[PyObjectId, T]]:
|
|
141
|
+
"""Return all entities of schema matching the filter criteria."""
|
|
142
|
+
|
|
143
|
+
@abstractmethod
|
|
144
|
+
async def find_one(self, schema: Type[T], **kwargs: str) -> Optional[T]:
|
|
145
|
+
"""Return one entitiy of schema matching the filter criteria, raise if multiple exist."""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class DictDatabase(Database):
|
|
149
|
+
"""Simple Database implementation with dictionary."""
|
|
150
|
+
|
|
151
|
+
def __init__(self) -> None:
|
|
152
|
+
self.data: Dict[Type[DatabaseItem], Dict[PyObjectId, DatabaseItem]] = {}
|
|
153
|
+
|
|
154
|
+
async def update(self, item: DatabaseItem) -> None:
|
|
155
|
+
"""Update data."""
|
|
156
|
+
item_type = type(item)
|
|
157
|
+
if item_type not in self.data:
|
|
158
|
+
self.data[item_type] = {}
|
|
159
|
+
self.data[item_type][item.identifier] = copy.deepcopy(item)
|
|
160
|
+
|
|
161
|
+
async def get(self, schema: Type[T], identifier: PyObjectId) -> T:
|
|
162
|
+
try:
|
|
163
|
+
return self.data[schema][identifier] # type: ignore
|
|
164
|
+
except KeyError as exc:
|
|
165
|
+
raise UnknownEntityError(f"Unknown identifier: {identifier}") from exc
|
|
166
|
+
|
|
167
|
+
async def get_all(self, schema: Type[T]) -> Dict[str, T]:
|
|
168
|
+
try:
|
|
169
|
+
return self.data[schema] # type: ignore
|
|
170
|
+
except KeyError as exc:
|
|
171
|
+
raise DatabaseError(f"Unkonwn schema: {schema}") from exc
|
|
172
|
+
|
|
173
|
+
async def delete(self, schema: Type[T], identifier: PyObjectId, cascade: bool = False) -> None:
|
|
174
|
+
try:
|
|
175
|
+
del self.data[schema][identifier]
|
|
176
|
+
except KeyError as exc:
|
|
177
|
+
raise UnknownEntityError(f"Unknown identifier: {identifier}") from exc
|
|
178
|
+
if cascade:
|
|
179
|
+
for db in self.data:
|
|
180
|
+
for identifier, item in self.data[db].items():
|
|
181
|
+
for attribute in item.__class__.model_fields:
|
|
182
|
+
if isinstance(attribute, ForeignKey) and attribute == item.identifier:
|
|
183
|
+
del self.data[db][identifier]
|
|
184
|
+
|
|
185
|
+
async def find(self, schema: Type[T], **kwargs: str) -> Optional[Dict[PyObjectId, T]]:
|
|
186
|
+
try:
|
|
187
|
+
results = []
|
|
188
|
+
for item in self.data[schema].values(): # type: ignore
|
|
189
|
+
if all(getattr(item, k) == v for k, v in kwargs.items()):
|
|
190
|
+
results.append(item) # type: ignore
|
|
191
|
+
return {item.identifier: item for item in results} # type: ignore
|
|
192
|
+
except KeyError as exc:
|
|
193
|
+
raise DatabaseError(f"Unkonwn schema: {schema}") from exc
|
|
194
|
+
|
|
195
|
+
async def find_one(self, schema: Type[T], **kwargs: str) -> Optional[T]:
|
|
196
|
+
if results := await self.find(schema, **kwargs):
|
|
197
|
+
if len(results) > 1:
|
|
198
|
+
raise DatabaseError(f"Multiple entities found for {schema} with {kwargs}")
|
|
199
|
+
return list(results.values())[0]
|
|
200
|
+
return None
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Redis Database implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Mapping, Optional, Type
|
|
4
|
+
|
|
5
|
+
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
|
|
6
|
+
from objectdb.database import Database, DatabaseItem, PyObjectId, T, UnknownEntityError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MongoDBDatabase(Database):
|
|
10
|
+
"""MongoDB database implementation."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, mongodb_client: AsyncIOMotorClient, name: str) -> None:
|
|
13
|
+
self.connection: AsyncIOMotorClient[Mapping[str, dict[str, Any]]] = mongodb_client
|
|
14
|
+
self.database: AsyncIOMotorDatabase[Mapping[str, dict[str, Any]]] = self.connection[name]
|
|
15
|
+
|
|
16
|
+
async def update(self, item: DatabaseItem):
|
|
17
|
+
"""Update data."""
|
|
18
|
+
item_type = type(item)
|
|
19
|
+
item.model_validate(item)
|
|
20
|
+
await self.database[item_type.__name__].update_one(
|
|
21
|
+
filter={"_id": item.identifier},
|
|
22
|
+
update={"$set": item.model_dump(by_alias=True, exclude={"_id"})},
|
|
23
|
+
upsert=True,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
async def get(self, schema: Type[T], identifier: PyObjectId) -> T:
|
|
27
|
+
collection = self.database[schema.__name__]
|
|
28
|
+
if res := await collection.find_one(filter={"_id": identifier}):
|
|
29
|
+
return schema.model_validate(res)
|
|
30
|
+
raise UnknownEntityError(f"Unknown identifier: {identifier}")
|
|
31
|
+
|
|
32
|
+
async def get_all(self, schema: Type[T]) -> Dict[str, T]:
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
|
|
35
|
+
async def delete(self, schema: Type[T], identifier: PyObjectId, cascade: bool = False) -> None:
|
|
36
|
+
collection = self.database[schema.__name__]
|
|
37
|
+
result = await collection.delete_one(filter={"_id": identifier})
|
|
38
|
+
if result.deleted_count != 1:
|
|
39
|
+
raise UnknownEntityError(f"Unknown identifier: {identifier}")
|
|
40
|
+
|
|
41
|
+
async def find(self, schema: Type[T], **kwargs: Any) -> Optional[Dict[PyObjectId, T]]:
|
|
42
|
+
collection = self.database[schema.__name__]
|
|
43
|
+
if results := collection.find(filter=kwargs):
|
|
44
|
+
return {res["_id"]: schema.model_validate(res) async for res in results}
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
async def find_one(self, schema: Type[T], **kwargs: Any) -> Optional[T]:
|
|
48
|
+
"""Find one item matching the criteria."""
|
|
49
|
+
collection = self.database[schema.__name__]
|
|
50
|
+
if result := await collection.find_one(filter=kwargs):
|
|
51
|
+
return schema.model_validate(result)
|
|
52
|
+
return None
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import AsyncGenerator
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from mongomock_motor import AsyncMongoMockClient
|
|
5
|
+
from motor.motor_asyncio import AsyncIOMotorClient
|
|
6
|
+
from objectdb.database_mongodb import MongoDBDatabase # your class
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
async def mongo_db() -> AsyncGenerator[MongoDBDatabase, None]:
|
|
11
|
+
"""Return a MongoDBDatabase instance backed by mongomock_motor."""
|
|
12
|
+
client: AsyncIOMotorClient = AsyncMongoMockClient()
|
|
13
|
+
db = MongoDBDatabase(mongodb_client=client, name="test_db")
|
|
14
|
+
yield db
|
|
15
|
+
await client.drop_database("test_db")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Tests for database module."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from objectdb.database import DatabaseItem, DictDatabase, ForeignKey, UnknownEntityError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Customer(DatabaseItem):
|
|
8
|
+
"""Sample item for testing."""
|
|
9
|
+
|
|
10
|
+
name: str
|
|
11
|
+
city: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Product(DatabaseItem):
|
|
15
|
+
"""Sample item for testing."""
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
customer: ForeignKey[Customer]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.asyncio
|
|
22
|
+
async def test_add_item() -> None:
|
|
23
|
+
"""Test adding item to database."""
|
|
24
|
+
test_item = Customer(name="name", city="city")
|
|
25
|
+
db = DictDatabase()
|
|
26
|
+
await db.update(test_item)
|
|
27
|
+
assert db.get(Customer, test_item.identifier)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.asyncio
|
|
31
|
+
async def test_update_item() -> None:
|
|
32
|
+
"""Test updating item in database."""
|
|
33
|
+
test_item = Customer(name="name", city="city")
|
|
34
|
+
db = DictDatabase()
|
|
35
|
+
await db.update(test_item)
|
|
36
|
+
item_to_change = await db.get(Customer, test_item.identifier)
|
|
37
|
+
item_to_change.city = "changed_city"
|
|
38
|
+
await db.update(item_to_change)
|
|
39
|
+
updated_item = await db.get(Customer, test_item.identifier)
|
|
40
|
+
assert updated_item.city == "changed_city"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.asyncio
|
|
44
|
+
async def test_delete_item() -> None:
|
|
45
|
+
"""Test deleting items."""
|
|
46
|
+
test_item = Customer(name="name", city="city")
|
|
47
|
+
db = DictDatabase()
|
|
48
|
+
await db.update(test_item)
|
|
49
|
+
assert db.get(type(test_item), test_item.identifier)
|
|
50
|
+
await db.delete(type(test_item), test_item.identifier)
|
|
51
|
+
with pytest.raises(UnknownEntityError):
|
|
52
|
+
await db.get(type(test_item), test_item.identifier)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.mark.skip(reason="Not implemented yet")
|
|
56
|
+
@pytest.mark.asyncio
|
|
57
|
+
async def test_cascading_delete() -> None:
|
|
58
|
+
"""Test cascading delete of items with foreign keys."""
|
|
59
|
+
customer = Customer(name="name", city="city")
|
|
60
|
+
db = DictDatabase()
|
|
61
|
+
await db.update(customer)
|
|
62
|
+
product = Product(name="product", customer=customer) # type: ignore
|
|
63
|
+
await db.update(product)
|
|
64
|
+
await db.delete(type(customer), customer.identifier)
|
|
65
|
+
with pytest.raises(UnknownEntityError):
|
|
66
|
+
await db.get(type(product), product.identifier)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Tests for the MongoDB database implementation."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from objectdb.database import DatabaseItem
|
|
5
|
+
from objectdb.database_mongodb import MongoDBDatabase, UnknownEntityError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class User(DatabaseItem):
|
|
9
|
+
"""Test user entity."""
|
|
10
|
+
|
|
11
|
+
name: str
|
|
12
|
+
email: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.asyncio
|
|
16
|
+
async def test_insert_and_get(mongo_db: MongoDBDatabase) -> None:
|
|
17
|
+
"""Test inserting and retrieving an item."""
|
|
18
|
+
# GIVEN a user
|
|
19
|
+
user = User(name="Alice", email="alice@example.com")
|
|
20
|
+
# WHEN inserting it into the database
|
|
21
|
+
await mongo_db.update(user)
|
|
22
|
+
# THEN it can be retrieved by its identifier
|
|
23
|
+
fetched = await mongo_db.get(User, identifier=user.identifier)
|
|
24
|
+
assert fetched.name == "Alice"
|
|
25
|
+
assert fetched.identifier == user.identifier
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.mark.asyncio
|
|
29
|
+
async def test_update_existing(mongo_db: MongoDBDatabase) -> None:
|
|
30
|
+
"""Test updating an existing item."""
|
|
31
|
+
# GIVEN a user in the database
|
|
32
|
+
user = User(name="Bob", email="box@example.com")
|
|
33
|
+
await mongo_db.update(user)
|
|
34
|
+
# WHEN updating the user's email
|
|
35
|
+
user.email = "bob@example.com"
|
|
36
|
+
await mongo_db.update(user)
|
|
37
|
+
# THEN the change is reflected in the database
|
|
38
|
+
fetched = await mongo_db.get(User, identifier=user.identifier)
|
|
39
|
+
assert fetched.email == "bob@example.com"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.asyncio
|
|
43
|
+
async def test_delete_existing(mongo_db: MongoDBDatabase) -> None:
|
|
44
|
+
"""Test deleting an item."""
|
|
45
|
+
# GIVEN a user in the database
|
|
46
|
+
user = User(name="Charlie", email="charlie@example.com")
|
|
47
|
+
await mongo_db.update(user)
|
|
48
|
+
# WHEN deleting the user
|
|
49
|
+
await mongo_db.delete(type(user), user.identifier)
|
|
50
|
+
# THEN the user can no longer be retrieved
|
|
51
|
+
with pytest.raises(UnknownEntityError):
|
|
52
|
+
await mongo_db.get(User, identifier=user.identifier)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.mark.asyncio
|
|
56
|
+
async def test_get_unknown(mongo_db: MongoDBDatabase) -> None:
|
|
57
|
+
"""Test retrieving an unknown item raises an error."""
|
|
58
|
+
# GIVEN a user that does not exist in the database
|
|
59
|
+
user = User(name="Dave", email="dave@example.com")
|
|
60
|
+
# WHEN trying to get a user with a random identifier
|
|
61
|
+
with pytest.raises(UnknownEntityError):
|
|
62
|
+
await mongo_db.get(User, identifier=user.identifier)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.asyncio
|
|
66
|
+
async def test_find_users(mongo_db: MongoDBDatabase) -> None:
|
|
67
|
+
"""Test finding users by attribute."""
|
|
68
|
+
# GIVEN multiple users in the database
|
|
69
|
+
user1 = User(name="Eve", email="eve@example.com")
|
|
70
|
+
user2 = User(name="Frank", email="frank@example.com")
|
|
71
|
+
await mongo_db.update(user1)
|
|
72
|
+
await mongo_db.update(user2)
|
|
73
|
+
# WHEN finding users by name
|
|
74
|
+
results = await mongo_db.find(User, name="Eve")
|
|
75
|
+
# THEN only the matching user is returned
|
|
76
|
+
assert results == {user1.identifier: user1}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.asyncio
|
|
80
|
+
async def test_find_one_user(mongo_db: MongoDBDatabase) -> None:
|
|
81
|
+
"""Test finding a single user by attribute."""
|
|
82
|
+
# GIVEN two users in the database
|
|
83
|
+
user = User(name="Grace", email="grace@example.com")
|
|
84
|
+
User(name="Heidi", email="heidi@example.com")
|
|
85
|
+
await mongo_db.update(user)
|
|
86
|
+
# WHEN finding the user by name
|
|
87
|
+
result = await mongo_db.find_one(User, name="Grace")
|
|
88
|
+
# THEN the correct user is returned
|
|
89
|
+
assert result == user
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.mark.asyncio
|
|
93
|
+
async def test_find_one_user_not_found(mongo_db: MongoDBDatabase) -> None:
|
|
94
|
+
"""Test finding a single user that does not exist."""
|
|
95
|
+
# GIVEN no users in the database
|
|
96
|
+
# WHEN finding a user by name that does not exist
|
|
97
|
+
# THEN None is returned
|
|
98
|
+
assert await mongo_db.find_one(User, name="NonExistentUser") is None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@pytest.mark.asyncio
|
|
102
|
+
async def test_delete_unknown(mongo_db: MongoDBDatabase) -> None:
|
|
103
|
+
"""Test deleting an unknown item raises an error."""
|
|
104
|
+
# GIVEN a user that does not exist in the database
|
|
105
|
+
user = User(name="Ivan", email="ivan@example.com")
|
|
106
|
+
# WHEN trying to delete the user
|
|
107
|
+
# THEN an UnknownEntityError is raised
|
|
108
|
+
with pytest.raises(UnknownEntityError):
|
|
109
|
+
await mongo_db.delete(User, user.identifier)
|