Skip to content

Commit dcdfa30

Browse files
committed
Updated readme, requirements and more due to virtual properties
1 parent 3813e16 commit dcdfa30

File tree

6 files changed

+284
-12
lines changed

6 files changed

+284
-12
lines changed

README.md

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,4 +325,165 @@ with_50_score = await collection.as_queryable.where(
325325
print(with_50_score.first_name)
326326
```
327327

328-
_That's all for now, check `Collection` object for more ..._
328+
### Getting data ( Fast way )
329+
330+
There are two methods that are probably faster than `as_queryable` way.
331+
332+
1. `get(__id: str)`
333+
334+
Get an entity by it's id, Which is (`entity.id`). We use this for `update` and `delete` method.
335+
336+
2. `iter_by_prop_value(__prop: str, __value: Any)`
337+
338+
Use this to do an `async iterate` over all entities with `entity[__prop] == __value`.
339+
We use this to work with virtual objects.
340+
341+
```py
342+
async for student in engine.students.iter_by_prop_value("first_name", "Jill"):
343+
print(student.last_name)
344+
```
345+
346+
or
347+
348+
```py
349+
async for student in engine.students.iter_by_prop_value(lambda s: s.first_name, "Jill"):
350+
print(student.last_name)
351+
```
352+
353+
### Virtual properties
354+
355+
It was a good idea to use another model inside our main model to store additional data, but we don't actually want to load all of data while getting our main model. ( You don't want to load students's grade every time, since it's costly. )
356+
357+
It's better use a separate entity for grade and make it related to the student. Here, the grade will be a `virtual complex property`.
358+
359+
And the grade will use a `reference property` to the student id.
360+
361+
Since the `Grade` is going to be a separate entity, we should add it to our `AppEngine`.
362+
363+
1. First, let's modify the `Student` class.
364+
365+
We will replace `ListProperty` with `VirtualListProperty`.
366+
367+
```py
368+
# ---- sniff ----
369+
370+
from sjd.entity.properties import VirtualListProperty
371+
372+
class Student(TEntity):
373+
__json_init__ = True
374+
375+
student_id = IntProperty(required=True)
376+
first_name = StrProperty(required=True)
377+
last_name = StrProperty()
378+
379+
grades = VirtualListProperty(Grade, "student_id")
380+
381+
def __init__(
382+
self,
383+
student_id: int,
384+
first_name: str,
385+
last_name: str,
386+
grades: list[Grade] = [],
387+
):
388+
self.student_id = student_id
389+
self.first_name = first_name
390+
self.last_name = last_name
391+
self.grades = grades
392+
393+
```
394+
395+
`VirtualListProperty` takes type of entity it refers to and the name of `ReferenceProperty` which we'll declare later inside `Grade` class ( `student_id` ).
396+
397+
2. Modifying `Grade`.
398+
399+
The class should inherit from `TEntity` instead of `EmbedEntity`, since it's a separate entity now.
400+
401+
```py
402+
# ---- sniff ----
403+
404+
from sjd.entity.properties import ReferenceProperty
405+
406+
class Grade(TEntity):
407+
__json_init__ = True
408+
409+
course_id = IntProperty(required=True)
410+
course_name = StrProperty(required=True)
411+
score = IntProperty(required=True)
412+
413+
student_id = ReferenceProperty()
414+
415+
def __init__(self, course_id: int, course_name: str, score: int):
416+
self.course_id = course_id
417+
self.course_name = course_name
418+
self.score = score
419+
```
420+
421+
Note that we added `student_id = ReferenceProperty()`. The attribute name should be the same we declared inside `Student`'s `VirtualListProperty`'s second parameter (`student_id`).
422+
423+
3. Finally, modifying `AppEngine`
424+
425+
We only need to add `Grade` to the `AppEngine`, just like `Student`.
426+
427+
```py
428+
class AppEngine(Engine):
429+
430+
students = __Collection__(Student)
431+
grades = __Collection__(Grade)
432+
433+
def __init__(self):
434+
super().__init__("__test_db__")
435+
```
436+
437+
4. **Add data**. You can now add your data just like before.
438+
439+
```py
440+
engine = AppEngine()
441+
442+
await engine.students.add(
443+
Student(1, "Arash", "Doe", [Grade(1, "Physics", 20)]),
444+
)
445+
```
446+
447+
5. Getting data
448+
449+
If you try getting one of your students now, you'll see the `grades` property is an empty list.
450+
451+
```py
452+
arash = await engine.students.as_queryable.first(
453+
lambda s: s.first_name == "Arash"
454+
)
455+
print(arash.grades)
456+
457+
# []
458+
```
459+
460+
_The `grades` is some kind of `lazy property`._
461+
462+
to load virtual data, you can use method `load_virtual_props`
463+
464+
```py
465+
await engine.students.load_virtual_props(arash)
466+
print(arash.grades)
467+
468+
# [<__main__.Grade object at 0x0000021B3AF7FAC0>]
469+
```
470+
471+
or you can specify property's name ( if you have more than one ).
472+
473+
```py
474+
await engine.students.load_virtual_props(arash, "grades")
475+
print(arash.grades)
476+
```
477+
478+
Here you go, you have your grades.
479+
480+
**Or even better**, you can iter over grades WITHOUT calling `load_virtual_props` ( less costly again ).
481+
482+
You use `iter_referenced_by`:
483+
484+
```py
485+
async for grade in engine.students.iter_referenced_by(arash, lambda s: s.grades):
486+
print(grade.course_name)
487+
```
488+
489+
_Working examples are available under [src/examples](src/examples)._

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
aiofiles
1+
aiofiles
2+
ijson

setup.cfg

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = json-entity
3-
version = 0.0.0rc1
3+
version = 0.0.1rc0
44
author = immmdreza
55
author_email = ir310022@gmail.com
66
description = A simple and async json database.
@@ -21,6 +21,7 @@ packages = find:
2121
python_requires = >=3.10
2222
install_requires =
2323
aiofiles
24+
ijson
2425
include-package-data = True
2526

2627
[options.packages.find]

src/examples/readme_example.py renamed to src/examples/simple.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import asyncio
22
from pathlib import Path
33

4-
from src.sjd.database import Engine, __Collection__
5-
from src.sjd.entity import TEntity, EmbedEntity
6-
from src.sjd.entity.properties import IntProperty, StrProperty, ListProperty
4+
from sjd.database import Engine, __Collection__
5+
from sjd.entity import TEntity, EmbedEntity
6+
from sjd.entity.properties import IntProperty, StrProperty, ListProperty
77

88

99
class Grade(EmbedEntity):

src/examples/virtual_props.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import asyncio
2+
3+
4+
if __name__ == "__main__":
5+
6+
from sjd.database import Engine, __Collection__
7+
from sjd.entity import TEntity
8+
from sjd.entity.properties import (
9+
IntProperty,
10+
StrProperty,
11+
VirtualListProperty,
12+
ReferenceProperty,
13+
)
14+
15+
class Grade(TEntity):
16+
__json_init__ = True
17+
18+
course_id = IntProperty(required=True)
19+
course_name = StrProperty(required=True)
20+
score = IntProperty(required=True)
21+
22+
student_id = ReferenceProperty()
23+
24+
def __init__(self, course_id: int, course_name: str, score: int):
25+
self.course_id = course_id
26+
self.course_name = course_name
27+
self.score = score
28+
29+
class Student(TEntity):
30+
__json_init__ = True
31+
32+
student_id = IntProperty(required=True)
33+
first_name = StrProperty(required=True)
34+
last_name = StrProperty()
35+
36+
grades = VirtualListProperty(Grade, "student_id")
37+
38+
def __init__(
39+
self,
40+
student_id: int,
41+
first_name: str,
42+
last_name: str,
43+
grades: list[Grade] = [],
44+
):
45+
self.student_id = student_id
46+
self.first_name = first_name
47+
self.last_name = last_name
48+
self.grades = grades
49+
50+
class AppEngine(Engine):
51+
52+
students = __Collection__(Student)
53+
grades = __Collection__(Grade)
54+
55+
def __init__(self):
56+
super().__init__("__test_db__")
57+
58+
async def main():
59+
60+
engine = AppEngine()
61+
62+
await engine.students.add(
63+
Student(1, "Arash", "Eshi", [Grade(1, "Physics", 20)]),
64+
)
65+
66+
arash = await engine.students.as_queryable.first(
67+
lambda s: s.first_name == "Arash"
68+
)
69+
70+
async for grade in engine.students.iter_referenced_by(
71+
arash, lambda s: s.grades
72+
):
73+
print(grade.course_name)
74+
75+
asyncio.run(main())

src/sjd/database/_collection.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,21 @@
55
from pathlib import Path
66
from typing import (
77
Any,
8+
AsyncGenerator,
89
AsyncIterable,
910
Callable,
1011
Generic,
1112
Optional,
1213
TYPE_CHECKING,
1314
TypeVar,
1415
final,
16+
overload,
1517
)
1618

1719
import ijson # type: ignore
1820
import aiofiles
1921

20-
from ..entity import TEntity
22+
from ..entity import TEntity, TProperty
2123
from ..query import AsyncQueryable
2224
from ..serialization import deserialize, serialize
2325
from ..serialization._shared import T
@@ -303,17 +305,49 @@ async def get(self, __id: str):
303305
return data
304306
return None
305307

306-
async def iter_by_prop_value(self, __prop_name: str, __value: Any, /):
308+
@overload
309+
def iter_by_prop_value(
310+
self, selector: str, __value: Any, /
311+
) -> AsyncGenerator[T, None]:
307312
"""Iterate over all entities that have a certain property value.
308313
309314
Args:
310-
__prop_name (`str`): The name of the property
315+
selector (`str`): The name of the property
311316
(You can use something like `item.name`, but The item should be an EmbedEntity).
312317
__value (`Any`): The value of the property.
313318
"""
319+
...
320+
321+
@overload
322+
def iter_by_prop_value(
323+
self, selector: Callable[[type[T]], Any], __value: Any, /
324+
) -> AsyncGenerator[T, None]:
325+
"""Iterate over all entities that have a certain property value.
326+
327+
Args:
328+
selector (`str`): A function to select a property (Can't allow nested props).
329+
__value (`Any`): The value of the property.
330+
"""
331+
...
332+
333+
async def iter_by_prop_value(
334+
self, selector: str | Callable[[type[T]], Any], __value: Any, /
335+
) -> AsyncGenerator[T, None]:
336+
"""Iterate over all entities that have a certain property value.
337+
338+
Args:
339+
selector (`str`): The name of the property
340+
(You can use something like `item.name`, but The item should be an EmbedEntity).
341+
__value (`Any`): The value of the property.
342+
"""
343+
344+
if callable(selector):
345+
prop = selector(self._entity_type)
346+
if isinstance(prop, TProperty):
347+
selector = prop.json_property_name or prop.actual_name
314348

315349
async for line in self._tmp_collection_file("rb"):
316-
for item in ijson.items(line, __prop_name):
350+
for item in ijson.items(line, selector):
317351
if item == __value:
318352
data = deserialize(self._entity_type, json.loads(line))
319353
if data is not None:
@@ -407,7 +441,7 @@ async def add_many(self, *entities: T) -> None:
407441
await self._ensure_collection_exists()
408442
await self._add_many_to_file_async(*entities)
409443

410-
async def load_virtual_props(self, entity: T, props: Optional[list[str]] = None):
444+
async def load_virtual_props(self, entity: T, *props: str):
411445
# TODO: Specify properties names as optional option
412446
"""Load all virtual properties based on references."""
413447

@@ -439,7 +473,7 @@ async def load_virtual_props(self, entity: T, props: Optional[list[str]] = None)
439473
else:
440474
raise TypeError("Entity should be an instance of TEntity.")
441475

442-
async def iter_virtual_objects(
476+
async def iter_referenced_by(
443477
self,
444478
entity: T,
445479
selector: Callable[[type[T]], Optional[_T] | Optional[list[_T]]],

0 commit comments

Comments
 (0)