sqlalchemy-query-helpers 1.0.3__tar.gz → 1.0.107__tar.gz

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.
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlalchemy-query-helpers
3
+ Version: 1.0.107
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
+ Dynamic: license-file
16
+
17
+ Пакет содержит хелперы для пакета SQLAlchmy - методов `insert`, `upsert`, `update` и другое.
18
+
19
+ Инициализация: `db = DB(db_name, Base, use_os_env=False, echo=False)`. Где:
20
+ * `use_os_env`. Значение `True` означает брать определения для подключения к базе данных из переменных окружения хоста: `os.environ['DB_USER'], os.environ['DB_PASSWORD'], os.environ['DB_HOST']`. При значении `False` переменные user, password, host берутся из файла `cfg.py`, который должен быть создан в каталоге скрипта.
21
+
22
+ ## Экземпляр класса содержит
23
+ Методы:
24
+ * `insert_many`, `upsert` и другие
25
+
26
+ Свойства:
27
+ * `engine` - подключение к базе данных
28
+ * `name` - имя базы данных
29
+ * `base`: DeclarativeMeta
30
+ * `Session: sessionmaker` - фабрика сессий
31
+ * `session: Session` - инициализированная сессия
32
+
33
+ ## Пример использования
34
+
35
+ Определяем модель таблицы в файле `db_model.py`:
36
+ ```python
37
+ from sqlalchemy import Column, Integer, String, Date, ForeignKey
38
+ from sqlalchemy.dialects.mysql import TINYINT, SMALLINT, INTEGER, ENUM, FLOAT
39
+ from sqlalchemy.schema import Index
40
+ from sqlalchemy.orm import declarative_base
41
+
42
+ Base = declarative_base()
43
+
44
+ db_name = 'some_database'
45
+
46
+
47
+ class ATable(Base):
48
+ __tablename__ = 'A_table'
49
+ id = Column(INTEGER(unsigned=True), primary_key=True, autoincrement=True)
50
+ name = Column(String(100), nullable=False)
51
+ ```
52
+
53
+ Основной файл:
54
+ ```pythonа
55
+ from sqlalchemy_query_helpers import DB
56
+ from db_model import db_name, Base, ATable
57
+
58
+ db = DB(db_name, Base)
59
+
60
+ values_from_database = db.session.query(ATable).all()
61
+ ```
62
+
63
+ #### Пример `cfg.py`
64
+ ```python
65
+ # Settings of database
66
+ host = '100.100.100.100'
67
+ user = 'root'
68
+ password = 'qwerty'
69
+ ```
@@ -0,0 +1,53 @@
1
+ Пакет содержит хелперы для пакета SQLAlchmy - методов `insert`, `upsert`, `update` и другое.
2
+
3
+ Инициализация: `db = DB(db_name, Base, use_os_env=False, echo=False)`. Где:
4
+ * `use_os_env`. Значение `True` означает брать определения для подключения к базе данных из переменных окружения хоста: `os.environ['DB_USER'], os.environ['DB_PASSWORD'], os.environ['DB_HOST']`. При значении `False` переменные user, password, host берутся из файла `cfg.py`, который должен быть создан в каталоге скрипта.
5
+
6
+ ## Экземпляр класса содержит
7
+ Методы:
8
+ * `insert_many`, `upsert` и другие
9
+
10
+ Свойства:
11
+ * `engine` - подключение к базе данных
12
+ * `name` - имя базы данных
13
+ * `base`: DeclarativeMeta
14
+ * `Session: sessionmaker` - фабрика сессий
15
+ * `session: Session` - инициализированная сессия
16
+
17
+ ## Пример использования
18
+
19
+ Определяем модель таблицы в файле `db_model.py`:
20
+ ```python
21
+ from sqlalchemy import Column, Integer, String, Date, ForeignKey
22
+ from sqlalchemy.dialects.mysql import TINYINT, SMALLINT, INTEGER, ENUM, FLOAT
23
+ from sqlalchemy.schema import Index
24
+ from sqlalchemy.orm import declarative_base
25
+
26
+ Base = declarative_base()
27
+
28
+ db_name = 'some_database'
29
+
30
+
31
+ class ATable(Base):
32
+ __tablename__ = 'A_table'
33
+ id = Column(INTEGER(unsigned=True), primary_key=True, autoincrement=True)
34
+ name = Column(String(100), nullable=False)
35
+ ```
36
+
37
+ Основной файл:
38
+ ```pythonа
39
+ from sqlalchemy_query_helpers import DB
40
+ from db_model import db_name, Base, ATable
41
+
42
+ db = DB(db_name, Base)
43
+
44
+ values_from_database = db.session.query(ATable).all()
45
+ ```
46
+
47
+ #### Пример `cfg.py`
48
+ ```python
49
+ # Settings of database
50
+ host = '100.100.100.100'
51
+ user = 'root'
52
+ password = 'qwerty'
53
+ ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sqlalchemy-query-helpers"
7
- version = "1.0.3"
7
+ version = "1.0.107"
8
8
  authors = [
9
9
  { name = "vladiscripts", email = "blagopoluchie12@gmail.com" },
10
10
  ]
@@ -0,0 +1,410 @@
1
+ from sqlalchemy import create_engine, MetaData, Table, select, update
2
+ from sqlalchemy.orm import sessionmaker, Query, DeclarativeMeta, session
3
+ from sqlalchemy.orm import declarative_base
4
+ from sqlalchemy.orm.session import Session
5
+ from sqlalchemy.orm.attributes import InstrumentedAttribute
6
+ from sqlalchemy.dialects.mysql import insert
7
+ from sqlalchemy.exc import IntegrityError
8
+ from sqlalchemy import sql
9
+ from typing import Iterable, Union, List, Sequence
10
+
11
+
12
+ class DB:
13
+ is_updated = False
14
+ name: str
15
+ base: DeclarativeMeta = None
16
+ engine = None
17
+ Session: sessionmaker = None
18
+ session: Session = None
19
+
20
+ def __init__(self, db_name, base: DeclarativeMeta, db_url: str | None = None, echo=False, create_tables=False):
21
+ """
22
+ :param db_url: URL as "{user}:{password}@{host}" without [schema + netloc],
23
+ that will use as f'mysql+pymysql://{db_url}'.
24
+ It's convenient to store this string as an OS environment variable.
25
+ None - Use OS environment variables: 'DB_USER', 'DB_PASSWORD', 'DB_HOST'.
26
+
27
+ Доступ к БД по логину:паролю через localhost:3306 блокирутся по сети. Необходимо использовать ssh.
28
+ В консоли надо запустить переброску порта на localhost:3307:
29
+ ssh -L 3307:127.0.0.1:3306 root@remote_ip -N
30
+ И строку подключения заменить на: "user:password@localhost:3307"
31
+ db_url='user:password@127.0.0.1:3307'
32
+ """
33
+ self.base = base
34
+ engine_str = self.make_engine_str(db_url)
35
+ self.engine = create_engine(f'{engine_str}/{db_name}', echo=echo)
36
+ self.Session = sessionmaker(bind=self.engine)
37
+ # self.session = self.Session()
38
+
39
+ if create_tables:
40
+ base.metadata.create_all(self.engine) # create tables and index if not exists
41
+ self.name = self.engine.url.database
42
+
43
+ def __del__(self):
44
+ if self.session:
45
+ self.session.close()
46
+
47
+ @staticmethod
48
+ def make_engine_str(db_url: str | None) -> str:
49
+ """
50
+ Create an engine string (schema + netloc), like "mysql+pymysql://USER:PASSWORD@HOST"
51
+ :param db_url: URL as "{user}:{password}@{host}" that will use as f'mysql+pymysql://{db_url}'.
52
+ It's convenient to store this string as an OS environment variable.
53
+ None - Use OS environment variables: 'DB_USER', 'DB_PASSWORD', 'DB_HOST'.
54
+ """
55
+ if not db_url:
56
+ import os
57
+ try:
58
+ user = os.environ['DB_USER']
59
+ password = os.environ['DB_PASSWORD']
60
+ host = os.environ['DB_HOST']
61
+ except KeyError:
62
+ raise RuntimeError("Set the 'DB_USER', 'DB_PASSWORD', 'DB_HOST' OS env variables")
63
+ engine_str = f'mysql+pymysql://{user}:{password}@{host}'
64
+ else:
65
+ engine_str = f'mysql+pymysql://{db_url}'
66
+ return engine_str
67
+
68
+ def get_predefined_table(self, table_name: str, base_metadata=None) -> Table:
69
+ table = Table(table_name, base_metadata or declarative_base().metadata, autoload_with=self.engine)
70
+ return table
71
+
72
+ @staticmethod
73
+ def __check_modelkeys(row: dict, cause_dict: Iterable[InstrumentedAttribute]) -> (dict, dict):
74
+ """
75
+ :param row: from self.to_dict()
76
+ :param cause_dict: the model keys with values to search in database
77
+ :return:
78
+ cause_dict: the model keys with values to search in database
79
+ to_insert_dict: names of database's columns with new values to change
80
+ """
81
+ model_keys = [n.key for n in cause_dict]
82
+ cause_dict = {k: v for k, v in row.items() if k in model_keys}
83
+ to_insert_dict = {k: v for k, v in row.items() if k not in model_keys}
84
+ return cause_dict, to_insert_dict
85
+
86
+ def __to_dict(self, row: Union[dict, list], mfields: Sequence[InstrumentedAttribute] = None,
87
+ use_mfield_keys=True, use_orm_keys=False) -> dict:
88
+ """ Convert to dict.
89
+
90
+ :param row: List values or dict with column name/values. Column names can be stings or model columns.
91
+ :param mfields: List of fields of table's model. As sqlalchemy Column, not strings.
92
+ :param use_mfield_keys: Leave mfields as model fields, without converting it to strings.
93
+ """
94
+ if isinstance(row, dict):
95
+ if [k for k, v in row.items() if isinstance(k, InstrumentedAttribute)]:
96
+ d = {k.key: v for k, v in row.items()} if use_orm_keys else {k.name: v for k, v in row.items()}
97
+ else:
98
+ d = row
99
+ d = self.clean_values(d)
100
+ return d
101
+ elif mfields:
102
+ assert isinstance(row, (list, tuple, str))
103
+ fields = [f.key for f in mfields] if use_mfield_keys else [f.name for f in mfields]
104
+ if isinstance(row, (list, tuple)):
105
+ assert len(mfields) == len(row), "len(mfields) != len(row)"
106
+ elif isinstance(row, str):
107
+ assert len(mfields) == 1, "len(mfields) != len(row)"
108
+ row = [row]
109
+ d = dict(zip(fields, row))
110
+ d = self.clean_values(d)
111
+ return d
112
+ raise RuntimeError("unknown type 'row'")
113
+
114
+ def clean_values(self, d: dict):
115
+ """ strip() for str values"""
116
+ d_new = {k: v.strip() or None if isinstance(v, str) else v for k, v in d.items()}
117
+ return d_new
118
+
119
+ def insert(self, t, row: Union[dict, list, tuple], mfields: Union[list, tuple] = None, do_commit=True):
120
+ """ todo: new_rowid is None. Function 'insert_many' doesn't return anything. But it can be used in some old scripts."""
121
+ new_rowid = self.insert_many(t, [row], mfields, do_commit)
122
+ return new_rowid
123
+
124
+ def insert_many(self, t, rows: Union[list, tuple], mfields: Union[list, tuple] = None, do_commit=True):
125
+ dicts = [self.__to_dict(row, mfields, use_orm_keys=True) for row in rows]
126
+ if not dicts:
127
+ return
128
+
129
+ with self.Session() as session:
130
+ if hasattr(t, '_sa_class_manager'): # ORM-модель
131
+ session.bulk_insert_mappings(t, dicts)
132
+ else: # Table
133
+ stmt = insert(t).values(dicts)
134
+ session.execute(stmt)
135
+ if do_commit:
136
+ session.commit()
137
+
138
+ def insert_one(self, t, row: Union[dict, list, tuple], mfields: Union[list, tuple] = None, ignore=False):
139
+ q = insert(t).values(self.__to_dict(row, mfields))
140
+ if ignore:
141
+ q = q.prefix_with('IGNORE', dialect='mysql')
142
+ with self.Session() as s:
143
+ r = s.execute(q)
144
+ s.commit()
145
+ return r.lastrowid
146
+
147
+ def insert_ignore(self, t, row: Union[dict, list, tuple], mfields: Iterable[InstrumentedAttribute] = None) -> bool:
148
+ is_inserted = self.insert_ignore_many(t, [row], mfields)
149
+ return is_inserted
150
+
151
+ def insert_ignore_many(self, t, rows: List[dict], mfields: Iterable[InstrumentedAttribute] = None) -> bool:
152
+ with self.Session() as s:
153
+ is_inserted = False
154
+ for row in rows:
155
+ row = self.__to_dict(row, mfields, use_orm_keys=True)
156
+ try:
157
+ with s.begin_nested():
158
+ m = t(**row)
159
+ s.add(m)
160
+ is_inserted = True
161
+ except IntegrityError:
162
+ # print(f'already in DB: {row}')
163
+ pass
164
+ s.commit()
165
+ return is_inserted
166
+
167
+ def insert_ignore_core(self, t, row: Union[dict, list, tuple], mfields: Union[list, tuple] = None) -> None:
168
+ """Core instead ORM. IGNORE can ignore don't only doubles. Many warnings."""
169
+ self.insert_ignore_many_core(t, [row], mfields)
170
+
171
+ def insert_ignore_many_core(self, t, rows: List[Union[dict, list, tuple]], mfields: Union[list, tuple] = None) -> None:
172
+ """If can better use upsert, or insert after select with filtering exists rows. Problems of IGNORE:
173
+ * This make very large skips of row ids in table.
174
+ * Can ignore don't only doubles but other errors. Many warnings."""
175
+ dicts = [self.__to_dict(row, mfields) for row in rows] # , use_orm_keys=True
176
+ if not dicts:
177
+ return
178
+ q = insert(t).values(dicts).prefix_with('IGNORE', dialect='mysql')
179
+ with self.Session() as session:
180
+ session.execute(q)
181
+ session.commit()
182
+
183
+ def insert_ignore_instanses(self, instances):
184
+ if not isinstance(instances, Iterable): instances = (instances,)
185
+ for m in instances:
186
+ try:
187
+ with self.session.begin_nested():
188
+ self.session.add(m)
189
+ self.session.flush()
190
+ # print(f'DB: added {m}')
191
+ except IntegrityError:
192
+ pass
193
+ # print(f'DB: already in {m}')
194
+ # self.session.commit()
195
+
196
+ def update(self, t, row: Union[dict, list, tuple], cause_keys: Union[list, tuple], mfields: Union[list, tuple] = None) -> (bool, bool):
197
+ row = self.__to_dict(row, mfields)
198
+ in_keys, not_in_keys = self.__check_modelkeys(row, cause_keys) # get_check_args(row, keys)
199
+ with self.Session() as s:
200
+ rows_updates = s.query(t).filter_by(**in_keys).update(not_in_keys)
201
+ # q = update(t).values(**not_in_keys).where(**in_keys)
202
+ # rows_updates = self.db.session.execute(q)
203
+ # self.db.session.commit()
204
+ return rows_updates
205
+
206
+ def upsert_many_with_flags(self, t, rows: List[Union[dict, list, tuple]], cause_keys: Union[list, tuple],
207
+ mfields: Union[list, tuple] = None) -> dict:
208
+ """
209
+ Пакетный upsert с возвратом флагов.
210
+ :param t: ORM-модель таблицы
211
+ :param rows: Список данных (dict или list)
212
+ :param cause_keys: Ключи, по которым проверяется наличие (например, [Data.Date, 'ProductID'])
213
+ :param mfields: Поля модели, если `rows` — списки
214
+ :return: {
215
+ 'inserted': [...], # список вставленных row-объектов
216
+ 'updated': [...], # список обновлённых row-объектов
217
+ 'skipped': [...] # без изменений
218
+ }
219
+ """
220
+ from decimal import Decimal
221
+ import datetime as dt
222
+
223
+ def _values_equal(a, b) -> bool:
224
+ """
225
+ Сравнивает два значения как 'равные' с точки зрения бизнес-логики и SQL.
226
+ Важно: не использует float() для Decimal, чтобы избежать потерь.
227
+ """
228
+ if a is None and b is None:
229
+ return True
230
+ if a is None or b is None:
231
+ return False
232
+
233
+ # Случай: int, float, Decimal — сравниваем как числа
234
+ numeric_types = (int, float, Decimal)
235
+ if isinstance(a, numeric_types) and isinstance(b, numeric_types):
236
+ # Приводим к Decimal для точного сравнения
237
+ try:
238
+ dec_a = Decimal(a) if not isinstance(a, Decimal) else a
239
+ dec_b = Decimal(b) if not isinstance(b, Decimal) else b
240
+ return dec_a == dec_b
241
+ except (InvalidOperation, TypeError):
242
+ return False # если не удалось привести
243
+
244
+ # Случай: дата/время
245
+ datetime_types = (dt.date, dt.datetime, dt.time)
246
+ if isinstance(a, datetime_types) and isinstance(b, datetime_types):
247
+ return a == b
248
+
249
+ # Все остальное — строгое сравнение
250
+ return a == b
251
+
252
+ dicts = [self.__to_dict(row, mfields, use_orm_keys=True) for row in rows]
253
+
254
+ is_orm_model = hasattr(t, '_sa_class_manager')
255
+ is_table = isinstance(t, Table)
256
+ if not (is_orm_model or is_table):
257
+ raise TypeError("t must be ORM class or sqlalchemy.Table")
258
+
259
+ # Обработка cause_keys
260
+ key_columns = []
261
+ key_names = []
262
+ for key in cause_keys:
263
+ if isinstance(key, str):
264
+ col = getattr(t, key) if is_orm_model else t.c[key]
265
+ key_columns.append(col)
266
+ key_names.append(key)
267
+ elif hasattr(key, 'key'):
268
+ key_columns.append(key)
269
+ key_names.append(key.key)
270
+ else:
271
+ raise TypeError(f"Unsupported cause_keys element: {type(key)}")
272
+
273
+ def make_key(row):
274
+ if isinstance(row, dict):
275
+ return tuple(row[k] for k in key_names)
276
+ else:
277
+ return tuple(getattr(row, k) for k in key_names)
278
+
279
+ update_candidates = [(row, make_key(row)) for row in dicts]
280
+
281
+ # === 1. Массовый SELECT ===
282
+ existing = {}
283
+ keys_to_find = [key for _, key in update_candidates]
284
+ if keys_to_find:
285
+ filters = []
286
+ for i, col in enumerate(key_columns):
287
+ values = [key[i] for key in keys_to_find]
288
+ filters.append(col.in_(values))
289
+
290
+ with self.Session() as session:
291
+ if is_orm_model:
292
+ query = session.query(t).filter(*filters).all()
293
+ else:
294
+ stmt = select(t).filter(*filters)
295
+ query = session.execute(stmt).fetchall()
296
+ query = [row._asdict() for row in query]
297
+
298
+ for row in query:
299
+ key = make_key(row)
300
+ existing[key] = row
301
+
302
+ # === 2. Сравнение ===
303
+ inserted = []
304
+ updated = []
305
+ skipped = []
306
+
307
+ for row_dict, key in update_candidates:
308
+ if key in existing:
309
+ db_row = existing[key]
310
+ do_update = False
311
+ for k, v in row_dict.items():
312
+ if k in key_names:
313
+ continue
314
+ db_val = getattr(db_row, k) if not isinstance(db_row, dict) else db_row[k]
315
+ if not _values_equal(db_val, v):
316
+ do_update = True
317
+ break
318
+ if do_update:
319
+ updated.append(row_dict)
320
+ else:
321
+ skipped.append(row_dict)
322
+ else:
323
+ inserted.append(row_dict)
324
+
325
+ # === 3. Пакетная вставка/обновление ===
326
+ if inserted or updated:
327
+ combined = inserted + updated
328
+ stmt = insert(t).values(combined)
329
+
330
+ if is_orm_model:
331
+ cols = t.__table__.columns
332
+ else:
333
+ cols = t.columns
334
+ upsert_cols = [c.name for c in cols if not (c.primary_key or c.unique)]
335
+ update_dict = {col: getattr(stmt.inserted, col) for col in upsert_cols if col not in key_names}
336
+
337
+ if update_dict:
338
+ upsert_query = stmt.on_duplicate_key_update(update_dict)
339
+ with self.Session() as session:
340
+ session.execute(upsert_query)
341
+ session.commit()
342
+
343
+ return {'inserted': inserted, 'updated': updated, 'skipped': skipped}
344
+
345
+ def update_with_select(self, t, row: Union[dict, list, tuple], cause_dict: Union[list, tuple], mfields: Union[list, tuple] = None) -> (bool,
346
+ bool):
347
+ """
348
+ Проверяет, существует ли запись. Если да — обновляет, только если значения отличаются.
349
+ Возвращает: (is_exists, is_updated)
350
+ """
351
+ row = self.__to_dict(row, mfields, use_orm_keys=True)
352
+ cause_dict, to_insert_dict = self.__check_modelkeys(row, cause_dict)
353
+ is_updated = is_exists = False
354
+
355
+ with self.Session() as session:
356
+ r = session.query(t).filter_by(**cause_dict).first()
357
+ if not r:
358
+ return is_updated, is_exists
359
+
360
+ is_exists = True
361
+ do_update = False
362
+ for k, v in to_insert_dict.items():
363
+ if getattr(r, k) != v:
364
+ setattr(r, k, v)
365
+ do_update = True
366
+
367
+ if do_update:
368
+ session.commit()
369
+ is_updated = True
370
+
371
+ return is_updated, is_exists
372
+
373
+ def upsert_with_select(self, t, row: Union[dict, list, tuple], cause_keys: Union[list, tuple], mfields: Union[list, tuple] = None) -> (bool,
374
+ bool):
375
+ """
376
+ Упрощённая версия, использующая upsert_many_with_flags.
377
+ """
378
+ result = self.upsert_many_with_flags(t, [row], cause_keys, mfields)
379
+ inserted = len(result['inserted']) > 0
380
+ updated = len(result['updated']) > 0
381
+ return updated, inserted
382
+
383
+ def upsert(self, t, rows: Union[list[dict], tuple[dict]], mfields=None, do_commit=True, filter_unque_primary_keys=True):
384
+ dicts = [self.__to_dict(row, mfields) for row in rows]
385
+ if not dicts:
386
+ return
387
+
388
+ stmt = insert(t).values(dicts)
389
+ # need to remove primary or unique keys on using, else will error
390
+ if filter_unque_primary_keys:
391
+ table_columns = t.columns._all_columns if isinstance(t, Table) else t._sa_class_manager.mapper.columns._all_columns
392
+ update_dict = {x.name: x for x in stmt.inserted for c in table_columns
393
+ if x.name == c.name and c.unique is not True and c.primary_key is not True}
394
+ else:
395
+ update_dict = {x.name: x for x in stmt.inserted}
396
+ if not update_dict:
397
+ return
398
+ upsert_query = stmt.on_duplicate_key_update(update_dict)
399
+ with self.Session() as session:
400
+ session.execute(upsert_query)
401
+ if do_commit:
402
+ session.commit()
403
+
404
+ def execute_sqls(self, sqls: Union[str, list, tuple]):
405
+ assert isinstance(sqls, (str, list, tuple))
406
+ if isinstance(sqls, str):
407
+ sqls = [sqls]
408
+ with self.engine.connect() as conn:
409
+ for s in sqls:
410
+ conn.execute(sql.text(s))
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlalchemy-query-helpers
3
+ Version: 1.0.107
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
+ Dynamic: license-file
16
+
17
+ Пакет содержит хелперы для пакета SQLAlchmy - методов `insert`, `upsert`, `update` и другое.
18
+
19
+ Инициализация: `db = DB(db_name, Base, use_os_env=False, echo=False)`. Где:
20
+ * `use_os_env`. Значение `True` означает брать определения для подключения к базе данных из переменных окружения хоста: `os.environ['DB_USER'], os.environ['DB_PASSWORD'], os.environ['DB_HOST']`. При значении `False` переменные user, password, host берутся из файла `cfg.py`, который должен быть создан в каталоге скрипта.
21
+
22
+ ## Экземпляр класса содержит
23
+ Методы:
24
+ * `insert_many`, `upsert` и другие
25
+
26
+ Свойства:
27
+ * `engine` - подключение к базе данных
28
+ * `name` - имя базы данных
29
+ * `base`: DeclarativeMeta
30
+ * `Session: sessionmaker` - фабрика сессий
31
+ * `session: Session` - инициализированная сессия
32
+
33
+ ## Пример использования
34
+
35
+ Определяем модель таблицы в файле `db_model.py`:
36
+ ```python
37
+ from sqlalchemy import Column, Integer, String, Date, ForeignKey
38
+ from sqlalchemy.dialects.mysql import TINYINT, SMALLINT, INTEGER, ENUM, FLOAT
39
+ from sqlalchemy.schema import Index
40
+ from sqlalchemy.orm import declarative_base
41
+
42
+ Base = declarative_base()
43
+
44
+ db_name = 'some_database'
45
+
46
+
47
+ class ATable(Base):
48
+ __tablename__ = 'A_table'
49
+ id = Column(INTEGER(unsigned=True), primary_key=True, autoincrement=True)
50
+ name = Column(String(100), nullable=False)
51
+ ```
52
+
53
+ Основной файл:
54
+ ```pythonа
55
+ from sqlalchemy_query_helpers import DB
56
+ from db_model import db_name, Base, ATable
57
+
58
+ db = DB(db_name, Base)
59
+
60
+ values_from_database = db.session.query(ATable).all()
61
+ ```
62
+
63
+ #### Пример `cfg.py`
64
+ ```python
65
+ # Settings of database
66
+ host = '100.100.100.100'
67
+ user = 'root'
68
+ password = 'qwerty'
69
+ ```
@@ -1,14 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: sqlalchemy-query-helpers
3
- Version: 1.0.3
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
File without changes
@@ -1,277 +0,0 @@
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
-
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))
@@ -1,14 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: sqlalchemy-query-helpers
3
- Version: 1.0.3
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