ormlambda 2.7.2__py3-none-any.whl → 2.9.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 +18 -9
- ormlambda/common/abstract_classes/abstract_model.py +16 -17
- ormlambda/common/abstract_classes/decomposition_query.py +23 -0
- ormlambda/common/interfaces/IStatements.py +68 -36
- ormlambda/databases/my_sql/clauses/__init__.py +1 -1
- ormlambda/databases/my_sql/{functions → clauses}/group_by.py +0 -2
- ormlambda/databases/my_sql/clauses/insert.py +24 -12
- ormlambda/databases/my_sql/clauses/order.py +31 -17
- ormlambda/databases/my_sql/clauses/select.py +8 -4
- ormlambda/databases/my_sql/clauses/update.py +11 -17
- ormlambda/databases/my_sql/functions/__init__.py +5 -4
- ormlambda/databases/my_sql/functions/sum.py +39 -0
- ormlambda/databases/my_sql/repository.py +56 -6
- ormlambda/databases/my_sql/statements.py +45 -25
- ormlambda/utils/column.py +52 -13
- ormlambda/utils/dtypes.py +4 -12
- ormlambda/utils/fields.py +60 -0
- ormlambda/utils/table_constructor.py +67 -95
- {ormlambda-2.7.2.dist-info → ormlambda-2.9.0.dist-info}/METADATA +3 -2
- {ormlambda-2.7.2.dist-info → ormlambda-2.9.0.dist-info}/RECORD +22 -20
- {ormlambda-2.7.2.dist-info → ormlambda-2.9.0.dist-info}/LICENSE +0 -0
- {ormlambda-2.7.2.dist-info → ormlambda-2.9.0.dist-info}/WHEEL +0 -0
@@ -1,7 +1,8 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
from pathlib import Path
|
3
|
-
from typing import Any, Optional, Type, override, Callable
|
3
|
+
from typing import Any, Optional, Type, override, Callable, TYPE_CHECKING
|
4
4
|
import functools
|
5
|
+
import shapely as shp
|
5
6
|
|
6
7
|
# from mysql.connector.pooling import MySQLConnectionPool
|
7
8
|
from mysql.connector import MySQLConnection, Error # noqa: F401
|
@@ -16,12 +17,20 @@ from .clauses import DropDatabase
|
|
16
17
|
from .clauses import DropTable
|
17
18
|
|
18
19
|
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from src.ormlambda.common.abstract_classes.decomposition_query import ClauseInfo
|
22
|
+
from ormlambda import Table
|
23
|
+
from src.ormlambda.databases.my_sql.clauses.select import Select
|
24
|
+
|
25
|
+
|
19
26
|
class Response[TFlavour, *Ts]:
|
20
|
-
def __init__(self, response_values: list[tuple[*Ts]], columns: tuple[str], flavour: Type[TFlavour], **kwargs) -> None:
|
27
|
+
def __init__(self, response_values: list[tuple[*Ts]], columns: tuple[str], flavour: Type[TFlavour], model: Optional[Table] = None, select: Optional[Select] = None, **kwargs) -> None:
|
21
28
|
self._response_values: list[tuple[*Ts]] = response_values
|
22
29
|
self._columns: tuple[str] = columns
|
23
30
|
self._flavour: Type[TFlavour] = flavour
|
24
31
|
self._kwargs: dict[str, Any] = kwargs
|
32
|
+
self._model: Table = model
|
33
|
+
self._select: Select = select
|
25
34
|
|
26
35
|
self._response_values_index: int = len(self._response_values)
|
27
36
|
# self.select_values()
|
@@ -42,8 +51,10 @@ class Response[TFlavour, *Ts]:
|
|
42
51
|
def response(self) -> tuple[dict[str, tuple[*Ts]]] | tuple[tuple[*Ts]] | tuple[TFlavour]:
|
43
52
|
if not self.is_there_response:
|
44
53
|
return tuple([])
|
45
|
-
|
46
|
-
|
54
|
+
clean_response = self._response_values
|
55
|
+
if self._select is not None:
|
56
|
+
clean_response = self._parser_response()
|
57
|
+
return tuple(self._cast_to_flavour(clean_response))
|
47
58
|
|
48
59
|
def _cast_to_flavour(self, data: list[tuple[*Ts]]) -> list[dict[str, tuple[*Ts]]] | list[tuple[*Ts]] | list[TFlavour]:
|
49
60
|
def _dict() -> list[dict[str, tuple[*Ts]]]:
|
@@ -73,6 +84,38 @@ class Response[TFlavour, *Ts]:
|
|
73
84
|
|
74
85
|
return selector.get(self._flavour, _default)()
|
75
86
|
|
87
|
+
def _parser_response(self) -> TFlavour:
|
88
|
+
new_response: list[list] = []
|
89
|
+
for row in self._response_values:
|
90
|
+
new_row: list = []
|
91
|
+
for i, data in enumerate(row):
|
92
|
+
alias = self._columns[i]
|
93
|
+
clause_info = self._select[alias]
|
94
|
+
if not self._is_parser_required(clause_info):
|
95
|
+
new_row = row
|
96
|
+
break
|
97
|
+
else:
|
98
|
+
parser_data = self.parser_data(clause_info, data)
|
99
|
+
new_row.append(parser_data)
|
100
|
+
if not isinstance(new_row, tuple):
|
101
|
+
new_row = tuple(new_row)
|
102
|
+
|
103
|
+
new_response.append(new_row)
|
104
|
+
return new_response
|
105
|
+
|
106
|
+
@staticmethod
|
107
|
+
def _is_parser_required[T: Table](clause_info: ClauseInfo[T]) -> bool:
|
108
|
+
if clause_info is None:
|
109
|
+
return False
|
110
|
+
|
111
|
+
return clause_info.dtype is shp.Point
|
112
|
+
|
113
|
+
@staticmethod
|
114
|
+
def parser_data[T: Table, TProp](clause_info: ClauseInfo[T], data: TProp):
|
115
|
+
if clause_info.dtype is shp.Point:
|
116
|
+
return shp.from_wkt(data)
|
117
|
+
return data
|
118
|
+
|
76
119
|
|
77
120
|
class MySQLRepository(IRepositoryBase[MySQLConnection]):
|
78
121
|
def get_connection(func: Callable[..., Any]):
|
@@ -107,11 +150,19 @@ class MySQLRepository(IRepositoryBase[MySQLConnection]):
|
|
107
150
|
- flavour: Type[TFlavour]: Useful to return tuple of any Iterable type as dict,set,list...
|
108
151
|
"""
|
109
152
|
|
153
|
+
def get_and_drop_key(key: str) -> Optional[Any]:
|
154
|
+
if key in kwargs:
|
155
|
+
return kwargs.pop(key)
|
156
|
+
return None
|
157
|
+
|
158
|
+
model: Table = get_and_drop_key("model")
|
159
|
+
select: Select = get_and_drop_key("select")
|
160
|
+
|
110
161
|
with cnx.cursor(buffered=True) as cursor:
|
111
162
|
cursor.execute(query)
|
112
163
|
values: list[tuple] = cursor.fetchall()
|
113
164
|
columns: tuple[str] = cursor.column_names
|
114
|
-
return Response[TFlavour](response_values=values, columns=columns, flavour=flavour, **kwargs).response
|
165
|
+
return Response[TFlavour](model=model, response_values=values, columns=columns, flavour=flavour, select=select, **kwargs).response
|
115
166
|
|
116
167
|
# FIXME [ ]: this method does not comply with the implemented interface
|
117
168
|
@get_connection
|
@@ -192,7 +243,6 @@ class MySQLRepository(IRepositoryBase[MySQLConnection]):
|
|
192
243
|
def create_database(self, name: str, if_exists: TypeExists = "fail") -> None:
|
193
244
|
return CreateDatabase(self).execute(name, if_exists)
|
194
245
|
|
195
|
-
|
196
246
|
@property
|
197
247
|
def database(self) -> Optional[str]:
|
198
248
|
return self._data_config.get("database", None)
|
@@ -2,12 +2,12 @@ from __future__ import annotations
|
|
2
2
|
from typing import Iterable, override, Type, TYPE_CHECKING, Any, Callable, Optional
|
3
3
|
import inspect
|
4
4
|
from mysql.connector import MySQLConnection, errors, errorcode
|
5
|
-
|
5
|
+
import functools
|
6
6
|
|
7
7
|
if TYPE_CHECKING:
|
8
8
|
from ormlambda import Table
|
9
9
|
from ormlambda.components.where.abstract_where import AbstractWhere
|
10
|
-
from ormlambda.common.interfaces.IStatements import
|
10
|
+
from ormlambda.common.interfaces.IStatements import OrderTypes
|
11
11
|
from ormlambda.common.interfaces import IQuery, IRepositoryBase, IStatements_two_generic
|
12
12
|
from ormlambda.common.interfaces.IRepositoryBase import TypeExists
|
13
13
|
from ormlambda.common.interfaces import IAggregate
|
@@ -34,6 +34,18 @@ from ormlambda.common.enums import JoinType
|
|
34
34
|
from . import functions as func
|
35
35
|
|
36
36
|
|
37
|
+
# COMMENT: It's so important to prevent information generated by other tests from being retained in the class.
|
38
|
+
def clear_list(f: Callable[..., Any]):
|
39
|
+
@functools.wraps(f)
|
40
|
+
def wrapper(self: MySQLStatements, *args, **kwargs):
|
41
|
+
try:
|
42
|
+
return f(self, *args, **kwargs)
|
43
|
+
finally:
|
44
|
+
self._query_list.clear()
|
45
|
+
|
46
|
+
return wrapper
|
47
|
+
|
48
|
+
|
37
49
|
class MySQLStatements[T: Table](AbstractSQLStatements[T, MySQLConnection]):
|
38
50
|
def __init__(self, model: T, repository: IRepositoryBase[MySQLConnection]) -> None:
|
39
51
|
super().__init__(model, repository=repository)
|
@@ -71,11 +83,11 @@ class MySQLStatements[T: Table](AbstractSQLStatements[T, MySQLConnection]):
|
|
71
83
|
return self._repository.table_exists(self._model.__table_name__)
|
72
84
|
|
73
85
|
@override
|
86
|
+
@clear_list
|
74
87
|
def insert(self, instances: T | list[T]) -> None:
|
75
88
|
insert = InsertQuery(self._model, self._repository)
|
76
89
|
insert.insert(instances)
|
77
90
|
insert.execute()
|
78
|
-
self._query_list.clear()
|
79
91
|
return None
|
80
92
|
|
81
93
|
@override
|
@@ -95,44 +107,43 @@ class MySQLStatements[T: Table](AbstractSQLStatements[T, MySQLConnection]):
|
|
95
107
|
return None
|
96
108
|
|
97
109
|
@override
|
110
|
+
@clear_list
|
98
111
|
def upsert(self, instances: T | list[T]) -> None:
|
99
112
|
upsert = UpsertQuery(self._model, self._repository)
|
100
113
|
upsert.upsert(instances)
|
101
114
|
upsert.execute()
|
102
|
-
self._query_list.clear()
|
103
115
|
return None
|
104
116
|
|
105
117
|
@override
|
118
|
+
@clear_list
|
106
119
|
def update(self, dicc: dict[str, Any] | list[dict[str, Any]]) -> None:
|
107
120
|
update = UpdateQuery(self._model, self._repository, self._query_list["where"])
|
108
121
|
update.update(dicc)
|
109
122
|
update.execute()
|
110
|
-
|
123
|
+
|
111
124
|
return None
|
112
125
|
|
113
126
|
@override
|
114
127
|
def limit(self, number: int) -> IStatements_two_generic[T, MySQLConnection]:
|
115
128
|
limit = LimitQuery(number)
|
116
129
|
# Only can be one LIMIT SQL parameter. We only use the last LimitQuery
|
117
|
-
|
118
|
-
if len(limit_list) > 0:
|
119
|
-
self._query_list["limit"] = [limit]
|
120
|
-
else:
|
121
|
-
self._query_list["limit"].append(limit)
|
130
|
+
self._query_list["limit"] = [limit]
|
122
131
|
return self
|
123
132
|
|
124
133
|
@override
|
125
134
|
def offset(self, number: int) -> IStatements_two_generic[T, MySQLConnection]:
|
126
135
|
offset = OffsetQuery(number)
|
127
|
-
self._query_list["offset"]
|
136
|
+
self._query_list["offset"] = [offset]
|
128
137
|
return self
|
129
138
|
|
130
139
|
@override
|
131
|
-
def count(
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
140
|
+
def count(
|
141
|
+
self,
|
142
|
+
selection: Callable[[T], tuple] = lambda x: "*",
|
143
|
+
alias=True,
|
144
|
+
alias_name=None,
|
145
|
+
) -> IQuery:
|
146
|
+
return Count[T](self._model, selection, alias=alias, alias_name=alias_name)
|
136
147
|
|
137
148
|
@override
|
138
149
|
def join(self, table_left: Table, table_right: Table, *, by: str) -> IStatements_two_generic[T, MySQLConnection]:
|
@@ -155,7 +166,7 @@ class MySQLStatements[T: Table](AbstractSQLStatements[T, MySQLConnection]):
|
|
155
166
|
return self
|
156
167
|
|
157
168
|
@override
|
158
|
-
def order[TValue](self, _lambda_col: Callable[[T], TValue], order_type:
|
169
|
+
def order[TValue](self, _lambda_col: Callable[[T], TValue], order_type: OrderTypes) -> IStatements_two_generic[T, MySQLConnection]:
|
159
170
|
order = OrderQuery[T](self._model, _lambda_col, order_type)
|
160
171
|
self._query_list["order"].append(order)
|
161
172
|
return self
|
@@ -172,6 +183,10 @@ class MySQLStatements[T: Table](AbstractSQLStatements[T, MySQLConnection]):
|
|
172
183
|
def min[TProp](self, column: Callable[[T], TProp], alias: bool = True, alias_name: str = "min") -> TProp:
|
173
184
|
return func.Min[T](self._model, column=column, alias=alias, alias_name=alias_name)
|
174
185
|
|
186
|
+
@override
|
187
|
+
def sum[TProp](self, column: Callable[[T], TProp], alias: bool = True, alias_name: str = "sum") -> TProp:
|
188
|
+
return func.Sum[T](self._model, column=column, alias=alias, alias_name=alias_name)
|
189
|
+
|
175
190
|
@override
|
176
191
|
def select[TValue, TFlavour, *Ts](self, selector: Optional[Callable[[T], tuple[TValue, *Ts]]] = lambda: None, *, flavour: Optional[Type[TFlavour]] = None, by: JoinType = JoinType.INNER_JOIN):
|
177
192
|
if len(inspect.signature(selector).parameters) == 0:
|
@@ -187,7 +202,7 @@ class MySQLStatements[T: Table](AbstractSQLStatements[T, MySQLConnection]):
|
|
187
202
|
|
188
203
|
query: str = self._build()
|
189
204
|
if flavour:
|
190
|
-
result = self._return_flavour(query, flavour)
|
205
|
+
result = self._return_flavour(query, flavour, select)
|
191
206
|
if issubclass(flavour, tuple) and isinstance(selector(self._model), property):
|
192
207
|
return tuple([x[0] for x in result])
|
193
208
|
return result
|
@@ -211,13 +226,19 @@ class MySQLStatements[T: Table](AbstractSQLStatements[T, MySQLConnection]):
|
|
211
226
|
return tuple([res[0] for res in response])
|
212
227
|
|
213
228
|
@override
|
214
|
-
def group_by[
|
215
|
-
|
229
|
+
def group_by[*Ts](self, column: str | Callable[[T], Any]) -> IStatements_two_generic[T, MySQLConnection]:
|
230
|
+
if isinstance(column, str):
|
231
|
+
groupby = GroupBy[T, tuple[*Ts]](self._model, lambda x: column)
|
232
|
+
else:
|
233
|
+
groupby = GroupBy[T, tuple[*Ts]](self._model, column)
|
234
|
+
# Only can be one LIMIT SQL parameter. We only use the last LimitQuery
|
235
|
+
self._query_list["group by"].append(groupby)
|
236
|
+
return self
|
216
237
|
|
217
238
|
@override
|
239
|
+
@clear_list
|
218
240
|
def _build(self) -> str:
|
219
|
-
|
220
|
-
|
241
|
+
query_list: list[str] = []
|
221
242
|
for x in self.__order__:
|
222
243
|
sub_query: Optional[list[IQuery]] = self._query_list.get(x, None)
|
223
244
|
if sub_query is None:
|
@@ -244,9 +265,8 @@ class MySQLStatements[T: Table](AbstractSQLStatements[T, MySQLConnection]):
|
|
244
265
|
else:
|
245
266
|
query_ = "\n".join([x.query for x in sub_query])
|
246
267
|
|
247
|
-
|
248
|
-
|
249
|
-
return query
|
268
|
+
query_list.append(query_)
|
269
|
+
return "\n".join(query_list)
|
250
270
|
|
251
271
|
def __build_where_clause(self, where_condition: list[AbstractWhere]) -> str:
|
252
272
|
query: str = where_condition[0].query
|
ormlambda/utils/column.py
CHANGED
@@ -1,7 +1,14 @@
|
|
1
|
-
from
|
1
|
+
from __future__ import annotations
|
2
|
+
from typing import Type, Optional, Callable, TYPE_CHECKING, Any
|
3
|
+
import shapely as sph
|
4
|
+
|
5
|
+
if TYPE_CHECKING:
|
6
|
+
from .table_constructor import Field
|
2
7
|
|
3
8
|
|
4
9
|
class Column[T]:
|
10
|
+
CHAR: str = "%s"
|
11
|
+
|
5
12
|
__slots__ = (
|
6
13
|
"dtype",
|
7
14
|
"column_name",
|
@@ -31,22 +38,54 @@ class Column[T]:
|
|
31
38
|
self.is_auto_increment: bool = is_auto_increment
|
32
39
|
self.is_unique: bool = is_unique
|
33
40
|
|
41
|
+
@property
|
42
|
+
def column_value_to_query(self) -> T:
|
43
|
+
"""
|
44
|
+
This property must ensure that any variable requiring casting by different database methods is properly wrapped.
|
45
|
+
"""
|
46
|
+
if self.dtype is sph.Point:
|
47
|
+
return sph.to_wkt(self.column_value, -1)
|
48
|
+
return self.column_value
|
49
|
+
|
50
|
+
@property
|
51
|
+
def placeholder(self) -> str:
|
52
|
+
return self.placeholder_resolutor(self.dtype)
|
53
|
+
|
54
|
+
@property
|
55
|
+
def placeholder_resolutor(self) -> Callable[[Type, T], str]:
|
56
|
+
return self.__fetch_wrapped_method
|
57
|
+
|
58
|
+
# FIXME [ ]: this method is allocating the Column class with MySQL database
|
59
|
+
@classmethod
|
60
|
+
def __fetch_wrapped_method(cls, type_: Type) -> Optional[str]:
|
61
|
+
"""
|
62
|
+
This method must ensure that any variable requiring casting by different database methods is properly wrapped.
|
63
|
+
"""
|
64
|
+
caster: dict[Type[Any], Callable[[str], str]] = {
|
65
|
+
sph.Point: lambda x: f"ST_GeomFromText({x})",
|
66
|
+
}
|
67
|
+
return caster.get(type_, lambda x: x)(cls.CHAR)
|
68
|
+
|
34
69
|
def __repr__(self) -> str:
|
35
|
-
return f"<Column: {self.
|
70
|
+
return f"<Column: {self.dtype}>"
|
36
71
|
|
37
|
-
def __to_string__(self,
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
"
|
72
|
+
def __to_string__(self, field: Field):
|
73
|
+
column_class_string: str = f"{Column.__name__}[{field.type_name}]("
|
74
|
+
|
75
|
+
dicc: dict[str, Callable[[Field], str]] = {
|
76
|
+
"dtype": lambda field: field.type_name,
|
77
|
+
"column_name": lambda field: f"'{field.name}'",
|
78
|
+
"column_value": lambda field: field.name, # must be the same variable name as the instance variable name in Table's __init__ class
|
42
79
|
}
|
43
|
-
|
44
|
-
|
45
|
-
|
80
|
+
for self_var in self.__init__.__annotations__:
|
81
|
+
if not hasattr(self, self_var):
|
82
|
+
continue
|
83
|
+
|
84
|
+
self_value = dicc.get(self_var, lambda field: getattr(self, self_var))(field)
|
85
|
+
column_class_string += f" {self_var}={self_value}, "
|
46
86
|
|
47
|
-
|
48
|
-
|
49
|
-
return exec_str
|
87
|
+
column_class_string += ")"
|
88
|
+
return column_class_string
|
50
89
|
|
51
90
|
def __hash__(self) -> int:
|
52
91
|
return hash(
|
ormlambda/utils/dtypes.py
CHANGED
@@ -48,8 +48,10 @@ MySQL 8.0 does not support year in two-digit format.
|
|
48
48
|
"""
|
49
49
|
|
50
50
|
from decimal import Decimal
|
51
|
-
import datetime
|
52
51
|
from typing import Any, Literal
|
52
|
+
import datetime
|
53
|
+
|
54
|
+
from shapely import Point
|
53
55
|
import numpy as np
|
54
56
|
|
55
57
|
from .column import Column
|
@@ -66,17 +68,7 @@ DATE = Literal["DATE", "DATETIME(fsp)", "TIMESTAMP(fsp)", "TIME(fsp)", "YEAR"]
|
|
66
68
|
def transform_py_dtype_into_query_dtype(dtype: Any) -> str:
|
67
69
|
# TODOL: must be found a better way to convert python data type into SQL clauses
|
68
70
|
# float -> DECIMAL(5,2) is an error
|
69
|
-
dicc: dict[Any, str] = {
|
70
|
-
int: "INTEGER",
|
71
|
-
float: "FLOAT(5,2)",
|
72
|
-
Decimal: "FLOAT",
|
73
|
-
datetime.datetime: "DATETIME",
|
74
|
-
datetime.date: "DATE",
|
75
|
-
bytes: "BLOB",
|
76
|
-
bytearray: "BLOB",
|
77
|
-
str: "VARCHAR(255)",
|
78
|
-
np.uint64: "BIGINT UNSIGNED",
|
79
|
-
}
|
71
|
+
dicc: dict[Any, str] = {int: "INTEGER", float: "FLOAT(5,2)", Decimal: "FLOAT", datetime.datetime: "DATETIME", datetime.date: "DATE", bytes: "BLOB", bytearray: "BLOB", str: "VARCHAR(255)", np.uint64: "BIGINT UNSIGNED", Point: "Point"}
|
80
72
|
|
81
73
|
res = dicc.get(dtype, None)
|
82
74
|
if res is None:
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import typing as tp
|
2
|
+
from .column import Column
|
3
|
+
|
4
|
+
__all__ = ["get_fields"]
|
5
|
+
|
6
|
+
MISSING = lambda: Column() # COMMENT: Very Important to avoid reusing the same variable across different classes. # noqa: E731
|
7
|
+
|
8
|
+
|
9
|
+
class Field[TProp: tp.AnnotatedAny]:
|
10
|
+
def __init__(self, name: str, type_: tp.Type, default: Column[TProp]) -> None:
|
11
|
+
self.name: str = name
|
12
|
+
self.type_: tp.Type[TProp] = type_
|
13
|
+
self.default: Column[TProp] = default
|
14
|
+
|
15
|
+
def __repr__(self) -> str:
|
16
|
+
return f"{Field.__name__}(name = {self.name}, type_ = {self.type_}, default = {self.default})"
|
17
|
+
|
18
|
+
@property
|
19
|
+
def has_default(self) -> bool:
|
20
|
+
return self.default is not MISSING()
|
21
|
+
|
22
|
+
@property
|
23
|
+
def init_arg(self) -> str:
|
24
|
+
default = f"={self.default_name}" # if self.has_default else ""}"
|
25
|
+
return f"{self.name}: {self.type_name}{default}"
|
26
|
+
|
27
|
+
@property
|
28
|
+
def default_name(self) -> str:
|
29
|
+
return f"_dflt_{self.name}"
|
30
|
+
|
31
|
+
@property
|
32
|
+
def type_name(self) -> str:
|
33
|
+
return f"_type_{self.name}"
|
34
|
+
|
35
|
+
@property
|
36
|
+
def assginment(self) -> str:
|
37
|
+
return f"self._{self.name} = {self.default.__to_string__(self)}"
|
38
|
+
|
39
|
+
|
40
|
+
def get_fields[T, TProp](cls: tp.Type[T]) -> tp.Iterable[Field]:
|
41
|
+
# COMMENT: Used the 'get_type_hints' method to resolve typing when 'from __future__ import annotations' is in use
|
42
|
+
annotations = {key: val for key, val in tp.get_type_hints(cls).items() if not key.startswith("_")}
|
43
|
+
|
44
|
+
# delete_special_variables(annotations)
|
45
|
+
fields = []
|
46
|
+
for name, type_ in annotations.items():
|
47
|
+
if hasattr(type_, "__origin__") and type_.__origin__ is Column: # __origin__ to get type of Generic value
|
48
|
+
field_type = type_.__args__[0]
|
49
|
+
else:
|
50
|
+
# type_ must by Column object
|
51
|
+
field_type: TProp = type_
|
52
|
+
|
53
|
+
default: Column = getattr(cls, name, MISSING())
|
54
|
+
|
55
|
+
default.dtype = field_type # COMMENT: Useful for setting the dtype variable after instantiation.
|
56
|
+
fields.append(Field[TProp](name, field_type, default))
|
57
|
+
|
58
|
+
# Update __annotations__ to create Columns
|
59
|
+
cls.__annotations__[name] = default
|
60
|
+
return fields
|
@@ -1,75 +1,21 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from decimal import Decimal
|
3
|
+
from typing import Any, Optional, Type, dataclass_transform, overload, TYPE_CHECKING
|
1
4
|
import base64
|
2
5
|
import datetime
|
3
|
-
from decimal import Decimal
|
4
|
-
from typing import Any, Iterable, Optional, Type, dataclass_transform, get_type_hints
|
5
6
|
import json
|
6
7
|
|
7
|
-
|
8
|
-
from .module_tree.dfs_traversal import DFSTraversal
|
9
|
-
from .column import Column
|
10
|
-
|
11
|
-
from .foreign_key import ForeignKey, TableInfo
|
12
|
-
|
13
|
-
MISSING = Column()
|
14
|
-
|
15
|
-
|
16
|
-
class Field:
|
17
|
-
def __init__(self, name: str, type_: Type, default: object) -> None:
|
18
|
-
self.name: str = name
|
19
|
-
self.type_: Type = type_
|
20
|
-
self.default: Column = default
|
21
|
-
|
22
|
-
def __repr__(self) -> str:
|
23
|
-
return f"{Field.__name__}(name = {self.name}, type_ = {self.type_}, default = {self.default})"
|
24
|
-
|
25
|
-
@property
|
26
|
-
def has_default(self) -> bool:
|
27
|
-
return self.default is not MISSING
|
8
|
+
import shapely as sph
|
28
9
|
|
29
|
-
@property
|
30
|
-
def init_arg(self) -> str:
|
31
|
-
# default = f"={self.default_name if self.has_default else None}"
|
32
|
-
default = f"={None}"
|
33
10
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
@property
|
41
|
-
def type_name(self) -> str:
|
42
|
-
return f"_type_{self.name}"
|
43
|
-
|
44
|
-
@property
|
45
|
-
def assginment(self) -> str:
|
46
|
-
return f"self._{self.name} = {self.default.__to_string__(self.name,self.name,self.type_name)}"
|
47
|
-
|
48
|
-
|
49
|
-
def delete_special_variables(dicc: dict[str, object]) -> None:
|
50
|
-
keys = tuple(dicc.keys())
|
51
|
-
for key in keys:
|
52
|
-
if key.startswith("__"):
|
53
|
-
del dicc[key]
|
54
|
-
|
55
|
-
|
56
|
-
def get_fields[T](cls: Type[T]) -> Iterable[Field]:
|
57
|
-
# COMMENT: Used the 'get_type_hints' method to resolve typing when 'from __future__ import annotations' is in use
|
58
|
-
annotations = {key: val for key, val in get_type_hints(cls).items() if not key.startswith("_")}
|
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))
|
11
|
+
from .column import Column
|
12
|
+
from .dtypes import get_query_clausule
|
13
|
+
from .fields import get_fields
|
14
|
+
from .foreign_key import ForeignKey, TableInfo
|
15
|
+
from .module_tree.dfs_traversal import DFSTraversal
|
69
16
|
|
70
|
-
|
71
|
-
|
72
|
-
return fields
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
from .fields import Field
|
73
19
|
|
74
20
|
|
75
21
|
@dataclass_transform()
|
@@ -78,23 +24,32 @@ def __init_constructor__[T](cls: Type[T]) -> Type[T]:
|
|
78
24
|
# TODOL: I don't know if it's better to create a global dictionary like in commit '7de69443d7a8e7264b8d5d604c95da0e5d7e9cc0'
|
79
25
|
setattr(cls, "__properties_mapped__", {})
|
80
26
|
fields = get_fields(cls)
|
81
|
-
|
82
|
-
|
27
|
+
|
28
|
+
locals_: dict[str, Any] = {}
|
29
|
+
init_args: list[str] = ["self"]
|
30
|
+
assignments: list[str] = []
|
83
31
|
|
84
32
|
for field in fields:
|
85
|
-
if
|
86
|
-
|
33
|
+
if field.name.startswith("__"):
|
34
|
+
continue
|
35
|
+
|
36
|
+
locals_[field.type_name] = field.type_
|
37
|
+
locals_[field.default_name] = field.default.column_value
|
87
38
|
|
88
|
-
|
89
|
-
|
90
|
-
|
39
|
+
init_args.append(field.init_arg)
|
40
|
+
assignments.append(field.assginment)
|
41
|
+
__create_properties(cls, field)
|
42
|
+
|
43
|
+
string_locals_ = ", ".join(locals_)
|
44
|
+
string_init_args = ", ".join(init_args)
|
45
|
+
string_assignments = "\n\t\t".join(assignments)
|
91
46
|
|
92
47
|
wrapper_fn = "\n".join(
|
93
48
|
[
|
94
|
-
f"def wrapper({
|
95
|
-
f"
|
96
|
-
"\n
|
97
|
-
"
|
49
|
+
f"def wrapper({string_locals_}):",
|
50
|
+
f"\n\tdef __init__({string_init_args}):",
|
51
|
+
f"\n\t\t{string_assignments}",
|
52
|
+
"\treturn __init__",
|
98
53
|
]
|
99
54
|
)
|
100
55
|
|
@@ -109,11 +64,11 @@ def __init_constructor__[T](cls: Type[T]) -> Type[T]:
|
|
109
64
|
|
110
65
|
def __create_properties(cls: Type["Table"], field: Field) -> property:
|
111
66
|
_name: str = f"_{field.name}"
|
112
|
-
|
67
|
+
|
113
68
|
# we need to get Table attributes (Column class) and then called __getattribute__ or __setattr__ to make changes inside of Column
|
114
69
|
prop = property(
|
115
|
-
fget=lambda self:
|
116
|
-
fset=lambda self, value:
|
70
|
+
fget=lambda self: getattr(self, _name).__getattribute__("column_value"),
|
71
|
+
fset=lambda self, value: getattr(self, _name).__setattr__("column_value", value),
|
117
72
|
)
|
118
73
|
|
119
74
|
# set property in public name
|
@@ -122,22 +77,6 @@ def __create_properties(cls: Type["Table"], field: Field) -> property:
|
|
122
77
|
return None
|
123
78
|
|
124
79
|
|
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
80
|
class TableMeta(type):
|
142
81
|
def __new__[T](cls: "Table", name: str, bases: tuple, dct: dict[str, Any]) -> Type[T]:
|
143
82
|
"""
|
@@ -244,6 +183,7 @@ class Table(metaclass=TableMeta):
|
|
244
183
|
Decimal: str,
|
245
184
|
bytes: byte_to_string,
|
246
185
|
set: list,
|
186
|
+
sph.Point: lambda x: sph.to_wkt(x, rounding_precision=-1),
|
247
187
|
}
|
248
188
|
|
249
189
|
if (dtype := type(_value)) in transform_map:
|
@@ -324,3 +264,35 @@ class Table(metaclass=TableMeta):
|
|
324
264
|
)
|
325
265
|
)
|
326
266
|
return False
|
267
|
+
|
268
|
+
@classmethod
|
269
|
+
def get_property_name(cls, _property: property) -> str:
|
270
|
+
name: str = cls.__properties_mapped__.get(_property, None)
|
271
|
+
if not name:
|
272
|
+
raise KeyError(f"Class '{cls.__name__}' has not propery '{_property}' mapped.")
|
273
|
+
return name
|
274
|
+
|
275
|
+
@overload
|
276
|
+
@classmethod
|
277
|
+
def get_column(cls, column: str) -> Column: ...
|
278
|
+
@overload
|
279
|
+
@classmethod
|
280
|
+
def get_column(cls, column: property) -> Column: ...
|
281
|
+
@overload
|
282
|
+
@classmethod
|
283
|
+
def get_column[TProp](cls, column: property, value: TProp) -> Column[TProp]: ...
|
284
|
+
@overload
|
285
|
+
@classmethod
|
286
|
+
def get_column[TProp](cls, column: str, value: TProp) -> Column[TProp]: ...
|
287
|
+
@classmethod
|
288
|
+
def get_column[TProp](cls, column: str | property, value: Optional[TProp] = None) -> Column[TProp]:
|
289
|
+
if isinstance(column, property):
|
290
|
+
_column = cls.get_property_name(column)
|
291
|
+
elif isinstance(column, str) and column in cls.get_columns():
|
292
|
+
_column = column
|
293
|
+
else:
|
294
|
+
raise ValueError(f"'Column' param with value'{column}' is not expected.")
|
295
|
+
|
296
|
+
instance_table: Table = cls(**{_column: value})
|
297
|
+
|
298
|
+
return getattr(instance_table, f"_{_column}")
|
@@ -1,14 +1,15 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: ormlambda
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.9.0
|
4
4
|
Summary: ORM designed to interact with the database (currently with MySQL) using lambda functions and nested functions
|
5
5
|
Author: p-hzamora
|
6
6
|
Author-email: p.hzamora@icloud.com
|
7
7
|
Requires-Python: >=3.12,<4.0
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
9
9
|
Classifier: Programming Language :: Python :: 3.12
|
10
|
-
Requires-Dist: fluent-validation (
|
10
|
+
Requires-Dist: fluent-validation (==4.3.1)
|
11
11
|
Requires-Dist: mysql-connector-python (>=9.0.0,<10.0.0)
|
12
|
+
Requires-Dist: shapely (>=2.0.6,<3.0.0)
|
12
13
|
Description-Content-Type: text/markdown
|
13
14
|
|
14
15
|

|