iker-python-common 1.0.49__py3-none-any.whl → 1.0.50__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iker/common/utils/dbutils.py +94 -100
- iker/common/utils/dtutils.py +22 -0
- iker/common/utils/testutils.py +1 -1
- {iker_python_common-1.0.49.dist-info → iker_python_common-1.0.50.dist-info}/METADATA +1 -1
- {iker_python_common-1.0.49.dist-info → iker_python_common-1.0.50.dist-info}/RECORD +7 -7
- {iker_python_common-1.0.49.dist-info → iker_python_common-1.0.50.dist-info}/WHEEL +0 -0
- {iker_python_common-1.0.49.dist-info → iker_python_common-1.0.50.dist-info}/top_level.txt +0 -0
iker/common/utils/dbutils.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import dataclasses
|
|
3
|
-
import
|
|
4
|
-
from typing import Any
|
|
3
|
+
from typing import Any, Sequence
|
|
5
4
|
|
|
6
5
|
import packaging.version
|
|
7
6
|
import psycopg
|
|
@@ -10,12 +9,13 @@ import sqlalchemy
|
|
|
10
9
|
import sqlalchemy.ext.compiler
|
|
11
10
|
import sqlalchemy.orm
|
|
12
11
|
|
|
13
|
-
from iker.common.utils.
|
|
14
|
-
from iker.common.utils.strutils import is_blank
|
|
12
|
+
from iker.common.utils.jsonutils import JsonType
|
|
15
13
|
|
|
16
14
|
__all__ = [
|
|
15
|
+
"Dialects",
|
|
16
|
+
"Drivers",
|
|
17
|
+
"make_scheme",
|
|
17
18
|
"ConnectionMaker",
|
|
18
|
-
"DBAdapter",
|
|
19
19
|
"orm_to_dict",
|
|
20
20
|
"orm_clone",
|
|
21
21
|
"mysql_insert_ignore",
|
|
@@ -23,87 +23,73 @@ __all__ = [
|
|
|
23
23
|
]
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
class Dialects:
|
|
27
|
+
mysql = "mysql"
|
|
28
|
+
postgresql = "postgresql"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Drivers:
|
|
32
|
+
pymysql = pymysql.__name__
|
|
33
|
+
psycopg = psycopg.__name__
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def make_scheme(dialect: str, driver: str | None = None) -> str:
|
|
37
|
+
"""
|
|
38
|
+
Constructs a SQLAlchemy scheme string based on the provided dialect and driver.
|
|
39
|
+
|
|
40
|
+
:param dialect: The database dialect (e.g., 'mysql', 'postgresql').
|
|
41
|
+
:param driver: Optional database driver (e.g., 'pymysql', 'psycopg').
|
|
42
|
+
:return: A SQLAlchemy scheme string.
|
|
43
|
+
"""
|
|
44
|
+
return dialect if driver is None else f"{dialect}+{driver}"
|
|
45
|
+
|
|
46
|
+
|
|
26
47
|
class ConnectionMaker(object):
|
|
27
48
|
"""
|
|
28
49
|
Provides utilities to simplify establishing database connections and sessions, including connection string
|
|
29
50
|
construction, engine and session creation, and model management.
|
|
30
51
|
|
|
31
|
-
:param
|
|
32
|
-
:param host: The database host.
|
|
33
|
-
:param port: The database port.
|
|
34
|
-
:param user: The database user.
|
|
35
|
-
:param password: The database password.
|
|
36
|
-
:param database: The database name.
|
|
52
|
+
:param url: A SQLAlchemy URL string or ``URL`` object representing the database connection.
|
|
37
53
|
:param engine_opts: Optional dictionary of SQLAlchemy engine options.
|
|
38
54
|
:param session_opts: Optional dictionary of SQLAlchemy session options.
|
|
39
55
|
"""
|
|
40
56
|
|
|
41
|
-
class Drivers:
|
|
42
|
-
Mysql = f"mysql+{pymysql.__name__}"
|
|
43
|
-
Postgresql = f"postgresql+{psycopg.__name__}"
|
|
44
|
-
|
|
45
57
|
def __init__(
|
|
46
58
|
self,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
password: str,
|
|
52
|
-
database: str,
|
|
53
|
-
engine_opts: dict[str, Any] | None = None,
|
|
54
|
-
session_opts: dict[str, Any] | None = None,
|
|
59
|
+
url: sqlalchemy.URL,
|
|
60
|
+
*,
|
|
61
|
+
engine_opts: dict[str, JsonType] | None = None,
|
|
62
|
+
session_opts: dict[str, JsonType] | None = None,
|
|
55
63
|
):
|
|
56
|
-
self.
|
|
57
|
-
self.host = host
|
|
58
|
-
self.port = port
|
|
59
|
-
self.user = user
|
|
60
|
-
self.password = password
|
|
61
|
-
self.database = database
|
|
64
|
+
self.url = url
|
|
62
65
|
self.engine_opts = engine_opts or {}
|
|
63
66
|
self.session_opts = session_opts or {}
|
|
64
67
|
|
|
65
68
|
@staticmethod
|
|
66
69
|
def from_url(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
engine_opts: dict[str,
|
|
70
|
-
session_opts: dict[str,
|
|
70
|
+
url: str | sqlalchemy.URL,
|
|
71
|
+
*,
|
|
72
|
+
engine_opts: dict[str, JsonType] | None = None,
|
|
73
|
+
session_opts: dict[str, JsonType] | None = None,
|
|
71
74
|
) -> "ConnectionMaker":
|
|
72
75
|
"""
|
|
73
|
-
Creates a ``ConnectionMaker``
|
|
76
|
+
Creates a new instance of ``ConnectionMaker`` from a SQLAlchemy URL string or object.
|
|
74
77
|
|
|
75
|
-
:param
|
|
76
|
-
:param url: The database URL as a string or ``ParseResult``.
|
|
78
|
+
:param url: A SQLAlchemy URL string or ``URL`` object representing the database connection.
|
|
77
79
|
:param engine_opts: Optional dictionary of SQLAlchemy engine options.
|
|
78
80
|
:param session_opts: Optional dictionary of SQLAlchemy session options.
|
|
79
|
-
:return: A ``ConnectionMaker``
|
|
81
|
+
:return: A new instance of ``ConnectionMaker`` configured with the provided URL and options.
|
|
80
82
|
"""
|
|
81
|
-
|
|
82
|
-
return ConnectionMaker.from_url(driver, urllib.parse.urlparse(url), engine_opts, session_opts)
|
|
83
|
-
if isinstance(url, urllib.parse.ParseResult):
|
|
84
|
-
return ConnectionMaker(driver,
|
|
85
|
-
url.hostname,
|
|
86
|
-
url.port,
|
|
87
|
-
url.username,
|
|
88
|
-
url.password,
|
|
89
|
-
url.path.strip("/"),
|
|
90
|
-
engine_opts,
|
|
91
|
-
session_opts)
|
|
92
|
-
raise ValueError("malformed parameter 'url'")
|
|
83
|
+
return ConnectionMaker(sqlalchemy.make_url(url), engine_opts=engine_opts, session_opts=session_opts)
|
|
93
84
|
|
|
94
85
|
@property
|
|
95
86
|
def connection_string(self) -> str:
|
|
96
87
|
"""
|
|
97
|
-
Constructs
|
|
88
|
+
Constructs a SQLAlchemy connection string for the database using the provided parameters.
|
|
98
89
|
|
|
99
|
-
:return:
|
|
90
|
+
:return: A string representing the database connection.
|
|
100
91
|
"""
|
|
101
|
-
|
|
102
|
-
user_part = urllib.parse.quote(self.user, safe="")
|
|
103
|
-
password_part = "" if is_blank(self.password) else (":%s" % urllib.parse.quote(self.password, safe=""))
|
|
104
|
-
database_part = urllib.parse.quote(self.database, safe="")
|
|
105
|
-
|
|
106
|
-
return f"{self.driver}://{user_part}{password_part}@{self.host}{port_part}/{database_part}"
|
|
92
|
+
return self.url.render_as_string(hide_password=False)
|
|
107
93
|
|
|
108
94
|
@property
|
|
109
95
|
def engine(self) -> sqlalchemy.Engine:
|
|
@@ -114,7 +100,7 @@ class ConnectionMaker(object):
|
|
|
114
100
|
"""
|
|
115
101
|
return sqlalchemy.create_engine(self.connection_string, **self.engine_opts)
|
|
116
102
|
|
|
117
|
-
def make_connection(self):
|
|
103
|
+
def make_connection(self) -> sqlalchemy.Connection:
|
|
118
104
|
"""
|
|
119
105
|
Establishes and returns a new database connection using the SQLAlchemy engine.
|
|
120
106
|
|
|
@@ -165,11 +151,11 @@ class ConnectionMaker(object):
|
|
|
165
151
|
:param params: The parameters dictionary for the SQL statement.
|
|
166
152
|
:return: None.
|
|
167
153
|
"""
|
|
168
|
-
with self.
|
|
169
|
-
|
|
170
|
-
|
|
154
|
+
with self.make_session() as session:
|
|
155
|
+
session.execute(sqlalchemy.text(sql), params)
|
|
156
|
+
session.commit()
|
|
171
157
|
|
|
172
|
-
def query_all(self, sql: str, **params) ->
|
|
158
|
+
def query_all(self, sql: str, **params) -> Sequence[sqlalchemy.Row[tuple[Any, ...]]]:
|
|
173
159
|
"""
|
|
174
160
|
Executes the given SQL query with the specified parameters and returns all result tuples.
|
|
175
161
|
|
|
@@ -177,23 +163,22 @@ class ConnectionMaker(object):
|
|
|
177
163
|
:param params: The parameters dictionary for the SQL query.
|
|
178
164
|
:return: A list of result tuples.
|
|
179
165
|
"""
|
|
180
|
-
with self.
|
|
181
|
-
|
|
182
|
-
|
|
166
|
+
with self.make_session() as session:
|
|
167
|
+
result = session.execute(sqlalchemy.text(sql), params)
|
|
168
|
+
return result.all()
|
|
183
169
|
|
|
184
|
-
def query_first(self, sql: str, **params) -> tuple | None:
|
|
170
|
+
def query_first(self, sql: str, **params) -> sqlalchemy.Row[tuple[Any, ...]] | None:
|
|
185
171
|
"""
|
|
186
|
-
Executes the given SQL query with the specified parameters and returns the first result tuple, or ``None``
|
|
187
|
-
results are found.
|
|
172
|
+
Executes the given SQL query with the specified parameters and returns the first result tuple, or ``None``
|
|
173
|
+
if no results are found.
|
|
188
174
|
|
|
189
175
|
:param sql: The SQL query to execute.
|
|
190
176
|
:param params: The parameters dictionary for the SQL query.
|
|
191
177
|
:return: The first result tuple, or ``None`` if no results are found.
|
|
192
178
|
"""
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
DBAdapter = ConnectionMaker
|
|
179
|
+
with self.make_session() as session:
|
|
180
|
+
result = session.execute(sqlalchemy.text(sql), params)
|
|
181
|
+
return result.first()
|
|
197
182
|
|
|
198
183
|
|
|
199
184
|
def orm_to_dict(orm, exclude: set[str] = None) -> dict[str, Any]:
|
|
@@ -241,40 +226,49 @@ def orm_clone(orm, exclude: set[str] = None, no_autoincrement: bool = False):
|
|
|
241
226
|
return new_orm
|
|
242
227
|
|
|
243
228
|
|
|
244
|
-
|
|
229
|
+
@contextlib.contextmanager
|
|
230
|
+
def mysql_insert_ignore():
|
|
245
231
|
"""
|
|
246
|
-
Registers a SQLAlchemy compiler extension to add ``IGNORE`` to MySQL ``INSERT`` statements
|
|
247
|
-
|
|
248
|
-
:param enabled: Whether to enable the ``IGNORE`` prefix for MySQL ``INSERT`` statements.
|
|
249
|
-
:return: None.
|
|
232
|
+
Registers a SQLAlchemy compiler extension to add ``IGNORE`` to MySQL ``INSERT`` statements.
|
|
250
233
|
"""
|
|
251
234
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
235
|
+
def context(enabled: bool):
|
|
236
|
+
@sqlalchemy.ext.compiler.compiles(sqlalchemy.sql.Insert, Dialects.mysql)
|
|
237
|
+
def dispatch(insert: sqlalchemy.sql.Insert, compiler: sqlalchemy.sql.compiler.SQLCompiler, **kwargs) -> str:
|
|
238
|
+
if not enabled:
|
|
239
|
+
return compiler.visit_insert(insert, **kwargs)
|
|
256
240
|
|
|
257
|
-
|
|
241
|
+
return compiler.visit_insert(insert.prefix_with("IGNORE"), **kwargs)
|
|
258
242
|
|
|
243
|
+
context(True)
|
|
244
|
+
try:
|
|
245
|
+
yield
|
|
246
|
+
finally:
|
|
247
|
+
context(False)
|
|
259
248
|
|
|
260
|
-
def postgresql_insert_on_conflict_do_nothing(enabled: bool = True):
|
|
261
|
-
"""
|
|
262
|
-
Registers a SQLAlchemy compiler extension to add ``ON CONFLICT DO NOTHING`` to PostgreSQL ``INSERT`` statements if
|
|
263
|
-
``enabled``.
|
|
264
249
|
|
|
265
|
-
|
|
266
|
-
|
|
250
|
+
@contextlib.contextmanager
|
|
251
|
+
def postgresql_insert_on_conflict_do_nothing():
|
|
252
|
+
"""
|
|
253
|
+
Registers a SQLAlchemy compiler extension to add ``ON CONFLICT DO NOTHING`` to Postgresql ``INSERT`` statements.
|
|
267
254
|
"""
|
|
268
255
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
256
|
+
def context(enabled: bool):
|
|
257
|
+
@sqlalchemy.ext.compiler.compiles(sqlalchemy.sql.Insert, Dialects.postgresql)
|
|
258
|
+
def dispatch(insert: sqlalchemy.sql.Insert, compiler: sqlalchemy.sql.compiler.SQLCompiler, **kwargs) -> str:
|
|
259
|
+
if not enabled:
|
|
260
|
+
return compiler.visit_insert(insert, **kwargs)
|
|
261
|
+
|
|
262
|
+
statement = compiler.visit_insert(insert, **kwargs)
|
|
263
|
+
# If we have a ``RETURNING`` clause, we must insert before it
|
|
264
|
+
returning_position = statement.find("RETURNING")
|
|
265
|
+
if returning_position >= 0:
|
|
266
|
+
return statement[:returning_position] + " ON CONFLICT DO NOTHING " + statement[returning_position:]
|
|
267
|
+
else:
|
|
268
|
+
return statement + " ON CONFLICT DO NOTHING"
|
|
269
|
+
|
|
270
|
+
context(True)
|
|
271
|
+
try:
|
|
272
|
+
yield
|
|
273
|
+
finally:
|
|
274
|
+
context(False)
|
iker/common/utils/dtutils.py
CHANGED
|
@@ -24,6 +24,8 @@ __all__ = [
|
|
|
24
24
|
"dt_utc",
|
|
25
25
|
"td_to_us",
|
|
26
26
|
"td_from_us",
|
|
27
|
+
"td_to_time",
|
|
28
|
+
"td_from_time",
|
|
27
29
|
"dt_to_ts",
|
|
28
30
|
"dt_to_ts_us",
|
|
29
31
|
"dt_from_ts",
|
|
@@ -229,6 +231,26 @@ def td_from_us(us: int) -> datetime.timedelta:
|
|
|
229
231
|
return datetime.timedelta(microseconds=us)
|
|
230
232
|
|
|
231
233
|
|
|
234
|
+
def td_to_time(td: datetime.timedelta) -> datetime.time:
|
|
235
|
+
"""
|
|
236
|
+
Returns a ``time`` object representing the given ``timedelta``.
|
|
237
|
+
|
|
238
|
+
:param td: The ``timedelta`` to convert.
|
|
239
|
+
:return: The corresponding ``time`` object.
|
|
240
|
+
"""
|
|
241
|
+
return (dt_utc_min() + td).timetz()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def td_from_time(t: datetime.time) -> datetime.timedelta:
|
|
245
|
+
"""
|
|
246
|
+
Returns a ``timedelta`` representing the given ``time``.
|
|
247
|
+
|
|
248
|
+
:param t: The ``time`` to convert.
|
|
249
|
+
:return: The corresponding ``timedelta``.
|
|
250
|
+
"""
|
|
251
|
+
return datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second, microseconds=t.microsecond)
|
|
252
|
+
|
|
253
|
+
|
|
232
254
|
def dt_to_td(dt: datetime.datetime) -> datetime.timedelta:
|
|
233
255
|
"""
|
|
234
256
|
Returns the ``timedelta`` between the given ``datetime`` and the POSIX epoch.
|
iker/common/utils/testutils.py
CHANGED
|
@@ -69,7 +69,7 @@ class ApproxNestedMapping(ApproxNestedMixin, ApproxMapping):
|
|
|
69
69
|
yield from mapping._yield_comparisons(actual[k])
|
|
70
70
|
|
|
71
71
|
|
|
72
|
-
def nested_approx(expected, rel: float = None, abs: float = None, nan_ok: bool = False):
|
|
72
|
+
def nested_approx(expected, rel: float = None, abs: float = None, nan_ok: bool = False) -> ApproxBase:
|
|
73
73
|
if isinstance(expected, dict):
|
|
74
74
|
return ApproxNestedMapping(expected, rel, abs, nan_ok)
|
|
75
75
|
if isinstance(expected, (tuple, list)):
|
|
@@ -3,9 +3,9 @@ iker/common/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
|
|
|
3
3
|
iker/common/utils/argutils.py,sha256=hMLNqdZs_Kjc2hw4Npm6N47RivP6JRNzCKIbJr1jYy8,9274
|
|
4
4
|
iker/common/utils/config.py,sha256=z8rLqli961A-qAV9EaELp-pKuhNUNaq1Btdv-uwG7_I,4690
|
|
5
5
|
iker/common/utils/csv.py,sha256=_V9OUrKcojec2L-hWagEIVnL2uvGjyJAFTrD7tHNr48,7573
|
|
6
|
-
iker/common/utils/dbutils.py,sha256=
|
|
6
|
+
iker/common/utils/dbutils.py,sha256=q-rZ9KNFxAEsSXWcqcihQTsqiDlWbSFHZ5XGVPj2tnk,10335
|
|
7
7
|
iker/common/utils/dockerutils.py,sha256=n2WuzXaZB6_WocSljvPOnfExSIjIHRUbuWp2oBbaPKQ,8004
|
|
8
|
-
iker/common/utils/dtutils.py,sha256=
|
|
8
|
+
iker/common/utils/dtutils.py,sha256=86vbaa4pgcBWERZvTfJ92PKB3IimxP6tf0O11ho2Ffk,12554
|
|
9
9
|
iker/common/utils/funcutils.py,sha256=A08f5wjoLgLQKyRJcYeWJnqVm2QcerIx0l-Se2600bc,5869
|
|
10
10
|
iker/common/utils/jsonutils.py,sha256=xYKimWtsqQKiQDQr3EMIhpGrmWSNPzgZR1Sdz60CxSo,16536
|
|
11
11
|
iker/common/utils/logger.py,sha256=FJaai6Sbchy4wKHcUMUCrrkBcXvIxq4qByERZ_TJBps,3881
|
|
@@ -17,9 +17,9 @@ iker/common/utils/sequtils.py,sha256=MfYL82TygBRlGK4sw0RJcoB5P3hO6l7saVH5oZ3YDP8
|
|
|
17
17
|
iker/common/utils/shutils.py,sha256=44_Qkzkhrs9LsfDflsaY_4Va0IpVLU3o8K_NvqCB04w,7859
|
|
18
18
|
iker/common/utils/span.py,sha256=yiXqk86cLKxkMdkO3pAHEfU5bUvHsGo3p--pAWo_yfM,5999
|
|
19
19
|
iker/common/utils/strutils.py,sha256=Tu_qFeH3K-SfwvMxdrZAc9iLPV8ZmtX4ntyyFGNslf8,5094
|
|
20
|
-
iker/common/utils/testutils.py,sha256=
|
|
20
|
+
iker/common/utils/testutils.py,sha256=2VieV5yeCDntSKQSpIeyqRT8BZmZYE_ArMeQz3g7fXY,5568
|
|
21
21
|
iker/common/utils/typeutils.py,sha256=RVkYkFRgDrx77OHFH7PavMV0AIB0S8ly40rs4g7JWE4,8220
|
|
22
|
-
iker_python_common-1.0.
|
|
23
|
-
iker_python_common-1.0.
|
|
24
|
-
iker_python_common-1.0.
|
|
25
|
-
iker_python_common-1.0.
|
|
22
|
+
iker_python_common-1.0.50.dist-info/METADATA,sha256=-1LTQO70aQMjIiAieCqzoziAGeRaCE9DKd9WdXBmCrk,1001
|
|
23
|
+
iker_python_common-1.0.50.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
+
iker_python_common-1.0.50.dist-info/top_level.txt,sha256=4_B8Prfc_lxFafFYTQThIU1ZqOYQ4pHHHnJ_fQ_oHs8,5
|
|
25
|
+
iker_python_common-1.0.50.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|