iker-python-common 1.0.49__tar.gz → 1.0.51__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.
Files changed (84) hide show
  1. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/PKG-INFO +1 -1
  2. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/dbutils.py +127 -100
  3. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/dtutils.py +22 -0
  4. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/testutils.py +1 -1
  5. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker_python_common.egg-info/PKG-INFO +1 -1
  6. iker_python_common-1.0.51/test/iker_tests/common/utils/dbutils_test.py +1022 -0
  7. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/common/utils/dtutils_test.py +57 -1
  8. iker_python_common-1.0.49/test/iker_tests/common/utils/dbutils_test.py +0 -339
  9. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/.editorconfig +0 -0
  10. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/.github/workflows/pr.yml +0 -0
  11. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/.github/workflows/push.yml +0 -0
  12. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/.gitignore +0 -0
  13. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/MANIFEST.in +0 -0
  14. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/README.md +0 -0
  15. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/VERSION +0 -0
  16. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/pyproject.toml +0 -0
  17. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/config/config.cfg +0 -0
  18. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/csv/data.csv +0 -0
  19. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/csv/data.tsv +0 -0
  20. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/s3utils/dir.baz/file.bar.baz +0 -0
  21. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/s3utils/dir.baz/file.foo.bar +0 -0
  22. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/s3utils/dir.baz/file.foo.baz +0 -0
  23. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/s3utils/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
  24. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
  25. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
  26. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
  27. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/s3utils/dir.foo/file.bar +0 -0
  28. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/s3utils/dir.foo/file.baz +0 -0
  29. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/s3utils/dir.foo/file.foo +0 -0
  30. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/shutils/dir.baz/file.bar.baz +0 -0
  31. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/shutils/dir.baz/file.foo.bar +0 -0
  32. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/shutils/dir.baz/file.foo.baz +0 -0
  33. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/shutils/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
  34. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
  35. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
  36. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
  37. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/shutils/dir.foo/file.bar +0 -0
  38. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/shutils/dir.foo/file.baz +0 -0
  39. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/resources/unittest/shutils/dir.foo/file.foo +0 -0
  40. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/setup.cfg +0 -0
  41. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/setup.py +0 -0
  42. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/__init__.py +0 -0
  43. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/__init__.py +0 -0
  44. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/argutils.py +0 -0
  45. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/config.py +0 -0
  46. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/csv.py +0 -0
  47. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/dockerutils.py +0 -0
  48. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/funcutils.py +0 -0
  49. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/jsonutils.py +0 -0
  50. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/logger.py +0 -0
  51. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/numutils.py +0 -0
  52. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/randutils.py +0 -0
  53. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/retry.py +0 -0
  54. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/s3utils.py +0 -0
  55. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/sequtils.py +0 -0
  56. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/shutils.py +0 -0
  57. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/span.py +0 -0
  58. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/strutils.py +0 -0
  59. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker/common/utils/typeutils.py +0 -0
  60. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker_python_common.egg-info/SOURCES.txt +0 -0
  61. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker_python_common.egg-info/dependency_links.txt +0 -0
  62. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker_python_common.egg-info/not-zip-safe +0 -0
  63. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker_python_common.egg-info/requires.txt +0 -0
  64. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/src/iker_python_common.egg-info/top_level.txt +0 -0
  65. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_test.py +0 -0
  66. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/__init__.py +0 -0
  67. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/common/utils/argutils_test.py +0 -0
  68. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/common/utils/config_test.py +0 -0
  69. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/common/utils/csv_test.py +0 -0
  70. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/common/utils/dockerutils_test.py +0 -0
  71. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/common/utils/funcutils_test.py +0 -0
  72. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/common/utils/jsonutils_test.py +0 -0
  73. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/common/utils/logger_test.py +0 -0
  74. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/common/utils/numutils_test.py +0 -0
  75. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/common/utils/randutils_test.py +0 -0
  76. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/common/utils/retry_test.py +0 -0
  77. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/common/utils/s3utils_test.py +0 -0
  78. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/common/utils/sequtils_test.py +0 -0
  79. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/common/utils/shutils_test.py +0 -0
  80. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/common/utils/span_test.py +0 -0
  81. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/common/utils/strutils_test.py +0 -0
  82. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/common/utils/testutils_test.py +0 -0
  83. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/common/utils/typeutils_test.py +0 -0
  84. {iker_python_common-1.0.49 → iker_python_common-1.0.51}/test/iker_tests/docker_fixtures.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iker-python-common
3
- Version: 1.0.49
3
+ Version: 1.0.51
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.11
6
6
  Classifier: Programming Language :: Python :: 3.12
@@ -1,7 +1,6 @@
1
1
  import contextlib
2
2
  import dataclasses
3
- import urllib.parse
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.sequtils import head_or_none
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,106 @@ __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 driver: The database driver string (e.g., ``mysql+pymysql``).
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
- driver: str,
48
- host: str,
49
- port: int,
50
- user: str,
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.driver = driver
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
 
68
+ @staticmethod
69
+ def create(
70
+ driver: str | None = None,
71
+ host: str | None = None,
72
+ port: int | None = None,
73
+ username: str | None = None,
74
+ password: str | None = None,
75
+ database: str | None = None,
76
+ *,
77
+ engine_opts: dict[str, JsonType] | None = None,
78
+ session_opts: dict[str, JsonType] | None = None,
79
+ ):
80
+ """
81
+ Creates a new instance of ``ConnectionMaker`` using the provided parameters to construct a SQLAlchemy URL.
82
+
83
+ :param driver: Optional database driver.
84
+ :param host: The database host (e.g., 'localhost').
85
+ :param port: The database port.
86
+ :param username: The database username.
87
+ :param password: The database password.
88
+ :param database: The name of the database to connect to.
89
+ :param engine_opts: Optional dictionary of SQLAlchemy engine options.
90
+ :param session_opts: Optional dictionary of SQLAlchemy session options.
91
+ """
92
+ return ConnectionMaker(sqlalchemy.URL.create(drivername=driver,
93
+ host=host,
94
+ port=port,
95
+ username=username,
96
+ password=password,
97
+ database=database),
98
+ engine_opts=engine_opts,
99
+ session_opts=session_opts)
100
+
65
101
  @staticmethod
66
102
  def from_url(
67
- driver: str,
68
- url: str | urllib.parse.ParseResult,
69
- engine_opts: dict[str, Any] | None = None,
70
- session_opts: dict[str, Any] | None = None,
103
+ url: str | sqlalchemy.URL,
104
+ *,
105
+ engine_opts: dict[str, JsonType] | None = None,
106
+ session_opts: dict[str, JsonType] | None = None,
71
107
  ) -> "ConnectionMaker":
72
108
  """
73
- Creates a ``ConnectionMaker`` instance from a URL string or ``ParseResult``, extracting connection parameters.
109
+ Creates a new instance of ``ConnectionMaker`` from a SQLAlchemy URL string or object.
74
110
 
75
- :param driver: The database driver string.
76
- :param url: The database URL as a string or ``ParseResult``.
111
+ :param url: A SQLAlchemy URL string or ``URL`` object representing the database connection.
77
112
  :param engine_opts: Optional dictionary of SQLAlchemy engine options.
78
113
  :param session_opts: Optional dictionary of SQLAlchemy session options.
79
- :return: A ``ConnectionMaker`` instance.
114
+ :return: A new instance of ``ConnectionMaker`` configured with the provided URL and options.
80
115
  """
81
- if isinstance(url, str):
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'")
116
+ return ConnectionMaker(sqlalchemy.make_url(url), engine_opts=engine_opts, session_opts=session_opts)
93
117
 
94
118
  @property
95
119
  def connection_string(self) -> str:
96
120
  """
97
- Constructs the SQLAlchemy connection string for the database using the provided parameters.
121
+ Constructs a SQLAlchemy connection string for the database using the provided parameters.
98
122
 
99
- :return: The connection string as a string.
123
+ :return: A string representing the database connection.
100
124
  """
101
- port_part = "" if self.port is None else (":%d" % self.port)
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}"
125
+ return self.url.render_as_string(hide_password=False)
107
126
 
108
127
  @property
109
128
  def engine(self) -> sqlalchemy.Engine:
@@ -114,7 +133,7 @@ class ConnectionMaker(object):
114
133
  """
115
134
  return sqlalchemy.create_engine(self.connection_string, **self.engine_opts)
116
135
 
117
- def make_connection(self):
136
+ def make_connection(self) -> sqlalchemy.Connection:
118
137
  """
119
138
  Establishes and returns a new database connection using the SQLAlchemy engine.
120
139
 
@@ -165,11 +184,11 @@ class ConnectionMaker(object):
165
184
  :param params: The parameters dictionary for the SQL statement.
166
185
  :return: None.
167
186
  """
168
- with self.make_connection() as connection:
169
- connection.execute(sqlalchemy.text(sql), params)
170
- connection.commit()
187
+ with self.make_session() as session:
188
+ session.execute(sqlalchemy.text(sql), params)
189
+ session.commit()
171
190
 
172
- def query_all(self, sql: str, **params) -> list[tuple]:
191
+ def query_all(self, sql: str, **params) -> Sequence[sqlalchemy.Row[tuple[Any, ...]]]:
173
192
  """
174
193
  Executes the given SQL query with the specified parameters and returns all result tuples.
175
194
 
@@ -177,23 +196,22 @@ class ConnectionMaker(object):
177
196
  :param params: The parameters dictionary for the SQL query.
178
197
  :return: A list of result tuples.
179
198
  """
180
- with self.make_connection() as connection:
181
- with contextlib.closing(connection.execute(sqlalchemy.text(sql), params)) as proxy:
182
- return [item for item in proxy.fetchall()]
199
+ with self.make_session() as session:
200
+ result = session.execute(sqlalchemy.text(sql), params)
201
+ return result.all()
183
202
 
184
- def query_first(self, sql: str, **params) -> tuple | None:
203
+ def query_first(self, sql: str, **params) -> sqlalchemy.Row[tuple[Any, ...]] | None:
185
204
  """
186
- Executes the given SQL query with the specified parameters and returns the first result tuple, or ``None`` if no
187
- results are found.
205
+ Executes the given SQL query with the specified parameters and returns the first result tuple, or ``None``
206
+ if no results are found.
188
207
 
189
208
  :param sql: The SQL query to execute.
190
209
  :param params: The parameters dictionary for the SQL query.
191
210
  :return: The first result tuple, or ``None`` if no results are found.
192
211
  """
193
- return head_or_none(self.query_all(sql, **params))
194
-
195
-
196
- DBAdapter = ConnectionMaker
212
+ with self.make_session() as session:
213
+ result = session.execute(sqlalchemy.text(sql), params)
214
+ return result.first()
197
215
 
198
216
 
199
217
  def orm_to_dict(orm, exclude: set[str] = None) -> dict[str, Any]:
@@ -241,40 +259,49 @@ def orm_clone(orm, exclude: set[str] = None, no_autoincrement: bool = False):
241
259
  return new_orm
242
260
 
243
261
 
244
- def mysql_insert_ignore(enabled: bool = True):
262
+ @contextlib.contextmanager
263
+ def mysql_insert_ignore():
245
264
  """
246
- Registers a SQLAlchemy compiler extension to add ``IGNORE`` to MySQL ``INSERT`` statements if ``enabled``.
247
-
248
- :param enabled: Whether to enable the ``IGNORE`` prefix for MySQL ``INSERT`` statements.
249
- :return: None.
265
+ Registers a SQLAlchemy compiler extension to add ``IGNORE`` to MySQL ``INSERT`` statements.
250
266
  """
251
267
 
252
- @sqlalchemy.ext.compiler.compiles(sqlalchemy.sql.Insert, "mysql")
253
- def dispatch(insert: sqlalchemy.sql.Insert, compiler: sqlalchemy.sql.compiler.SQLCompiler, **kwargs) -> str:
254
- if not enabled:
255
- return compiler.visit_insert(insert, **kwargs)
268
+ def context(enabled: bool):
269
+ @sqlalchemy.ext.compiler.compiles(sqlalchemy.sql.Insert, Dialects.mysql)
270
+ def dispatch(insert: sqlalchemy.sql.Insert, compiler: sqlalchemy.sql.compiler.SQLCompiler, **kwargs) -> str:
271
+ if not enabled:
272
+ return compiler.visit_insert(insert, **kwargs)
256
273
 
257
- return compiler.visit_insert(insert.prefix_with("IGNORE"), **kwargs)
274
+ return compiler.visit_insert(insert.prefix_with("IGNORE"), **kwargs)
258
275
 
276
+ context(True)
277
+ try:
278
+ yield
279
+ finally:
280
+ context(False)
259
281
 
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
282
 
265
- :param enabled: Whether to enable the ``ON CONFLICT DO NOTHING`` clause for PostgreSQL ``INSERT`` statements.
266
- :return: None.
283
+ @contextlib.contextmanager
284
+ def postgresql_insert_on_conflict_do_nothing():
285
+ """
286
+ Registers a SQLAlchemy compiler extension to add ``ON CONFLICT DO NOTHING`` to Postgresql ``INSERT`` statements.
267
287
  """
268
288
 
269
- @sqlalchemy.ext.compiler.compiles(sqlalchemy.sql.Insert, "postgresql")
270
- def dispatch(insert: sqlalchemy.sql.Insert, compiler: sqlalchemy.sql.compiler.SQLCompiler, **kwargs) -> str:
271
- if not enabled:
272
- return compiler.visit_insert(insert, **kwargs)
273
-
274
- statement = compiler.visit_insert(insert, **kwargs)
275
- # If we have a ``RETURNING`` clause, we must insert before it
276
- returning_position = statement.find("RETURNING")
277
- if returning_position >= 0:
278
- return statement[:returning_position] + " ON CONFLICT DO NOTHING " + statement[returning_position:]
279
- else:
280
- return statement + " ON CONFLICT DO NOTHING"
289
+ def context(enabled: bool):
290
+ @sqlalchemy.ext.compiler.compiles(sqlalchemy.sql.Insert, Dialects.postgresql)
291
+ def dispatch(insert: sqlalchemy.sql.Insert, compiler: sqlalchemy.sql.compiler.SQLCompiler, **kwargs) -> str:
292
+ if not enabled:
293
+ return compiler.visit_insert(insert, **kwargs)
294
+
295
+ statement = compiler.visit_insert(insert, **kwargs)
296
+ # If we have a ``RETURNING`` clause, we must insert before it
297
+ returning_position = statement.find("RETURNING")
298
+ if returning_position >= 0:
299
+ return statement[:returning_position] + " ON CONFLICT DO NOTHING " + statement[returning_position:]
300
+ else:
301
+ return statement + " ON CONFLICT DO NOTHING"
302
+
303
+ context(True)
304
+ try:
305
+ yield
306
+ finally:
307
+ context(False)
@@ -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.
@@ -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)):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iker-python-common
3
- Version: 1.0.49
3
+ Version: 1.0.51
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.11
6
6
  Classifier: Programming Language :: Python :: 3.12