Skip to content

Commit f5b5b40

Browse files
committed
Allow virtual entities to delete references as well and improvements.
1 parent 38b2fe6 commit f5b5b40

File tree

9 files changed

+166
-43
lines changed

9 files changed

+166
-43
lines changed

src/examples/embed_props.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class Student(TEntity):
2222

2323
student_id = props.string(required=True)
2424
first_name = props.string(required=True)
25-
last_name = props.optional(props.string())
25+
last_name = props.string().optional()
2626
grades = props.array(Grade)
2727

2828
def __init__(

src/examples/engine_configs_1.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import asyncio
2+
from typing import Optional
3+
4+
from sjd import Engine, TEntity, properties as props
5+
6+
7+
class _Days(TEntity):
8+
employee_id = props.reference()
9+
10+
def __init__(self, days: int) -> None:
11+
self.days = days
12+
13+
14+
@props.auto_collect()
15+
class RestInfo(_Days):
16+
pass
17+
18+
19+
@props.auto_collect()
20+
class StandbyInfo(_Days):
21+
pass
22+
23+
24+
@props.auto_collect() # This won't override attributes you've already defined ( rest_info & standby_info here ).
25+
class Employee(TEntity):
26+
27+
rest_info = props.from_entity(RestInfo, "employee_id")
28+
standby_info = props.from_entity(StandbyInfo, "employee_id")
29+
30+
def __init__(
31+
self,
32+
employee_id: int,
33+
first_name: str,
34+
last_name: str,
35+
rest_info: Optional[RestInfo] = None,
36+
standby_info: Optional[StandbyInfo] = None,
37+
) -> None:
38+
self.employee_id = employee_id
39+
self.first_name = first_name
40+
self.last_name = last_name
41+
self.rest_info = rest_info
42+
self.standby_info = standby_info
43+
44+
45+
class AppEngine(Engine):
46+
47+
employees = Engine.set(Employee)
48+
# No need to add rest_info & standby_info collections here. but you can.
49+
# rest_info = Engine.set(RestInfo)
50+
# standby_info = Engine.set(StandbyInfo)
51+
52+
def __init__(self):
53+
super().__init__("__test_db__")
54+
55+
# Say welcome to lambda hell ...
56+
self._configs.config_collection(
57+
lambda engine: engine.employees,
58+
lambda collection: collection.config_property(
59+
lambda employee: employee.rest_info,
60+
lambda config: config.delete_whole_reference(),
61+
).config_property(
62+
lambda employee: employee.standby_info,
63+
lambda config: config.delete_reference_prop(),
64+
),
65+
),
66+
67+
68+
async def main():
69+
engine = AppEngine()
70+
employees_col = engine.employees
71+
72+
await employees_col.add(Employee(1, "John", "Doe", RestInfo(5), StandbyInfo(5)))
73+
74+
async for item in employees_col:
75+
await employees_col.delete(item)
76+
# Due to engine configs, this'll delete the rest_info entity too.
77+
# But the standby_info entity will not be deleted entirely only StandbyInfo.employee_id will be None.
78+
79+
80+
if __name__ == "__main__":
81+
asyncio.run(main())

src/sjd/database/_collection.py

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,6 @@ async def _manage_referral_properties(self, entity: TEntity):
195195

196196
# get a collection for reference type
197197
col = self._engine.get_collection(prop.type_of_entity) # type: ignore
198-
if col is None:
199-
raise ValueError(
200-
f"No collection for referenced type {prop.type_of_entity} found" # type: ignore
201-
)
202198

203199
await col.add(value) # type: ignore
204200
delattr(entity, prop.actual_name)
@@ -288,6 +284,34 @@ async def _update_async(
288284
else:
289285
os.remove(tmp_path.absolute())
290286

287+
async def _care_about_virtual_props(self, entity: TEntity):
288+
col_config = self.configuration
289+
if col_config is None:
290+
return
291+
292+
for prop in entity.get_properties():
293+
if isinstance(prop, (VirtualComplexProperty, VirtualListProperty)):
294+
prop_config = col_config.get_property_config(prop.actual_name)
295+
if prop_config is None:
296+
continue
297+
298+
match prop_config.delete_action.value:
299+
case "delete_entity":
300+
entity_col = self._engine.get_collection(prop.type_of_entity)
301+
302+
async for item in self.iter_referenced_by(entity, lambda _: prop): # type: ignore
303+
await entity_col.delete(item)
304+
305+
case "delete_reference":
306+
entity_col = self._engine.get_collection(prop.type_of_entity)
307+
308+
async for item in self.iter_referenced_by(entity, lambda _: prop): # type: ignore
309+
setattr(item, prop.refers_to, None)
310+
await entity_col.update(item)
311+
312+
case "ignore":
313+
continue
314+
291315
@final
292316
async def _update_entity_async(self, entity: T, delete: bool = False) -> None:
293317

@@ -319,6 +343,9 @@ async def _update_entity_async(self, entity: T, delete: bool = False) -> None:
319343
if modified:
320344
os.remove(file_path.absolute())
321345
os.rename(tmp_path.absolute(), file_path.absolute())
346+
347+
if delete:
348+
await self._care_about_virtual_props(entity) # type: ignore
322349
else:
323350
os.remove(tmp_path.absolute())
324351

@@ -520,10 +547,6 @@ async def load_virtual_props(self, entity: T, *props: str):
520547
except_one = isinstance(prop, VirtualComplexProperty)
521548
# get a collection for reference type
522549
col = self._engine.get_collection(prop.type_of_entity) # type: ignore
523-
if col is None:
524-
raise ValueError(
525-
f"No collection for referenced type {prop.type_of_entity} found" # type: ignore
526-
)
527550

528551
# TODO: Is this performance wise ?
529552
results: list[Any] = []
@@ -554,10 +577,7 @@ async def iter_referenced_by(
554577
if isinstance(prop, (VirtualComplexProperty, VirtualListProperty)):
555578
# get a collection for reference type
556579
col = self._engine.get_collection(prop.type_of_entity)
557-
if col is None:
558-
raise ValueError(
559-
f"No collection for referenced type {prop.type_of_entity} found"
560-
)
580+
561581
async for item in col.iterate_by(prop.refers_to, entity.id):
562582
yield item # type: ignore
563583
else:

src/sjd/database/_configuration.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,25 @@
1414

1515

1616
class DeleteAction(Enum):
17-
DELETE_VIRTUAL = "delete"
17+
DELETE_ENTITY = "delete_entity"
1818
""" Deletes all virtual entities associated with the entity. """
1919
DELETE_REFERENCE = "delete_reference"
2020
""" Removes the reference property from the virtual entities. """
2121
IGNORE = "ignore"
2222
""" Ignores the virtual entities. """
2323

24+
@property
25+
def ignore(self) -> bool:
26+
return self == DeleteAction.IGNORE
27+
28+
@property
29+
def delete_reference(self) -> bool:
30+
return self == DeleteAction.DELETE_REFERENCE
31+
32+
@property
33+
def delete_entity(self) -> bool:
34+
return self == DeleteAction.DELETE_ENTITY
35+
2436

2537
@dataclasses.dataclass(repr=True)
2638
class PropertyConfiguration:
@@ -39,6 +51,14 @@ def set_delete_action(self, action: DeleteAction) -> "PropertyConfiguration":
3951
self.delete_action = action
4052
return self
4153

54+
def delete_whole_reference(self) -> "PropertyConfiguration":
55+
"""Indicates if the entity which this property referees to should be deleted when current entity is deleted."""
56+
return self.set_delete_action(DeleteAction.DELETE_ENTITY)
57+
58+
def delete_reference_prop(self) -> "PropertyConfiguration":
59+
"""Indicates if the reference property should be deleted ( from reference entity ) when current entity is deleted."""
60+
return self.set_delete_action(DeleteAction.DELETE_REFERENCE)
61+
4262

4363
@dataclasses.dataclass(repr=True)
4464
class CollectionConfiguration(Generic[T]):

src/sjd/database/_descriptors.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,7 @@ def __get__(
4141
) -> "__Collection__[T]" | AbstractCollection[T]:
4242
if obj is None:
4343
return self
44-
col = cast("Engine", obj).get_collection(self._entity_type)
45-
if col is None:
46-
raise AttributeError(f"Can't get such collection {self._entity_type}")
47-
return col
44+
return cast("Engine", obj).get_collection(self._entity_type)
4845

4946
def __set__(self, obj: object, value: T) -> None:
5047
raise AttributeError("Engine collections are read-only.")
@@ -82,10 +79,6 @@ def __get__(
8279
if obj is None:
8380
return self
8481
t_col = cast("Engine", obj).get_collection(self._entity_type)
85-
if t_col is None:
86-
raise AttributeError(
87-
f"Can't get such typed collection {self._entity_type}, {self._collection_type}"
88-
)
8982
return cast(_TCol, t_col)
9083

9184
def __set__(self, obj: object, value: T) -> None:

src/sjd/database/_engine.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,7 @@ def __getitem__(self, entity_type: type[T]) -> AbstractCollection[T]:
7070
Args:
7171
entity_type (`type[T]`): The entity type of the collection.
7272
"""
73-
if (col := self.get_collection(entity_type)) is not None:
74-
return col
75-
raise KeyError(f"Collection of type {entity_type} is not registered")
73+
return self.get_collection(entity_type)
7674

7775
def __set_collections(self):
7876
for _, col in inspect.getmembers(type(self)):
@@ -111,7 +109,7 @@ def get_base_path(self, collection: AbstractCollection[Any]) -> Path:
111109
return self._base_path
112110

113111
@final
114-
def get_collection(self, entity_type: type[T]) -> Optional[AbstractCollection[T]]:
112+
def get_collection(self, entity_type: type[T]) -> AbstractCollection[T]:
115113
"""Returns the collection of the entity type.
116114
117115
Args:
@@ -126,7 +124,7 @@ def get_collection(self, entity_type: type[T]) -> Optional[AbstractCollection[T]
126124
if not self.__initialized:
127125
raise EngineNotInitialized()
128126
if entity_type not in self.__collections:
129-
return None
127+
self.register_collection(entity_type)
130128
return self.__collections[entity_type]
131129

132130
@final
@@ -158,9 +156,13 @@ def register_collection(
158156
raise CollectionEntityTypeDuplicated(
159157
name or entity_type.__name__, entity_type
160158
)
159+
160+
if entity_type is None or not issubclass(entity_type, TEntity):
161+
raise ValueError("entity_type must be a TEntity.")
162+
161163
col = Collection(self, entity_type, name)
162164
self.__collections[entity_type] = col
163-
return col
165+
return col # type: ignore
164166

165167
def register_typed_collection(
166168
self, collection: type[AbstractCollection[T]]

src/sjd/entity/_property.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,21 @@ def default_factory(self) -> Optional[Callable[[], Optional[T]]]:
139139
def is_virtual(self) -> bool:
140140
"""Whether the property is virtual."""
141141
return getattr(self, "__virtual__", False)
142+
143+
def optional(self):
144+
"""Returns a clone of the property with the required flag set to False and hints as optional."""
145+
146+
if self.required:
147+
raise ValueError(
148+
"Cannot create an optional property from a required property."
149+
)
150+
151+
return TProperty[Optional[T]](
152+
self.type_of_entity,
153+
init=self.init,
154+
required=False,
155+
default_factory=lambda: None,
156+
is_list=self.is_list,
157+
json_property_name=self.json_property_name,
158+
is_complex=self.is_complex,
159+
)

src/sjd/entity/properties/__init__.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -98,21 +98,10 @@ def entity(
9898
)
9999

100100

101-
def optional(property: TProperty[T]) -> OptionalProperty[T]:
101+
def optional(property: TProperty[T]) -> TProperty[Optional[T]]:
102102
if isinstance(property, OptionalProperty):
103103
return property # type: ignore
104-
105-
if property.required:
106-
raise ValueError("Cannot create an optional property from a required property.")
107-
108-
return OptionalProperty(
109-
property.type_of_entity,
110-
init=property.init,
111-
default_factory=lambda: None,
112-
is_list=property.is_list,
113-
json_property_name=property.json_property_name,
114-
is_complex=property.is_complex,
115-
)
104+
return property.optional()
116105

117106

118107
def from_entity(
@@ -146,7 +135,7 @@ def reference():
146135

147136

148137
__all__ = [
149-
"collect_props_from_init",
138+
"auto_collect",
150139
"integer",
151140
"string",
152141
"double",

src/sjd/entity/properties/_property_grabber.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def __grab_props(__init__: Callable[..., Any]):
6767

6868
def auto_collect(
6969
*, method_name: str = "__init__", ignore_params: Optional[list[str]] = None
70-
) -> Callable[..., Any]:
70+
) -> Callable[[type[T]], type[T]]:
7171
"""Automatically collect properties from `__init__` method.
7272
7373
Note that only valid types are allowed.

0 commit comments

Comments
 (0)