iker-python-common 1.0.48__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.48 → iker_python_common-1.0.50}/PKG-INFO +1 -1
  2. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/dbutils.py +94 -100
  3. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/dtutils.py +39 -17
  4. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/testutils.py +1 -1
  5. {iker_python_common-1.0.48 → 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.48 → iker_python_common-1.0.50}/test/iker_tests/common/utils/dtutils_test.py +81 -25
  8. iker_python_common-1.0.48/test/iker_tests/common/utils/dbutils_test.py +0 -339
  9. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/.editorconfig +0 -0
  10. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/.github/workflows/pr.yml +0 -0
  11. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/.github/workflows/push.yml +0 -0
  12. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/.gitignore +0 -0
  13. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/MANIFEST.in +0 -0
  14. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/README.md +0 -0
  15. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/VERSION +0 -0
  16. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/pyproject.toml +0 -0
  17. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/resources/unittest/config/config.cfg +0 -0
  18. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/resources/unittest/csv/data.csv +0 -0
  19. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/resources/unittest/csv/data.tsv +0 -0
  20. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/resources/unittest/s3utils/dir.baz/file.bar.baz +0 -0
  21. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/resources/unittest/s3utils/dir.baz/file.foo.bar +0 -0
  22. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/resources/unittest/s3utils/dir.baz/file.foo.baz +0 -0
  23. {iker_python_common-1.0.48 → 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.48 → 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.48 → 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.48 → 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.48 → iker_python_common-1.0.50}/resources/unittest/s3utils/dir.foo/file.bar +0 -0
  28. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/resources/unittest/s3utils/dir.foo/file.baz +0 -0
  29. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/resources/unittest/s3utils/dir.foo/file.foo +0 -0
  30. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/resources/unittest/shutils/dir.baz/file.bar.baz +0 -0
  31. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/resources/unittest/shutils/dir.baz/file.foo.bar +0 -0
  32. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/resources/unittest/shutils/dir.baz/file.foo.baz +0 -0
  33. {iker_python_common-1.0.48 → 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.48 → 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.48 → 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.48 → 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.48 → iker_python_common-1.0.50}/resources/unittest/shutils/dir.foo/file.bar +0 -0
  38. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/resources/unittest/shutils/dir.foo/file.baz +0 -0
  39. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/resources/unittest/shutils/dir.foo/file.foo +0 -0
  40. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/setup.cfg +0 -0
  41. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/setup.py +0 -0
  42. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/__init__.py +0 -0
  43. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/__init__.py +0 -0
  44. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/argutils.py +0 -0
  45. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/config.py +0 -0
  46. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/csv.py +0 -0
  47. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/dockerutils.py +0 -0
  48. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/funcutils.py +0 -0
  49. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/jsonutils.py +0 -0
  50. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/logger.py +0 -0
  51. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/numutils.py +0 -0
  52. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/randutils.py +0 -0
  53. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/retry.py +0 -0
  54. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/s3utils.py +0 -0
  55. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/sequtils.py +0 -0
  56. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/shutils.py +0 -0
  57. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/span.py +0 -0
  58. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/strutils.py +0 -0
  59. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker/common/utils/typeutils.py +0 -0
  60. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker_python_common.egg-info/SOURCES.txt +0 -0
  61. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker_python_common.egg-info/dependency_links.txt +0 -0
  62. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker_python_common.egg-info/not-zip-safe +0 -0
  63. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker_python_common.egg-info/requires.txt +0 -0
  64. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/src/iker_python_common.egg-info/top_level.txt +0 -0
  65. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_test.py +0 -0
  66. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_tests/__init__.py +0 -0
  67. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_tests/common/utils/argutils_test.py +0 -0
  68. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_tests/common/utils/config_test.py +0 -0
  69. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_tests/common/utils/csv_test.py +0 -0
  70. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_tests/common/utils/dockerutils_test.py +0 -0
  71. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_tests/common/utils/funcutils_test.py +0 -0
  72. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_tests/common/utils/jsonutils_test.py +0 -0
  73. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_tests/common/utils/logger_test.py +0 -0
  74. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_tests/common/utils/numutils_test.py +0 -0
  75. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_tests/common/utils/randutils_test.py +0 -0
  76. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_tests/common/utils/retry_test.py +0 -0
  77. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_tests/common/utils/s3utils_test.py +0 -0
  78. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_tests/common/utils/sequtils_test.py +0 -0
  79. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_tests/common/utils/shutils_test.py +0 -0
  80. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_tests/common/utils/span_test.py +0 -0
  81. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_tests/common/utils/strutils_test.py +0 -0
  82. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_tests/common/utils/testutils_test.py +0 -0
  83. {iker_python_common-1.0.48 → iker_python_common-1.0.50}/test/iker_tests/common/utils/typeutils_test.py +0 -0
  84. {iker_python_common-1.0.48 → 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.48
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",
@@ -46,16 +48,16 @@ def basic_date_format() -> str:
46
48
 
47
49
 
48
50
  @memorized
49
- def basic_time_format(with_ms: bool = False, with_tz: bool = False) -> str:
51
+ def basic_time_format(with_us: bool = False, with_tz: bool = False) -> str:
50
52
  """
51
- Returns the basic time format string, with optional milliseconds and timezone.
53
+ Returns the basic time format string, with optional microseconds and timezone.
52
54
 
53
- :param with_ms: If ``True``, include milliseconds in the format.
55
+ :param with_us: If ``True``, include microseconds in the format.
54
56
  :param with_tz: If ``True``, include timezone in the format.
55
57
  :return: The basic time format string.
56
58
  """
57
59
  fmt_str = "T%H%M%S"
58
- if with_ms:
60
+ if with_us:
59
61
  fmt_str = fmt_str + ".%f"
60
62
  if with_tz:
61
63
  fmt_str = fmt_str + "%z"
@@ -63,15 +65,15 @@ def basic_time_format(with_ms: bool = False, with_tz: bool = False) -> str:
63
65
 
64
66
 
65
67
  @memorized
66
- def basic_format(with_ms: bool = False, with_tz: bool = False) -> str:
68
+ def basic_format(with_us: bool = False, with_tz: bool = False) -> str:
67
69
  """
68
- Returns the basic combined date and time format string, with optional milliseconds and timezone.
70
+ Returns the basic combined date and time format string, with optional microseconds and timezone.
69
71
 
70
- :param with_ms: If ``True``, include milliseconds in the time format.
72
+ :param with_us: If ``True``, include microseconds in the time format.
71
73
  :param with_tz: If ``True``, include timezone in the time format.
72
74
  :return: The basic combined date and time format string.
73
75
  """
74
- return basic_date_format() + basic_time_format(with_ms, with_tz)
76
+ return basic_date_format() + basic_time_format(with_us, with_tz)
75
77
 
76
78
 
77
79
  @singleton
@@ -85,16 +87,16 @@ def extended_date_format() -> str:
85
87
 
86
88
 
87
89
  @memorized
88
- def extended_time_format(with_ms: bool = False, with_tz: bool = False) -> str:
90
+ def extended_time_format(with_us: bool = False, with_tz: bool = False) -> str:
89
91
  """
90
- Returns the extended time format string, with optional milliseconds and timezone.
92
+ Returns the extended time format string, with optional microseconds and timezone.
91
93
 
92
- :param with_ms: If ``True``, include milliseconds in the format.
94
+ :param with_us: If ``True``, include microseconds in the format.
93
95
  :param with_tz: If ``True``, include timezone in the format.
94
96
  :return: The extended time format string.
95
97
  """
96
98
  fmt_str = "T%H:%M:%S"
97
- if with_ms:
99
+ if with_us:
98
100
  fmt_str = fmt_str + ".%f"
99
101
  if with_tz:
100
102
  fmt_str = fmt_str + "%:z"
@@ -102,15 +104,15 @@ def extended_time_format(with_ms: bool = False, with_tz: bool = False) -> str:
102
104
 
103
105
 
104
106
  @memorized
105
- def extended_format(with_ms: bool = False, with_tz: bool = False) -> str:
107
+ def extended_format(with_us: bool = False, with_tz: bool = False) -> str:
106
108
  """
107
- Returns the extended combined date and time format string, with optional milliseconds and timezone.
109
+ Returns the extended combined date and time format string, with optional microseconds and timezone.
108
110
 
109
- :param with_ms: If ``True``, include milliseconds in the time format.
111
+ :param with_us: If ``True``, include microseconds in the time format.
110
112
  :param with_tz: If ``True``, include timezone in the time format.
111
113
  :return: The extended combined date and time format string.
112
114
  """
113
- return extended_date_format() + extended_time_format(with_ms, with_tz)
115
+ return extended_date_format() + extended_time_format(with_us, with_tz)
114
116
 
115
117
 
116
118
  iso_date_format = extended_date_format
@@ -122,7 +124,7 @@ iso_format = extended_format
122
124
  def iso_formats() -> list[str]:
123
125
  """
124
126
  Returns a list of supported ISO 8601 date and time format strings, including both basic and extended forms, with
125
- optional milliseconds and timezone.
127
+ optional microseconds and timezone.
126
128
 
127
129
  :return: A list of ISO 8601 format strings.
128
130
  """
@@ -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.48
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