dycw-utilities 0.114.2__tar.gz → 0.114.3__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 (228) hide show
  1. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/PKG-INFO +1 -1
  2. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/pyproject.toml +2 -2
  3. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_asyncio.py +8 -5
  4. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_redis.py +93 -2
  5. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_sqlalchemy.py +45 -0
  6. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/__init__.py +1 -1
  7. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/asyncio.py +22 -5
  8. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/redis.py +42 -3
  9. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/sqlalchemy.py +56 -2
  10. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/.gitignore +0 -0
  11. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/LICENSE +0 -0
  12. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/README.md +0 -0
  13. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/__init__.py +0 -0
  14. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/conftest.py +0 -0
  15. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/modules/__init__.py +0 -0
  16. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/modules/package_missing/__init__.py +0 -0
  17. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/modules/package_missing/module.py +0 -0
  18. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/modules/package_with/__init__.py +0 -0
  19. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/modules/package_with/outer_1.py +0 -0
  20. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/modules/package_with/outer_2.py +0 -0
  21. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/modules/package_with/subpackage/__init__.py +0 -0
  22. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/modules/package_with/subpackage/inner_1.py +0 -0
  23. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/modules/package_with/subpackage/inner_2.py +0 -0
  24. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/modules/package_with/subpackage/inner_3.py +0 -0
  25. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/modules/package_without/__init__.py +0 -0
  26. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/modules/package_without/module_1.py +0 -0
  27. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/modules/package_without/module_2.py +0 -0
  28. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/modules/standalone.py +0 -0
  29. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/modules/with_imports.py +0 -0
  30. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/regressions/test_pytest_regressions/TestMultipleRegressionFixtures__test_main__obj.json +0 -0
  31. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/regressions/test_pytest_regressions/TestMultipleRegressionFixtures__test_main__series.json +0 -0
  32. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_int.json +0 -0
  33. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_literal__false.json +0 -0
  34. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_literal__true.json +0 -0
  35. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_nested.json +0 -0
  36. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/regressions/test_pytest_regressions/TestPolarsRegressionFixture__test_dataframe.json +0 -0
  37. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/regressions/test_pytest_regressions/TestPolarsRegressionFixture__test_series.json +0 -0
  38. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/scripts/__init__.py +0 -0
  39. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/scripts/test_async_service/__init__.py +0 -0
  40. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/scripts/test_async_service/__main__.py +0 -0
  41. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/scripts/test_async_service/run.sh +0 -0
  42. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/scripts/test_queue_processor/__init__.py +0 -0
  43. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/scripts/test_queue_processor/__main__.py +0 -0
  44. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/scripts/test_queue_processor/run.sh +0 -0
  45. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_altair.py +0 -0
  46. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_astor.py +0 -0
  47. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_atomicwrites.py +0 -0
  48. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_atools.py +0 -0
  49. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_cachetools.py +0 -0
  50. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_click.py +0 -0
  51. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_concurrent.py +0 -0
  52. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_contextlib.py +0 -0
  53. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_contextvars.py +0 -0
  54. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_cryptography.py +0 -0
  55. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_cvxpy.py +0 -0
  56. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_dataclasses.py +0 -0
  57. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_datetime.py +0 -0
  58. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_enum.py +0 -0
  59. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_errors.py +0 -0
  60. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_eventkit.py +0 -0
  61. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_fastapi.py +0 -0
  62. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_fpdf2.py +0 -0
  63. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_functions.py +0 -0
  64. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_functools.py +0 -0
  65. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_getpass.py +0 -0
  66. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_git.py +0 -0
  67. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_hashlib.py +0 -0
  68. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_http.py +0 -0
  69. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_hypothesis.py +0 -0
  70. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_ipython.py +0 -0
  71. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_iterables.py +0 -0
  72. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_jupyter.py +0 -0
  73. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_lightweight_charts.py +0 -0
  74. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_logging.py +0 -0
  75. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_loguru.py +0 -0
  76. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_luigi.py +0 -0
  77. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_math.py +0 -0
  78. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_memory_profiler.py +0 -0
  79. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_modules.py +0 -0
  80. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_more_itertools.py +0 -0
  81. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_numpy.py +0 -0
  82. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_operator.py +0 -0
  83. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_optuna.py +0 -0
  84. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_orjson.py +0 -0
  85. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_os.py +0 -0
  86. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_parse.py +0 -0
  87. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_pathlib.py +0 -0
  88. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_period.py +0 -0
  89. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_pickle.py +0 -0
  90. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_platform.py +0 -0
  91. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_polars.py +0 -0
  92. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_polars_ols.py +0 -0
  93. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_pqdm.py +0 -0
  94. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_pydantic.py +0 -0
  95. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_pyinstrument.py +0 -0
  96. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_pyrsistent.py +0 -0
  97. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_pytest.py +0 -0
  98. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_pytest_regressions.py +0 -0
  99. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_python_dotenv.py +0 -0
  100. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_random.py +0 -0
  101. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_re.py +0 -0
  102. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_reprlib.py +0 -0
  103. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_rich.py +0 -0
  104. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_scipy.py +0 -0
  105. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_sentinel.py +0 -0
  106. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_shelve.py +0 -0
  107. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_slack_sdk.py +0 -0
  108. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_socket.py +0 -0
  109. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_sqlalchemy_polars.py +0 -0
  110. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_statsmodel.py +0 -0
  111. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_streamlit.py +0 -0
  112. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_sys.py +0 -0
  113. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_tempfile.py +0 -0
  114. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_tenacity.py +0 -0
  115. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_text.py +0 -0
  116. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_threading.py +0 -0
  117. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_timer.py +0 -0
  118. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_traceback.py +0 -0
  119. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_traceback_funcs/__init__.py +0 -0
  120. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_traceback_funcs/chain.py +0 -0
  121. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_traceback_funcs/decorated_async.py +0 -0
  122. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_traceback_funcs/decorated_sync.py +0 -0
  123. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_traceback_funcs/error_bind.py +0 -0
  124. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_traceback_funcs/many.py +0 -0
  125. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_traceback_funcs/one.py +0 -0
  126. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_traceback_funcs/recursive.py +0 -0
  127. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_traceback_funcs/task_group_one.py +0 -0
  128. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_traceback_funcs/task_group_two.py +0 -0
  129. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_traceback_funcs/two.py +0 -0
  130. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_traceback_funcs/untraced.py +0 -0
  131. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_types.py +0 -0
  132. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_typing.py +0 -0
  133. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_typing_funcs/__init__.py +0 -0
  134. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_typing_funcs/no_future.py +0 -0
  135. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_typing_funcs/with_future.py +0 -0
  136. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_tzdata.py +0 -0
  137. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_tzlocal.py +0 -0
  138. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_uuid.py +0 -0
  139. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_version.py +0 -0
  140. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_warnings.py +0 -0
  141. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_whenever.py +0 -0
  142. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_zipfile.py +0 -0
  143. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/tests/test_zoneinfo.py +0 -0
  144. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/altair.py +0 -0
  145. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/astor.py +0 -0
  146. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/atomicwrites.py +0 -0
  147. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/atools.py +0 -0
  148. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/cachetools.py +0 -0
  149. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/click.py +0 -0
  150. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/concurrent.py +0 -0
  151. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/contextlib.py +0 -0
  152. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/contextvars.py +0 -0
  153. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/cryptography.py +0 -0
  154. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/cvxpy.py +0 -0
  155. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/dataclasses.py +0 -0
  156. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/datetime.py +0 -0
  157. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/enum.py +0 -0
  158. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/errors.py +0 -0
  159. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/eventkit.py +0 -0
  160. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/fastapi.py +0 -0
  161. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/fpdf2.py +0 -0
  162. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/functions.py +0 -0
  163. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/functools.py +0 -0
  164. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/getpass.py +0 -0
  165. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/git.py +0 -0
  166. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/hashlib.py +0 -0
  167. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/http.py +0 -0
  168. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/hypothesis.py +0 -0
  169. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/ipython.py +0 -0
  170. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/iterables.py +0 -0
  171. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/jupyter.py +0 -0
  172. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/lightweight_charts.py +0 -0
  173. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/logging.py +0 -0
  174. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/loguru.py +0 -0
  175. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/luigi.py +0 -0
  176. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/math.py +0 -0
  177. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/memory_profiler.py +0 -0
  178. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/modules.py +0 -0
  179. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/more_itertools.py +0 -0
  180. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/numpy.py +0 -0
  181. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/operator.py +0 -0
  182. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/optuna.py +0 -0
  183. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/orjson.py +0 -0
  184. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/os.py +0 -0
  185. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/parse.py +0 -0
  186. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/pathlib.py +0 -0
  187. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/period.py +0 -0
  188. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/pickle.py +0 -0
  189. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/platform.py +0 -0
  190. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/polars.py +0 -0
  191. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/polars_ols.py +0 -0
  192. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/pqdm.py +0 -0
  193. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/py.typed +0 -0
  194. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/pydantic.py +0 -0
  195. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/pyinstrument.py +0 -0
  196. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/pyrsistent.py +0 -0
  197. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/pytest.py +0 -0
  198. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/pytest_regressions.py +0 -0
  199. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/python_dotenv.py +0 -0
  200. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/random.py +0 -0
  201. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/re.py +0 -0
  202. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/reprlib.py +0 -0
  203. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/rich.py +0 -0
  204. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/scipy.py +0 -0
  205. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/sentinel.py +0 -0
  206. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/shelve.py +0 -0
  207. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/slack_sdk.py +0 -0
  208. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/socket.py +0 -0
  209. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/sqlalchemy_polars.py +0 -0
  210. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/statsmodels.py +0 -0
  211. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/streamlit.py +0 -0
  212. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/sys.py +0 -0
  213. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/tempfile.py +0 -0
  214. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/tenacity.py +0 -0
  215. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/text.py +0 -0
  216. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/threading.py +0 -0
  217. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/timer.py +0 -0
  218. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/traceback.py +0 -0
  219. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/types.py +0 -0
  220. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/typing.py +0 -0
  221. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/tzdata.py +0 -0
  222. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/tzlocal.py +0 -0
  223. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/uuid.py +0 -0
  224. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/version.py +0 -0
  225. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/warnings.py +0 -0
  226. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/whenever.py +0 -0
  227. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/zipfile.py +0 -0
  228. {dycw_utilities-0.114.2 → dycw_utilities-0.114.3}/src/utilities/zoneinfo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.114.2
3
+ Version: 0.114.3
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -92,7 +92,7 @@ dependencies = [
92
92
  name = "dycw-utilities"
93
93
  readme = "README.md"
94
94
  requires-python = ">= 3.12"
95
- version = "0.114.2"
95
+ version = "0.114.3"
96
96
 
97
97
  [project.optional-dependencies]
98
98
  test = [
@@ -334,7 +334,7 @@ zzz-test-zoneinfo = [
334
334
  # bump-my-version
335
335
  [tool.bumpversion]
336
336
  allow_dirty = true
337
- current_version = "0.114.2"
337
+ current_version = "0.114.3"
338
338
 
339
339
  [[tool.bumpversion.files]]
340
340
  filename = "src/utilities/__init__.py"
@@ -21,6 +21,7 @@ from hypothesis.strategies import (
21
21
  DataObject,
22
22
  data,
23
23
  integers,
24
+ just,
24
25
  lists,
25
26
  none,
26
27
  permutations,
@@ -434,7 +435,8 @@ class TestInfiniteLooper:
434
435
  assert 10 <= parent.child.counter <= 15
435
436
  assert 3 <= parent.counter <= 7
436
437
 
437
- async def test_error_upon_initialize(self) -> None:
438
+ @given(logger=just("logger") | none())
439
+ async def test_error_upon_initialize(self, *, logger: str | None) -> None:
438
440
  class CustomError(Exception): ...
439
441
 
440
442
  @dataclass(kw_only=True)
@@ -449,9 +451,10 @@ class TestInfiniteLooper:
449
451
 
450
452
  with raises(TimeoutError):
451
453
  async with timeout_dur(duration=0.5):
452
- _ = await Example(sleep_core=0.1)()
454
+ _ = await Example(sleep_core=0.1, logger=logger)()
453
455
 
454
- async def test_error_upon_core(self) -> None:
456
+ @given(logger=just("logger") | none())
457
+ async def test_error_upon_core(self, *, logger: str | None) -> None:
455
458
  class CustomError(Exception): ...
456
459
 
457
460
  @dataclass(kw_only=True)
@@ -468,7 +471,7 @@ class TestInfiniteLooper:
468
471
 
469
472
  with raises(TimeoutError):
470
473
  async with timeout_dur(duration=0.5):
471
- _ = await Example(sleep_core=0.1)()
474
+ _ = await Example(sleep_core=0.1, logger=logger)()
472
475
 
473
476
  async def test_error_no_event_found(self) -> None:
474
477
  @dataclass(kw_only=True)
@@ -536,7 +539,7 @@ class TestInfiniteQueueLooper:
536
539
 
537
540
  @override
538
541
  async def _process_items(self, *items: int) -> None:
539
- raise CustomError
542
+ raise CustomError(*items)
540
543
 
541
544
  processor = Example(sleep_core=0.1)
542
545
  processor.put_items_nowait(1)
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from asyncio import create_task, get_running_loop, sleep
4
+ from io import BytesIO, StringIO
4
5
  from typing import TYPE_CHECKING, Any
5
6
 
6
7
  from hypothesis import HealthCheck, Phase, given, settings
@@ -17,6 +18,7 @@ from redis.asyncio import Redis
17
18
 
18
19
  from tests.conftest import SKIPIF_CI_AND_NOT_LINUX
19
20
  from tests.test_operator import make_objects
21
+ from utilities.asyncio import EnhancedTaskGroup, sleep_dur
20
22
  from utilities.functions import get_class_name
21
23
  from utilities.hypothesis import (
22
24
  int64s,
@@ -28,6 +30,8 @@ from utilities.hypothesis import (
28
30
  from utilities.orjson import deserialize, serialize
29
31
  from utilities.redis import (
30
32
  Publisher,
33
+ PublisherIQL,
34
+ PublisherIQLError,
31
35
  publish,
32
36
  redis_hash_map_key,
33
37
  redis_key,
@@ -118,7 +122,7 @@ class TestPublisher:
118
122
  @given(
119
123
  data=data(),
120
124
  channel=text_ascii(min_size=1).map(
121
- lambda c: f"{get_class_name(TestPublishAndSubscribe)}_obj_ser_{c}"
125
+ lambda c: f"{get_class_name(TestPublisher)}_obj_ser_{c}"
122
126
  ),
123
127
  obj=make_objects(),
124
128
  )
@@ -157,7 +161,7 @@ class TestPublisher:
157
161
  @given(
158
162
  data=data(),
159
163
  channel=text_ascii(min_size=1).map(
160
- lambda c: f"{get_class_name(TestPublishAndSubscribe)}_text_no_ser_{c}"
164
+ lambda c: f"{get_class_name(TestPublisher)}_text_no_ser_{c}"
161
165
  ),
162
166
  text=text_ascii(min_size=1),
163
167
  )
@@ -188,6 +192,93 @@ class TestPublisher:
188
192
  _ = task.cancel()
189
193
 
190
194
 
195
+ class TestPublisherIQL:
196
+ @given(
197
+ data=data(),
198
+ channel=text_ascii(min_size=1).map(
199
+ lambda c: f"{get_class_name(TestPublisherIQL)}_main_{c}"
200
+ ),
201
+ obj=make_objects(),
202
+ )
203
+ @mark.flaky
204
+ @settings(
205
+ max_examples=1,
206
+ phases={Phase.generate},
207
+ suppress_health_check={HealthCheck.function_scoped_fixture},
208
+ )
209
+ @SKIPIF_CI_AND_NOT_LINUX
210
+ async def test_main(self, *, data: DataObject, channel: str, obj: Any) -> None:
211
+ buffer = StringIO()
212
+ async with yield_test_redis(data) as test:
213
+
214
+ async def listener() -> None:
215
+ async for obj_i in subscribe(
216
+ test.redis.pubsub(), channel, deserializer=deserialize
217
+ ):
218
+ _ = buffer.write(str(obj_i))
219
+
220
+ publisher = PublisherIQL(
221
+ redis=test.redis, serializer=serialize, sleep_core=0.1
222
+ )
223
+
224
+ async def sleep_then_put() -> None:
225
+ await sleep_dur(duration=0.1)
226
+ publisher.put_items_nowait((channel, obj))
227
+
228
+ with raises(ExceptionGroup): # noqa: PT012
229
+ async with EnhancedTaskGroup(timeout=1.0) as tg:
230
+ _ = tg.create_task(publisher())
231
+ _ = tg.create_task(listener())
232
+ _ = tg.create_task(sleep_then_put())
233
+
234
+ assert buffer.getvalue() == str(obj)
235
+
236
+ @given(
237
+ data=data(),
238
+ channel=text_ascii(min_size=1).map(
239
+ lambda c: f"{get_class_name(TestPublisherIQL)}_text_without_serialize_{c}"
240
+ ),
241
+ text=text_ascii(min_size=1),
242
+ )
243
+ @settings(
244
+ max_examples=1,
245
+ phases={Phase.generate},
246
+ suppress_health_check={HealthCheck.function_scoped_fixture},
247
+ )
248
+ @SKIPIF_CI_AND_NOT_LINUX
249
+ async def test_text_without_serialize(
250
+ self, *, data: DataObject, channel: str, text: str
251
+ ) -> None:
252
+ buffer = BytesIO()
253
+ async with yield_test_redis(data) as test:
254
+
255
+ async def listener() -> None:
256
+ async for bytes_i in subscribe(test.redis.pubsub(), channel):
257
+ _ = buffer.write(bytes_i)
258
+
259
+ publisher = PublisherIQL(redis=test.redis, sleep_core=0.1)
260
+
261
+ async def sleep_then_put() -> None:
262
+ await sleep_dur(duration=0.1)
263
+ publisher.put_items_nowait((channel, text))
264
+
265
+ with raises(ExceptionGroup): # noqa: PT012
266
+ async with EnhancedTaskGroup(timeout=1.0) as tg:
267
+ _ = tg.create_task(publisher())
268
+ _ = tg.create_task(listener())
269
+ _ = tg.create_task(sleep_then_put())
270
+
271
+ assert buffer.getvalue() == text.encode()
272
+
273
+ @given(data=data())
274
+ @SKIPIF_CI_AND_NOT_LINUX
275
+ async def test_error(self, *, data: DataObject) -> None:
276
+ async with yield_test_redis(data) as test:
277
+ publisher = PublisherIQL(redis=test.redis)
278
+ with raises(PublisherIQLError, match="Error running 'PublisherIQL'"):
279
+ raise PublisherIQLError(publisher=publisher)
280
+
281
+
191
282
  class TestSubscribeMessages:
192
283
  @given(
193
284
  channel=text_ascii(min_size=1).map(
@@ -27,6 +27,7 @@ from sqlalchemy.exc import DatabaseError, OperationalError, ProgrammingError
27
27
  from sqlalchemy.ext.asyncio import AsyncEngine
28
28
  from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, mapped_column
29
29
 
30
+ from utilities.asyncio import EnhancedTaskGroup, sleep_dur
30
31
  from utilities.hypothesis import (
31
32
  int32s,
32
33
  pairs,
@@ -44,6 +45,8 @@ from utilities.sqlalchemy import (
44
45
  TablenameMixin,
45
46
  TableOrORMInstOrClass,
46
47
  Upserter,
48
+ UpserterIQL,
49
+ UpserterIQLError,
47
50
  UpsertItemsError,
48
51
  _get_dialect,
49
52
  _get_dialect_max_params,
@@ -1177,6 +1180,48 @@ class TestUpserter:
1177
1180
  assert set(res) == set(pairs)
1178
1181
 
1179
1182
 
1183
+ class TestUpserterIQL:
1184
+ @given(
1185
+ data=data(),
1186
+ name=_table_names(),
1187
+ triples=_upsert_lists(nullable=True, min_size=1),
1188
+ )
1189
+ @mark.flaky
1190
+ @settings(max_examples=1, phases={Phase.generate})
1191
+ async def test_main(
1192
+ self, *, data: DataObject, name: str, triples: list[tuple[int, bool, bool]]
1193
+ ) -> None:
1194
+ table = Table(
1195
+ name,
1196
+ MetaData(),
1197
+ Column("id_", Integer, primary_key=True),
1198
+ Column("value", Boolean, nullable=True),
1199
+ )
1200
+ engine = await sqlalchemy_engines(data, table)
1201
+ upserter = UpserterIQL(engine=engine, sleep_core=0.1)
1202
+ pairs = [(id_, init) for id_, init, _ in triples]
1203
+
1204
+ async def sleep_then_put() -> None:
1205
+ await sleep_dur(duration=0.1)
1206
+ upserter.put_items_nowait((pairs, table))
1207
+
1208
+ with raises(ExceptionGroup): # noqa: PT012
1209
+ async with EnhancedTaskGroup(timeout=1.0) as tg:
1210
+ _ = tg.create_task(upserter())
1211
+ _ = tg.create_task(sleep_then_put())
1212
+ sel = select(table)
1213
+ async with engine.begin() as conn:
1214
+ res = (await conn.execute(sel)).all()
1215
+ assert set(res) == set(pairs)
1216
+
1217
+ @given(data=data())
1218
+ async def test_error(self, *, data: DataObject) -> None:
1219
+ engine = await sqlalchemy_engines(data)
1220
+ upserter = UpserterIQL(engine=engine)
1221
+ with raises(UpserterIQLError, match="Error running 'UpserterIQL'"):
1222
+ raise UpserterIQLError(upserter=upserter)
1223
+
1224
+
1180
1225
  class TestUpsertItems:
1181
1226
  @given(data=data(), name=_table_names(), triple=_upsert_triples(nullable=True))
1182
1227
  @settings_with_reduced_examples(phases={Phase.generate})
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.114.2"
3
+ __version__ = "0.114.3"
@@ -25,6 +25,7 @@ from contextlib import (
25
25
  )
26
26
  from dataclasses import dataclass, field
27
27
  from io import StringIO
28
+ from logging import getLogger
28
29
  from subprocess import PIPE
29
30
  from sys import stderr, stdout
30
31
  from typing import (
@@ -41,7 +42,7 @@ from typing import (
41
42
 
42
43
  from utilities.datetime import MILLISECOND, MINUTE, SECOND, datetime_duration_to_float
43
44
  from utilities.errors import ImpossibleCaseError
44
- from utilities.functions import ensure_int, ensure_not_none
45
+ from utilities.functions import ensure_int, ensure_not_none, get_class_name
45
46
  from utilities.sentinel import Sentinel, sentinel
46
47
  from utilities.types import (
47
48
  MaybeCallableEvent,
@@ -59,6 +60,7 @@ if TYPE_CHECKING:
59
60
 
60
61
  from utilities.types import Duration
61
62
 
63
+
62
64
  _T = TypeVar("_T")
63
65
 
64
66
 
@@ -328,11 +330,12 @@ class ExceptionProcessor(QueueProcessor[Exception | type[Exception]]):
328
330
  class InfiniteLooper(ABC, Generic[THashable]):
329
331
  """An infinite loop which can throw exceptions by setting events."""
330
332
 
333
+ sleep_core: Duration = SECOND
334
+ sleep_restart: Duration = MINUTE
335
+ logger: str | None = None
331
336
  _events: Mapping[THashable, Event] = field(
332
337
  default_factory=dict, init=False, repr=False
333
338
  )
334
- sleep_core: Duration = SECOND
335
- sleep_restart: Duration = MINUTE
336
339
 
337
340
  def __post_init__(self) -> None:
338
341
  self._events = {
@@ -395,11 +398,23 @@ class InfiniteLooper(ABC, Generic[THashable]):
395
398
 
396
399
  def _error_upon_initialize(self, error: Exception, /) -> None:
397
400
  """Handle any errors upon initializing the looper."""
398
- _ = error
401
+ if self.logger is not None:
402
+ getLogger(name=self.logger).error(
403
+ "Error initializing %r due to %s; sleeping for %s...",
404
+ get_class_name(self),
405
+ error,
406
+ self.sleep_restart,
407
+ )
399
408
 
400
409
  def _error_upon_core(self, error: Exception, /) -> None:
401
410
  """Handle any errors upon running the core function."""
402
- _ = error
411
+ if self.logger is not None:
412
+ getLogger(name=self.logger).error(
413
+ "Error running core part of %r due to %s; sleeping for %s...",
414
+ get_class_name(self),
415
+ error,
416
+ self.sleep_restart,
417
+ )
403
418
 
404
419
  def _raise_error(self, event: THashable, /) -> NoReturn:
405
420
  """Raise the error corresponding to given event."""
@@ -684,7 +699,9 @@ __all__ = [
684
699
  "AsyncService",
685
700
  "EnhancedTaskGroup",
686
701
  "ExceptionProcessor",
702
+ "InfiniteLooper",
687
703
  "InfiniteLooperError",
704
+ "InfiniteQueueLooper",
688
705
  "QueueProcessor",
689
706
  "StreamCommandOutput",
690
707
  "UniquePriorityQueue",
@@ -21,7 +21,7 @@ from uuid import UUID, uuid4
21
21
  from redis.asyncio import Redis
22
22
  from redis.typing import EncodableT
23
23
 
24
- from utilities.asyncio import QueueProcessor, timeout_dur
24
+ from utilities.asyncio import InfiniteQueueLooper, QueueProcessor, timeout_dur
25
25
  from utilities.datetime import (
26
26
  MILLISECOND,
27
27
  SECOND,
@@ -30,7 +30,7 @@ from utilities.datetime import (
30
30
  get_now,
31
31
  )
32
32
  from utilities.errors import ImpossibleCaseError
33
- from utilities.functions import ensure_int
33
+ from utilities.functions import ensure_int, get_class_name
34
34
  from utilities.iterables import always_iterable, one
35
35
 
36
36
  if TYPE_CHECKING:
@@ -40,6 +40,7 @@ if TYPE_CHECKING:
40
40
  Awaitable,
41
41
  Callable,
42
42
  Iterable,
43
+ Iterator,
43
44
  Mapping,
44
45
  Sequence,
45
46
  )
@@ -49,7 +50,7 @@ if TYPE_CHECKING:
49
50
  from redis.typing import ResponseT
50
51
 
51
52
  from utilities.iterables import MaybeIterable
52
- from utilities.types import Duration, TypeLike
53
+ from utilities.types import Duration, MaybeType, TypeLike
53
54
 
54
55
 
55
56
  _K = TypeVar("_K")
@@ -603,6 +604,42 @@ class Publisher(QueueProcessor[tuple[str, EncodableT]]):
603
604
  )
604
605
 
605
606
 
607
+ @dataclass(kw_only=True)
608
+ class PublisherIQL(InfiniteQueueLooper[None, tuple[str, EncodableT]]):
609
+ """Publish a set of messages to Redis."""
610
+
611
+ redis: Redis
612
+ serializer: Callable[[Any], EncodableT] | None = None
613
+ timeout: Duration = _PUBLISH_TIMEOUT
614
+
615
+ @override
616
+ async def _process_items(self, *items: tuple[str, EncodableT]) -> None:
617
+ for item in items: # skipif-ci-and-not-linux
618
+ channel, data = item
619
+ _ = await publish(
620
+ self.redis,
621
+ channel,
622
+ data,
623
+ serializer=self.serializer,
624
+ timeout=self.timeout,
625
+ )
626
+
627
+ @override
628
+ def _yield_events_and_exceptions(
629
+ self,
630
+ ) -> Iterator[tuple[None, MaybeType[BaseException]]]:
631
+ yield (None, PublisherIQLError) # skipif-ci-and-not-linux
632
+
633
+
634
+ @dataclass(kw_only=True)
635
+ class PublisherIQLError(Exception):
636
+ publisher: PublisherIQL
637
+
638
+ @override
639
+ def __str__(self) -> str:
640
+ return f"Error running {get_class_name(self.publisher)!r}" # skipif-ci-and-not-linux
641
+
642
+
606
643
  ##
607
644
 
608
645
 
@@ -780,6 +817,8 @@ _ = _TestRedis
780
817
 
781
818
  __all__ = [
782
819
  "Publisher",
820
+ "PublisherIQL",
821
+ "PublisherIQLError",
783
822
  "RedisHashMapKey",
784
823
  "RedisKey",
785
824
  "publish",
@@ -57,7 +57,7 @@ from sqlalchemy.orm import (
57
57
  from sqlalchemy.orm.exc import UnmappedClassError
58
58
  from sqlalchemy.pool import NullPool, Pool
59
59
 
60
- from utilities.asyncio import QueueProcessor, timeout_dur
60
+ from utilities.asyncio import InfiniteQueueLooper, QueueProcessor, timeout_dur
61
61
  from utilities.functions import (
62
62
  ensure_str,
63
63
  get_class_name,
@@ -80,7 +80,13 @@ from utilities.iterables import (
80
80
  )
81
81
  from utilities.reprlib import get_repr
82
82
  from utilities.text import snake_case
83
- from utilities.types import Duration, MaybeIterable, StrMapping, TupleOrStrMapping
83
+ from utilities.types import (
84
+ Duration,
85
+ MaybeIterable,
86
+ MaybeType,
87
+ StrMapping,
88
+ TupleOrStrMapping,
89
+ )
84
90
 
85
91
  _T = TypeVar("_T")
86
92
  type _EngineOrConnectionOrAsync = Engine | Connection | AsyncEngine | AsyncConnection
@@ -644,6 +650,51 @@ class Upserter(QueueProcessor[_InsertItem]):
644
650
  await self._post_upsert(items)
645
651
 
646
652
 
653
+ @dataclass(kw_only=True)
654
+ class UpserterIQL(InfiniteQueueLooper[None, _InsertItem]):
655
+ """Upsert a set of items to a database."""
656
+
657
+ engine: AsyncEngine
658
+ snake: bool = False
659
+ selected_or_all: _SelectedOrAll = "selected"
660
+ chunk_size_frac: float = CHUNK_SIZE_FRAC
661
+ assume_tables_exist: bool = False
662
+ timeout_create: Duration | None = None
663
+ error_create: type[Exception] = TimeoutError
664
+ timeout_insert: Duration | None = None
665
+ error_insert: type[Exception] = TimeoutError
666
+
667
+ @override
668
+ async def _process_items(self, *items: _InsertItem) -> None:
669
+ await upsert_items(
670
+ self.engine,
671
+ *items,
672
+ snake=self.snake,
673
+ selected_or_all=self.selected_or_all,
674
+ chunk_size_frac=self.chunk_size_frac,
675
+ assume_tables_exist=self.assume_tables_exist,
676
+ timeout_create=self.timeout_create,
677
+ error_create=self.error_create,
678
+ timeout_insert=self.timeout_insert,
679
+ error_insert=self.error_insert,
680
+ )
681
+
682
+ @override
683
+ def _yield_events_and_exceptions(
684
+ self,
685
+ ) -> Iterator[tuple[None, MaybeType[BaseException]]]:
686
+ yield (None, UpserterIQLError)
687
+
688
+
689
+ @dataclass(kw_only=True)
690
+ class UpserterIQLError(Exception):
691
+ upserter: UpserterIQL
692
+
693
+ @override
694
+ def __str__(self) -> str:
695
+ return f"Error running {get_class_name(self.upserter)!r}"
696
+
697
+
647
698
  ##
648
699
 
649
700
 
@@ -1099,6 +1150,9 @@ __all__ = [
1099
1150
  "InsertItemsError",
1100
1151
  "TablenameMixin",
1101
1152
  "UpsertItemsError",
1153
+ "Upserter",
1154
+ "UpserterIQL",
1155
+ "UpserterIQLError",
1102
1156
  "check_engine",
1103
1157
  "columnwise_max",
1104
1158
  "columnwise_min",