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.
- sqlalchemy_query_helpers-1.0.107/PKG-INFO +69 -0
- sqlalchemy_query_helpers-1.0.107/README.md +53 -0
- {sqlalchemy_query_helpers-1.0.3 → sqlalchemy_query_helpers-1.0.107}/pyproject.toml +1 -1
- sqlalchemy_query_helpers-1.0.107/src/sqlalchemy_query_helpers/main.py +410 -0
- sqlalchemy_query_helpers-1.0.107/src/sqlalchemy_query_helpers.egg-info/PKG-INFO +69 -0
- sqlalchemy_query_helpers-1.0.3/PKG-INFO +0 -14
- sqlalchemy_query_helpers-1.0.3/README.md +0 -0
- sqlalchemy_query_helpers-1.0.3/src/sqlalchemy_query_helpers/main.py +0 -277
- sqlalchemy_query_helpers-1.0.3/src/sqlalchemy_query_helpers.egg-info/PKG-INFO +0 -14
- {sqlalchemy_query_helpers-1.0.3 → sqlalchemy_query_helpers-1.0.107}/LICENSE +0 -0
- {sqlalchemy_query_helpers-1.0.3 → sqlalchemy_query_helpers-1.0.107}/setup.cfg +0 -0
- {sqlalchemy_query_helpers-1.0.3 → sqlalchemy_query_helpers-1.0.107}/src/sqlalchemy_query_helpers/__init__.py +0 -0
- {sqlalchemy_query_helpers-1.0.3 → sqlalchemy_query_helpers-1.0.107}/src/sqlalchemy_query_helpers.egg-info/SOURCES.txt +0 -0
- {sqlalchemy_query_helpers-1.0.3 → sqlalchemy_query_helpers-1.0.107}/src/sqlalchemy_query_helpers.egg-info/dependency_links.txt +0 -0
- {sqlalchemy_query_helpers-1.0.3 → sqlalchemy_query_helpers-1.0.107}/src/sqlalchemy_query_helpers.egg-info/requires.txt +0 -0
- {sqlalchemy_query_helpers-1.0.3 → sqlalchemy_query_helpers-1.0.107}/src/sqlalchemy_query_helpers.egg-info/top_level.txt +0 -0
|
@@ -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
|
+
```
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|