dycw-utilities 0.116.6__tar.gz → 0.117.1__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 (230) hide show
  1. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/PKG-INFO +1 -1
  2. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/pyproject.toml +2 -2
  3. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_asyncio.py +73 -10
  4. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_datetime.py +56 -56
  5. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/__init__.py +1 -1
  6. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/asyncio.py +71 -12
  7. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/datetime.py +58 -55
  8. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/whenever.py +2 -2
  9. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/.gitignore +0 -0
  10. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/LICENSE +0 -0
  11. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/README.md +0 -0
  12. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/__init__.py +0 -0
  13. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/conftest.py +0 -0
  14. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/modules/__init__.py +0 -0
  15. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/modules/package_missing/__init__.py +0 -0
  16. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/modules/package_missing/module.py +0 -0
  17. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/modules/package_with/__init__.py +0 -0
  18. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/modules/package_with/outer_1.py +0 -0
  19. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/modules/package_with/outer_2.py +0 -0
  20. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/modules/package_with/subpackage/__init__.py +0 -0
  21. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/modules/package_with/subpackage/inner_1.py +0 -0
  22. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/modules/package_with/subpackage/inner_2.py +0 -0
  23. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/modules/package_with/subpackage/inner_3.py +0 -0
  24. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/modules/package_without/__init__.py +0 -0
  25. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/modules/package_without/module_1.py +0 -0
  26. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/modules/package_without/module_2.py +0 -0
  27. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/modules/standalone.py +0 -0
  28. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/modules/with_imports.py +0 -0
  29. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/regressions/test_pytest_regressions/TestMultipleRegressionFixtures__test_main__obj.json +0 -0
  30. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/regressions/test_pytest_regressions/TestMultipleRegressionFixtures__test_main__series.json +0 -0
  31. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_int.json +0 -0
  32. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_literal__false.json +0 -0
  33. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_literal__true.json +0 -0
  34. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_nested.json +0 -0
  35. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/regressions/test_pytest_regressions/TestPolarsRegressionFixture__test_dataframe.json +0 -0
  36. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/regressions/test_pytest_regressions/TestPolarsRegressionFixture__test_series.json +0 -0
  37. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/scripts/__init__.py +0 -0
  38. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/scripts/test_async_service/__init__.py +0 -0
  39. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/scripts/test_async_service/__main__.py +0 -0
  40. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/scripts/test_async_service/run.sh +0 -0
  41. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/scripts/test_queue_processor/__init__.py +0 -0
  42. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/scripts/test_queue_processor/__main__.py +0 -0
  43. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/scripts/test_queue_processor/run.sh +0 -0
  44. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_altair.py +0 -0
  45. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_astor.py +0 -0
  46. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_atomicwrites.py +0 -0
  47. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_atools.py +0 -0
  48. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_cachetools.py +0 -0
  49. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_click.py +0 -0
  50. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_concurrent.py +0 -0
  51. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_contextlib.py +0 -0
  52. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_contextvars.py +0 -0
  53. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_cryptography.py +0 -0
  54. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_cvxpy.py +0 -0
  55. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_dataclasses.py +0 -0
  56. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_enum.py +0 -0
  57. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_errors.py +0 -0
  58. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_eventkit.py +0 -0
  59. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_fastapi.py +0 -0
  60. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_fpdf2.py +0 -0
  61. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_functions.py +0 -0
  62. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_functools.py +0 -0
  63. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_getpass.py +0 -0
  64. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_git.py +0 -0
  65. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_hashlib.py +0 -0
  66. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_http.py +0 -0
  67. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_hypothesis.py +0 -0
  68. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_importlib.py +0 -0
  69. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_ipython.py +0 -0
  70. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_iterables.py +0 -0
  71. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_jupyter.py +0 -0
  72. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_lightweight_charts.py +0 -0
  73. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_logging.py +0 -0
  74. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_loguru.py +0 -0
  75. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_luigi.py +0 -0
  76. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_math.py +0 -0
  77. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_memory_profiler.py +0 -0
  78. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_modules.py +0 -0
  79. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_more_itertools.py +0 -0
  80. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_numpy.py +0 -0
  81. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_operator.py +0 -0
  82. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_optuna.py +0 -0
  83. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_orjson.py +0 -0
  84. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_os.py +0 -0
  85. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_parse.py +0 -0
  86. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_pathlib.py +0 -0
  87. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_period.py +0 -0
  88. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_pickle.py +0 -0
  89. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_platform.py +0 -0
  90. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_polars.py +0 -0
  91. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_polars_ols.py +0 -0
  92. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_pqdm.py +0 -0
  93. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_pydantic.py +0 -0
  94. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_pyinstrument.py +0 -0
  95. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_pyrsistent.py +0 -0
  96. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_pytest.py +0 -0
  97. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_pytest_regressions.py +0 -0
  98. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_python_dotenv.py +0 -0
  99. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_random.py +0 -0
  100. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_re.py +0 -0
  101. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_redis.py +0 -0
  102. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_reprlib.py +0 -0
  103. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_rich.py +0 -0
  104. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_scipy.py +0 -0
  105. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_sentinel.py +0 -0
  106. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_shelve.py +0 -0
  107. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_slack_sdk.py +0 -0
  108. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_socket.py +0 -0
  109. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_sqlalchemy.py +0 -0
  110. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_sqlalchemy_polars.py +0 -0
  111. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_statsmodel.py +0 -0
  112. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_streamlit.py +0 -0
  113. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_sys.py +0 -0
  114. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_tempfile.py +0 -0
  115. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_tenacity.py +0 -0
  116. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_text.py +0 -0
  117. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_threading.py +0 -0
  118. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_timer.py +0 -0
  119. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_traceback.py +0 -0
  120. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_traceback_funcs/__init__.py +0 -0
  121. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_traceback_funcs/chain.py +0 -0
  122. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_traceback_funcs/decorated_async.py +0 -0
  123. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_traceback_funcs/decorated_sync.py +0 -0
  124. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_traceback_funcs/error_bind.py +0 -0
  125. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_traceback_funcs/many.py +0 -0
  126. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_traceback_funcs/one.py +0 -0
  127. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_traceback_funcs/recursive.py +0 -0
  128. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_traceback_funcs/task_group_one.py +0 -0
  129. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_traceback_funcs/task_group_two.py +0 -0
  130. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_traceback_funcs/two.py +0 -0
  131. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_traceback_funcs/untraced.py +0 -0
  132. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_types.py +0 -0
  133. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_typing.py +0 -0
  134. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_typing_funcs/__init__.py +0 -0
  135. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_typing_funcs/no_future.py +0 -0
  136. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_typing_funcs/with_future.py +0 -0
  137. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_tzdata.py +0 -0
  138. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_tzlocal.py +0 -0
  139. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_uuid.py +0 -0
  140. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_version.py +0 -0
  141. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_warnings.py +0 -0
  142. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_whenever.py +0 -0
  143. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_zipfile.py +0 -0
  144. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/tests/test_zoneinfo.py +0 -0
  145. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/altair.py +0 -0
  146. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/astor.py +0 -0
  147. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/atomicwrites.py +0 -0
  148. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/atools.py +0 -0
  149. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/cachetools.py +0 -0
  150. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/click.py +0 -0
  151. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/concurrent.py +0 -0
  152. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/contextlib.py +0 -0
  153. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/contextvars.py +0 -0
  154. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/cryptography.py +0 -0
  155. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/cvxpy.py +0 -0
  156. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/dataclasses.py +0 -0
  157. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/enum.py +0 -0
  158. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/errors.py +0 -0
  159. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/eventkit.py +0 -0
  160. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/fastapi.py +0 -0
  161. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/fpdf2.py +0 -0
  162. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/functions.py +0 -0
  163. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/functools.py +0 -0
  164. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/getpass.py +0 -0
  165. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/git.py +0 -0
  166. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/hashlib.py +0 -0
  167. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/http.py +0 -0
  168. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/hypothesis.py +0 -0
  169. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/importlib.py +0 -0
  170. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/ipython.py +0 -0
  171. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/iterables.py +0 -0
  172. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/jupyter.py +0 -0
  173. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/lightweight_charts.py +0 -0
  174. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/logging.py +0 -0
  175. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/loguru.py +0 -0
  176. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/luigi.py +0 -0
  177. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/math.py +0 -0
  178. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/memory_profiler.py +0 -0
  179. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/modules.py +0 -0
  180. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/more_itertools.py +0 -0
  181. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/numpy.py +0 -0
  182. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/operator.py +0 -0
  183. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/optuna.py +0 -0
  184. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/orjson.py +0 -0
  185. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/os.py +0 -0
  186. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/parse.py +0 -0
  187. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/pathlib.py +0 -0
  188. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/period.py +0 -0
  189. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/pickle.py +0 -0
  190. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/platform.py +0 -0
  191. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/polars.py +0 -0
  192. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/polars_ols.py +0 -0
  193. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/pqdm.py +0 -0
  194. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/py.typed +0 -0
  195. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/pydantic.py +0 -0
  196. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/pyinstrument.py +0 -0
  197. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/pyrsistent.py +0 -0
  198. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/pytest.py +0 -0
  199. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/pytest_regressions.py +0 -0
  200. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/python_dotenv.py +0 -0
  201. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/random.py +0 -0
  202. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/re.py +0 -0
  203. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/redis.py +0 -0
  204. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/reprlib.py +0 -0
  205. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/rich.py +0 -0
  206. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/scipy.py +0 -0
  207. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/sentinel.py +0 -0
  208. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/shelve.py +0 -0
  209. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/slack_sdk.py +0 -0
  210. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/socket.py +0 -0
  211. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/sqlalchemy.py +0 -0
  212. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/sqlalchemy_polars.py +0 -0
  213. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/statsmodels.py +0 -0
  214. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/streamlit.py +0 -0
  215. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/sys.py +0 -0
  216. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/tempfile.py +0 -0
  217. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/tenacity.py +0 -0
  218. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/text.py +0 -0
  219. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/threading.py +0 -0
  220. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/timer.py +0 -0
  221. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/traceback.py +0 -0
  222. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/types.py +0 -0
  223. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/typing.py +0 -0
  224. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/tzdata.py +0 -0
  225. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/tzlocal.py +0 -0
  226. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/uuid.py +0 -0
  227. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/version.py +0 -0
  228. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/warnings.py +0 -0
  229. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/zipfile.py +0 -0
  230. {dycw_utilities-0.116.6 → dycw_utilities-0.117.1}/src/utilities/zoneinfo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.116.6
3
+ Version: 0.117.1
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.116.6"
95
+ version = "0.117.1"
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.116.6"
337
+ current_version = "0.117.1"
338
338
 
339
339
  [[tool.bumpversion.files]]
340
340
  filename = "src/utilities/__init__.py"
@@ -17,7 +17,7 @@ from itertools import chain, count
17
17
  from re import search
18
18
  from typing import TYPE_CHECKING, Self, override
19
19
 
20
- from hypothesis import Phase, given, settings
20
+ from hypothesis import HealthCheck, Phase, given, settings
21
21
  from hypothesis.strategies import (
22
22
  DataObject,
23
23
  data,
@@ -28,7 +28,7 @@ from hypothesis.strategies import (
28
28
  permutations,
29
29
  sampled_from,
30
30
  )
31
- from pytest import approx, mark, param, raises
31
+ from pytest import LogCaptureFixture, approx, mark, param, raises
32
32
 
33
33
  from utilities.asyncio import (
34
34
  AsyncLoopingService,
@@ -42,17 +42,25 @@ from utilities.asyncio import (
42
42
  QueueProcessor,
43
43
  UniquePriorityQueue,
44
44
  UniqueQueue,
45
+ _DurationOrEvery,
45
46
  get_event,
46
47
  get_items,
47
48
  get_items_nowait,
48
49
  put_items,
49
50
  put_items_nowait,
50
51
  sleep_dur,
52
+ sleep_until,
53
+ sleep_until_rounded,
51
54
  stream_command,
52
55
  timeout_dur,
53
56
  )
54
57
  from utilities.dataclasses import replace_non_sentinel
55
- from utilities.datetime import MILLISECOND, datetime_duration_to_timedelta
58
+ from utilities.datetime import (
59
+ MILLISECOND,
60
+ MINUTE,
61
+ datetime_duration_to_timedelta,
62
+ get_now,
63
+ )
56
64
  from utilities.hypothesis import sentinels, text_ascii
57
65
  from utilities.iterables import one, unique_everseen
58
66
  from utilities.pytest import skipif_windows
@@ -341,8 +349,8 @@ class TestGetEvent:
341
349
 
342
350
 
343
351
  class TestInfiniteLooper:
344
- @given(n=integers(10, 11))
345
- async def test_main(self, *, n: int) -> None:
352
+ @given(n=integers(10, 11), sleep_core=sampled_from([0.1, ("every", 0.1)]))
353
+ async def test_main(self, *, n: int, sleep_core: _DurationOrEvery) -> None:
346
354
  class TrueError(BaseException): ...
347
355
 
348
356
  class FalseError(BaseException): ...
@@ -368,7 +376,7 @@ class TestInfiniteLooper:
368
376
  yield (True, TrueError)
369
377
  yield (False, FalseError)
370
378
 
371
- looper = Example(sleep_core=0.1)
379
+ looper = Example(sleep_core=sleep_core)
372
380
  match n % 2 == 0:
373
381
  case True:
374
382
  with raises(TrueError):
@@ -520,7 +528,24 @@ class TestInfiniteLooper:
520
528
  assert 1 <= looper.counter <= 6
521
529
 
522
530
  @given(logger=just("logger") | none())
523
- async def test_error_upon_initialize(self, *, logger: str | None) -> None:
531
+ @mark.parametrize(
532
+ ("sleep_restart", "desc"),
533
+ [
534
+ (60.0, "for 0:01:00"),
535
+ (MINUTE, "for 0:01:00"),
536
+ (("every", 60), "until next 0:01:00"),
537
+ (("every", MINUTE), "until next 0:01:00"),
538
+ ],
539
+ )
540
+ @settings(suppress_health_check={HealthCheck.function_scoped_fixture})
541
+ async def test_error_upon_initialize(
542
+ self,
543
+ *,
544
+ sleep_restart: _DurationOrEvery,
545
+ desc: str,
546
+ logger: str | None,
547
+ caplog: LogCaptureFixture,
548
+ ) -> None:
524
549
  class CustomError(Exception): ...
525
550
 
526
551
  @dataclass(kw_only=True)
@@ -533,13 +558,34 @@ class TestInfiniteLooper:
533
558
  async def _core(self) -> None:
534
559
  raise NotImplementedError
535
560
 
536
- looper = Example(sleep_core=0.1, logger=logger)
561
+ looper = Example(sleep_core=0.1, sleep_restart=sleep_restart, logger=logger)
537
562
  with raises(TimeoutError):
538
563
  async with timeout_dur(duration=0.5):
539
564
  _ = await looper()
565
+ if logger is not None:
566
+ message = caplog.messages[0]
567
+ expected = f"'Example' encountered 'CustomError()' whilst initializing; sleeping {desc}..."
568
+ assert message == expected
540
569
 
541
570
  @given(logger=just("logger") | none())
542
- async def test_error_group_upon_coroutines(self, *, logger: str | None) -> None:
571
+ @mark.parametrize(
572
+ ("sleep_restart", "desc"),
573
+ [
574
+ (60.0, "for 0:01:00"),
575
+ (MINUTE, "for 0:01:00"),
576
+ (("every", 60), "until next 0:01:00"),
577
+ (("every", MINUTE), "until next 0:01:00"),
578
+ ],
579
+ )
580
+ @settings(suppress_health_check={HealthCheck.function_scoped_fixture})
581
+ async def test_error_group_upon_coroutines(
582
+ self,
583
+ *,
584
+ sleep_restart: _DurationOrEvery,
585
+ desc: str,
586
+ logger: str | None,
587
+ caplog: LogCaptureFixture,
588
+ ) -> None:
543
589
  class CustomError(Exception): ...
544
590
 
545
591
  @dataclass(kw_only=True)
@@ -554,10 +600,14 @@ class TestInfiniteLooper:
554
600
  ) -> Iterator[tuple[None, MaybeType[BaseException]]]:
555
601
  yield (None, CustomError)
556
602
 
557
- looper = Example(sleep_core=0.1, logger=logger)
603
+ looper = Example(sleep_core=0.1, sleep_restart=sleep_restart, logger=logger)
558
604
  with raises(TimeoutError):
559
605
  async with timeout_dur(duration=0.5):
560
606
  _ = await looper()
607
+ if logger is not None:
608
+ message = caplog.messages[0]
609
+ expected = f"'Example' encountered 'CustomError()'; sleeping {desc}..."
610
+ assert message == expected
561
611
 
562
612
  async def test_error_no_event_found(self) -> None:
563
613
  @dataclass(kw_only=True)
@@ -951,6 +1001,19 @@ class TestSleepDur:
951
1001
  assert timer <= 0.01
952
1002
 
953
1003
 
1004
+ class TestSleepUntil:
1005
+ async def test_main(self) -> None:
1006
+ now = get_now()
1007
+ with Timer() as timer:
1008
+ await sleep_until(now + 10 * MILLISECOND)
1009
+ assert timer >= datetime_duration_to_timedelta(5 * MILLISECOND)
1010
+
1011
+
1012
+ class TestSleepUntilRounded:
1013
+ async def test_main(self) -> None:
1014
+ await sleep_until_rounded(10 * MILLISECOND)
1015
+
1016
+
954
1017
  class TestStreamCommand:
955
1018
  @skipif_windows
956
1019
  async def test_main(self) -> None:
@@ -88,6 +88,8 @@ from utilities.datetime import (
88
88
  date_to_datetime,
89
89
  date_to_month,
90
90
  datetime_duration_to_float,
91
+ datetime_duration_to_microseconds,
92
+ datetime_duration_to_milliseconds,
91
93
  datetime_duration_to_timedelta,
92
94
  datetime_utc,
93
95
  days_since_epoch,
@@ -128,8 +130,6 @@ from utilities.datetime import (
128
130
  serialize_month,
129
131
  sub_duration,
130
132
  timedelta_since_epoch,
131
- timedelta_to_microseconds,
132
- timedelta_to_milliseconds,
133
133
  yield_days,
134
134
  yield_weekdays,
135
135
  )
@@ -520,6 +520,60 @@ class TestDateTimeDurationToFloat:
520
520
  assert result == timedelta.total_seconds()
521
521
 
522
522
 
523
+ class TestDateTimeDurationToMicrosecondsOrMilliseconds:
524
+ @given(timedelta=timedeltas())
525
+ def test_timedelta_to_microseconds(self, *, timedelta: dt.timedelta) -> None:
526
+ microseconds = datetime_duration_to_microseconds(timedelta)
527
+ result = microseconds_to_timedelta(microseconds)
528
+ assert result == timedelta
529
+
530
+ @given(microseconds=integers())
531
+ def test_microseconds_to_timedelta(self, *, microseconds: int) -> None:
532
+ with assume_does_not_raise(OverflowError):
533
+ timedelta = microseconds_to_timedelta(microseconds)
534
+ result = datetime_duration_to_microseconds(timedelta)
535
+ assert result == microseconds
536
+
537
+ @given(timedelta=timedeltas(), strict=booleans())
538
+ @settings(suppress_health_check={HealthCheck.filter_too_much})
539
+ def test_timedelta_to_milliseconds_exact(
540
+ self, *, timedelta: dt.timedelta, strict: bool
541
+ ) -> None:
542
+ _, remainder = divmod(timedelta.microseconds, _MICROSECONDS_PER_MILLISECOND)
543
+ _ = assume(remainder == 0)
544
+ milliseconds = datetime_duration_to_milliseconds(timedelta, strict=strict)
545
+ assert isinstance(milliseconds, int)
546
+ result = milliseconds_to_timedelta(milliseconds)
547
+ assert result == timedelta
548
+
549
+ @given(timedelta=timedeltas())
550
+ def test_timedelta_to_milliseconds_inexact(
551
+ self, *, timedelta: dt.timedelta
552
+ ) -> None:
553
+ _, remainder = divmod(timedelta.microseconds, _MICROSECONDS_PER_MILLISECOND)
554
+ _ = assume(remainder != 0)
555
+ milliseconds = datetime_duration_to_milliseconds(timedelta)
556
+ result = milliseconds_to_timedelta(round(milliseconds))
557
+ assert abs(result - timedelta) <= SECOND
558
+
559
+ @given(timedelta=timedeltas())
560
+ def test_timedelta_to_milliseconds_error(self, *, timedelta: dt.timedelta) -> None:
561
+ _, microseconds = divmod(timedelta.microseconds, _MICROSECONDS_PER_MILLISECOND)
562
+ _ = assume(microseconds != 0)
563
+ with raises(
564
+ TimedeltaToMillisecondsError,
565
+ match=r"Unable to convert .* to milliseconds; got .* microsecond\(s\)",
566
+ ):
567
+ _ = datetime_duration_to_milliseconds(timedelta, strict=True)
568
+
569
+ @given(milliseconds=int32s())
570
+ def test_milliseconds_to_timedelta(self, *, milliseconds: int) -> None:
571
+ with assume_does_not_raise(OverflowError):
572
+ timedelta = milliseconds_to_timedelta(milliseconds)
573
+ result = datetime_duration_to_milliseconds(timedelta)
574
+ assert result == milliseconds
575
+
576
+
523
577
  class TestDateTimeDurationToTimeDelta:
524
578
  @given(n=int32s())
525
579
  def test_int(self, *, n: int) -> None:
@@ -1205,60 +1259,6 @@ class TestTimedeltaSinceEpoch:
1205
1259
  assert result1 == result2
1206
1260
 
1207
1261
 
1208
- class TestTimedeltaToMicrosecondsOrMilliseconds:
1209
- @given(timedelta=timedeltas())
1210
- def test_timedelta_to_microseconds(self, *, timedelta: dt.timedelta) -> None:
1211
- microseconds = timedelta_to_microseconds(timedelta)
1212
- result = microseconds_to_timedelta(microseconds)
1213
- assert result == timedelta
1214
-
1215
- @given(microseconds=integers())
1216
- def test_microseconds_to_timedelta(self, *, microseconds: int) -> None:
1217
- with assume_does_not_raise(OverflowError):
1218
- timedelta = microseconds_to_timedelta(microseconds)
1219
- result = timedelta_to_microseconds(timedelta)
1220
- assert result == microseconds
1221
-
1222
- @given(timedelta=timedeltas(), strict=booleans())
1223
- @settings(suppress_health_check={HealthCheck.filter_too_much})
1224
- def test_timedelta_to_milliseconds_exact(
1225
- self, *, timedelta: dt.timedelta, strict: bool
1226
- ) -> None:
1227
- _, remainder = divmod(timedelta.microseconds, _MICROSECONDS_PER_MILLISECOND)
1228
- _ = assume(remainder == 0)
1229
- milliseconds = timedelta_to_milliseconds(timedelta, strict=strict)
1230
- assert isinstance(milliseconds, int)
1231
- result = milliseconds_to_timedelta(milliseconds)
1232
- assert result == timedelta
1233
-
1234
- @given(timedelta=timedeltas())
1235
- def test_timedelta_to_milliseconds_inexact(
1236
- self, *, timedelta: dt.timedelta
1237
- ) -> None:
1238
- _, remainder = divmod(timedelta.microseconds, _MICROSECONDS_PER_MILLISECOND)
1239
- _ = assume(remainder != 0)
1240
- milliseconds = timedelta_to_milliseconds(timedelta)
1241
- result = milliseconds_to_timedelta(round(milliseconds))
1242
- assert abs(result - timedelta) <= SECOND
1243
-
1244
- @given(timedelta=timedeltas())
1245
- def test_timedelta_to_milliseconds_error(self, *, timedelta: dt.timedelta) -> None:
1246
- _, microseconds = divmod(timedelta.microseconds, _MICROSECONDS_PER_MILLISECOND)
1247
- _ = assume(microseconds != 0)
1248
- with raises(
1249
- TimedeltaToMillisecondsError,
1250
- match=r"Unable to convert .* to milliseconds; got .* microsecond\(s\)",
1251
- ):
1252
- _ = timedelta_to_milliseconds(timedelta, strict=True)
1253
-
1254
- @given(milliseconds=int32s())
1255
- def test_milliseconds_to_timedelta(self, *, milliseconds: int) -> None:
1256
- with assume_does_not_raise(OverflowError):
1257
- timedelta = milliseconds_to_timedelta(milliseconds)
1258
- result = timedelta_to_milliseconds(timedelta)
1259
- assert result == milliseconds
1260
-
1261
-
1262
1262
  class TestTimedeltas:
1263
1263
  @mark.parametrize(
1264
1264
  "timedelta",
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.116.6"
3
+ __version__ = "0.117.1"
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import datetime as dt
3
4
  from abc import ABC, abstractmethod
4
5
  from asyncio import (
5
6
  CancelledError,
@@ -32,6 +33,7 @@ from typing import (
32
33
  TYPE_CHECKING,
33
34
  Any,
34
35
  Generic,
36
+ Literal,
35
37
  NoReturn,
36
38
  Self,
37
39
  TextIO,
@@ -41,7 +43,15 @@ from typing import (
41
43
  override,
42
44
  )
43
45
 
44
- from utilities.datetime import MILLISECOND, MINUTE, SECOND, datetime_duration_to_float
46
+ from utilities.datetime import (
47
+ MILLISECOND,
48
+ MINUTE,
49
+ SECOND,
50
+ datetime_duration_to_float,
51
+ datetime_duration_to_timedelta,
52
+ get_now,
53
+ round_datetime,
54
+ )
45
55
  from utilities.errors import ImpossibleCaseError, repr_error
46
56
  from utilities.functions import ensure_int, ensure_not_none, get_class_name
47
57
  from utilities.reprlib import get_repr
@@ -329,12 +339,15 @@ class ExceptionProcessor(QueueProcessor[Exception | type[Exception]]):
329
339
  ##
330
340
 
331
341
 
342
+ type _DurationOrEvery = Duration | tuple[Literal["every"], Duration]
343
+
344
+
332
345
  @dataclass(kw_only=True, unsafe_hash=True)
333
346
  class InfiniteLooper(ABC, Generic[THashable]):
334
347
  """An infinite loop which can throw exceptions by setting events."""
335
348
 
336
- sleep_core: Duration = SECOND
337
- sleep_restart: Duration = MINUTE
349
+ sleep_core: _DurationOrEvery = SECOND
350
+ sleep_restart: _DurationOrEvery = MINUTE
338
351
  logger: str | None = None
339
352
  _events: Mapping[THashable, Event] = field(
340
353
  default_factory=dict, init=False, repr=False, hash=False
@@ -361,7 +374,7 @@ class InfiniteLooper(ABC, Generic[THashable]):
361
374
  await self._initialize()
362
375
  except Exception as error: # noqa: BLE001
363
376
  self._error_upon_initialize(error)
364
- await sleep_dur(duration=self.sleep_restart)
377
+ await self._run_sleep(self.sleep_restart)
365
378
  else:
366
379
  while True:
367
380
  try:
@@ -372,14 +385,14 @@ class InfiniteLooper(ABC, Generic[THashable]):
372
385
  )
373
386
  except StopIteration:
374
387
  await self._core()
375
- await sleep_dur(duration=self.sleep_core)
388
+ await self._run_sleep(self.sleep_core)
376
389
  else:
377
390
  self._raise_error(event)
378
391
  except InfiniteLooperError:
379
392
  raise
380
393
  except Exception as error: # noqa: BLE001
381
394
  self._error_upon_core(error)
382
- await sleep_dur(duration=self.sleep_restart)
395
+ await self._run_sleep(self.sleep_restart)
383
396
 
384
397
  async def _run_looper_with_coroutines(
385
398
  self, *coroutines: Callable[[], Coroutine1[None]]
@@ -393,7 +406,7 @@ class InfiniteLooper(ABC, Generic[THashable]):
393
406
  _ = [tg.create_task(c()) for c in coroutines]
394
407
  except ExceptionGroup as error:
395
408
  self._error_group_upon_coroutines(error)
396
- await sleep_dur(duration=self.sleep_restart)
409
+ await self._run_sleep(self.sleep_restart)
397
410
 
398
411
  async def _initialize(self) -> None:
399
412
  """Initialize the loop."""
@@ -405,20 +418,20 @@ class InfiniteLooper(ABC, Generic[THashable]):
405
418
  """Handle any errors upon initializing the looper."""
406
419
  if self.logger is not None:
407
420
  getLogger(name=self.logger).error(
408
- "%r encountered %r whilst initializing; sleeping for %s...",
421
+ "%r encountered %r whilst initializing; sleeping %s...",
409
422
  get_class_name(self),
410
423
  repr_error(error),
411
- self.sleep_restart,
424
+ self._sleep_restart_desc,
412
425
  )
413
426
 
414
427
  def _error_upon_core(self, error: Exception, /) -> None:
415
428
  """Handle any errors upon running the core function."""
416
429
  if self.logger is not None:
417
430
  getLogger(name=self.logger).error(
418
- "%r encountered %r; sleeping for %s...",
431
+ "%r encountered %r; sleeping %s...",
419
432
  get_class_name(self),
420
433
  repr_error(error),
421
- self.sleep_restart,
434
+ self._sleep_restart_desc,
422
435
  )
423
436
 
424
437
  def _error_group_upon_coroutines(self, group: ExceptionGroup, /) -> None:
@@ -431,7 +444,7 @@ class InfiniteLooper(ABC, Generic[THashable]):
431
444
  f"- Error #{i}/{n}: {repr_error(e)}"
432
445
  for i, e in enumerate(errors, start=1)
433
446
  )
434
- msgs.append(f"Sleeping for {self.sleep_restart}...")
447
+ msgs.append(f"Sleeping {self._sleep_restart_desc}...")
435
448
  getLogger(name=self.logger).error("\n".join(msgs))
436
449
 
437
450
  def _raise_error(self, event: THashable, /) -> NoReturn:
@@ -446,6 +459,29 @@ class InfiniteLooper(ABC, Generic[THashable]):
446
459
  event: Event() for event, _ in self._yield_events_and_exceptions()
447
460
  }
448
461
 
462
+ async def _run_sleep(self, sleep: _DurationOrEvery, /) -> None:
463
+ """Sleep until the next part of the loop."""
464
+ match sleep:
465
+ case int() | float() | dt.timedelta() as duration:
466
+ await sleep_dur(duration=duration)
467
+ case "every", (int() | float() | dt.timedelta()) as duration:
468
+ await sleep_until_rounded(duration)
469
+ case _ as never:
470
+ assert_never(never)
471
+
472
+ @property
473
+ def _sleep_restart_desc(self) -> str:
474
+ """Get a description of the sleep until restart."""
475
+ match self.sleep_restart:
476
+ case int() | float() | dt.timedelta() as duration:
477
+ timedelta = datetime_duration_to_timedelta(duration)
478
+ return f"for {timedelta}"
479
+ case "every", (int() | float() | dt.timedelta()) as duration:
480
+ timedelta = datetime_duration_to_timedelta(duration)
481
+ return f"until next {timedelta}"
482
+ case _ as never:
483
+ assert_never(never)
484
+
449
485
  def _set_event(self, event: THashable, /) -> None:
450
486
  """Set the given event."""
451
487
  try:
@@ -686,6 +722,27 @@ async def sleep_dur(*, duration: Duration | None = None) -> None:
686
722
  ##
687
723
 
688
724
 
725
+ async def sleep_until(datetime: dt.datetime, /) -> None:
726
+ """Sleep until a given time."""
727
+ await sleep_dur(duration=datetime - get_now())
728
+
729
+
730
+ ##
731
+
732
+
733
+ async def sleep_until_rounded(
734
+ duration: Duration, /, *, rel_tol: float | None = None, abs_tol: float | None = None
735
+ ) -> None:
736
+ """Sleep until a rounded time; accepts durations."""
737
+ datetime = round_datetime(
738
+ get_now(), duration, mode="ceil", rel_tol=rel_tol, abs_tol=abs_tol
739
+ )
740
+ await sleep_until(datetime)
741
+
742
+
743
+ ##
744
+
745
+
689
746
  @dataclass(kw_only=True, slots=True)
690
747
  class StreamCommandOutput:
691
748
  process: Process
@@ -768,6 +825,8 @@ __all__ = [
768
825
  "put_items",
769
826
  "put_items_nowait",
770
827
  "sleep_dur",
828
+ "sleep_until",
829
+ "sleep_until_rounded",
771
830
  "stream_command",
772
831
  "timeout_dur",
773
832
  ]