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.

@@ -0,0 +1,3 @@
1
+ from .main import DB
2
+
3
+ __all__ = ['DB']
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (74.1.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ sqlalchemy-query-helpers