dycw-utilities 0.114.2__tar.gz → 0.114.4__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.4}/PKG-INFO +1 -1
  2. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/pyproject.toml +2 -2
  3. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_asyncio.py +32 -5
  4. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_redis.py +93 -2
  5. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_slack_sdk.py +49 -1
  6. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_sqlalchemy.py +45 -0
  7. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/__init__.py +1 -1
  8. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/asyncio.py +24 -7
  9. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/redis.py +42 -3
  10. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/slack_sdk.py +50 -2
  11. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/sqlalchemy.py +56 -2
  12. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/.gitignore +0 -0
  13. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/LICENSE +0 -0
  14. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/README.md +0 -0
  15. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/__init__.py +0 -0
  16. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/conftest.py +0 -0
  17. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/modules/__init__.py +0 -0
  18. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/modules/package_missing/__init__.py +0 -0
  19. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/modules/package_missing/module.py +0 -0
  20. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/modules/package_with/__init__.py +0 -0
  21. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/modules/package_with/outer_1.py +0 -0
  22. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/modules/package_with/outer_2.py +0 -0
  23. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/modules/package_with/subpackage/__init__.py +0 -0
  24. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/modules/package_with/subpackage/inner_1.py +0 -0
  25. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/modules/package_with/subpackage/inner_2.py +0 -0
  26. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/modules/package_with/subpackage/inner_3.py +0 -0
  27. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/modules/package_without/__init__.py +0 -0
  28. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/modules/package_without/module_1.py +0 -0
  29. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/modules/package_without/module_2.py +0 -0
  30. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/modules/standalone.py +0 -0
  31. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/modules/with_imports.py +0 -0
  32. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/regressions/test_pytest_regressions/TestMultipleRegressionFixtures__test_main__obj.json +0 -0
  33. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/regressions/test_pytest_regressions/TestMultipleRegressionFixtures__test_main__series.json +0 -0
  34. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_int.json +0 -0
  35. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_literal__false.json +0 -0
  36. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_literal__true.json +0 -0
  37. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_nested.json +0 -0
  38. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/regressions/test_pytest_regressions/TestPolarsRegressionFixture__test_dataframe.json +0 -0
  39. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/regressions/test_pytest_regressions/TestPolarsRegressionFixture__test_series.json +0 -0
  40. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/scripts/__init__.py +0 -0
  41. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/scripts/test_async_service/__init__.py +0 -0
  42. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/scripts/test_async_service/__main__.py +0 -0
  43. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/scripts/test_async_service/run.sh +0 -0
  44. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/scripts/test_queue_processor/__init__.py +0 -0
  45. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/scripts/test_queue_processor/__main__.py +0 -0
  46. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/scripts/test_queue_processor/run.sh +0 -0
  47. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_altair.py +0 -0
  48. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_astor.py +0 -0
  49. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_atomicwrites.py +0 -0
  50. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_atools.py +0 -0
  51. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_cachetools.py +0 -0
  52. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_click.py +0 -0
  53. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_concurrent.py +0 -0
  54. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_contextlib.py +0 -0
  55. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_contextvars.py +0 -0
  56. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_cryptography.py +0 -0
  57. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_cvxpy.py +0 -0
  58. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_dataclasses.py +0 -0
  59. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_datetime.py +0 -0
  60. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_enum.py +0 -0
  61. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_errors.py +0 -0
  62. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_eventkit.py +0 -0
  63. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_fastapi.py +0 -0
  64. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_fpdf2.py +0 -0
  65. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_functions.py +0 -0
  66. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_functools.py +0 -0
  67. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_getpass.py +0 -0
  68. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_git.py +0 -0
  69. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_hashlib.py +0 -0
  70. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_http.py +0 -0
  71. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_hypothesis.py +0 -0
  72. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_ipython.py +0 -0
  73. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_iterables.py +0 -0
  74. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_jupyter.py +0 -0
  75. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_lightweight_charts.py +0 -0
  76. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_logging.py +0 -0
  77. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_loguru.py +0 -0
  78. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_luigi.py +0 -0
  79. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_math.py +0 -0
  80. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_memory_profiler.py +0 -0
  81. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_modules.py +0 -0
  82. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_more_itertools.py +0 -0
  83. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_numpy.py +0 -0
  84. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_operator.py +0 -0
  85. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_optuna.py +0 -0
  86. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_orjson.py +0 -0
  87. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_os.py +0 -0
  88. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_parse.py +0 -0
  89. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_pathlib.py +0 -0
  90. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_period.py +0 -0
  91. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_pickle.py +0 -0
  92. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_platform.py +0 -0
  93. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_polars.py +0 -0
  94. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_polars_ols.py +0 -0
  95. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_pqdm.py +0 -0
  96. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_pydantic.py +0 -0
  97. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_pyinstrument.py +0 -0
  98. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_pyrsistent.py +0 -0
  99. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_pytest.py +0 -0
  100. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_pytest_regressions.py +0 -0
  101. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_python_dotenv.py +0 -0
  102. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_random.py +0 -0
  103. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_re.py +0 -0
  104. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_reprlib.py +0 -0
  105. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_rich.py +0 -0
  106. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_scipy.py +0 -0
  107. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_sentinel.py +0 -0
  108. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_shelve.py +0 -0
  109. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_socket.py +0 -0
  110. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_sqlalchemy_polars.py +0 -0
  111. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_statsmodel.py +0 -0
  112. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_streamlit.py +0 -0
  113. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_sys.py +0 -0
  114. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_tempfile.py +0 -0
  115. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_tenacity.py +0 -0
  116. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_text.py +0 -0
  117. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_threading.py +0 -0
  118. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_timer.py +0 -0
  119. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_traceback.py +0 -0
  120. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_traceback_funcs/__init__.py +0 -0
  121. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_traceback_funcs/chain.py +0 -0
  122. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_traceback_funcs/decorated_async.py +0 -0
  123. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_traceback_funcs/decorated_sync.py +0 -0
  124. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_traceback_funcs/error_bind.py +0 -0
  125. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_traceback_funcs/many.py +0 -0
  126. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_traceback_funcs/one.py +0 -0
  127. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_traceback_funcs/recursive.py +0 -0
  128. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_traceback_funcs/task_group_one.py +0 -0
  129. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_traceback_funcs/task_group_two.py +0 -0
  130. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_traceback_funcs/two.py +0 -0
  131. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_traceback_funcs/untraced.py +0 -0
  132. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_types.py +0 -0
  133. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_typing.py +0 -0
  134. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_typing_funcs/__init__.py +0 -0
  135. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_typing_funcs/no_future.py +0 -0
  136. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_typing_funcs/with_future.py +0 -0
  137. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_tzdata.py +0 -0
  138. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_tzlocal.py +0 -0
  139. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_uuid.py +0 -0
  140. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_version.py +0 -0
  141. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_warnings.py +0 -0
  142. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_whenever.py +0 -0
  143. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_zipfile.py +0 -0
  144. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/tests/test_zoneinfo.py +0 -0
  145. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/altair.py +0 -0
  146. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/astor.py +0 -0
  147. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/atomicwrites.py +0 -0
  148. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/atools.py +0 -0
  149. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/cachetools.py +0 -0
  150. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/click.py +0 -0
  151. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/concurrent.py +0 -0
  152. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/contextlib.py +0 -0
  153. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/contextvars.py +0 -0
  154. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/cryptography.py +0 -0
  155. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/cvxpy.py +0 -0
  156. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/dataclasses.py +0 -0
  157. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/datetime.py +0 -0
  158. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/enum.py +0 -0
  159. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/errors.py +0 -0
  160. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/eventkit.py +0 -0
  161. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/fastapi.py +0 -0
  162. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/fpdf2.py +0 -0
  163. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/functions.py +0 -0
  164. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/functools.py +0 -0
  165. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/getpass.py +0 -0
  166. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/git.py +0 -0
  167. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/hashlib.py +0 -0
  168. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/http.py +0 -0
  169. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/hypothesis.py +0 -0
  170. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/ipython.py +0 -0
  171. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/iterables.py +0 -0
  172. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/jupyter.py +0 -0
  173. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/lightweight_charts.py +0 -0
  174. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/logging.py +0 -0
  175. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/loguru.py +0 -0
  176. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/luigi.py +0 -0
  177. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/math.py +0 -0
  178. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/memory_profiler.py +0 -0
  179. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/modules.py +0 -0
  180. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/more_itertools.py +0 -0
  181. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/numpy.py +0 -0
  182. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/operator.py +0 -0
  183. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/optuna.py +0 -0
  184. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/orjson.py +0 -0
  185. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/os.py +0 -0
  186. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/parse.py +0 -0
  187. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/pathlib.py +0 -0
  188. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/period.py +0 -0
  189. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/pickle.py +0 -0
  190. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/platform.py +0 -0
  191. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/polars.py +0 -0
  192. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/polars_ols.py +0 -0
  193. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/pqdm.py +0 -0
  194. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/py.typed +0 -0
  195. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/pydantic.py +0 -0
  196. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/pyinstrument.py +0 -0
  197. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/pyrsistent.py +0 -0
  198. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/pytest.py +0 -0
  199. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/pytest_regressions.py +0 -0
  200. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/python_dotenv.py +0 -0
  201. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/random.py +0 -0
  202. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/re.py +0 -0
  203. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/reprlib.py +0 -0
  204. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/rich.py +0 -0
  205. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/scipy.py +0 -0
  206. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/sentinel.py +0 -0
  207. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/shelve.py +0 -0
  208. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/socket.py +0 -0
  209. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/sqlalchemy_polars.py +0 -0
  210. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/statsmodels.py +0 -0
  211. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/streamlit.py +0 -0
  212. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/sys.py +0 -0
  213. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/tempfile.py +0 -0
  214. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/tenacity.py +0 -0
  215. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/text.py +0 -0
  216. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/threading.py +0 -0
  217. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/timer.py +0 -0
  218. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/traceback.py +0 -0
  219. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/types.py +0 -0
  220. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/typing.py +0 -0
  221. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/tzdata.py +0 -0
  222. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/tzlocal.py +0 -0
  223. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/uuid.py +0 -0
  224. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/version.py +0 -0
  225. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/warnings.py +0 -0
  226. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/whenever.py +0 -0
  227. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/src/utilities/zipfile.py +0 -0
  228. {dycw_utilities-0.114.2 → dycw_utilities-0.114.4}/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.4
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.4"
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.4"
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,
@@ -374,6 +375,30 @@ class TestInfiniteLooper:
374
375
  with raises(FalseError):
375
376
  _ = await looper()
376
377
 
378
+ async def test_hashable(self) -> None:
379
+ class CustomError(BaseException): ...
380
+
381
+ @dataclass(kw_only=True, unsafe_hash=True)
382
+ class Example(InfiniteLooper[None]):
383
+ counter: int = 0
384
+
385
+ @override
386
+ async def _initialize(self) -> None:
387
+ self.counter = 0
388
+
389
+ @override
390
+ async def _core(self) -> None:
391
+ self.counter += 1
392
+
393
+ @override
394
+ def _yield_events_and_exceptions(
395
+ self,
396
+ ) -> Iterator[tuple[bool, MaybeType[BaseException]]]:
397
+ yield (None, CustomError)
398
+
399
+ looper = Example(sleep_core=0.1)
400
+ _ = hash(looper)
401
+
377
402
  async def test_multiple(self) -> None:
378
403
  class ChildError(BaseException): ...
379
404
 
@@ -434,7 +459,8 @@ class TestInfiniteLooper:
434
459
  assert 10 <= parent.child.counter <= 15
435
460
  assert 3 <= parent.counter <= 7
436
461
 
437
- async def test_error_upon_initialize(self) -> None:
462
+ @given(logger=just("logger") | none())
463
+ async def test_error_upon_initialize(self, *, logger: str | None) -> None:
438
464
  class CustomError(Exception): ...
439
465
 
440
466
  @dataclass(kw_only=True)
@@ -449,9 +475,10 @@ class TestInfiniteLooper:
449
475
 
450
476
  with raises(TimeoutError):
451
477
  async with timeout_dur(duration=0.5):
452
- _ = await Example(sleep_core=0.1)()
478
+ _ = await Example(sleep_core=0.1, logger=logger)()
453
479
 
454
- async def test_error_upon_core(self) -> None:
480
+ @given(logger=just("logger") | none())
481
+ async def test_error_upon_core(self, *, logger: str | None) -> None:
455
482
  class CustomError(Exception): ...
456
483
 
457
484
  @dataclass(kw_only=True)
@@ -468,7 +495,7 @@ class TestInfiniteLooper:
468
495
 
469
496
  with raises(TimeoutError):
470
497
  async with timeout_dur(duration=0.5):
471
- _ = await Example(sleep_core=0.1)()
498
+ _ = await Example(sleep_core=0.1, logger=logger)()
472
499
 
473
500
  async def test_error_no_event_found(self) -> None:
474
501
  @dataclass(kw_only=True)
@@ -536,7 +563,7 @@ class TestInfiniteQueueLooper:
536
563
 
537
564
  @override
538
565
  async def _process_items(self, *items: int) -> None:
539
- raise CustomError
566
+ raise CustomError(*items)
540
567
 
541
568
  processor = Example(sleep_core=0.1)
542
569
  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(
@@ -9,11 +9,17 @@ from aiohttp import InvalidUrlClientError
9
9
  from pytest import mark, raises
10
10
  from slack_sdk.webhook.async_client import AsyncWebhookClient
11
11
 
12
+ from utilities.asyncio import EnhancedTaskGroup, sleep_dur
12
13
  from utilities.datetime import MINUTE
13
14
  from utilities.iterables import one
14
15
  from utilities.os import get_env_var
15
16
  from utilities.pytest import throttle
16
- from utilities.slack_sdk import SlackHandler, _get_client, send_to_slack
17
+ from utilities.slack_sdk import (
18
+ SlackHandler,
19
+ SlackHandlerIQL,
20
+ _get_client,
21
+ send_to_slack,
22
+ )
17
23
 
18
24
  if TYPE_CHECKING:
19
25
  from collections.abc import Sequence
@@ -138,3 +144,45 @@ class TestSlackHandler:
138
144
  "message %d from %s", i, TestSlackHandler.test_real.__qualname__
139
145
  )
140
146
  await sleep(0.1)
147
+
148
+
149
+ class TestSlackHandlerIQL:
150
+ async def test_main(self, *, tmp_path: Path) -> None:
151
+ messages: Sequence[str] = []
152
+
153
+ async def sender(_: str, text: str, /) -> None:
154
+ await sleep(0.01)
155
+ messages.append(text)
156
+
157
+ logger = getLogger(str(tmp_path))
158
+ logger.addHandler(handler := SlackHandlerIQL("url", sender=sender))
159
+
160
+ async def sleep_then_log() -> None:
161
+ await sleep_dur(duration=0.1)
162
+ logger.warning("message")
163
+
164
+ with raises(ExceptionGroup): # noqa: PT012
165
+ async with EnhancedTaskGroup(timeout=0.5) as tg:
166
+ _ = tg.create_task(handler())
167
+ _ = tg.create_task(sleep_then_log())
168
+
169
+ assert messages == ["message"]
170
+
171
+ @mark.skipif(get_env_var("SLACK", nullable=True) is None, reason="'SLACK' not set")
172
+ @throttle(duration=5 * MINUTE)
173
+ async def test_real(self, *, tmp_path: Path) -> None:
174
+ url = get_env_var("SLACK")
175
+ logger = getLogger(str(tmp_path))
176
+ logger.addHandler(handler := SlackHandlerIQL(url))
177
+
178
+ async def sleep_then_log() -> None:
179
+ await sleep_dur(duration=0.1)
180
+ for i in range(10):
181
+ logger.warning(
182
+ "message %d from %s", i, TestSlackHandlerIQL.test_real.__qualname__
183
+ )
184
+
185
+ with raises(ExceptionGroup): # noqa: PT012
186
+ async with EnhancedTaskGroup(timeout=0.5) as tg:
187
+ _ = tg.create_task(handler())
188
+ _ = tg.create_task(sleep_then_log())
@@ -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.4"
@@ -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
 
@@ -324,15 +326,16 @@ class ExceptionProcessor(QueueProcessor[Exception | type[Exception]]):
324
326
  ##
325
327
 
326
328
 
327
- @dataclass(kw_only=True)
329
+ @dataclass(kw_only=True, unsafe_hash=True)
328
330
  class InfiniteLooper(ABC, Generic[THashable]):
329
331
  """An infinite loop which can throw exceptions by setting events."""
330
332
 
331
- _events: Mapping[THashable, Event] = field(
332
- default_factory=dict, init=False, repr=False
333
- )
334
333
  sleep_core: Duration = SECOND
335
334
  sleep_restart: Duration = MINUTE
335
+ logger: str | None = None
336
+ _events: Mapping[THashable, Event] = field(
337
+ default_factory=dict, init=False, repr=False, hash=False
338
+ )
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",
@@ -9,7 +9,12 @@ from typing import TYPE_CHECKING, override
9
9
 
10
10
  from slack_sdk.webhook.async_client import AsyncWebhookClient
11
11
 
12
- from utilities.asyncio import QueueProcessor, sleep_dur, timeout_dur
12
+ from utilities.asyncio import (
13
+ InfiniteQueueLooper,
14
+ QueueProcessor,
15
+ sleep_dur,
16
+ timeout_dur,
17
+ )
13
18
  from utilities.datetime import MINUTE, SECOND, datetime_duration_to_float
14
19
  from utilities.functools import cache
15
20
  from utilities.math import safe_round
@@ -95,6 +100,49 @@ class SlackHandler(Handler, QueueProcessor[str]):
95
100
  await sleep_dur(duration=self.sleep)
96
101
 
97
102
 
103
+ @dataclass(init=False, unsafe_hash=True)
104
+ class SlackHandlerIQL(Handler, InfiniteQueueLooper[None, str]):
105
+ """Handler for sending messages to Slack."""
106
+
107
+ @override
108
+ def __init__(
109
+ self,
110
+ url: str,
111
+ /,
112
+ *,
113
+ level: int = NOTSET,
114
+ sleep_core: Duration = _SLEEP,
115
+ sleep_restart: Duration = _SLEEP,
116
+ queue_type: type[Queue[str]] = Queue,
117
+ sender: Callable[[str, str], Coroutine1[None]] = _send_adapter,
118
+ timeout: Duration = _TIMEOUT,
119
+ ) -> None:
120
+ InfiniteQueueLooper.__init__( # InfiniteQueueLooper first
121
+ self, queue_type=queue_type
122
+ )
123
+ InfiniteQueueLooper.__post_init__(self)
124
+ Handler.__init__(self, level=level)
125
+ self.url = url
126
+ self.sender = sender
127
+ self.timeout = timeout
128
+ self.sleep_core = sleep_core
129
+ self.sleep_restart = sleep_restart
130
+
131
+ @override
132
+ def emit(self, record: LogRecord) -> None:
133
+ try:
134
+ self.put_items_nowait(self.format(record))
135
+ except Exception: # noqa: BLE001 # pragma: no cover
136
+ self.handleError(record)
137
+
138
+ @override
139
+ async def _process_items(self, *items: str) -> None:
140
+ """Process the first item."""
141
+ text = "\n".join(items)
142
+ async with timeout_dur(duration=self.timeout):
143
+ await self.sender(self.url, text)
144
+
145
+
98
146
  ##
99
147
 
100
148
 
@@ -128,4 +176,4 @@ def _get_client(url: str, /, *, timeout: Duration = _TIMEOUT) -> AsyncWebhookCli
128
176
  return AsyncWebhookClient(url, timeout=timeout_use)
129
177
 
130
178
 
131
- __all__ = ["SendToSlackError", "SlackHandler", "send_to_slack"]
179
+ __all__ = ["SendToSlackError", "SlackHandler", "SlackHandlerIQL", "send_to_slack"]
@@ -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",