lcdp-sqlalchemy-utils 1.5.8__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.
- lcdp_sqlalchemy_utils-1.5.8/PKG-INFO +19 -0
- lcdp_sqlalchemy_utils-1.5.8/lcdp_sqlalchemy_utils/__init__.py +0 -0
- lcdp_sqlalchemy_utils-1.5.8/lcdp_sqlalchemy_utils/env.py +64 -0
- lcdp_sqlalchemy_utils-1.5.8/lcdp_sqlalchemy_utils/env_async.py +73 -0
- lcdp_sqlalchemy_utils-1.5.8/lcdp_sqlalchemy_utils/exceptions/InvalidOrderByException.py +3 -0
- lcdp_sqlalchemy_utils-1.5.8/lcdp_sqlalchemy_utils/exceptions/__init__.py +0 -0
- lcdp_sqlalchemy_utils-1.5.8/lcdp_sqlalchemy_utils/migrate.py +11 -0
- lcdp_sqlalchemy_utils-1.5.8/lcdp_sqlalchemy_utils/order_by_mapper.py +26 -0
- lcdp_sqlalchemy_utils-1.5.8/lcdp_sqlalchemy_utils/postgres.py +214 -0
- lcdp_sqlalchemy_utils-1.5.8/lcdp_sqlalchemy_utils/query_executor.py +116 -0
- lcdp_sqlalchemy_utils-1.5.8/pyproject.toml +26 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lcdp-sqlalchemy-utils
|
|
3
|
+
Version: 1.5.8
|
|
4
|
+
Summary: SQLAlchemy Utils
|
|
5
|
+
Author: Le Comptoir Des Pharmacies
|
|
6
|
+
Author-email: webmaster@lecomptoirdespharmacies.fr
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Dist: SQLAlchemy (==2.0.40)
|
|
17
|
+
Requires-Dist: alembic (==1.8.1)
|
|
18
|
+
Requires-Dist: dataclasses-json (==0.5.7)
|
|
19
|
+
Requires-Dist: nest_asyncio (==1.5.8)
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from logging.config import fileConfig
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import engine_from_config
|
|
4
|
+
from sqlalchemy import pool
|
|
5
|
+
|
|
6
|
+
from alembic import context
|
|
7
|
+
|
|
8
|
+
# this is the Alembic Config object, which provides
|
|
9
|
+
# access to the values within the .ini file in use.
|
|
10
|
+
config = context.config
|
|
11
|
+
|
|
12
|
+
# Interpret the config file for Python logging.
|
|
13
|
+
# This line sets up loggers basically.
|
|
14
|
+
if config.config_file_name is not None:
|
|
15
|
+
fileConfig(config.config_file_name)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run_migrations_offline(**kwargs):
|
|
19
|
+
"""Run migrations in 'offline' mode.
|
|
20
|
+
|
|
21
|
+
This configures the context with just a URL
|
|
22
|
+
and not an Engine, though an Engine is acceptable
|
|
23
|
+
here as well. By skipping the Engine creation
|
|
24
|
+
we don't even need a DBAPI to be available.
|
|
25
|
+
|
|
26
|
+
Calls to context.execute() here emit the given string to the
|
|
27
|
+
script output.
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
31
|
+
context.configure(
|
|
32
|
+
url=url,
|
|
33
|
+
literal_binds=True,
|
|
34
|
+
dialect_opts={"paramstyle": "named"},
|
|
35
|
+
**kwargs
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
with context.begin_transaction():
|
|
39
|
+
context.run_migrations()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def run_migrations_online(db_url=None, **kwargs):
|
|
43
|
+
"""Run migrations in 'online' mode.
|
|
44
|
+
|
|
45
|
+
In this scenario we need to create an Engine
|
|
46
|
+
and associate a connection with the context.
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
alembic_config = config.get_section(config.config_ini_section)
|
|
50
|
+
connectable = engine_from_config(
|
|
51
|
+
alembic_config,
|
|
52
|
+
url=db_url,
|
|
53
|
+
prefix="sqlalchemy.",
|
|
54
|
+
poolclass=pool.NullPool,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
with connectable.connect() as connection:
|
|
58
|
+
# for compare_type=True, https://alembic.sqlalchemy.org/en/latest/autogenerate.html#comparing-types
|
|
59
|
+
context.configure(
|
|
60
|
+
connection=connection, compare_type=True, **kwargs
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
with context.begin_transaction():
|
|
64
|
+
context.run_migrations()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from logging.config import fileConfig
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import engine_from_config
|
|
5
|
+
from sqlalchemy import pool
|
|
6
|
+
|
|
7
|
+
from alembic import context
|
|
8
|
+
from sqlalchemy.engine import Connection
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
10
|
+
|
|
11
|
+
# this is the Alembic Config object, which provides
|
|
12
|
+
# access to the values within the .ini file in use.
|
|
13
|
+
config = context.config
|
|
14
|
+
|
|
15
|
+
# Interpret the config file for Python logging.
|
|
16
|
+
# This line sets up loggers basically.
|
|
17
|
+
if config.config_file_name is not None:
|
|
18
|
+
fileConfig(config.config_file_name)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def run_migrations_offline(**kwargs):
|
|
22
|
+
"""Run migrations in 'offline' mode.
|
|
23
|
+
|
|
24
|
+
This configures the context with just a URL
|
|
25
|
+
and not an Engine, though an Engine is acceptable
|
|
26
|
+
here as well. By skipping the Engine creation
|
|
27
|
+
we don't even need a DBAPI to be available.
|
|
28
|
+
|
|
29
|
+
Calls to context.execute() here emit the given string to the
|
|
30
|
+
script output.
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
34
|
+
context.configure(
|
|
35
|
+
url=url,
|
|
36
|
+
literal_binds=True,
|
|
37
|
+
dialect_opts={"paramstyle": "named"},
|
|
38
|
+
**kwargs
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
async with context.begin_transaction():
|
|
42
|
+
context.run_migrations()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def do_run_migrations(connection: Connection, **kwargs) -> None:
|
|
46
|
+
# target_metadata = MetaData()
|
|
47
|
+
context.configure(connection=connection, compare_type=True, **kwargs)
|
|
48
|
+
|
|
49
|
+
with context.begin_transaction():
|
|
50
|
+
context.run_migrations()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def run_migrations_online(db_url=None, **kwargs):
|
|
54
|
+
"""Run migrations in 'online' mode.
|
|
55
|
+
|
|
56
|
+
In this scenario we need to create an Engine
|
|
57
|
+
and associate a connection with the context.
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
alembic_config = config.get_section(config.config_ini_section)
|
|
61
|
+
connectable = AsyncEngine(
|
|
62
|
+
engine_from_config(
|
|
63
|
+
alembic_config,
|
|
64
|
+
url=db_url,
|
|
65
|
+
prefix="sqlalchemy.",
|
|
66
|
+
poolclass=pool.NullPool,
|
|
67
|
+
future=True,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
async with connectable.connect() as connection:
|
|
72
|
+
await connection.run_sync(do_run_migrations, **kwargs)
|
|
73
|
+
await connectable.dispose()
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from alembic.config import Config
|
|
4
|
+
from alembic import command
|
|
5
|
+
|
|
6
|
+
# Upgrade migration
|
|
7
|
+
# see https://alembic.sqlalchemy.org/en/latest/api/config.html
|
|
8
|
+
def launch_migration_upgrade(alembic_config_path, db_url):
|
|
9
|
+
alembic_cfg = Config(alembic_config_path)
|
|
10
|
+
alembic_cfg.set_main_option("sqlalchemy.url", db_url)
|
|
11
|
+
command.upgrade(alembic_cfg, "head")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from sqlalchemy import desc, asc
|
|
2
|
+
from .exceptions.InvalidOrderByException import InvalidOrderByException
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def build(order_by_dto, dao_mapper):
|
|
6
|
+
if order_by_dto is None:
|
|
7
|
+
return None
|
|
8
|
+
|
|
9
|
+
to = []
|
|
10
|
+
for order_by in order_by_dto:
|
|
11
|
+
try:
|
|
12
|
+
parts = order_by.split(':', 2)
|
|
13
|
+
column = dao_mapper(parts[0])
|
|
14
|
+
to.append(__add_sorting_order(parts[1], column))
|
|
15
|
+
except IndexError as e:
|
|
16
|
+
raise InvalidOrderByException("Order by dto is invalid cannot be convert to sqlalchemy order by", e)
|
|
17
|
+
return to
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def __add_sorting_order(sorting_order, column):
|
|
21
|
+
if sorting_order.upper() == 'DESC':
|
|
22
|
+
return desc(column)
|
|
23
|
+
elif sorting_order.upper() == 'ASC':
|
|
24
|
+
return asc(column)
|
|
25
|
+
else:
|
|
26
|
+
raise InvalidOrderByException(f"{sorting_order.upper()} is neither ASC or DESC")
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime, timezone, date
|
|
3
|
+
|
|
4
|
+
import sqlalchemy as sa
|
|
5
|
+
import sqlalchemy.types as types
|
|
6
|
+
from dataclasses_json import dataclass_json
|
|
7
|
+
from dataclasses_json.mm import TYPES, _IsoField
|
|
8
|
+
from sqlalchemy import create_engine
|
|
9
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
10
|
+
from sqlalchemy.ext.asyncio import async_scoped_session
|
|
11
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
12
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
13
|
+
from sqlalchemy.orm import sessionmaker, scoped_session
|
|
14
|
+
|
|
15
|
+
LOCAL_TIMEZONE = datetime.now(timezone.utc).astimezone().tzinfo
|
|
16
|
+
# See : https://mike.depalatis.net/blog/sqlalchemy-timestamps.html
|
|
17
|
+
class Timestamp(sa.types.TypeDecorator):
|
|
18
|
+
impl = sa.types.DateTime
|
|
19
|
+
|
|
20
|
+
cache_ok = True
|
|
21
|
+
|
|
22
|
+
def process_bind_param(self, value: datetime, dialect):
|
|
23
|
+
if value is None:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
if value.tzinfo is None:
|
|
27
|
+
value = value.astimezone(LOCAL_TIMEZONE)
|
|
28
|
+
|
|
29
|
+
value = value.astimezone(timezone.utc)
|
|
30
|
+
|
|
31
|
+
if self.timezone:
|
|
32
|
+
return value
|
|
33
|
+
else:
|
|
34
|
+
return value.replace(tzinfo=None)
|
|
35
|
+
|
|
36
|
+
def process_result_value(self, value, dialect):
|
|
37
|
+
if value is None:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
if value.tzinfo is None:
|
|
41
|
+
return value.replace(tzinfo=timezone.utc)
|
|
42
|
+
|
|
43
|
+
return value.astimezone(timezone.utc)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Customize encoder/decoder to use isoformat to store datetime objects (See : https://github.com/lidatong/dataclasses-json/issues/332)
|
|
47
|
+
TYPES[datetime] = _IsoField
|
|
48
|
+
|
|
49
|
+
def JSONBDataclass(cls, many=False):
|
|
50
|
+
serializable_cls = dataclass_json(cls)
|
|
51
|
+
schema = serializable_cls.schema(many=many)
|
|
52
|
+
|
|
53
|
+
def process_bind_param(self, value, dialect):
|
|
54
|
+
if value is None:
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
return schema.dump(value)
|
|
58
|
+
|
|
59
|
+
def process_result_value(self, value, dialect):
|
|
60
|
+
if value is None:
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
return schema.load(value)
|
|
64
|
+
|
|
65
|
+
def coerce_compared_value(self, op, value):
|
|
66
|
+
return self.impl.coerce_compared_value(op, value)
|
|
67
|
+
|
|
68
|
+
return type(cls.__name__, (types.TypeDecorator,), {
|
|
69
|
+
"impl": JSONB,
|
|
70
|
+
"process_bind_param": process_bind_param,
|
|
71
|
+
"process_result_value": process_result_value,
|
|
72
|
+
"cache_ok": True,
|
|
73
|
+
"coerce_compared_value": coerce_compared_value,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
import json
|
|
77
|
+
def json_serializer(x):
|
|
78
|
+
def default(obj):
|
|
79
|
+
"""JSON serializer for objects not serializable by default json code"""
|
|
80
|
+
|
|
81
|
+
if isinstance(obj, (datetime, date)):
|
|
82
|
+
return obj.isoformat()
|
|
83
|
+
raise TypeError("Type %s not serializable" % type(obj))
|
|
84
|
+
|
|
85
|
+
return json.dumps(x, default=default)
|
|
86
|
+
|
|
87
|
+
class PostgresConnector(object):
|
|
88
|
+
def __init__(self, host, db_name, username, password,
|
|
89
|
+
pool_size=None,
|
|
90
|
+
max_overflow=None,
|
|
91
|
+
pool_timeout=None,
|
|
92
|
+
pool_recycle=None):
|
|
93
|
+
"""
|
|
94
|
+
Args:
|
|
95
|
+
host: Database host
|
|
96
|
+
db_name: Database name
|
|
97
|
+
username: Database username
|
|
98
|
+
password: Database password
|
|
99
|
+
pool_size: Number of permanent connections (env: DB_POOL_SIZE, default: 5)
|
|
100
|
+
HikariCP equivalent: minimumIdle = 5
|
|
101
|
+
max_overflow: Max additional connections beyond pool_size (env: DB_MAX_OVERFLOW, default: 5)
|
|
102
|
+
HikariCP equivalent: maximumPoolSize - minimumIdle = 10 - 5 = 5
|
|
103
|
+
pool_timeout: Seconds to wait for a connection (env: DB_POOL_TIMEOUT, default: 30)
|
|
104
|
+
HikariCP equivalent: connectionTimeout (not set in current config, recommended: 30000ms)
|
|
105
|
+
pool_recycle: Seconds before recycling a connection (env: DB_POOL_RECYCLE, default: 480)
|
|
106
|
+
HikariCP equivalent: maxLifetime = 480000ms (8 minutes)
|
|
107
|
+
"""
|
|
108
|
+
self.host = host
|
|
109
|
+
self.username = username
|
|
110
|
+
self.password = password
|
|
111
|
+
self.db_name = db_name
|
|
112
|
+
self.pool_size = pool_size if pool_size is not None else int(os.getenv('DB_POOL_SIZE', '5'))
|
|
113
|
+
self.max_overflow = max_overflow if max_overflow is not None else int(os.getenv('DB_MAX_OVERFLOW', '5'))
|
|
114
|
+
self.pool_timeout = pool_timeout if pool_timeout is not None else int(os.getenv('DB_POOL_TIMEOUT', '30'))
|
|
115
|
+
self.pool_recycle = pool_recycle if pool_recycle is not None else int(os.getenv('DB_POOL_RECYCLE', '480'))
|
|
116
|
+
self._engine = None
|
|
117
|
+
self._session_factory = None
|
|
118
|
+
|
|
119
|
+
def get_engine(self):
|
|
120
|
+
if not self._engine:
|
|
121
|
+
# https://docs.sqlalchemy.org/en/14/core/pooling.html#dealing-with-disconnects
|
|
122
|
+
self._engine = create_engine(self.get_url(),
|
|
123
|
+
pool_pre_ping=True,
|
|
124
|
+
pool_size=self.pool_size,
|
|
125
|
+
max_overflow=self.max_overflow,
|
|
126
|
+
pool_timeout=self.pool_timeout,
|
|
127
|
+
pool_recycle=self.pool_recycle,
|
|
128
|
+
pool_use_lifo=True,
|
|
129
|
+
connect_args={},
|
|
130
|
+
json_serializer=json_serializer
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return self._engine
|
|
134
|
+
|
|
135
|
+
def get_scoped_session(self, _ident_func):
|
|
136
|
+
return scoped_session(self.get_session_factory(), scopefunc=_ident_func)
|
|
137
|
+
|
|
138
|
+
def get_session_factory(self):
|
|
139
|
+
if not self._session_factory:
|
|
140
|
+
self._session_factory = sessionmaker(bind=self.get_engine())
|
|
141
|
+
return self._session_factory
|
|
142
|
+
|
|
143
|
+
def get_url(self) -> str:
|
|
144
|
+
return "postgresql+psycopg2://{}:{}@{}/{}".format(
|
|
145
|
+
self.username,
|
|
146
|
+
self.password,
|
|
147
|
+
self.host,
|
|
148
|
+
self.db_name
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class AsyncPostgresConnector(object):
|
|
153
|
+
def __init__(self, host, db_name, username, password,
|
|
154
|
+
pool_size=None,
|
|
155
|
+
max_overflow=None,
|
|
156
|
+
pool_timeout=None,
|
|
157
|
+
pool_recycle=None):
|
|
158
|
+
"""
|
|
159
|
+
Args:
|
|
160
|
+
host: Database host
|
|
161
|
+
db_name: Database name
|
|
162
|
+
username: Database username
|
|
163
|
+
password: Database password
|
|
164
|
+
pool_size: Number of permanent connections (env: DB_POOL_SIZE, default: 5)
|
|
165
|
+
HikariCP equivalent: minimumIdle = 5
|
|
166
|
+
max_overflow: Max additional connections beyond pool_size (env: DB_MAX_OVERFLOW, default: 5)
|
|
167
|
+
HikariCP equivalent: maximumPoolSize - minimumIdle = 10 - 5 = 5
|
|
168
|
+
pool_timeout: Seconds to wait for a connection (env: DB_POOL_TIMEOUT, default: 30)
|
|
169
|
+
HikariCP equivalent: connectionTimeout (not set in current config, recommended: 30000ms)
|
|
170
|
+
pool_recycle: Seconds before recycling a connection (env: DB_POOL_RECYCLE, default: 480)
|
|
171
|
+
HikariCP equivalent: maxLifetime = 480000ms (8 minutes)
|
|
172
|
+
"""
|
|
173
|
+
self.host = host
|
|
174
|
+
self.username = username
|
|
175
|
+
self.password = password
|
|
176
|
+
self.db_name = db_name
|
|
177
|
+
self.pool_size = pool_size if pool_size is not None else int(os.getenv('DB_POOL_SIZE', '5'))
|
|
178
|
+
self.max_overflow = max_overflow if max_overflow is not None else int(os.getenv('DB_MAX_OVERFLOW', '5'))
|
|
179
|
+
self.pool_timeout = pool_timeout if pool_timeout is not None else int(os.getenv('DB_POOL_TIMEOUT', '30'))
|
|
180
|
+
self.pool_recycle = pool_recycle if pool_recycle is not None else int(os.getenv('DB_POOL_RECYCLE', '480'))
|
|
181
|
+
self._engine = None
|
|
182
|
+
self._session_factory = None
|
|
183
|
+
|
|
184
|
+
def get_engine(self):
|
|
185
|
+
if not self._engine:
|
|
186
|
+
# https://docs.sqlalchemy.org/en/14/core/pooling.html#dealing-with-disconnects
|
|
187
|
+
self._engine = create_async_engine(self.get_url(),
|
|
188
|
+
pool_pre_ping=True,
|
|
189
|
+
pool_size=self.pool_size,
|
|
190
|
+
max_overflow=self.max_overflow,
|
|
191
|
+
pool_timeout=self.pool_timeout,
|
|
192
|
+
pool_recycle=self.pool_recycle,
|
|
193
|
+
pool_use_lifo=True,
|
|
194
|
+
connect_args={},
|
|
195
|
+
json_serializer=json_serializer
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
return self._engine
|
|
199
|
+
|
|
200
|
+
def get_scoped_session(self, _ident_func):
|
|
201
|
+
return async_scoped_session(self.get_session_factory(), scopefunc=_ident_func)
|
|
202
|
+
|
|
203
|
+
def get_session_factory(self):
|
|
204
|
+
if not self._session_factory:
|
|
205
|
+
self._session_factory = async_sessionmaker(bind=self.get_engine())
|
|
206
|
+
return self._session_factory
|
|
207
|
+
|
|
208
|
+
def get_url(self) -> str:
|
|
209
|
+
return "postgresql+asyncpg://{}:{}@{}/{}".format(
|
|
210
|
+
self.username,
|
|
211
|
+
self.password,
|
|
212
|
+
self.host,
|
|
213
|
+
self.db_name
|
|
214
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from sqlalchemy import func
|
|
2
|
+
import asyncio
|
|
3
|
+
import nest_asyncio
|
|
4
|
+
|
|
5
|
+
# Avoid RuntimeError: This event loop is already running
|
|
6
|
+
# https://github.com/erdewit/nest_asyncio
|
|
7
|
+
nest_asyncio.apply()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SearchResult:
|
|
11
|
+
_items: list
|
|
12
|
+
_total_found: int
|
|
13
|
+
_total_visible: int
|
|
14
|
+
|
|
15
|
+
def __init__(self, items=None, total_visible=None, total_found=None):
|
|
16
|
+
self._items = items
|
|
17
|
+
self._total_visible = total_visible
|
|
18
|
+
self._total_found = total_found
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def items(self):
|
|
22
|
+
return self._items
|
|
23
|
+
|
|
24
|
+
@items.setter
|
|
25
|
+
def items(self, items):
|
|
26
|
+
self._items = items
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def total_visible(self):
|
|
30
|
+
return self._total_visible
|
|
31
|
+
|
|
32
|
+
@total_visible.setter
|
|
33
|
+
def total_visible(self, total_visible):
|
|
34
|
+
self._total_visible = total_visible
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def total_found(self):
|
|
38
|
+
return self._total_found
|
|
39
|
+
|
|
40
|
+
@total_found.setter
|
|
41
|
+
def total_found(self, total_found):
|
|
42
|
+
self._total_found = total_found
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class QueryExecutor:
|
|
46
|
+
def __init__(self, model):
|
|
47
|
+
self._model = model
|
|
48
|
+
|
|
49
|
+
async def __get_query_count(self, query):
|
|
50
|
+
counter = query.statement.with_only_columns(func.count()).select_from(self._model).order_by(None)
|
|
51
|
+
return query.session.execute(counter).scalar()
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
async def __get_query_result(query):
|
|
55
|
+
return query.all()
|
|
56
|
+
|
|
57
|
+
async def __execute(self, query, order_bys, page, limit):
|
|
58
|
+
if not page:
|
|
59
|
+
page = 0
|
|
60
|
+
if not order_bys:
|
|
61
|
+
order_bys = []
|
|
62
|
+
|
|
63
|
+
count_query = query
|
|
64
|
+
if order_bys:
|
|
65
|
+
query = query.order_by(*order_bys)
|
|
66
|
+
|
|
67
|
+
if limit:
|
|
68
|
+
query = query.limit(limit).offset(page * limit)
|
|
69
|
+
|
|
70
|
+
async with asyncio.TaskGroup() as tg:
|
|
71
|
+
items_task = tg.create_task(self.__get_query_result(query))
|
|
72
|
+
count_task = tg.create_task(self.__get_query_count(count_query))
|
|
73
|
+
return SearchResult(items_task.result(), count_task.result(), count_task.result())
|
|
74
|
+
|
|
75
|
+
def execute(self, query, order_bys=None, page=None, limit=None):
|
|
76
|
+
return asyncio.run(self.__execute(query, order_bys, page, limit))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class QueryExecutorAsync:
|
|
80
|
+
def __init__(self, model):
|
|
81
|
+
self._model = model
|
|
82
|
+
|
|
83
|
+
async def __get_query_count(self, get_db_session, query):
|
|
84
|
+
counter = query.with_only_columns(func.count()).select_from(self._model).order_by(None)
|
|
85
|
+
result = await get_db_session().execute(counter)
|
|
86
|
+
return result.scalar_one()
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
async def __get_query_result(get_db_session, query):
|
|
90
|
+
result = await get_db_session().execute(query)
|
|
91
|
+
return result.scalars().all()
|
|
92
|
+
|
|
93
|
+
async def __execute(self, get_db_session, query, order_bys, page, limit):
|
|
94
|
+
if not page:
|
|
95
|
+
page = 0
|
|
96
|
+
if not order_bys:
|
|
97
|
+
order_bys = []
|
|
98
|
+
|
|
99
|
+
count_query = query
|
|
100
|
+
if order_bys:
|
|
101
|
+
query = query.order_by(*order_bys)
|
|
102
|
+
|
|
103
|
+
if limit:
|
|
104
|
+
query = query.limit(limit).offset(page * limit)
|
|
105
|
+
|
|
106
|
+
async with asyncio.TaskGroup() as tg:
|
|
107
|
+
count_task = tg.create_task(self.__get_query_count(get_db_session, count_query))
|
|
108
|
+
items = await self.__get_query_result(get_db_session, query)
|
|
109
|
+
# Important note : As count is run in a parallel task, the returned value will not take into account modifications
|
|
110
|
+
# of DB state in the current task session (ex : object have been updated before, without transaction commit)
|
|
111
|
+
await asyncio.shield(count_task)
|
|
112
|
+
return SearchResult(items, count_task.result(), count_task.result())
|
|
113
|
+
|
|
114
|
+
# Pass get_db_session as a method because we need to call two distinct session for counts and values
|
|
115
|
+
async def execute(self, get_db_session, query, order_bys=None, page=None, limit=None):
|
|
116
|
+
return await self.__execute(get_db_session, query, order_bys, page, limit)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "lcdp-sqlalchemy-utils"
|
|
3
|
+
# https://github.com/python-poetry/poetry/issues/1208
|
|
4
|
+
version = "1.5.8"
|
|
5
|
+
description = "SQLAlchemy Utils"
|
|
6
|
+
authors = ["Le Comptoir Des Pharmacies <webmaster@lecomptoirdespharmacies.fr>"]
|
|
7
|
+
|
|
8
|
+
[tool.poetry-dynamic-versioning]
|
|
9
|
+
enable = false
|
|
10
|
+
vcs = "git"
|
|
11
|
+
|
|
12
|
+
[tool.poetry.requires-plugins]
|
|
13
|
+
poetry-dynamic-versioning = { version = ">=1.0.0,<2.0.0", extras = ["plugin"] }
|
|
14
|
+
|
|
15
|
+
[tool.poetry.dependencies]
|
|
16
|
+
python = ">=3.8"
|
|
17
|
+
SQLAlchemy = "2.0.40"
|
|
18
|
+
alembic="1.8.1"
|
|
19
|
+
dataclasses-json = "0.5.7"
|
|
20
|
+
nest_asyncio = "1.5.8"
|
|
21
|
+
|
|
22
|
+
[tool.poetry.dev-dependencies]
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
|
|
26
|
+
build-backend = "poetry_dynamic_versioning.backend"
|