sqlalchemy-query-helpers 1.0.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.
Potentially problematic release.
This version of sqlalchemy-query-helpers might be problematic. Click here for more details.
- sqlalchemy-query-helpers/__init__.py +3 -0
- sqlalchemy-query-helpers/main.py +277 -0
- sqlalchemy_query_helpers-1.0.1.dist-info/LICENSE +21 -0
- sqlalchemy_query_helpers-1.0.1.dist-info/METADATA +15 -0
- sqlalchemy_query_helpers-1.0.1.dist-info/RECORD +7 -0
- sqlalchemy_query_helpers-1.0.1.dist-info/WHEEL +5 -0
- sqlalchemy_query_helpers-1.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
from sqlalchemy import create_engine, MetaData, Table, select
|
|
2
|
+
from sqlalchemy.orm import sessionmaker, Query, DeclarativeMeta
|
|
3
|
+
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
|
4
|
+
from sqlalchemy.dialects.mysql import insert
|
|
5
|
+
from sqlalchemy.exc import IntegrityError
|
|
6
|
+
from sqlalchemy import sql
|
|
7
|
+
from typing import Iterable, Union, List, Sequence
|
|
8
|
+
import sqlalchemyhelpers
|
|
9
|
+
|
|
10
|
+
class DB:
|
|
11
|
+
is_updated = False
|
|
12
|
+
name: str
|
|
13
|
+
base: DeclarativeMeta = None
|
|
14
|
+
|
|
15
|
+
def __init__(self, db_name, base: DeclarativeMeta, use_os_env=False, echo=False):
|
|
16
|
+
self.base = base
|
|
17
|
+
engine_str = self.make_engine_str(use_os_env)
|
|
18
|
+
self.engine = create_engine(f'{engine_str}/{db_name}', echo=echo)
|
|
19
|
+
self.Session = sessionmaker(bind=self.engine)
|
|
20
|
+
self.session = self.Session()
|
|
21
|
+
|
|
22
|
+
base.metadata.create_all(self.engine) # create tables and index if not exists
|
|
23
|
+
self.name = self.engine.url.database
|
|
24
|
+
|
|
25
|
+
def __del__(self):
|
|
26
|
+
self.session.close()
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def make_engine_str(use_os_env) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Create an engine string (schema + netloc), like "mysql+pymysql://USER:PASSWORD@HOST"
|
|
32
|
+
:param use_os_env: Use OS envs 'DB_USER', 'DB_PASSWORD', 'DB_HOST', instead the `cfg.py` file
|
|
33
|
+
"""
|
|
34
|
+
if use_os_env:
|
|
35
|
+
import os
|
|
36
|
+
try:
|
|
37
|
+
user = os.environ['DB_USER']
|
|
38
|
+
password = os.environ['DB_PASSWORD']
|
|
39
|
+
host = os.environ['DB_HOST']
|
|
40
|
+
except KeyError:
|
|
41
|
+
raise RuntimeError("Set the 'DB_USER', 'DB_PASSWORD', 'DB_HOST' OS env variables")
|
|
42
|
+
else:
|
|
43
|
+
from cfg import user, password, host
|
|
44
|
+
engine_str = f'mysql+pymysql://{user}:{password}@{host}'
|
|
45
|
+
return engine_str
|
|
46
|
+
|
|
47
|
+
def get_predifined_table(self, table_name: str, base_metadata):
|
|
48
|
+
table = Table(table_name, base_metadata, autoload_with=self.engine)
|
|
49
|
+
return table
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def __check_modelkeys(row: dict, cause_dict: Iterable[InstrumentedAttribute]) -> (dict, dict):
|
|
53
|
+
"""
|
|
54
|
+
:param row: from self.to_dict()
|
|
55
|
+
:param cause_dict: the model keys with values to search in database
|
|
56
|
+
:return:
|
|
57
|
+
cause_dict: the model keys with values to search in database
|
|
58
|
+
to_insert_dict: names of database's columns with new values to change
|
|
59
|
+
"""
|
|
60
|
+
model_keys = [n.key for n in cause_dict]
|
|
61
|
+
cause_dict = {k: v for k, v in row.items() if k in model_keys}
|
|
62
|
+
to_insert_dict = {k: v for k, v in row.items() if k not in model_keys}
|
|
63
|
+
return cause_dict, to_insert_dict
|
|
64
|
+
|
|
65
|
+
def __to_dict(self, row: Union[dict, list], mfields: Sequence[InstrumentedAttribute] = None,
|
|
66
|
+
use_mfield_keys=True, use_orm_keys=False) -> dict:
|
|
67
|
+
""" Convert to dict.
|
|
68
|
+
|
|
69
|
+
:param row: List values or dict with column name/values. Column names can be stings or model columns.
|
|
70
|
+
:param mfields: List of fields of table's model. As sqlalchemy Column, not strings.
|
|
71
|
+
:param use_mfield_keys: Leave mfields as model fields, without converting it to strings.
|
|
72
|
+
"""
|
|
73
|
+
if isinstance(row, dict):
|
|
74
|
+
if [k for k, v in row.items() if isinstance(k, InstrumentedAttribute)]:
|
|
75
|
+
d = {k.key: v for k, v in row.items()} if use_orm_keys else {k.name: v for k, v in row.items()}
|
|
76
|
+
else:
|
|
77
|
+
d = row
|
|
78
|
+
d = self.clean_values(d)
|
|
79
|
+
return d
|
|
80
|
+
elif mfields:
|
|
81
|
+
assert isinstance(row, (list, tuple, str))
|
|
82
|
+
fields = [f.key for f in mfields] if use_mfield_keys else [f.name for f in mfields]
|
|
83
|
+
if isinstance(row, (list, tuple)):
|
|
84
|
+
assert len(mfields) == len(row), "len(mfields) != len(row)"
|
|
85
|
+
elif isinstance(row, str):
|
|
86
|
+
assert len(mfields) == 1, "len(mfields) != len(row)"
|
|
87
|
+
row = [row]
|
|
88
|
+
d = dict(zip(fields, row))
|
|
89
|
+
d = self.clean_values(d)
|
|
90
|
+
return d
|
|
91
|
+
raise RuntimeError("unknown type 'row'")
|
|
92
|
+
|
|
93
|
+
def clean_values(self, d: dict):
|
|
94
|
+
""" strip() for str values"""
|
|
95
|
+
d_new = {k: v.strip() or None if isinstance(v, str) else v for k, v in d.items()}
|
|
96
|
+
return d_new
|
|
97
|
+
|
|
98
|
+
def insert(self, t, row: Union[dict, list, tuple], mfields: Union[list, tuple] = None, do_commit=True):
|
|
99
|
+
""" todo: new_rowid is None. Function 'insert_many' doesn't return anything. But it can be used in some old scripts."""
|
|
100
|
+
new_rowid = self.insert_many(t, [row], mfields, do_commit)
|
|
101
|
+
return new_rowid
|
|
102
|
+
|
|
103
|
+
def insert_many(self, t, rows: Union[list, tuple], mfields: Union[list, tuple] = None, do_commit=True):
|
|
104
|
+
for row in rows:
|
|
105
|
+
row = self.__to_dict(row, mfields, use_orm_keys=True)
|
|
106
|
+
m = t(**row)
|
|
107
|
+
self.session.add(m)
|
|
108
|
+
if do_commit:
|
|
109
|
+
self.session.commit()
|
|
110
|
+
|
|
111
|
+
def insert_one(self, t, row: Union[list, tuple], mfields: Union[list, tuple] = None, ignore=False):
|
|
112
|
+
q = insert(t).values(self.__to_dict(row, mfields))
|
|
113
|
+
if ignore:
|
|
114
|
+
q = q.prefix_with('IGNORE', dialect='mysql')
|
|
115
|
+
r = self.session.execute(q)
|
|
116
|
+
self.session.commit()
|
|
117
|
+
return r.lastrowid
|
|
118
|
+
|
|
119
|
+
def insert_ignore(self, t, row: Union[dict, list, tuple], mfields: Iterable[InstrumentedAttribute] = None) -> bool:
|
|
120
|
+
is_inserted = self.insert_ignore_many(t, [row], mfields)
|
|
121
|
+
return is_inserted
|
|
122
|
+
|
|
123
|
+
def insert_ignore_many(self, t, rows: List[dict], mfields: Iterable[InstrumentedAttribute] = None) -> bool:
|
|
124
|
+
is_inserted = False
|
|
125
|
+
for row in rows:
|
|
126
|
+
row = self.__to_dict(row, mfields, use_orm_keys=True)
|
|
127
|
+
try:
|
|
128
|
+
with self.session.begin_nested():
|
|
129
|
+
m = t(**row)
|
|
130
|
+
self.session.add(m)
|
|
131
|
+
is_inserted = True
|
|
132
|
+
except IntegrityError:
|
|
133
|
+
# print(f'already in DB: {row}')
|
|
134
|
+
pass
|
|
135
|
+
self.session.commit()
|
|
136
|
+
return is_inserted
|
|
137
|
+
|
|
138
|
+
def insert_ignore_core(self, t, row: Union[dict, list, tuple], mfields: Union[list, tuple] = None) -> None:
|
|
139
|
+
"""Core instead ORM. IGNORE can ignore don't only doubles. Many warnings."""
|
|
140
|
+
self.insert_ignore_many_core(t, [row], mfields)
|
|
141
|
+
|
|
142
|
+
def insert_ignore_many_core(self, t, rows: List[Union[dict, list, tuple]], mfields: Union[list, tuple] = None) -> None:
|
|
143
|
+
"""Core instead ORM. IGNORE can ignore don't only doubles but other errors. Many warnings."""
|
|
144
|
+
# for row in rows:
|
|
145
|
+
# row = self.to_dict(row, mfields)
|
|
146
|
+
# # self.session.execute(insert(t, values=row, prefixes=['IGNORE']))
|
|
147
|
+
# s = insert(t).values(row).prefix_with('IGNORE', dialect='mysql')
|
|
148
|
+
# # self.session.execute(s)
|
|
149
|
+
rows_to_insert = [self.__to_dict(row, mfields) for row in rows]
|
|
150
|
+
q = insert(t).values(rows_to_insert).prefix_with('IGNORE', dialect='mysql')
|
|
151
|
+
self.session.execute(q)
|
|
152
|
+
self.session.commit()
|
|
153
|
+
|
|
154
|
+
def insert_ignore_instanses(self, instances):
|
|
155
|
+
if not isinstance(instances, Iterable): instances = (instances,)
|
|
156
|
+
for m in instances:
|
|
157
|
+
try:
|
|
158
|
+
with self.session.begin_nested():
|
|
159
|
+
self.session.add(m)
|
|
160
|
+
self.session.flush()
|
|
161
|
+
# print(f'DB: added {m}')
|
|
162
|
+
except IntegrityError:
|
|
163
|
+
pass
|
|
164
|
+
# print(f'DB: already in {m}')
|
|
165
|
+
# self.session.commit()
|
|
166
|
+
|
|
167
|
+
def update(self, t, row: Union[dict, list, tuple], cause_keys: Union[list, tuple], mfields: Union[list, tuple] = None) -> (bool, bool):
|
|
168
|
+
row = self.__to_dict(row, mfields)
|
|
169
|
+
in_keys, not_in_keys = self.__check_modelkeys(row, cause_keys) # get_check_args(row, keys)
|
|
170
|
+
rows_updates = Query(t, session=self.session).filter_by(**in_keys).update(not_in_keys)
|
|
171
|
+
# q = update(t).values(**not_in_keys).where(**in_keys)
|
|
172
|
+
# rows_updates = self.db.session.execute(q)
|
|
173
|
+
# self.db.session.commit()
|
|
174
|
+
return rows_updates
|
|
175
|
+
|
|
176
|
+
def update_with_select(self, t, row: Union[dict, list, tuple], cause_dict: Union[list, tuple], mfields: Union[list, tuple] = None) -> (
|
|
177
|
+
bool, bool):
|
|
178
|
+
"""it dont works"""
|
|
179
|
+
row = self.__to_dict(row, mfields)
|
|
180
|
+
is_updated = exist = False
|
|
181
|
+
cause_dict, to_insert_dict = self.__check_modelkeys(row, cause_dict)
|
|
182
|
+
# r = Query(t, session=self.session).filter_by(**cause_dict).first()
|
|
183
|
+
q = select(t).where(**cause_dict).limit(1)
|
|
184
|
+
r = self.session.execute(q).first()
|
|
185
|
+
if r:
|
|
186
|
+
for k, v in to_insert_dict.items():
|
|
187
|
+
if vars(r)[k] != v:
|
|
188
|
+
vars(r)[k] = v # dont works
|
|
189
|
+
is_updated = True
|
|
190
|
+
exist = True
|
|
191
|
+
return exist, is_updated
|
|
192
|
+
|
|
193
|
+
def upsert_with_select(self, t, row: Union[dict, list, tuple], cause_keys: Union[list, tuple],
|
|
194
|
+
mfields: Union[list, tuple] = None) -> (bool, bool):
|
|
195
|
+
""" A use example
|
|
196
|
+
|
|
197
|
+
with dbhandle.atomic():
|
|
198
|
+
t = ProductsData
|
|
199
|
+
for date, fid, value in data:
|
|
200
|
+
is_updated, is_inserted = upsert(
|
|
201
|
+
t,
|
|
202
|
+
[date.date(), pid, flow_ids[fid], value],
|
|
203
|
+
mfields=[t.Date, t.ProductID, t.FlowID, t.Value],
|
|
204
|
+
cause_keys=[t.Date, t.ProductID, t.FlowID])
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
:param t: table model
|
|
208
|
+
:param row: data, dict, or given mfields then list
|
|
209
|
+
:param cause_keys: model keys for cause in update. Uniques keys for rows.
|
|
210
|
+
:param mfields: model keys, if row is list instead dict
|
|
211
|
+
:return: tuple(bool(is_updated), bool(is_inserted))
|
|
212
|
+
"""
|
|
213
|
+
row = self.__to_dict(row, mfields)
|
|
214
|
+
is_updated = is_inserted = False
|
|
215
|
+
self.session.begin_nested()
|
|
216
|
+
exist, is_updated = self.update_with_select(t, row, cause_keys)
|
|
217
|
+
if not exist and not is_updated:
|
|
218
|
+
c = self.insert(t, row)
|
|
219
|
+
# if c and c > 0:
|
|
220
|
+
# is_inserted = True
|
|
221
|
+
is_inserted = True
|
|
222
|
+
self.session.commit()
|
|
223
|
+
return is_updated, is_inserted
|
|
224
|
+
|
|
225
|
+
def upsert(self, t, rows: Union[list[dict], tuple[dict]], mfields=None):
|
|
226
|
+
rows_to_insert = [self.__to_dict(row, mfields) for row in rows]
|
|
227
|
+
stmt = insert(t).values(rows_to_insert)
|
|
228
|
+
update_dict = {x.name: x for x in stmt.inserted}
|
|
229
|
+
upsert_query = stmt.on_duplicate_key_update(update_dict)
|
|
230
|
+
self.session.execute(upsert_query)
|
|
231
|
+
|
|
232
|
+
# def upsert(self, t, row, mfields=None):
|
|
233
|
+
# row = self.to_dict(row, mfields)
|
|
234
|
+
# stmt = insert(t).values(row).on_duplicate_key_update(row)
|
|
235
|
+
# self.session.execute(stmt)
|
|
236
|
+
# # self.session.commit()
|
|
237
|
+
|
|
238
|
+
# def __upsert_with_get_all_in_db(self, t, rows: List[Union[dict, tuple]], cause_keys: list, mfields: list = None):
|
|
239
|
+
# # all_in_db = self.session.query(t).all()
|
|
240
|
+
# # in_keys, not_in_keys = self.get_check_modelkeys(row, cause_keys) # get_check_args(row, keys)
|
|
241
|
+
# all_in_db = self.session.query(*cause_keys).all()
|
|
242
|
+
# while rows:
|
|
243
|
+
# month, pid, fid, cid, v = rows.pop()
|
|
244
|
+
# for row in all_in_db:
|
|
245
|
+
# row = self.to_dict(row, mfields)
|
|
246
|
+
# in_keys, not_in_keys = self.get_check_modelkeys(row, cause_keys)
|
|
247
|
+
# if r.Month == month and r.id == pid and r.FlowID == fid and r.CountryID == cid:
|
|
248
|
+
# r.Value = v
|
|
249
|
+
# break
|
|
250
|
+
# else:
|
|
251
|
+
# m = t(month, pid, fid, cid, v)
|
|
252
|
+
# self.session.add(m)
|
|
253
|
+
# self.session.commit()
|
|
254
|
+
|
|
255
|
+
# def _upsert_with_get_all_in_db(self, t, rows: List[tuple], cause_keys, ):
|
|
256
|
+
# in_keys, not_in_keys = self.get_check_modelkeys(row[], cause_keys)
|
|
257
|
+
# row = dict(zip([f.key for f in cause_keys], row))
|
|
258
|
+
#
|
|
259
|
+
# all_in_db = self.session.query(t).all()
|
|
260
|
+
# while rows:
|
|
261
|
+
# month, pid, fid, cid, v = rows.pop()
|
|
262
|
+
# for r in all_in_db:
|
|
263
|
+
# if r.Month == month and r.id == pid and r.FlowID == fid and r.CountryID == cid:
|
|
264
|
+
# r.Value = v
|
|
265
|
+
# break
|
|
266
|
+
# else:
|
|
267
|
+
# m = t(month, pid, fid, cid, v)
|
|
268
|
+
# self.session.add(m)
|
|
269
|
+
# self.session.commit()
|
|
270
|
+
|
|
271
|
+
def execute_sqls(self, sqls: Union[str, list, tuple]):
|
|
272
|
+
assert isinstance(sqls, (str, list, tuple))
|
|
273
|
+
if isinstance(sqls, str):
|
|
274
|
+
sqls = [sqls]
|
|
275
|
+
conn = self.engine.connect()
|
|
276
|
+
for s in sqls:
|
|
277
|
+
conn.execute(sql.text(s))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 vladiscripts
|
|
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.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: sqlalchemy-query-helpers
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Some helpers for SQLAlchemy
|
|
5
|
+
Author-email: vladiscripts <blagopoluchie12@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/vladiscripts/sqlalchemy-query-helpers
|
|
7
|
+
Project-URL: Issues, https://github.com/vladiscripts/sqlalchemy-query-helpers/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: sqlalchemy
|
|
15
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
sqlalchemy-query-helpers/__init__.py,sha256=xxmsXX2HKm1K8Ay9sy_tXGkPDWz3NXJI0s4xiwPckr0,39
|
|
2
|
+
sqlalchemy-query-helpers/main.py,sha256=QKswZj-gHXAsDjdZbSAnv-vfkwjcfAP8JbN977fQfjs,12464
|
|
3
|
+
sqlalchemy_query_helpers-1.0.1.dist-info/LICENSE,sha256=XUL1pa84eZAnKR9S2WnYrv2M523Oysejy3KPlVSyCks,1069
|
|
4
|
+
sqlalchemy_query_helpers-1.0.1.dist-info/METADATA,sha256=GfG8LKFVMZUBeQXedKnXgulmlZnXuB4N_Kq6UYrWUs4,584
|
|
5
|
+
sqlalchemy_query_helpers-1.0.1.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
|
|
6
|
+
sqlalchemy_query_helpers-1.0.1.dist-info/top_level.txt,sha256=p5rFZ5ojMfUwTxbf9vR_YhgNfg7Tj8HGHvACGlB_36c,25
|
|
7
|
+
sqlalchemy_query_helpers-1.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sqlalchemy-query-helpers
|