iker-python-common 1.0.49__tar.gz → 1.0.50__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.50}/PKG-INFO +1 -1
  2. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/dbutils.py +94 -100
  3. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/dtutils.py +22 -0
  4. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/testutils.py +1 -1
  5. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker_python_common.egg-info/PKG-INFO +1 -1
  6. iker_python_common-1.0.50/test/iker_tests/common/utils/dbutils_test.py +1012 -0
  7. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/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.50}/.editorconfig +0 -0
  10. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/.github/workflows/pr.yml +0 -0
  11. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/.github/workflows/push.yml +0 -0
  12. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/.gitignore +0 -0
  13. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/MANIFEST.in +0 -0
  14. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/README.md +0 -0
  15. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/VERSION +0 -0
  16. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/pyproject.toml +0 -0
  17. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/resources/unittest/config/config.cfg +0 -0
  18. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/resources/unittest/csv/data.csv +0 -0
  19. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/resources/unittest/csv/data.tsv +0 -0
  20. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/resources/unittest/s3utils/dir.baz/file.bar.baz +0 -0
  21. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/resources/unittest/s3utils/dir.baz/file.foo.bar +0 -0
  22. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/resources/unittest/s3utils/dir.baz/file.foo.baz +0 -0
  23. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/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.50}/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.50}/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.50}/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.50}/resources/unittest/s3utils/dir.foo/file.bar +0 -0
  28. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/resources/unittest/s3utils/dir.foo/file.baz +0 -0
  29. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/resources/unittest/s3utils/dir.foo/file.foo +0 -0
  30. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/resources/unittest/shutils/dir.baz/file.bar.baz +0 -0
  31. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/resources/unittest/shutils/dir.baz/file.foo.bar +0 -0
  32. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/resources/unittest/shutils/dir.baz/file.foo.baz +0 -0
  33. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/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.50}/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.50}/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.50}/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.50}/resources/unittest/shutils/dir.foo/file.bar +0 -0
  38. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/resources/unittest/shutils/dir.foo/file.baz +0 -0
  39. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/resources/unittest/shutils/dir.foo/file.foo +0 -0
  40. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/setup.cfg +0 -0
  41. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/setup.py +0 -0
  42. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/__init__.py +0 -0
  43. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/__init__.py +0 -0
  44. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/argutils.py +0 -0
  45. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/config.py +0 -0
  46. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/csv.py +0 -0
  47. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/dockerutils.py +0 -0
  48. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/funcutils.py +0 -0
  49. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/jsonutils.py +0 -0
  50. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/logger.py +0 -0
  51. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/numutils.py +0 -0
  52. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/randutils.py +0 -0
  53. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/retry.py +0 -0
  54. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/s3utils.py +0 -0
  55. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/sequtils.py +0 -0
  56. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/shutils.py +0 -0
  57. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/span.py +0 -0
  58. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/strutils.py +0 -0
  59. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker/common/utils/typeutils.py +0 -0
  60. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker_python_common.egg-info/SOURCES.txt +0 -0
  61. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker_python_common.egg-info/dependency_links.txt +0 -0
  62. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker_python_common.egg-info/not-zip-safe +0 -0
  63. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker_python_common.egg-info/requires.txt +0 -0
  64. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/src/iker_python_common.egg-info/top_level.txt +0 -0
  65. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_test.py +0 -0
  66. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_tests/__init__.py +0 -0
  67. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_tests/common/utils/argutils_test.py +0 -0
  68. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_tests/common/utils/config_test.py +0 -0
  69. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_tests/common/utils/csv_test.py +0 -0
  70. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_tests/common/utils/dockerutils_test.py +0 -0
  71. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_tests/common/utils/funcutils_test.py +0 -0
  72. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_tests/common/utils/jsonutils_test.py +0 -0
  73. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_tests/common/utils/logger_test.py +0 -0
  74. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_tests/common/utils/numutils_test.py +0 -0
  75. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_tests/common/utils/randutils_test.py +0 -0
  76. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_tests/common/utils/retry_test.py +0 -0
  77. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_tests/common/utils/s3utils_test.py +0 -0
  78. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_tests/common/utils/sequtils_test.py +0 -0
  79. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_tests/common/utils/shutils_test.py +0 -0
  80. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_tests/common/utils/span_test.py +0 -0
  81. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_tests/common/utils/strutils_test.py +0 -0
  82. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_tests/common/utils/testutils_test.py +0 -0
  83. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/test/iker_tests/common/utils/typeutils_test.py +0 -0
  84. {iker_python_common-1.0.49 → iker_python_common-1.0.50}/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.50
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,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 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
 
65
68
  @staticmethod
66
69
  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,
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`` instance from a URL string or ``ParseResult``, extracting connection parameters.
76
+ Creates a new instance of ``ConnectionMaker`` from a SQLAlchemy URL string or object.
74
77
 
75
- :param driver: The database driver string.
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`` instance.
81
+ :return: A new instance of ``ConnectionMaker`` configured with the provided URL and options.
80
82
  """
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'")
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 the SQLAlchemy connection string for the database using the provided parameters.
88
+ Constructs a SQLAlchemy connection string for the database using the provided parameters.
98
89
 
99
- :return: The connection string as a string.
90
+ :return: A string representing the database connection.
100
91
  """
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}"
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.make_connection() as connection:
169
- connection.execute(sqlalchemy.text(sql), params)
170
- connection.commit()
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) -> list[tuple]:
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.make_connection() as connection:
181
- with contextlib.closing(connection.execute(sqlalchemy.text(sql), params)) as proxy:
182
- return [item for item in proxy.fetchall()]
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`` if no
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
- return head_or_none(self.query_all(sql, **params))
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
- def mysql_insert_ignore(enabled: bool = True):
229
+ @contextlib.contextmanager
230
+ def mysql_insert_ignore():
245
231
  """
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.
232
+ Registers a SQLAlchemy compiler extension to add ``IGNORE`` to MySQL ``INSERT`` statements.
250
233
  """
251
234
 
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)
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
- return compiler.visit_insert(insert.prefix_with("IGNORE"), **kwargs)
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
- :param enabled: Whether to enable the ``ON CONFLICT DO NOTHING`` clause for PostgreSQL ``INSERT`` statements.
266
- :return: None.
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
- @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"
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)
@@ -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.50
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.11
6
6
  Classifier: Programming Language :: Python :: 3.12