ormlambda 0.1.0__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.
- ormlambda/__init__.py +4 -0
- ormlambda/common/__init__.py +2 -0
- ormlambda/common/abstract_classes/__init__.py +3 -0
- ormlambda/common/abstract_classes/abstract_model.py +302 -0
- ormlambda/common/abstract_classes/non_query_base.py +33 -0
- ormlambda/common/abstract_classes/query_base.py +10 -0
- ormlambda/common/enums/__init__.py +2 -0
- ormlambda/common/enums/condition_types.py +16 -0
- ormlambda/common/enums/join_type.py +10 -0
- ormlambda/common/interfaces/INonQueryCommand.py +9 -0
- ormlambda/common/interfaces/IQueryCommand.py +11 -0
- ormlambda/common/interfaces/IRepositoryBase.py +67 -0
- ormlambda/common/interfaces/IStatements.py +227 -0
- ormlambda/common/interfaces/__init__.py +4 -0
- ormlambda/components/__init__.py +0 -0
- ormlambda/components/delete/IDelete.py +6 -0
- ormlambda/components/delete/__init__.py +2 -0
- ormlambda/components/delete/abstract_delete.py +14 -0
- ormlambda/components/insert/IInsert.py +6 -0
- ormlambda/components/insert/__init__.py +2 -0
- ormlambda/components/insert/abstract_insert.py +21 -0
- ormlambda/components/select/ISelect.py +14 -0
- ormlambda/components/select/__init__.py +2 -0
- ormlambda/components/select/table_column.py +39 -0
- ormlambda/components/update/IUpdate.py +7 -0
- ormlambda/components/update/__init__.py +2 -0
- ormlambda/components/update/abstract_update.py +25 -0
- ormlambda/components/upsert/IUpsert.py +6 -0
- ormlambda/components/upsert/__init__.py +2 -0
- ormlambda/components/upsert/abstract_upsert.py +21 -0
- ormlambda/components/where/__init__.py +1 -0
- ormlambda/components/where/abstract_where.py +11 -0
- ormlambda/databases/__init__.py +0 -0
- ormlambda/databases/my_sql/__init__.py +2 -0
- ormlambda/databases/my_sql/clauses/__init__.py +13 -0
- ormlambda/databases/my_sql/clauses/create_database.py +29 -0
- ormlambda/databases/my_sql/clauses/delete.py +54 -0
- ormlambda/databases/my_sql/clauses/drop_database.py +19 -0
- ormlambda/databases/my_sql/clauses/drop_table.py +23 -0
- ormlambda/databases/my_sql/clauses/insert.py +70 -0
- ormlambda/databases/my_sql/clauses/joins.py +103 -0
- ormlambda/databases/my_sql/clauses/limit.py +17 -0
- ormlambda/databases/my_sql/clauses/offset.py +17 -0
- ormlambda/databases/my_sql/clauses/order.py +29 -0
- ormlambda/databases/my_sql/clauses/select.py +172 -0
- ormlambda/databases/my_sql/clauses/update.py +52 -0
- ormlambda/databases/my_sql/clauses/upsert.py +68 -0
- ormlambda/databases/my_sql/clauses/where_condition.py +219 -0
- ormlambda/databases/my_sql/repository.py +192 -0
- ormlambda/databases/my_sql/statements.py +86 -0
- ormlambda/model_base.py +36 -0
- ormlambda/utils/__init__.py +3 -0
- ormlambda/utils/column.py +65 -0
- ormlambda/utils/dtypes.py +104 -0
- ormlambda/utils/foreign_key.py +36 -0
- ormlambda/utils/lambda_disassembler/__init__.py +4 -0
- ormlambda/utils/lambda_disassembler/dis_types.py +136 -0
- ormlambda/utils/lambda_disassembler/disassembler.py +69 -0
- ormlambda/utils/lambda_disassembler/dtypes.py +103 -0
- ormlambda/utils/lambda_disassembler/name_of.py +41 -0
- ormlambda/utils/lambda_disassembler/nested_element.py +44 -0
- ormlambda/utils/lambda_disassembler/tree_instruction.py +145 -0
- ormlambda/utils/module_tree/__init__.py +0 -0
- ormlambda/utils/module_tree/dfs_traversal.py +60 -0
- ormlambda/utils/module_tree/dynamic_module.py +237 -0
- ormlambda/utils/table_constructor.py +308 -0
- ormlambda-0.1.0.dist-info/LICENSE +21 -0
- ormlambda-0.1.0.dist-info/METADATA +268 -0
- ormlambda-0.1.0.dist-info/RECORD +70 -0
- ormlambda-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,237 @@
|
|
1
|
+
import sys
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import Optional, Type
|
4
|
+
from collections import defaultdict
|
5
|
+
import importlib.util
|
6
|
+
import inspect
|
7
|
+
import re
|
8
|
+
|
9
|
+
from ..table_constructor import Table
|
10
|
+
from .dfs_traversal import DFSTraversal
|
11
|
+
|
12
|
+
|
13
|
+
class Node:
|
14
|
+
pattern: str = r"from \.(\w+) import (\w+)"
|
15
|
+
|
16
|
+
def __init__(
|
17
|
+
self,
|
18
|
+
file: str | Path,
|
19
|
+
relative_path: str | Path,
|
20
|
+
class_name: str = None,
|
21
|
+
fks: list[str, str] = None,
|
22
|
+
):
|
23
|
+
self._file: Path = self._set_paths(file)
|
24
|
+
self._relative_path: Path = self._set_paths(relative_path)
|
25
|
+
self._class_name: str = class_name
|
26
|
+
self._fks: list[str, str] = fks
|
27
|
+
|
28
|
+
self._relative_modules: list["Node"] = self.extract_modules()
|
29
|
+
|
30
|
+
def __repr__(self) -> str:
|
31
|
+
return f"Module: {self.module_name}, class: {self.class_name} ->({Node.__name__})"
|
32
|
+
|
33
|
+
def __eq__(self, __value: "Node") -> bool:
|
34
|
+
return hash(__value) == hash(self)
|
35
|
+
|
36
|
+
def __hash__(self) -> int:
|
37
|
+
return hash(
|
38
|
+
(
|
39
|
+
self.class_name,
|
40
|
+
self.code,
|
41
|
+
)
|
42
|
+
)
|
43
|
+
|
44
|
+
def _validate_paths(self, path: Path | str) -> bool:
|
45
|
+
if isinstance(path, str):
|
46
|
+
return Path(path).exists()
|
47
|
+
return path.exists()
|
48
|
+
|
49
|
+
def _set_paths(self, path: str | Path):
|
50
|
+
if not self._validate_paths(path):
|
51
|
+
raise OSError(path)
|
52
|
+
return Path(path).resolve()
|
53
|
+
|
54
|
+
def extract_modules(self) -> list["Node"]:
|
55
|
+
if self._fks:
|
56
|
+
rel_modules: list[tuple[str, str]] = self._fks
|
57
|
+
else:
|
58
|
+
rel_modules: list[tuple[str, str]] = re.findall(self.pattern, self.code) if self._file is not None else []
|
59
|
+
|
60
|
+
if not rel_modules:
|
61
|
+
return []
|
62
|
+
return [
|
63
|
+
Node(
|
64
|
+
file=self._relative_path.joinpath(module_).with_suffix(".py"),
|
65
|
+
relative_path=self._relative_path,
|
66
|
+
class_name=class_,
|
67
|
+
)
|
68
|
+
for module_, class_ in rel_modules
|
69
|
+
]
|
70
|
+
|
71
|
+
@property
|
72
|
+
def code(self) -> str:
|
73
|
+
if not self._file:
|
74
|
+
return None
|
75
|
+
return self._file.read_text()
|
76
|
+
|
77
|
+
@property
|
78
|
+
def file(self) -> Path:
|
79
|
+
return self._file
|
80
|
+
|
81
|
+
@property
|
82
|
+
def class_name(self) -> str:
|
83
|
+
if not self._class_name:
|
84
|
+
pattern = re.compile(r"class\s(\w+)\(.*Table.*\):")
|
85
|
+
|
86
|
+
condition = pattern.search(self.code)
|
87
|
+
if condition:
|
88
|
+
self._class_name = condition.group(1)
|
89
|
+
return self.class_name
|
90
|
+
return None
|
91
|
+
return self._class_name
|
92
|
+
|
93
|
+
@property
|
94
|
+
def module_name(self) -> str:
|
95
|
+
return self._file.stem
|
96
|
+
|
97
|
+
@property
|
98
|
+
def relative_modules(self) -> list["Node"]:
|
99
|
+
return self._relative_modules
|
100
|
+
|
101
|
+
@relative_modules.setter
|
102
|
+
def relative_modules(self, value: list):
|
103
|
+
self._relative_modules = value
|
104
|
+
|
105
|
+
@property
|
106
|
+
def is_dependent(self) -> bool:
|
107
|
+
return len(self.relative_modules) > 0
|
108
|
+
|
109
|
+
|
110
|
+
class ModuleTree:
|
111
|
+
def __init__(self, module_path: Path) -> None:
|
112
|
+
if isinstance(module_path, str):
|
113
|
+
module_path = Path(module_path)
|
114
|
+
|
115
|
+
self.module_path: Path = module_path
|
116
|
+
self.order_module_tuple: tuple[Node | str] = self.get_order_module_tuple_from_path()
|
117
|
+
|
118
|
+
def get_order_module_tuple_from_path(self) -> tuple[Node | str]:
|
119
|
+
if self.module_path.is_dir():
|
120
|
+
return self.order_modules_from_folder()
|
121
|
+
return self.order_modules_from_file()
|
122
|
+
|
123
|
+
def order_modules_from_folder(self) -> tuple[Node]:
|
124
|
+
"""
|
125
|
+
Method whose main used is sorting all .py inside of folder to avoid import errors and overall for the creation of tables in SQL, comply with foreign key referenced table
|
126
|
+
This method's main purpose is to sort all .py inside a folder to avoid import errors and to ensure that tables referenced by foreign key in other tables are created first
|
127
|
+
"""
|
128
|
+
order_list: list[Node] = []
|
129
|
+
unorder_module_list: list[Node] = []
|
130
|
+
|
131
|
+
for p in self.module_path.iterdir():
|
132
|
+
if not p.is_dir():
|
133
|
+
unorder_module_list.append(Node(file=p, relative_path=self.module_path))
|
134
|
+
|
135
|
+
self.sort_dicc(unorder_module_list, order_list)
|
136
|
+
return tuple(order_list)
|
137
|
+
|
138
|
+
@staticmethod
|
139
|
+
def sort_dicc(list_nodes: list[Node], new_list: list[Node]) -> None:
|
140
|
+
"""
|
141
|
+
Iterated throughout list_nodes var and feed 'new_list' mutable var
|
142
|
+
|
143
|
+
Must create a mutable object such as list, to fill it with ordered items
|
144
|
+
"""
|
145
|
+
|
146
|
+
def add_children(list_nodes: list[Node], new_list: list[Node], node: Node):
|
147
|
+
"""
|
148
|
+
Recursive method called when try to sort all Nodes
|
149
|
+
|
150
|
+
ARGUMENT
|
151
|
+
-
|
152
|
+
|
153
|
+
- list_nodes: list[Node]
|
154
|
+
- new_list:list[Node]
|
155
|
+
"""
|
156
|
+
all_children_added = all(x in new_list for x in node.relative_modules)
|
157
|
+
|
158
|
+
if node not in new_list and node._file is not None and ((node.is_dependent and all_children_added) or not node.is_dependent):
|
159
|
+
new_list.append(node)
|
160
|
+
return None
|
161
|
+
|
162
|
+
if node in new_list and not node.is_dependent:
|
163
|
+
return None
|
164
|
+
|
165
|
+
for child_node in node.relative_modules:
|
166
|
+
add_children(list_nodes, new_list, child_node)
|
167
|
+
|
168
|
+
for node in list_nodes:
|
169
|
+
add_children(list_nodes, new_list, node)
|
170
|
+
|
171
|
+
if node not in new_list and all([child_node in new_list for child_node in node.relative_modules]):
|
172
|
+
new_list.append(node)
|
173
|
+
|
174
|
+
return None
|
175
|
+
|
176
|
+
def order_modules_from_file(self) -> tuple[str]:
|
177
|
+
"""
|
178
|
+
Method whose main used is sorting all .py inside of folder to avoid import errors and overall for the creation of tables in SQL, comply with foreign key referenced table
|
179
|
+
This method's main purpose is to sort all .py inside a folder to avoid import errors and to ensure that tables referenced by foreign key in other tables are created first
|
180
|
+
"""
|
181
|
+
tables: list[tuple[str, Table]] = self.get_member_table(self.load_module("", self.module_path))
|
182
|
+
|
183
|
+
graph: dict[Type[Table], list[Type[Table]]] = defaultdict(list)
|
184
|
+
for _, tbl in tables:
|
185
|
+
graph[tbl] = tbl.find_dependent_tables()
|
186
|
+
|
187
|
+
sorted_tables = DFSTraversal.sort(graph)
|
188
|
+
res = [x.create_table_query() for x in sorted_tables]
|
189
|
+
return res
|
190
|
+
|
191
|
+
@staticmethod
|
192
|
+
def find_module(module_name: str, nodes: list["Node"]) -> Optional["Node"]:
|
193
|
+
for node in nodes:
|
194
|
+
if module_name == node.class_name:
|
195
|
+
return node
|
196
|
+
return None
|
197
|
+
|
198
|
+
@staticmethod
|
199
|
+
def load_module(module_name: str, module_path: Path):
|
200
|
+
"""
|
201
|
+
Method whose main purpose is the dynamic addtion of modules using the importlib module.
|
202
|
+
Both the module name and its path must be specified.
|
203
|
+
|
204
|
+
!important: we need to add the dynamic modules to sys.modules for the loading to be successful
|
205
|
+
"""
|
206
|
+
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
207
|
+
module = importlib.util.module_from_spec(spec)
|
208
|
+
sys.modules[spec.name] = module
|
209
|
+
spec.loader.exec_module(module)
|
210
|
+
return module
|
211
|
+
|
212
|
+
def get_queries(self) -> tuple[str, ...]:
|
213
|
+
table_list: list[Table] = []
|
214
|
+
if isinstance(self.order_module_tuple[0], str):
|
215
|
+
return self.order_module_tuple
|
216
|
+
|
217
|
+
for node in self.order_module_tuple:
|
218
|
+
if node.class_name is None:
|
219
|
+
continue
|
220
|
+
# avoid __init__ because we can get relative import not found error
|
221
|
+
if node.file.is_dir() or node.class_name == "__init__":
|
222
|
+
continue
|
223
|
+
|
224
|
+
# loop over order modules tuple to load it into sys.modules
|
225
|
+
# COMMENT!: Checked why changing 'class_name' by 'module_name' the method works
|
226
|
+
submodule = self.load_module(f"{self.module_path.stem}.{node.module_name}", node.file)
|
227
|
+
table_class = self.get_member_table(submodule)
|
228
|
+
|
229
|
+
# we need to ensure that the object we going to add in table_list is the same
|
230
|
+
for name, obj in table_class:
|
231
|
+
if name == node.class_name:
|
232
|
+
table_list.append(obj.create_table_query())
|
233
|
+
return tuple(table_list)
|
234
|
+
|
235
|
+
@staticmethod
|
236
|
+
def get_member_table(module) -> list[tuple[str, Type[Table]]]:
|
237
|
+
return inspect.getmembers(module, lambda x: inspect.isclass(x) and issubclass(x, Table) and x is not Table)
|
@@ -0,0 +1,308 @@
|
|
1
|
+
import base64
|
2
|
+
from collections import defaultdict
|
3
|
+
import datetime
|
4
|
+
from decimal import Decimal
|
5
|
+
from typing import Any, Iterable, Optional, Type, dataclass_transform
|
6
|
+
import json
|
7
|
+
|
8
|
+
from .dtypes import get_query_clausule
|
9
|
+
from .module_tree.dfs_traversal import DFSTraversal
|
10
|
+
from .column import Column
|
11
|
+
|
12
|
+
from .foreign_key import ForeignKey
|
13
|
+
|
14
|
+
MISSING = Column()
|
15
|
+
|
16
|
+
|
17
|
+
class Field:
|
18
|
+
def __init__(self, name: str, type_: type, default: object) -> None:
|
19
|
+
self.name: str = name
|
20
|
+
self.type_: type = type_
|
21
|
+
self.default: Column = default
|
22
|
+
|
23
|
+
def __repr__(self) -> str:
|
24
|
+
return f"{Field.__name__}(name = {self.name}, type_ = {self.type_}, default = {self.default})"
|
25
|
+
|
26
|
+
@property
|
27
|
+
def has_default(self) -> bool:
|
28
|
+
return self.default is not MISSING
|
29
|
+
|
30
|
+
@property
|
31
|
+
def init_arg(self) -> str:
|
32
|
+
# default = f"={self.default_name if self.has_default else None}"
|
33
|
+
default = f"={None}"
|
34
|
+
|
35
|
+
return f"{self.name}: {self.type_name}{default}"
|
36
|
+
|
37
|
+
@property
|
38
|
+
def default_name(self) -> str:
|
39
|
+
return f"_dflt_{self.name}"
|
40
|
+
|
41
|
+
@property
|
42
|
+
def type_name(self) -> str:
|
43
|
+
return f"_type_{self.name}"
|
44
|
+
|
45
|
+
@property
|
46
|
+
def assginment(self) -> str:
|
47
|
+
return f"self._{self.name} = {self.default.__to_string__(self.name,self.name,self.type_name)}"
|
48
|
+
|
49
|
+
|
50
|
+
def delete_special_variables(dicc: dict[str, object]) -> None:
|
51
|
+
keys = tuple(dicc.keys())
|
52
|
+
for key in keys:
|
53
|
+
if key.startswith("__"):
|
54
|
+
del dicc[key]
|
55
|
+
|
56
|
+
|
57
|
+
def get_fields[T](cls: Type[T]) -> Iterable[Field]:
|
58
|
+
annotations = getattr(cls, "__annotations__", {})
|
59
|
+
|
60
|
+
# delete_special_variables(annotations)
|
61
|
+
fields = []
|
62
|
+
for name, type_ in annotations.items():
|
63
|
+
# type_ must by Column object
|
64
|
+
field_type = type_
|
65
|
+
if hasattr(type_, "__origin__") and type_.__origin__ is Column: # __origin__ to get type of Generic value
|
66
|
+
field_type = type_.__args__[0]
|
67
|
+
default: Column = getattr(cls, name, MISSING)
|
68
|
+
fields.append(Field(name, field_type, default))
|
69
|
+
|
70
|
+
# Update __annotations__ to create Columns
|
71
|
+
cls.__annotations__[name] = Column[field_type]
|
72
|
+
return fields
|
73
|
+
|
74
|
+
|
75
|
+
@dataclass_transform()
|
76
|
+
def __init_constructor__[T](cls: Type[T]) -> Type[T]:
|
77
|
+
# create '__properties_mapped__' dictionary for each Table to avoid shared information
|
78
|
+
# TODOL: I don't know if it's better to create a global dictionary like in commit '7de69443d7a8e7264b8d5d604c95da0e5d7e9cc0'
|
79
|
+
setattr(cls, "__properties_mapped__", {})
|
80
|
+
fields = get_fields(cls)
|
81
|
+
locals_ = {}
|
82
|
+
init_args = []
|
83
|
+
|
84
|
+
for field in fields:
|
85
|
+
if not field.name.startswith("__"):
|
86
|
+
locals_[field.type_name] = field.type_
|
87
|
+
|
88
|
+
init_args.append(field.init_arg)
|
89
|
+
locals_[field.default_name] = None # field.default.column_value
|
90
|
+
__create_properties(cls, field)
|
91
|
+
|
92
|
+
wrapper_fn = "\n".join(
|
93
|
+
[
|
94
|
+
f"def wrapper({', '.join(locals_.keys())}):",
|
95
|
+
f" def __init__(self, {', '.join(init_args)}):",
|
96
|
+
"\n".join([f" {f.assginment}" for f in fields]) or " pass",
|
97
|
+
" return __init__",
|
98
|
+
]
|
99
|
+
)
|
100
|
+
|
101
|
+
namespace = {}
|
102
|
+
|
103
|
+
exec(wrapper_fn, None, namespace)
|
104
|
+
init_fn = namespace["wrapper"](**locals_)
|
105
|
+
|
106
|
+
setattr(cls, "__init__", init_fn)
|
107
|
+
return cls
|
108
|
+
|
109
|
+
|
110
|
+
def __create_properties(cls: Type["Table"], field: Field) -> property:
|
111
|
+
_name: str = f"_{field.name}"
|
112
|
+
type_ = field.type_
|
113
|
+
# we need to get Table attributes (Column class) and then called __getattribute__ or __setattr__ to make changes inside of Column
|
114
|
+
prop = property(
|
115
|
+
fget=lambda self: __transform_getter(getattr(self, _name), type_),
|
116
|
+
fset=lambda self, value: __transform_setter(getattr(self, _name), value, type_),
|
117
|
+
)
|
118
|
+
|
119
|
+
# set property in public name
|
120
|
+
setattr(cls, field.name, prop)
|
121
|
+
cls.__properties_mapped__[prop] = field.name
|
122
|
+
return None
|
123
|
+
|
124
|
+
|
125
|
+
def __transform_getter[T](obj: object, type_: T) -> T:
|
126
|
+
return obj.__getattribute__("column_value")
|
127
|
+
|
128
|
+
# if type_ is str and isinstance(eval(obj), Iterable):
|
129
|
+
# getter = eval(obj)
|
130
|
+
# return getter
|
131
|
+
|
132
|
+
|
133
|
+
def __transform_setter[T](obj: object, value: Any, type_: T) -> None:
|
134
|
+
return obj.__setattr__("column_value", value)
|
135
|
+
|
136
|
+
# if type_ is list:
|
137
|
+
# setter = str(setter)
|
138
|
+
# return None
|
139
|
+
|
140
|
+
|
141
|
+
class TableMeta(type):
|
142
|
+
def __new__[T](cls: "Table", name: str, bases: tuple, dct: dict[str, Any]) -> Type[T]:
|
143
|
+
"""
|
144
|
+
That's the class we use to recreate the table's metadata.
|
145
|
+
It's useful because we can dynamically create the __init__ method just by using the type hints of the variables we want to use as column names.
|
146
|
+
We simply call '__init_constructor__' to create all the necessary variables and the method.
|
147
|
+
"""
|
148
|
+
cls_object: Table = super().__new__(cls, name, bases, dct)
|
149
|
+
|
150
|
+
if name == "Table":
|
151
|
+
return cls_object
|
152
|
+
|
153
|
+
if cls_object.__table_name__ is Ellipsis:
|
154
|
+
raise Exception(f"class variable '__table_name__' must be declared in '{cls_object.__name__}' class")
|
155
|
+
|
156
|
+
if not isinstance(cls_object.__table_name__, str):
|
157
|
+
raise Exception(f"class variable '__table_name__' of '{cls_object.__name__}' class must be 'str'")
|
158
|
+
|
159
|
+
TableMeta.__add_fk_if_exists__(cls_object)
|
160
|
+
self = __init_constructor__(cls_object)
|
161
|
+
return self
|
162
|
+
|
163
|
+
def __repr__(cls: "Table") -> str:
|
164
|
+
return f"{TableMeta.__name__}: {cls.__table_name__}"
|
165
|
+
|
166
|
+
@staticmethod
|
167
|
+
def __add_fk_if_exists__(cls: "Table") -> None:
|
168
|
+
"""
|
169
|
+
When creating a Table class, we cannot pass the class itself as a parameter in a function that initializes a class variable.
|
170
|
+
To fix this, we first add the table name and then, when instantiating the object, we replace the table name with the class itself.
|
171
|
+
"""
|
172
|
+
if data := ForeignKey.MAPPED.get(cls.__table_name__):
|
173
|
+
ForeignKey.MAPPED[cls] = data
|
174
|
+
del ForeignKey.MAPPED[cls.__table_name__]
|
175
|
+
return None
|
176
|
+
|
177
|
+
|
178
|
+
@dataclass_transform(eq_default=False)
|
179
|
+
class Table(metaclass=TableMeta):
|
180
|
+
"""
|
181
|
+
Class to mapped database tables with Python classes.
|
182
|
+
|
183
|
+
It uses __annotations__ special var to store all table columns. If you do not type class var it means this var is not store as table column
|
184
|
+
and it do not going to appear when you instantiate the object itself.
|
185
|
+
|
186
|
+
This principle it so powerful due to we can create Foreign Key references without break __init__ class method.
|
187
|
+
|
188
|
+
>>> class Address(Table):
|
189
|
+
>>> __table_name__ = "address"
|
190
|
+
|
191
|
+
>>> address_id: int = Column[int](is_primary_key=True)
|
192
|
+
>>> address: str
|
193
|
+
>>> address2: str
|
194
|
+
>>> district: str
|
195
|
+
>>> city_id: int
|
196
|
+
>>> postal_code: datetime
|
197
|
+
>>> phone: str
|
198
|
+
>>> location: datetime
|
199
|
+
>>> last_update: datetime = Column[datetime](is_auto_generated=True)
|
200
|
+
|
201
|
+
>>> city = ForeignKey["Address", City](__table_name__, City, lambda a, c: a.city_id == c.city_id)
|
202
|
+
"""
|
203
|
+
|
204
|
+
__table_name__: str = ...
|
205
|
+
__properties_mapped__: dict[property, str] = ...
|
206
|
+
|
207
|
+
def __str__(self) -> str:
|
208
|
+
params = self.to_dict()
|
209
|
+
return json.dumps(params, ensure_ascii=False, indent=2)
|
210
|
+
|
211
|
+
def __getattr__[T](self, __name: str) -> Column[T]:
|
212
|
+
return self.__dict__.get(__name, None)
|
213
|
+
|
214
|
+
def __repr__(self: "Table") -> str:
|
215
|
+
def __cast_long_variables(value: Any):
|
216
|
+
if not isinstance(value, str):
|
217
|
+
value = str(value)
|
218
|
+
if len(value) > 20:
|
219
|
+
return value[:20] + "..."
|
220
|
+
return value
|
221
|
+
|
222
|
+
dicc: dict[str, str] = {x: str(getattr(self, x)) for x in self.__annotations__}
|
223
|
+
equal_loop = ["=".join((x, __cast_long_variables(y))) for x, y in dicc.items()]
|
224
|
+
return f'{self.__class__.__name__}({", ".join(equal_loop)})'
|
225
|
+
|
226
|
+
def to_dict(self) -> dict[str, str | int]:
|
227
|
+
dicc: dict[str, Any] = {}
|
228
|
+
for x in self.__annotations__:
|
229
|
+
transform_data = self.__transform_data__(getattr(self, x))
|
230
|
+
dicc[x] = transform_data
|
231
|
+
return dicc
|
232
|
+
|
233
|
+
@staticmethod
|
234
|
+
def __transform_data__[T](_value: T) -> T:
|
235
|
+
def byte_to_string(value: bytes):
|
236
|
+
return base64.b64encode(value).decode("utf-8")
|
237
|
+
|
238
|
+
transform_map: dict = {
|
239
|
+
datetime.datetime: datetime.datetime.isoformat,
|
240
|
+
datetime.date: datetime.date.isoformat,
|
241
|
+
Decimal: str,
|
242
|
+
bytes: byte_to_string,
|
243
|
+
set: list,
|
244
|
+
}
|
245
|
+
|
246
|
+
if (dtype := type(_value)) in transform_map:
|
247
|
+
return transform_map[dtype](_value)
|
248
|
+
return _value
|
249
|
+
|
250
|
+
def get_pk(self) -> Optional[Column]:
|
251
|
+
for col_name in self.__annotations__.keys():
|
252
|
+
private_col = f"_{col_name}"
|
253
|
+
col_obj = getattr(self, private_col)
|
254
|
+
if isinstance(col_obj, Column) and col_obj.is_primary_key:
|
255
|
+
return col_obj
|
256
|
+
return None
|
257
|
+
|
258
|
+
@classmethod
|
259
|
+
def get_columns(cls) -> tuple[str, ...]:
|
260
|
+
return tuple(cls.__annotations__.keys())
|
261
|
+
|
262
|
+
@classmethod
|
263
|
+
def create_table_query(cls) -> str:
|
264
|
+
"""It's classmethod because of it does not matter the columns values to create the table"""
|
265
|
+
all_clauses: list[str] = []
|
266
|
+
|
267
|
+
all_clauses.extend(cls._create_sql_column_query())
|
268
|
+
all_clauses.extend(ForeignKey.create_query(cls))
|
269
|
+
|
270
|
+
return f"CREATE TABLE {cls.__table_name__} ({', '.join(all_clauses)});"
|
271
|
+
|
272
|
+
@classmethod
|
273
|
+
def _create_sql_column_query(cls) -> list[str]:
|
274
|
+
"""
|
275
|
+
It's imperative to instantiate cls() to initialize the 'Table' object and create private variables that will be Column objects.
|
276
|
+
Otherwise, we only can access to property method
|
277
|
+
"""
|
278
|
+
table_init_ = cls()
|
279
|
+
annotations: dict[str, Column] = table_init_.__annotations__
|
280
|
+
all_columns: list = []
|
281
|
+
for col_name in annotations.keys():
|
282
|
+
col_object: Column = getattr(table_init_, f"_{col_name}")
|
283
|
+
all_columns.append(get_query_clausule(col_object))
|
284
|
+
return all_columns
|
285
|
+
|
286
|
+
@classmethod
|
287
|
+
def find_dependent_tables(cls) -> tuple["Table", ...]:
|
288
|
+
def get_involved_tables(graph: dict[Table, list[Table]], table: Table) -> None:
|
289
|
+
for tbl in ForeignKey[Table, Table].MAPPED[table].keys():
|
290
|
+
if ForeignKey.MAPPED[tbl]:
|
291
|
+
get_involved_tables(graph, tbl)
|
292
|
+
graph[tbl] = list(ForeignKey.MAPPED[tbl].keys())
|
293
|
+
return None
|
294
|
+
|
295
|
+
graph: dict[Table, list[Table]] = defaultdict(list)
|
296
|
+
graph[cls] = list(ForeignKey.MAPPED[cls].keys())
|
297
|
+
get_involved_tables(graph, cls)
|
298
|
+
|
299
|
+
dfs = DFSTraversal.sort(graph)
|
300
|
+
return dfs[: dfs.index(cls)]
|
301
|
+
|
302
|
+
def __hash__(self) -> int:
|
303
|
+
return hash(self.to_dict().values())
|
304
|
+
|
305
|
+
def __eq__(self, __value: Any) -> bool:
|
306
|
+
if isinstance(__value, Table):
|
307
|
+
return hash(self) == hash(__value)
|
308
|
+
return False
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Pablo Hernández Zamora
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|