Skip to content

Commit 8f0fed7

Browse files
committed
wip
1 parent a379888 commit 8f0fed7

2 files changed

Lines changed: 181 additions & 42 deletions

File tree

dictdatabase/orm.py

Lines changed: 112 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,160 @@
11
from __future__ import annotations
22
from abc import ABC
3-
from typing import get_type_hints, get_origin, get_args, TypeVar
4-
from types import UnionType, NoneType,
3+
from typing import get_type_hints, get_origin, get_args, TypeVar, Tuple, Type
4+
from types import UnionType, NoneType
55
from . import io_safe
66

7-
T = TypeVar('T')
87

98

9+
T = TypeVar("T")
10+
T2 = TypeVar("T2")
1011

11-
class FolderBase(ABC):
12-
__folder__ = None
13-
__file__base__ = None
1412

13+
def get_type_hints_excluding_internals(cls):
14+
type_hints = {}
15+
for var_name, var_type in get_type_hints(cls).items():
16+
if var_name.startswith("__") and var_name.endswith("__"):
17+
continue
18+
type_hints[var_name] = var_type
19+
return type_hints
1520

16-
class FileDictBase(ABC):
21+
22+
23+
24+
25+
26+
def fill_object_from_dict_using_type_hints(obj, cls, data: dict):
27+
"""
28+
Attributes of obj are set using the data dict.
29+
The type hints of the class cls are used to determine which attributes to set.
30+
"""
31+
for var_name, var_type in get_type_hints_excluding_internals(cls).items():
32+
# Check if variable is nullable (e.g. email: str | None)
33+
nullable = get_origin(var_type) is UnionType and NoneType in get_args(var_type)
34+
# When it is not nullable but not in the data, raise an error
35+
if var_name not in data:
36+
print(var_name, get_origin(var_type), get_args(var_type))
37+
if not nullable:
38+
raise RuntimeError(f"Missing variable '{var_name}' in {cls.__name__}.")
39+
else:
40+
continue
41+
# When it is a list, fill the list with the items
42+
if get_origin(var_type) is list and len(arg := get_args(var_type)) == 1:
43+
item_type = arg[0]
44+
setattr(obj, var_name, [item_type.from_dict(x) for x in data[var_name]])
45+
else:
46+
setattr(obj, var_name, data.get(var_name, None))
47+
return obj
48+
49+
50+
51+
52+
53+
########################################################################################
54+
# Scenario 1:
55+
# Model a single file with FileDictModel, which is a dict at the top level.
56+
# Each key-value item is modeled by a DictItemModel.
57+
58+
59+
60+
class FileDictModel(ABC):
1761
"""
1862
A file base refers to a file that is stored in the database.
63+
At the top level the file must contain a dictionary with strings as keys.
1964
"""
2065

2166
__file__ = None
22-
__item_base__: T = None
23-
__create_file_on_write__ = False
24-
67+
__item_model__: T = None
2568

2669
@classmethod
2770
def get_by_key(cls, key) -> T:
2871
"""
2972
Gets an item by key.
73+
The data is partially read from the __file__.
3074
"""
31-
return cls.__item_base__.get_by_key(key)
32-
75+
return cls.__item_model__.partial_read_by_key(key)
3376

77+
@classmethod
78+
def items(cls) -> list[Tuple[str, T]]:
79+
"""
80+
Gets all items.
81+
"""
82+
data = io_safe.read(cls.__file__)
83+
return [cls.__item_model__(key, value) for key, value in data.items()]
3484

85+
@classmethod
86+
def session(cls):
87+
...
3588

3689

37-
class FileDictItemBase(ABC):
3890
@classmethod
39-
def get_by_key(cls, key):
40-
data = io_safe.partial_read(cls.__file__, key)
41-
return cls.from_data(data)
91+
def session_at_key(cls, key):
92+
...
93+
4294

4395

4496

4597
@classmethod
46-
def from_data(cls, data):
98+
def get_where(cls, where: callable) -> list[T]:
99+
"""
100+
Gets all items where the where function returns True.
101+
The where function takes an object of type __item_model__.
102+
"""
103+
return [item for item in cls.get_all() if where(item)]
47104

48-
instance = cls()
105+
# Not Implemented:
106+
# - select by filter callback (file_where): Not implemented because no performance advantage.
49107

50-
for var_name, var_type in get_type_hints(cls).items():
51-
# Check if variable is nullable (e.g. email: str | None)
52-
nullable = get_origin(var_type) is UnionType and NoneType in get_args(var_name)
53-
# When it is not nullable but not in the data, raise an error
54-
if var_name not in data and not nullable:
55-
raise RuntimeError(f"Missing variable '{var_name}' in file '{cls.__file__}'.")
56108

57-
setattr(instance, var_name, data.get(var_name, None))
58109

59-
return instance
110+
class FileDictItemModel(ABC):
111+
__key__: str
60112

113+
@classmethod
114+
def from_key_value(cls: Type[T2], key, value) -> T2:
115+
obj = fill_object_from_dict_using_type_hints(cls(), cls, value)
116+
obj.__key__ = key
117+
return obj
61118

119+
@classmethod
120+
def read_by_key(cls, key) -> T:
121+
data = io_safe.partial_read(cls.__file__, key)
122+
return cls.from_key_value(key, data)
62123

63124

64125

65126

66-
class User(FileDictItemBase):
67-
first_name: str | None
68-
last_name: str
69-
email: str
127+
class DictModel(ABC):
128+
129+
@classmethod
130+
def from_dict(cls, data) -> DictModel:
131+
obj = cls()
132+
return fill_object_from_dict_using_type_hints(obj, cls, data)
133+
70134

71-
def full_name(self):
72-
return f"{self.first_name} {self.last_name}"
135+
def to_dict(self) -> dict:
136+
res = {}
137+
for var_name in get_type_hints(self).keys():
138+
if (value := getattr(self, var_name)) is not None:
139+
res[var_name] = value
140+
return res
73141

74142

75-
class Users(FileDictBase):
76-
__file__ = "users"
77-
__item_base__ = User
78143

79144

145+
########################################################################################
146+
# Scenario 2:
147+
# Add in a later version of DDB
148+
# A folder containing multiple files, each containing json.
80149

81-
# Iterate FileDictBase
82-
# for user_id, user: User in Users.items():
83-
# print(user_id, user.first_name, user.last_name, user.email)
150+
# class FolderBase(ABC):
151+
# __folder__ = None
152+
# __file_model__: FileInFolderModel = None
84153

85-
# # Get one item
86-
# user: User = Users.get("user_id")
87154

155+
# class FileInFolderModel(ABC):
88156

89-
# Get by lambda
90-
# users: Users = Users.where(lambda user: user.first_name != "John")
157+
# @classmethod
158+
# def get_by_name(cls, file_name: str) -> FileInFolderModel:
159+
# data = io_safe.read(f"{cls.__folder__}/{file_name}")
160+
# return cls(**data)

test_orm.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from dictdatabase.orm import DictModel, FileDictItemModel, FileDictModel
2+
3+
4+
class WorkTime(DictModel):
5+
"""
6+
Represents a work time.
7+
"""
8+
start: str
9+
end: str
10+
11+
12+
class User(FileDictItemModel):
13+
first_name: str
14+
last_name: str
15+
email: str | None
16+
17+
work_times: list[WorkTime]
18+
19+
20+
def full_name(self):
21+
return f"{self.first_name} {self.last_name}"
22+
23+
24+
class Users(FileDictModel):
25+
__file__ = "users"
26+
__item_model__ = User
27+
28+
29+
30+
u = User.from_key_value("uid1", {
31+
"first_name": "John",
32+
"last_name": "Doe",
33+
"work_times": [
34+
{"start": "08:00", "end": "12:00"},
35+
{"start": "13:00", "end": "17:00"},
36+
]
37+
})
38+
39+
40+
assert u.first_name == "John"
41+
assert u.last_name == "Doe"
42+
assert u.full_name() == "John Doe"
43+
assert u.work_times[0].start == "08:00"
44+
assert u.work_times[0].end == "12:00"
45+
assert u.work_times[1].start == "13:00"
46+
assert u.work_times[1].end == "17:00"
47+
assert len(u.work_times) == 2
48+
49+
print("check")
50+
print(u)
51+
52+
53+
# # Iterate FileDictModel
54+
# for user_id, user in Users.items():
55+
# print(user_id, user.first_name, user.last_name, user.email)
56+
57+
# # Get one item
58+
# user: User = Users.get("user_id")
59+
60+
61+
# # Get by lambda
62+
# users: Users = Users.where(lambda user: user.first_name != "John")
63+
64+
65+
# with Users.session_at_key(user_id) as (session, user):
66+
# ...
67+
68+
# with Users.session() as (session, users): Dict[str, User]
69+
# ...

0 commit comments

Comments
 (0)