ic-python-db 0.7.1__py3-none-any.whl
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.
- ic_python_db/__init__.py +41 -0
- ic_python_db/_cdk.py +14 -0
- ic_python_db/constants.py +6 -0
- ic_python_db/context.py +37 -0
- ic_python_db/db_engine.py +335 -0
- ic_python_db/entity.py +941 -0
- ic_python_db/hooks.py +51 -0
- ic_python_db/mixins.py +65 -0
- ic_python_db/properties.py +639 -0
- ic_python_db/py.typed +0 -0
- ic_python_db/storage.py +69 -0
- ic_python_db/system_time.py +88 -0
- ic_python_db-0.7.1.dist-info/METADATA +356 -0
- ic_python_db-0.7.1.dist-info/RECORD +17 -0
- ic_python_db-0.7.1.dist-info/WHEEL +5 -0
- ic_python_db-0.7.1.dist-info/licenses/LICENSE +21 -0
- ic_python_db-0.7.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
"""Property definitions for Entity classes."""
|
|
2
|
+
|
|
3
|
+
from typing import (
|
|
4
|
+
TYPE_CHECKING,
|
|
5
|
+
Any,
|
|
6
|
+
Callable,
|
|
7
|
+
Generic,
|
|
8
|
+
Iterator,
|
|
9
|
+
List,
|
|
10
|
+
Optional,
|
|
11
|
+
Type,
|
|
12
|
+
TypeVar,
|
|
13
|
+
Union,
|
|
14
|
+
overload,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .entity import Entity
|
|
19
|
+
|
|
20
|
+
# TypeVar for property value types
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
# TypeVar for entity types in relations
|
|
23
|
+
E = TypeVar("E", bound="Entity")
|
|
24
|
+
|
|
25
|
+
# Prefix used for storing property values in entity __dict__
|
|
26
|
+
PROPERTY_STORAGE_PREFIX = "prop"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Property(Generic[T]):
|
|
30
|
+
"""Definition of an entity property.
|
|
31
|
+
|
|
32
|
+
A generic descriptor class that provides type-safe property access.
|
|
33
|
+
The type parameter T indicates the type of value this property holds.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
name: Name of the property
|
|
37
|
+
type: Type of the property (e.g. str, int)
|
|
38
|
+
default: Default value if not set
|
|
39
|
+
validator: Optional function to validate values
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
name: str
|
|
43
|
+
type: Type[T]
|
|
44
|
+
default: Optional[T]
|
|
45
|
+
validator: Optional[Callable[[T], bool]]
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
name: str = "",
|
|
50
|
+
type: Type[T] = type(None), # type: ignore[assignment]
|
|
51
|
+
default: Optional[T] = None,
|
|
52
|
+
validator: Optional[Callable[[T], bool]] = None,
|
|
53
|
+
):
|
|
54
|
+
self.name = name
|
|
55
|
+
self.type = type
|
|
56
|
+
self.default = default
|
|
57
|
+
self.validator = validator
|
|
58
|
+
|
|
59
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
60
|
+
"""Set the property name when class is created."""
|
|
61
|
+
self.name = name
|
|
62
|
+
|
|
63
|
+
@overload
|
|
64
|
+
def __get__(self, obj: None, objtype: Optional[type]) -> "Property[T]": ...
|
|
65
|
+
|
|
66
|
+
@overload
|
|
67
|
+
def __get__(self, obj: object, objtype: Optional[type]) -> Optional[T]: ...
|
|
68
|
+
|
|
69
|
+
def __get__(
|
|
70
|
+
self, obj: object, objtype: Optional[type] = None
|
|
71
|
+
) -> Union["Property[T]", Optional[T]]:
|
|
72
|
+
"""Get the property value."""
|
|
73
|
+
if obj is None:
|
|
74
|
+
return self
|
|
75
|
+
return obj.__dict__.get(f"_{PROPERTY_STORAGE_PREFIX}_{self.name}", self.default)
|
|
76
|
+
|
|
77
|
+
def __set__(self, obj, value):
|
|
78
|
+
"""Set the property value with type checking and validation."""
|
|
79
|
+
from .constants import ACTION_CREATE, ACTION_MODIFY
|
|
80
|
+
from .hooks import call_entity_hook
|
|
81
|
+
|
|
82
|
+
# Get old value and determine action
|
|
83
|
+
old_value = obj.__dict__.get(
|
|
84
|
+
f"_{PROPERTY_STORAGE_PREFIX}_{self.name}", self.default
|
|
85
|
+
)
|
|
86
|
+
action = (
|
|
87
|
+
ACTION_CREATE
|
|
88
|
+
if not hasattr(obj, "_loaded") or not obj._loaded
|
|
89
|
+
else ACTION_MODIFY
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Call hook before setting
|
|
93
|
+
allow, modified_value = call_entity_hook(
|
|
94
|
+
obj, self.name, old_value, value, action
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if not allow:
|
|
98
|
+
raise ValueError(f"Hook rejected change to {self.name}")
|
|
99
|
+
|
|
100
|
+
value = modified_value
|
|
101
|
+
|
|
102
|
+
if value is not None:
|
|
103
|
+
if not isinstance(value, self.type):
|
|
104
|
+
if isinstance(value, str) and self.type in (int, float, bool):
|
|
105
|
+
try:
|
|
106
|
+
if self.type == bool:
|
|
107
|
+
value = value.lower() in ("true", "1", "yes", "on")
|
|
108
|
+
else:
|
|
109
|
+
value = self.type(value)
|
|
110
|
+
except (ValueError, TypeError):
|
|
111
|
+
raise TypeError(
|
|
112
|
+
f"{self.name} must be of type {self.type.__name__}"
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
raise TypeError(f"{self.name} must be of type {self.type.__name__}")
|
|
116
|
+
|
|
117
|
+
if self.validator and not self.validator(value):
|
|
118
|
+
raise ValueError(f"Invalid value for {self.name}: {value}")
|
|
119
|
+
|
|
120
|
+
obj.__dict__[f"_{PROPERTY_STORAGE_PREFIX}_{self.name}"] = value
|
|
121
|
+
obj._save()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class String(Property[str]):
|
|
125
|
+
"""String property with optional length validation."""
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
min_length: Optional[int] = None,
|
|
130
|
+
max_length: Optional[int] = None,
|
|
131
|
+
default: Optional[str] = None,
|
|
132
|
+
):
|
|
133
|
+
def validator(value: str) -> bool:
|
|
134
|
+
if min_length is not None and len(value) < min_length:
|
|
135
|
+
return False
|
|
136
|
+
if max_length is not None and len(value) > max_length:
|
|
137
|
+
return False
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
super().__init__(name="", type=str, default=default, validator=validator)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class Integer(Property[int]):
|
|
144
|
+
"""Integer property with optional range validation."""
|
|
145
|
+
|
|
146
|
+
def __init__(
|
|
147
|
+
self,
|
|
148
|
+
min_value: Optional[int] = None,
|
|
149
|
+
max_value: Optional[int] = None,
|
|
150
|
+
default: Optional[int] = None,
|
|
151
|
+
):
|
|
152
|
+
def validator(value: int) -> bool:
|
|
153
|
+
if min_value is not None and value < min_value:
|
|
154
|
+
return False
|
|
155
|
+
if max_value is not None and value > max_value:
|
|
156
|
+
return False
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
super().__init__(name="", type=int, default=default, validator=validator)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class Float(Property[float]):
|
|
163
|
+
"""Float property with optional range validation."""
|
|
164
|
+
|
|
165
|
+
def __init__(
|
|
166
|
+
self,
|
|
167
|
+
min_value: Optional[float] = None,
|
|
168
|
+
max_value: Optional[float] = None,
|
|
169
|
+
default: Optional[float] = None,
|
|
170
|
+
):
|
|
171
|
+
def validator(value: float) -> bool:
|
|
172
|
+
if min_value is not None and value < min_value:
|
|
173
|
+
return False
|
|
174
|
+
if max_value is not None and value > max_value:
|
|
175
|
+
return False
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
super().__init__(name="", type=float, default=default, validator=validator)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class Boolean(Property[bool]):
|
|
182
|
+
"""Boolean property."""
|
|
183
|
+
|
|
184
|
+
def __init__(self, default: Optional[bool] = None):
|
|
185
|
+
super().__init__(name="", type=bool, default=default)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class Relation(Generic[E]):
|
|
189
|
+
"""Base property for defining and accessing relations.
|
|
190
|
+
|
|
191
|
+
This is the base class for all relation properties. It provides common functionality
|
|
192
|
+
for managing relationships between entities.
|
|
193
|
+
|
|
194
|
+
For one-to-one relationships (default), this property returns a single entity and
|
|
195
|
+
enforces single cardinality. For one-to-many or many-to-many relationships, it
|
|
196
|
+
returns a list of entities.
|
|
197
|
+
|
|
198
|
+
The type parameter E indicates the type of related entity.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
name: Optional[str]
|
|
202
|
+
entity_types: Union[str, List[str]]
|
|
203
|
+
reverse_name: Optional[str]
|
|
204
|
+
many: bool
|
|
205
|
+
|
|
206
|
+
def __init__(
|
|
207
|
+
self,
|
|
208
|
+
entity_types: Union[str, List[str]],
|
|
209
|
+
reverse_name: Optional[str] = None,
|
|
210
|
+
many: bool = False,
|
|
211
|
+
):
|
|
212
|
+
"""Initialize relation property.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
entity_types: Type names of related entities
|
|
216
|
+
reverse_name: Optional name for reverse relation
|
|
217
|
+
many: Whether this property can hold multiple entities (default: False)
|
|
218
|
+
"""
|
|
219
|
+
self.entity_types = entity_types
|
|
220
|
+
self.name = None
|
|
221
|
+
self.reverse_name = reverse_name
|
|
222
|
+
self.many = many
|
|
223
|
+
|
|
224
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
225
|
+
"""Set the property name when class is created."""
|
|
226
|
+
self.name = name
|
|
227
|
+
if self.reverse_name is None:
|
|
228
|
+
self.reverse_name = name
|
|
229
|
+
|
|
230
|
+
@overload
|
|
231
|
+
def __get__(self, obj: None, objtype: Optional[type]) -> "Relation[E]": ...
|
|
232
|
+
|
|
233
|
+
@overload
|
|
234
|
+
def __get__(
|
|
235
|
+
self, obj: object, objtype: Optional[type]
|
|
236
|
+
) -> Union[Optional[E], List[E]]: ...
|
|
237
|
+
|
|
238
|
+
def __get__(
|
|
239
|
+
self, obj: object, objtype: Optional[type] = None
|
|
240
|
+
) -> Union["Relation[E]", Optional[E], List[E]]:
|
|
241
|
+
"""Get related entities."""
|
|
242
|
+
if obj is None:
|
|
243
|
+
return self
|
|
244
|
+
relations = obj.get_relations(self.name)
|
|
245
|
+
if not self.many:
|
|
246
|
+
# For single relationships, return the first entity or None
|
|
247
|
+
return relations[0] if relations else None
|
|
248
|
+
return relations
|
|
249
|
+
|
|
250
|
+
def __set__(self, obj, value):
|
|
251
|
+
"""Set related entities.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
value: For many=False: a single Entity instance
|
|
255
|
+
For many=True: a list/tuple of Entity instances
|
|
256
|
+
"""
|
|
257
|
+
if self.many:
|
|
258
|
+
if not isinstance(value, (list, tuple)):
|
|
259
|
+
raise TypeError(f"{self.name} requires a list or tuple of entities")
|
|
260
|
+
values_list = value
|
|
261
|
+
else:
|
|
262
|
+
values_list = [value]
|
|
263
|
+
|
|
264
|
+
# Get existing and new relations as sets
|
|
265
|
+
existing = set(obj.get_relations(self.name))
|
|
266
|
+
new = set(values_list)
|
|
267
|
+
|
|
268
|
+
# For one-to-many, check if entities have existing relations
|
|
269
|
+
if isinstance(self, OneToMany):
|
|
270
|
+
for entity in new:
|
|
271
|
+
existing_relations = entity.get_relations(self.reverse_name)
|
|
272
|
+
if existing_relations:
|
|
273
|
+
old_relation = existing_relations[0]
|
|
274
|
+
if old_relation != obj:
|
|
275
|
+
# Remove the entity from the old relation's list
|
|
276
|
+
old_relation._relations[self.name].remove(entity)
|
|
277
|
+
# Remove the old relation from the entity's list
|
|
278
|
+
entity._relations[self.reverse_name].remove(old_relation)
|
|
279
|
+
|
|
280
|
+
# Remove relations that are not in new set
|
|
281
|
+
to_remove = existing - new
|
|
282
|
+
for entity in to_remove:
|
|
283
|
+
obj.remove_relation(self.name, self.reverse_name, entity)
|
|
284
|
+
|
|
285
|
+
# Add relations that are not in existing set
|
|
286
|
+
to_add = new - existing
|
|
287
|
+
for entity in to_add:
|
|
288
|
+
obj.add_relation(self.name, self.reverse_name, entity)
|
|
289
|
+
|
|
290
|
+
def validate_entity(self, entity: Any) -> bool:
|
|
291
|
+
"""Validate that an entity is of the correct type.
|
|
292
|
+
|
|
293
|
+
Handles both namespaced (e.g., "app::User") and non-namespaced (e.g., "User") types.
|
|
294
|
+
"""
|
|
295
|
+
from .entity import Entity
|
|
296
|
+
|
|
297
|
+
if entity is None:
|
|
298
|
+
return True
|
|
299
|
+
if not isinstance(entity, Entity):
|
|
300
|
+
raise TypeError(f"{self.name} must be set to Entity instances")
|
|
301
|
+
|
|
302
|
+
# Convert entity_types to list for uniform handling
|
|
303
|
+
allowed_types = (
|
|
304
|
+
[self.entity_types]
|
|
305
|
+
if isinstance(self.entity_types, str)
|
|
306
|
+
else self.entity_types
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# Check if entity type matches any allowed type
|
|
310
|
+
# This handles both exact matches and namespace variations
|
|
311
|
+
if entity._type not in allowed_types:
|
|
312
|
+
# Also check if the class name matches (for backward compatibility)
|
|
313
|
+
entity_class_name = (
|
|
314
|
+
entity._type.split("::")[-1] if "::" in entity._type else entity._type
|
|
315
|
+
)
|
|
316
|
+
type_matches = any(
|
|
317
|
+
entity_class_name == (t.split("::")[-1] if "::" in t else t)
|
|
318
|
+
for t in allowed_types
|
|
319
|
+
)
|
|
320
|
+
if not type_matches:
|
|
321
|
+
raise TypeError(
|
|
322
|
+
f"{self.name} must be set an Entity instance of any of the following types: {self.entity_types}, "
|
|
323
|
+
f"but got type '{entity._type}'"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
return True
|
|
327
|
+
|
|
328
|
+
def resolve_entity(self, obj: Any, value: Any) -> Optional[E]:
|
|
329
|
+
"""Resolve a value to an Entity instance.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
obj: The entity object that owns this relation
|
|
333
|
+
value: Can be an Entity instance, string ID, or string name/alias
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Entity instance or None
|
|
337
|
+
"""
|
|
338
|
+
from .entity import Entity
|
|
339
|
+
|
|
340
|
+
if value is None:
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
if isinstance(value, Entity):
|
|
344
|
+
return value # type: ignore[return-value]
|
|
345
|
+
|
|
346
|
+
if isinstance(value, (str, int)):
|
|
347
|
+
# Try to find entity by ID or name (alias) using each allowed entity type
|
|
348
|
+
entity_types = (
|
|
349
|
+
[self.entity_types]
|
|
350
|
+
if isinstance(self.entity_types, str)
|
|
351
|
+
else self.entity_types
|
|
352
|
+
)
|
|
353
|
+
for entity_type_name in entity_types:
|
|
354
|
+
# Get the entity class from the database registry
|
|
355
|
+
entity_class = obj.db()._entity_types.get(entity_type_name)
|
|
356
|
+
|
|
357
|
+
if entity_class:
|
|
358
|
+
found_entity = entity_class[value]
|
|
359
|
+
if found_entity:
|
|
360
|
+
return found_entity
|
|
361
|
+
|
|
362
|
+
raise ValueError(
|
|
363
|
+
f"No entity of types {self.entity_types} found with ID or name '{value}'"
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
raise TypeError(
|
|
367
|
+
f"{self.name} must be set to an Entity instance, string ID, or string name"
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class RelationList(Generic[E]):
|
|
372
|
+
"""Helper class for managing lists of related entities."""
|
|
373
|
+
|
|
374
|
+
def __init__(self, obj: Any, prop: "Relation[E]"):
|
|
375
|
+
self.obj = obj
|
|
376
|
+
self.prop = prop
|
|
377
|
+
|
|
378
|
+
def add(self, entity: Union[E, str, int]) -> None:
|
|
379
|
+
"""Add a new relation."""
|
|
380
|
+
# Resolve entity (supports string ID/name)
|
|
381
|
+
resolved = self.prop.resolve_entity(self.obj, entity)
|
|
382
|
+
|
|
383
|
+
# Validate entity type using the base validate_entity method
|
|
384
|
+
self.prop.validate_entity(resolved)
|
|
385
|
+
|
|
386
|
+
# For one-to-many, check if entity already has a relation
|
|
387
|
+
if isinstance(self.prop, OneToMany):
|
|
388
|
+
existing_relations = resolved.get_relations(self.prop.reverse_name)
|
|
389
|
+
if existing_relations:
|
|
390
|
+
# Remove existing relation since it's one-to-many
|
|
391
|
+
old_relation = existing_relations[0]
|
|
392
|
+
# Remove the entity from the old relation's list
|
|
393
|
+
if old_relation != self.obj:
|
|
394
|
+
old_relation._relations[self.prop.name].remove(resolved)
|
|
395
|
+
# Remove the old relation from the entity's list
|
|
396
|
+
resolved._relations[self.prop.reverse_name].remove(old_relation)
|
|
397
|
+
|
|
398
|
+
self.obj.add_relation(self.prop.name, self.prop.reverse_name, resolved)
|
|
399
|
+
|
|
400
|
+
def remove(self, entity: Union[E, str, int]) -> None:
|
|
401
|
+
"""Remove a relation."""
|
|
402
|
+
self.obj.remove_relation(self.prop.name, self.prop.reverse_name, entity)
|
|
403
|
+
|
|
404
|
+
def __iter__(self) -> "Iterator[E]":
|
|
405
|
+
return iter(self.obj.get_relations(self.prop.name))
|
|
406
|
+
|
|
407
|
+
def __len__(self) -> int:
|
|
408
|
+
return len(self.obj.get_relations(self.prop.name))
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class OneToOne(Relation[E]):
|
|
412
|
+
"""Property for defining one-to-one relationships.
|
|
413
|
+
|
|
414
|
+
This property type represents a one-to-one relationship where each entity
|
|
415
|
+
can be related to exactly one entity on the other side.
|
|
416
|
+
|
|
417
|
+
Example:
|
|
418
|
+
class Person(Entity):
|
|
419
|
+
profile = OneToOne('Profile', 'person')
|
|
420
|
+
|
|
421
|
+
class Profile(Entity):
|
|
422
|
+
person = OneToOne('Person', 'profile')
|
|
423
|
+
"""
|
|
424
|
+
|
|
425
|
+
def __init__(
|
|
426
|
+
self,
|
|
427
|
+
entity_types: Union[str, List[str]],
|
|
428
|
+
reverse_name: Optional[str] = None,
|
|
429
|
+
):
|
|
430
|
+
super().__init__(entity_types, reverse_name, many=False)
|
|
431
|
+
|
|
432
|
+
def __set__(self, obj, value):
|
|
433
|
+
"""Set the related entity with one-to-one constraints."""
|
|
434
|
+
if value is not None:
|
|
435
|
+
# Check if trying to set multiple values
|
|
436
|
+
if isinstance(value, (list, tuple)):
|
|
437
|
+
raise ValueError(
|
|
438
|
+
f"{self.name} cannot be set to multiple values (one-to-one relationship)"
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# Validate entity type
|
|
442
|
+
value = self.resolve_entity(obj, value)
|
|
443
|
+
|
|
444
|
+
# Check that the reverse property is OneToOne
|
|
445
|
+
reverse_prop = value.__class__.__dict__.get(self.reverse_name)
|
|
446
|
+
if not isinstance(reverse_prop, OneToOne):
|
|
447
|
+
raise ValueError(
|
|
448
|
+
f"Reverse property '{self.reverse_name}' must be OneToOne"
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Get current value if any
|
|
452
|
+
current = self.__get__(obj)
|
|
453
|
+
if current is not None:
|
|
454
|
+
# Remove existing relation
|
|
455
|
+
obj.remove_relation(self.name, self.reverse_name, current)
|
|
456
|
+
|
|
457
|
+
# Check if value is already related to another entity and remove that relation
|
|
458
|
+
existing = value.get_relations(self.reverse_name)
|
|
459
|
+
if existing:
|
|
460
|
+
existing_entity = existing[0]
|
|
461
|
+
# Remove the existing relation from both sides
|
|
462
|
+
existing_entity.remove_relation(self.name, self.reverse_name, value)
|
|
463
|
+
value.remove_relation(self.reverse_name, self.name, existing_entity)
|
|
464
|
+
|
|
465
|
+
# Set the new relation
|
|
466
|
+
super().__set__(obj, value)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
class OneToMany(Relation[E]):
|
|
470
|
+
"""Property for defining one-to-many relationships.
|
|
471
|
+
|
|
472
|
+
This property type represents a one-to-many relationship where the 'one' side
|
|
473
|
+
owns multiple instances of the 'many' side, but each 'many' instance belongs to
|
|
474
|
+
only one owner.
|
|
475
|
+
|
|
476
|
+
Example:
|
|
477
|
+
class Department(Entity):
|
|
478
|
+
employees = OneToMany('Employee', 'department')
|
|
479
|
+
|
|
480
|
+
class Employee(Entity):
|
|
481
|
+
department = ManyToOne('Department', 'employees')
|
|
482
|
+
"""
|
|
483
|
+
|
|
484
|
+
def __init__(self, entity_types: Union[str, List[str]], reverse_name: str):
|
|
485
|
+
super().__init__(entity_types, reverse_name, many=True)
|
|
486
|
+
|
|
487
|
+
def __set__(self, obj, values):
|
|
488
|
+
"""Set related entities with one-to-many constraints."""
|
|
489
|
+
if not isinstance(values, (list, tuple)):
|
|
490
|
+
raise TypeError(f"{self.name} must be set to a list of entities")
|
|
491
|
+
|
|
492
|
+
resolved_values = []
|
|
493
|
+
for value in values:
|
|
494
|
+
# Resolve value to Entity instance
|
|
495
|
+
resolved_value = self.resolve_entity(obj, value)
|
|
496
|
+
|
|
497
|
+
# Validate entity type using the base validate_entity method
|
|
498
|
+
self.validate_entity(resolved_value)
|
|
499
|
+
resolved_values.append(resolved_value)
|
|
500
|
+
|
|
501
|
+
# Check that the reverse property is ManyToOne
|
|
502
|
+
reverse_prop = resolved_value.__class__.__dict__.get(self.reverse_name)
|
|
503
|
+
if not isinstance(reverse_prop, ManyToOne):
|
|
504
|
+
raise ValueError(
|
|
505
|
+
f"Reverse property '{self.reverse_name}' must be ManyToOne"
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
# Replace original values with resolved entities
|
|
509
|
+
super().__set__(obj, resolved_values)
|
|
510
|
+
|
|
511
|
+
@overload
|
|
512
|
+
def __get__(self, obj: None, objtype: Optional[type]) -> "OneToMany[E]": ...
|
|
513
|
+
|
|
514
|
+
@overload
|
|
515
|
+
def __get__(self, obj: object, objtype: Optional[type]) -> "RelationList[E]": ...
|
|
516
|
+
|
|
517
|
+
def __get__(
|
|
518
|
+
self, obj: object, objtype: Optional[type] = None
|
|
519
|
+
) -> Union["OneToMany[E]", "RelationList[E]"]:
|
|
520
|
+
"""Get related entities as a RelationList."""
|
|
521
|
+
if obj is None:
|
|
522
|
+
return self
|
|
523
|
+
return RelationList(obj, self)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
class ManyToOne(Relation[E]):
|
|
527
|
+
"""Property for defining many-to-one relationships.
|
|
528
|
+
|
|
529
|
+
This property type represents a many-to-one relationship where multiple entities
|
|
530
|
+
can belong to a single owner entity.
|
|
531
|
+
|
|
532
|
+
Example:
|
|
533
|
+
class Employee(Entity):
|
|
534
|
+
department = ManyToOne('Department', 'employees')
|
|
535
|
+
|
|
536
|
+
class Department(Entity):
|
|
537
|
+
employees = OneToMany('Employee', 'department')
|
|
538
|
+
"""
|
|
539
|
+
|
|
540
|
+
def __init__(
|
|
541
|
+
self, entity_types: Union[str, List[str]], reverse_name: Optional[str] = None
|
|
542
|
+
):
|
|
543
|
+
super().__init__(entity_types, reverse_name, many=False)
|
|
544
|
+
|
|
545
|
+
def __set__(self, obj, value):
|
|
546
|
+
"""Set the related entity with many-to-one constraints."""
|
|
547
|
+
if value is not None:
|
|
548
|
+
# Check if trying to set multiple values
|
|
549
|
+
if isinstance(value, (list, tuple)):
|
|
550
|
+
raise ValueError(
|
|
551
|
+
f"{self.name} cannot be set to multiple values (many-to-one relationship)"
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
# Resolve value to Entity instance
|
|
555
|
+
value = self.resolve_entity(obj, value)
|
|
556
|
+
|
|
557
|
+
# Validate entity type using the base validate_entity method
|
|
558
|
+
self.validate_entity(value)
|
|
559
|
+
|
|
560
|
+
# Check that the reverse property is OneToMany
|
|
561
|
+
reverse_prop = value.__class__.__dict__.get(self.reverse_name)
|
|
562
|
+
if not reverse_prop:
|
|
563
|
+
raise ValueError(
|
|
564
|
+
f"Reverse property '{self.reverse_name}' not found in {value.__class__.__name__} entity"
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
if not isinstance(reverse_prop, OneToMany):
|
|
568
|
+
raise ValueError(
|
|
569
|
+
f"Reverse property '{self.reverse_name}' must be OneToMany and it is '{reverse_prop.__class__.__name__}'"
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
super().__set__(obj, value)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
class ManyToMany(Relation[E]):
|
|
576
|
+
"""Property for defining many-to-many relationships.
|
|
577
|
+
|
|
578
|
+
This property type represents a many-to-many relationship where entities
|
|
579
|
+
on both sides can be related to multiple entities on the other side.
|
|
580
|
+
|
|
581
|
+
Example:
|
|
582
|
+
class Student(Entity):
|
|
583
|
+
courses = ManyToMany('Course', 'students')
|
|
584
|
+
|
|
585
|
+
class Course(Entity):
|
|
586
|
+
students = ManyToMany('Student', 'courses')
|
|
587
|
+
"""
|
|
588
|
+
|
|
589
|
+
def __init__(self, entity_types: Union[str, List[str]], reverse_name: str):
|
|
590
|
+
super().__init__(entity_types, reverse_name, many=True)
|
|
591
|
+
|
|
592
|
+
def __set__(self, obj, values):
|
|
593
|
+
"""Set related entities with many-to-many constraints."""
|
|
594
|
+
from .entity import Entity
|
|
595
|
+
|
|
596
|
+
# Convert single entity to list for convenience
|
|
597
|
+
if isinstance(values, Entity):
|
|
598
|
+
values = [values]
|
|
599
|
+
elif isinstance(values, (str, int)):
|
|
600
|
+
# Handle single string ID/name
|
|
601
|
+
values = [values]
|
|
602
|
+
elif values is not None and not isinstance(values, (list, tuple)):
|
|
603
|
+
raise TypeError(f"{self.name} must be set to an entity or list of entities")
|
|
604
|
+
|
|
605
|
+
if values is not None:
|
|
606
|
+
resolved_values = []
|
|
607
|
+
for value in values:
|
|
608
|
+
# Resolve value to Entity instance
|
|
609
|
+
resolved_value = self.resolve_entity(obj, value)
|
|
610
|
+
|
|
611
|
+
# Validate entity type using the base validate_entity method
|
|
612
|
+
self.validate_entity(resolved_value)
|
|
613
|
+
resolved_values.append(resolved_value)
|
|
614
|
+
|
|
615
|
+
# Check that the reverse property is ManyToMany
|
|
616
|
+
reverse_prop = resolved_value.__class__.__dict__.get(self.reverse_name)
|
|
617
|
+
if not isinstance(reverse_prop, ManyToMany):
|
|
618
|
+
raise ValueError(
|
|
619
|
+
f"Reverse property '{self.reverse_name}' must be ManyToMany"
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
# Replace original values with resolved entities
|
|
623
|
+
values = resolved_values
|
|
624
|
+
|
|
625
|
+
super().__set__(obj, values)
|
|
626
|
+
|
|
627
|
+
@overload
|
|
628
|
+
def __get__(self, obj: None, objtype: Optional[type]) -> "ManyToMany[E]": ...
|
|
629
|
+
|
|
630
|
+
@overload
|
|
631
|
+
def __get__(self, obj: object, objtype: Optional[type]) -> "RelationList[E]": ...
|
|
632
|
+
|
|
633
|
+
def __get__(
|
|
634
|
+
self, obj: object, objtype: Optional[type] = None
|
|
635
|
+
) -> Union["ManyToMany[E]", "RelationList[E]"]:
|
|
636
|
+
"""Get related entities as a RelationList."""
|
|
637
|
+
if obj is None:
|
|
638
|
+
return self
|
|
639
|
+
return RelationList(obj, self)
|
ic_python_db/py.typed
ADDED
|
File without changes
|
ic_python_db/storage.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Storage backends for IC Python DB
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Dict, Iterator, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Storage(ABC):
|
|
10
|
+
"""Abstract base class for storage backends"""
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def insert(self, key: str, value: str) -> None:
|
|
14
|
+
"""Insert a key-value pair into storage"""
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def get(self, key: str) -> Optional[str]:
|
|
19
|
+
"""Retrieve a value by key"""
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def remove(self, key: str) -> None:
|
|
24
|
+
"""Remove a key-value pair from storage"""
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def items(self) -> Iterator[Tuple[str, str]]:
|
|
29
|
+
"""Return all items in storage"""
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def __contains__(self, key: str) -> bool:
|
|
34
|
+
"""Check if key exists in storage"""
|
|
35
|
+
raise NotImplementedError
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def keys(self) -> Iterator[str]:
|
|
39
|
+
"""Return all keys in storage"""
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MemoryStorage(Storage):
|
|
44
|
+
"""In-memory storage implementation using Python dictionary"""
|
|
45
|
+
|
|
46
|
+
def __init__(self):
|
|
47
|
+
self._data: Dict[str, str] = {}
|
|
48
|
+
|
|
49
|
+
def insert(self, key: str, value: str) -> None:
|
|
50
|
+
self._data[key] = value
|
|
51
|
+
|
|
52
|
+
def get(self, key: str) -> Optional[str]:
|
|
53
|
+
return self._data.get(key)
|
|
54
|
+
|
|
55
|
+
def remove(self, key: str) -> None:
|
|
56
|
+
if key in self._data:
|
|
57
|
+
del self._data[key]
|
|
58
|
+
else:
|
|
59
|
+
raise KeyError(f"Key '{key}' not found in storage")
|
|
60
|
+
|
|
61
|
+
def items(self) -> Iterator[Tuple[str, str]]:
|
|
62
|
+
return iter(self._data.items())
|
|
63
|
+
|
|
64
|
+
def __contains__(self, key: str) -> bool:
|
|
65
|
+
return key in self._data
|
|
66
|
+
|
|
67
|
+
def keys(self) -> Iterator[str]:
|
|
68
|
+
"""Return all keys in storage"""
|
|
69
|
+
return iter(self._data.keys())
|