dycw-utilities 0.147.3__tar.gz → 0.148.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 (214) hide show
  1. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/PKG-INFO +2 -2
  2. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/pyproject.toml +6 -6
  3. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_asyncio.py +58 -13
  4. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_errors.py +33 -1
  5. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_pottery.py +3 -4
  6. dycw_utilities-0.148.1/src/tests/test_yield_access/script.py +61 -0
  7. dycw_utilities-0.148.1/src/tests/test_yield_access/script.sh +54 -0
  8. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/__init__.py +1 -1
  9. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/asyncio.py +38 -8
  10. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/errors.py +16 -2
  11. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/pottery.py +6 -35
  12. dycw_utilities-0.148.1/src/utilities/pytest_plugins/__init__.py +1 -0
  13. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/types.py +5 -0
  14. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/.gitignore +0 -0
  15. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/LICENSE +0 -0
  16. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/README.md +0 -0
  17. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/__init__.py +0 -0
  18. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/conftest.py +0 -0
  19. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/modules/__init__.py +0 -0
  20. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/modules/package_missing/__init__.py +0 -0
  21. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/modules/package_missing/module.py +0 -0
  22. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/modules/package_with/__init__.py +0 -0
  23. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/modules/package_with/outer_1.py +0 -0
  24. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/modules/package_with/outer_2.py +0 -0
  25. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/modules/package_with/subpackage/__init__.py +0 -0
  26. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/modules/package_with/subpackage/inner_1.py +0 -0
  27. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/modules/package_with/subpackage/inner_2.py +0 -0
  28. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/modules/package_with/subpackage/inner_3.py +0 -0
  29. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/modules/package_without/__init__.py +0 -0
  30. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/modules/package_without/module_1.py +0 -0
  31. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/modules/package_without/module_2.py +0 -0
  32. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/modules/standalone.py +0 -0
  33. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/modules/with_imports.py +0 -0
  34. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/regressions/test_pytest_regressions/TestMultipleRegressionFixtures__test_main__obj.json +0 -0
  35. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/regressions/test_pytest_regressions/TestMultipleRegressionFixtures__test_main__series.json +0 -0
  36. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_int.json +0 -0
  37. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_literal__false.json +0 -0
  38. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_literal__true.json +0 -0
  39. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_nested.json +0 -0
  40. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/regressions/test_pytest_regressions/TestPolarsRegressionFixture__test_dataframe.json +0 -0
  41. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/regressions/test_pytest_regressions/TestPolarsRegressionFixture__test_series.json +0 -0
  42. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_altair.py +0 -0
  43. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_atomicwrites.py +0 -0
  44. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_atools.py +0 -0
  45. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_cachetools.py +0 -0
  46. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_click.py +0 -0
  47. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_concurrent.py +0 -0
  48. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_contextlib.py +0 -0
  49. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_contextvars.py +0 -0
  50. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_cryptography.py +0 -0
  51. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_cvxpy.py +0 -0
  52. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_dataclasses.py +0 -0
  53. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_enum.py +0 -0
  54. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_eventkit.py +0 -0
  55. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_fastapi.py +0 -0
  56. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_fpdf2.py +0 -0
  57. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_functions.py +0 -0
  58. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_functools.py +0 -0
  59. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_getpass.py +0 -0
  60. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_gzip.py +0 -0
  61. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_hashlib.py +0 -0
  62. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_http.py +0 -0
  63. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_hypothesis.py +0 -0
  64. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_importlib.py +0 -0
  65. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_inflect.py +0 -0
  66. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_ipython.py +0 -0
  67. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_iterables.py +0 -0
  68. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_json.py +0 -0
  69. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_jupyter.py +0 -0
  70. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_libcst.py +0 -0
  71. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_lightweight_charts.py +0 -0
  72. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_logging.py +0 -0
  73. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_math.py +0 -0
  74. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_memory_profiler.py +0 -0
  75. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_modules.py +0 -0
  76. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_more_itertools.py +0 -0
  77. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_numpy.py +0 -0
  78. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_objects/__init__.py +0 -0
  79. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_objects/objects.py +0 -0
  80. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_operator.py +0 -0
  81. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_optuna.py +0 -0
  82. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_orjson.py +0 -0
  83. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_os.py +0 -0
  84. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_parse.py +0 -0
  85. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_pathlib.py +0 -0
  86. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_period.py +0 -0
  87. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_pickle.py +0 -0
  88. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_platform.py +0 -0
  89. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_polars.py +0 -0
  90. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_polars_ols.py +0 -0
  91. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_postgres.py +0 -0
  92. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_pqdm.py +0 -0
  93. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_psutil.py +0 -0
  94. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_pyinstrument.py +0 -0
  95. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_pytest.py +0 -0
  96. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_pytest_randomly.py +0 -0
  97. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_pytest_regressions.py +0 -0
  98. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_random.py +0 -0
  99. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_re.py +0 -0
  100. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_redis.py +0 -0
  101. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_reprlib.py +0 -0
  102. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_scipy.py +0 -0
  103. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_sentinel.py +0 -0
  104. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_shelve.py +0 -0
  105. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_slack_sdk.py +0 -0
  106. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_socket.py +0 -0
  107. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_sqlalchemy.py +0 -0
  108. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_sqlalchemy_polars.py +0 -0
  109. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_statsmodels.py +0 -0
  110. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_string.py +0 -0
  111. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_tempfile.py +0 -0
  112. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_text.py +0 -0
  113. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_threading.py +0 -0
  114. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_timer.py +0 -0
  115. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_traceback.py +0 -0
  116. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_typed_settings.py +0 -0
  117. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_types.py +0 -0
  118. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_typing.py +0 -0
  119. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_typing_funcs/__init__.py +0 -0
  120. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_typing_funcs/no_future.py +0 -0
  121. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_typing_funcs/with_future.py +0 -0
  122. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_tzdata.py +0 -0
  123. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_tzlocal.py +0 -0
  124. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_uuid.py +0 -0
  125. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_version.py +0 -0
  126. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_warnings.py +0 -0
  127. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_whenever.py +0 -0
  128. {dycw_utilities-0.147.3/src/utilities/pytest_plugins → dycw_utilities-0.148.1/src/tests/test_yield_access}/__init__.py +0 -0
  129. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_zipfile.py +0 -0
  130. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/tests/test_zoneinfo.py +0 -0
  131. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/altair.py +0 -0
  132. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/atomicwrites.py +0 -0
  133. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/atools.py +0 -0
  134. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/cachetools.py +0 -0
  135. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/click.py +0 -0
  136. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/concurrent.py +0 -0
  137. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/contextlib.py +0 -0
  138. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/contextvars.py +0 -0
  139. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/cryptography.py +0 -0
  140. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/cvxpy.py +0 -0
  141. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/dataclasses.py +0 -0
  142. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/enum.py +0 -0
  143. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/eventkit.py +0 -0
  144. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/fastapi.py +0 -0
  145. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/fpdf2.py +0 -0
  146. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/functions.py +0 -0
  147. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/functools.py +0 -0
  148. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/getpass.py +0 -0
  149. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/gzip.py +0 -0
  150. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/hashlib.py +0 -0
  151. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/http.py +0 -0
  152. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/hypothesis.py +0 -0
  153. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/importlib.py +0 -0
  154. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/inflect.py +0 -0
  155. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/ipython.py +0 -0
  156. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/iterables.py +0 -0
  157. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/json.py +0 -0
  158. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/jupyter.py +0 -0
  159. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/libcst.py +0 -0
  160. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/lightweight_charts.py +0 -0
  161. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/logging.py +0 -0
  162. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/math.py +0 -0
  163. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/memory_profiler.py +0 -0
  164. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/modules.py +0 -0
  165. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/more_itertools.py +0 -0
  166. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/numpy.py +0 -0
  167. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/operator.py +0 -0
  168. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/optuna.py +0 -0
  169. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/orjson.py +0 -0
  170. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/os.py +0 -0
  171. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/parse.py +0 -0
  172. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/pathlib.py +0 -0
  173. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/period.py +0 -0
  174. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/pickle.py +0 -0
  175. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/platform.py +0 -0
  176. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/polars.py +0 -0
  177. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/polars_ols.py +0 -0
  178. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/postgres.py +0 -0
  179. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/pqdm.py +0 -0
  180. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/psutil.py +0 -0
  181. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/py.typed +0 -0
  182. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/pyinstrument.py +0 -0
  183. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/pytest.py +0 -0
  184. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/pytest_plugins/pytest_randomly.py +0 -0
  185. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/pytest_plugins/pytest_regressions.py +0 -0
  186. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/pytest_regressions.py +0 -0
  187. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/random.py +0 -0
  188. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/re.py +0 -0
  189. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/redis.py +0 -0
  190. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/reprlib.py +0 -0
  191. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/scipy.py +0 -0
  192. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/sentinel.py +0 -0
  193. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/shelve.py +0 -0
  194. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/slack_sdk.py +0 -0
  195. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/socket.py +0 -0
  196. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/sqlalchemy.py +0 -0
  197. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/sqlalchemy_polars.py +0 -0
  198. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/statsmodels.py +0 -0
  199. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/string.py +0 -0
  200. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/tempfile.py +0 -0
  201. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/text.py +0 -0
  202. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/threading.py +0 -0
  203. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/timer.py +0 -0
  204. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/traceback.py +0 -0
  205. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/typed_settings.py +0 -0
  206. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/typing.py +0 -0
  207. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/tzdata.py +0 -0
  208. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/tzlocal.py +0 -0
  209. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/uuid.py +0 -0
  210. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/version.py +0 -0
  211. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/warnings.py +0 -0
  212. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/whenever.py +0 -0
  213. {dycw_utilities-0.147.3 → dycw_utilities-0.148.1}/src/utilities/zipfile.py +0 -0
  214. {dycw_utilities-0.147.3 → dycw_utilities-0.148.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.147.3
3
+ Version: 0.148.1
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -14,7 +14,7 @@ Provides-Extra: test
14
14
  Requires-Dist: dycw-pytest-only<2.2,>=2.1.1; extra == 'test'
15
15
  Requires-Dist: hypothesis<6.136,>=6.135.24; extra == 'test'
16
16
  Requires-Dist: pudb<2025.2,>=2025.1; extra == 'test'
17
- Requires-Dist: pytest-asyncio<1.1,>=1.0.0; extra == 'test'
17
+ Requires-Dist: pytest-asyncio<1.2,>=1.1.0; extra == 'test'
18
18
  Requires-Dist: pytest-cov<6.3,>=6.2.1; extra == 'test'
19
19
  Requires-Dist: pytest-instafail<0.6,>=0.5.0; extra == 'test'
20
20
  Requires-Dist: pytest-lazy-fixtures<1.2,>=1.1.4; extra == 'test'
@@ -23,7 +23,7 @@ core = [
23
23
  "whenever >= 0.8.6, < 0.9",
24
24
  ]
25
25
  cryptography = ["cryptography >= 45.0.4, < 45.1"]
26
- cvxpy = ["cvxpy >= 1.6.5, < 1.7"]
26
+ cvxpy = ["cvxpy >= 1.7.0, < 1.8"]
27
27
  dataclasses-test = ["orjson", "polars"]
28
28
  dev = [
29
29
  "coloredlogs >= 15.0.1, < 15.1",
@@ -53,7 +53,7 @@ more-itertools = ["more-itertools >= 10.7.0, < 10.8"]
53
53
  numpy = ["numpy >= 2.3.1, < 2.4"]
54
54
  operator = ["polars"]
55
55
  optuna = ["optuna >= 4.4.0, < 4.5"]
56
- orjson = ["orjson >= 3.10.18, < 3.11"]
56
+ orjson = ["orjson >= 3.11.0, < 3.12"]
57
57
  orjson-test = ["polars"]
58
58
  polars = ["polars >= 1.31.0, < 1.32"]
59
59
  polars-ols = ["polars-ols >= 0.3.5, < 0.4"]
@@ -67,7 +67,7 @@ pyinstrument = ["pyinstrument >= 5.0.3, < 5.1"]
67
67
  pytest = [
68
68
  "pudb >= 2025.1, < 2025.2",
69
69
  "pytest >= 8.4.1, < 8.5",
70
- "pytest-asyncio >= 1.0.0, < 1.1",
70
+ "pytest-asyncio >= 1.1.0, < 1.2",
71
71
  "pytest-randomly >= 3.16.0, < 3.17",
72
72
  "pytest-timeout >= 2.4.0, < 2.5",
73
73
  "pytest-xdist >= 3.8.0, < 3.9",
@@ -102,7 +102,7 @@ dependencies = [
102
102
  name = "dycw-utilities"
103
103
  readme = "README.md"
104
104
  requires-python = ">= 3.12"
105
- version = "0.147.3"
105
+ version = "0.148.1"
106
106
 
107
107
  [project.entry-points.pytest11]
108
108
  pytest-randomly = "utilities.pytest_plugins.pytest_randomly"
@@ -117,7 +117,7 @@ test = [
117
117
  "hypothesis >= 6.135.24, < 6.136",
118
118
  "pudb >= 2025.1, < 2025.2",
119
119
  "pytest >= 8.4.1, < 8.5",
120
- "pytest-asyncio >= 1.0.0, < 1.1",
120
+ "pytest-asyncio >= 1.1.0, < 1.2",
121
121
  "pytest-cov >= 6.2.1, < 6.3",
122
122
  "pytest-instafail >= 0.5.0, < 0.6",
123
123
  "pytest-lazy-fixtures >= 1.1.4, < 1.2",
@@ -135,7 +135,7 @@ test = [
135
135
  # bump-my-version
136
136
  [tool.bumpversion]
137
137
  allow_dirty = true
138
- current_version = "0.147.3"
138
+ current_version = "0.148.1"
139
139
 
140
140
  [[tool.bumpversion.files]]
141
141
  filename = "src/utilities/__init__.py"
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from asyncio import Queue, run
3
+ from asyncio import Queue, TaskGroup, run
4
4
  from collections.abc import ItemsView, KeysView, ValuesView
5
5
  from contextlib import asynccontextmanager
6
6
  from re import search
@@ -8,11 +8,12 @@ from typing import TYPE_CHECKING, ClassVar
8
8
 
9
9
  from hypothesis import given
10
10
  from hypothesis.strategies import booleans, dictionaries, integers, lists, none
11
- from pytest import RaisesGroup, raises
11
+ from pytest import LogCaptureFixture, RaisesGroup, mark, param, raises
12
12
 
13
13
  from utilities.asyncio import (
14
14
  AsyncDict,
15
15
  EnhancedTaskGroup,
16
+ get_coroutine_name,
16
17
  get_items,
17
18
  get_items_nowait,
18
19
  loop_until_succeed,
@@ -27,6 +28,7 @@ from utilities.asyncio import (
27
28
  )
28
29
  from utilities.hypothesis import pairs, text_ascii
29
30
  from utilities.pytest import skipif_windows
31
+ from utilities.text import unique_str
30
32
  from utilities.timer import Timer
31
33
  from utilities.whenever import MILLISECOND, SECOND, get_now
32
34
 
@@ -287,6 +289,16 @@ class TestEnhancedTaskGroup:
287
289
  _ = tg.create_task(sleep_td(2 * self.delta))
288
290
 
289
291
 
292
+ class TestGetCoroutineName:
293
+ def test_main(self) -> None:
294
+ async def func() -> None:
295
+ return None
296
+
297
+ result = get_coroutine_name(func)
298
+ expected = "func"
299
+ assert result == expected
300
+
301
+
290
302
  class TestGetItems:
291
303
  @given(
292
304
  xs=lists(integers(), min_size=1),
@@ -306,34 +318,67 @@ class TestGetItems:
306
318
 
307
319
 
308
320
  class TestLoopUntilSucceed:
309
- async def test_main(self) -> None:
321
+ @mark.parametrize("sleep", [param(MILLISECOND), param(None)])
322
+ @mark.parametrize("use_logger", [param(True), param(False)])
323
+ async def test_main(
324
+ self, *, caplog: LogCaptureFixture, sleep: TimeDelta | None, use_logger: bool
325
+ ) -> None:
326
+ class CustomError(Exception): ...
327
+
328
+ caplog.set_level("DEBUG", logger=(name := unique_str()))
310
329
  counter = 0
311
330
 
312
331
  async def func() -> None:
313
332
  nonlocal counter
314
333
  counter += 1
315
334
  if counter <= 3:
316
- raise ValueError
335
+ raise CustomError
317
336
 
318
- _ = await loop_until_succeed(lambda: func())
337
+ assert await loop_until_succeed(
338
+ lambda: func(), logger=name if use_logger else None, sleep=sleep
339
+ )
319
340
  assert counter == 4
320
341
 
321
- async def test_error(self) -> None:
342
+ if use_logger:
343
+ messages = [r.message for r in caplog.records if r.name == name]
344
+ expected = 3 * (
345
+ ["Error running 'func'"]
346
+ + ([] if sleep is None else ["Sleeping for PT0.001S..."])
347
+ + ["Retrying 'func'..."]
348
+ )
349
+ assert messages == expected
350
+
351
+ async def test_error_flat(self) -> None:
352
+ class CustomError(Exception): ...
353
+
322
354
  counter = 0
323
- errors: list[Exception] = []
324
355
 
325
356
  async def func() -> None:
326
357
  nonlocal counter
327
358
  counter += 1
328
359
  if counter <= 3:
329
- raise ValueError
360
+ raise CustomError
330
361
 
331
- def error(error: Exception, /) -> None:
332
- errors.append(error)
362
+ assert not await loop_until_succeed(lambda: func(), errors=CustomError)
363
+ assert counter == 1
333
364
 
334
- _ = await loop_until_succeed(lambda: func(), error=error)
335
- assert counter == 4
336
- assert len(errors) == 3
365
+ async def test_error_nested(self) -> None:
366
+ class CustomError(Exception): ...
367
+
368
+ counter = 0
369
+
370
+ async def func() -> None:
371
+ async with TaskGroup() as tg:
372
+ _ = tg.create_task(inner())
373
+
374
+ async def inner() -> None:
375
+ nonlocal counter
376
+ counter += 1
377
+ if counter <= 3:
378
+ raise CustomError
379
+
380
+ assert not await loop_until_succeed(lambda: func(), errors=CustomError)
381
+ assert counter == 1
337
382
 
338
383
 
339
384
  class TestPutItems:
@@ -4,7 +4,7 @@ from asyncio import TaskGroup
4
4
 
5
5
  from pytest import RaisesGroup, raises
6
6
 
7
- from utilities.errors import ImpossibleCaseError, repr_error
7
+ from utilities.errors import ImpossibleCaseError, is_instance_error, repr_error
8
8
 
9
9
 
10
10
  class TestImpossibleCaseError:
@@ -14,6 +14,38 @@ class TestImpossibleCaseError:
14
14
  raise ImpossibleCaseError(case=[f"{x=}"])
15
15
 
16
16
 
17
+ class TestIsInstanceError:
18
+ def test_flat(self) -> None:
19
+ class CustomError(Exception): ...
20
+
21
+ with raises(CustomError) as exc_info:
22
+ raise CustomError
23
+
24
+ assert is_instance_error(exc_info.value, CustomError)
25
+
26
+ async def test_group(self) -> None:
27
+ class CustomError(Exception): ...
28
+
29
+ async def coroutine() -> None:
30
+ raise CustomError
31
+
32
+ with RaisesGroup(CustomError) as exc_info:
33
+ async with TaskGroup() as tg:
34
+ _ = tg.create_task(coroutine())
35
+
36
+ assert is_instance_error(exc_info.value, CustomError)
37
+
38
+ def test_false(self) -> None:
39
+ class Custom1Error(Exception): ...
40
+
41
+ class Custom2Error(Exception): ...
42
+
43
+ with raises(Custom1Error) as exc_info:
44
+ raise Custom1Error
45
+
46
+ assert not is_instance_error(exc_info.value, Custom2Error)
47
+
48
+
17
49
  class TestReprError:
18
50
  def test_class(self) -> None:
19
51
  class CustomError(Exception): ...
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from asyncio import TaskGroup
4
- from itertools import repeat
5
4
  from typing import TYPE_CHECKING, ClassVar
6
5
 
7
6
  from pottery import AIORedlock
@@ -74,7 +73,7 @@ class TestTryYieldCoroutineLooper:
74
73
  redis, key, timeout_acquire=self.delta, logger=logger
75
74
  ) as looper:
76
75
  if looper is not None:
77
- await looper(self.func_main, lst)
76
+ assert await looper(self.func_main, lst)
78
77
 
79
78
  async def delayed(
80
79
  self,
@@ -102,11 +101,11 @@ class TestTryYieldCoroutineLooper:
102
101
  logger=name if use_logger else None,
103
102
  ) as looper:
104
103
  assert looper is not None
105
- await looper(self.func_error, lst)
104
+ assert await looper(self.func_error, lst)
106
105
 
107
106
  if use_logger:
108
107
  messages = [r.message for r in caplog.records if r.name == name]
109
- expected = list(repeat("Error running 'func_error'", times=3))
108
+ expected = 3 * ["Error running 'func_error'", "Retrying 'func_error'..."]
110
109
  assert messages == expected
111
110
 
112
111
  async def func_error(self, lst: list[None], /) -> None:
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from asyncio import run
4
+ from logging import getLogger
5
+ from random import randint
6
+ from typing import TYPE_CHECKING
7
+
8
+ from redis.asyncio import Redis
9
+
10
+ from utilities.asyncio import sleep_td
11
+ from utilities.logging import setup_logging
12
+ from utilities.pathlib import get_repo_root
13
+ from utilities.pottery import extend_lock, try_yield_coroutine_looper
14
+ from utilities.whenever import SECOND
15
+
16
+ if TYPE_CHECKING:
17
+ from pottery import AIORedlock
18
+
19
+ _LOGGER = getLogger(__name__)
20
+
21
+
22
+ async def script(*, lock: AIORedlock | None = None) -> None:
23
+ total = 1000
24
+ fail = 30
25
+ success = 3
26
+ while True:
27
+ n = randint(0, total)
28
+ if n < fail:
29
+ _LOGGER.info("n = %d; failing...", n)
30
+ msg = f"n = {n}; failure"
31
+ raise ValueError(msg)
32
+ if fail <= n < (fail + success):
33
+ _LOGGER.info("n = %d; succeeding...", n)
34
+ return
35
+ _LOGGER.info("n = %d", n)
36
+ await extend_lock(lock=lock)
37
+ await sleep_td(SECOND / 3)
38
+
39
+
40
+ async def service() -> None:
41
+ redis = Redis()
42
+ async with try_yield_coroutine_looper(
43
+ redis,
44
+ "utilities-test",
45
+ num=1,
46
+ timeout_release=5 * SECOND,
47
+ logger=_LOGGER,
48
+ sleep_error=4 * SECOND,
49
+ ) as looper:
50
+ if looper is not None:
51
+ result = await looper(script, lock=looper.lock)
52
+ _LOGGER.info("script %s", "succeeded" if result else "failed")
53
+
54
+
55
+ def main() -> None:
56
+ setup_logging(logger=_LOGGER, files_dir=get_repo_root().joinpath(".logs"))
57
+ run(service())
58
+
59
+
60
+ if __name__ == "__main__":
61
+ main()
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env sh
2
+
3
+ # Add to your `crontab` as:
4
+ # * * * * * /Users/derekwan/work/python-utilities/tests/test_yield_access/script.sh
5
+
6
+ # helpers
7
+ echo_pid_date() { echo "[$$ | $(date +'%Y-%m-%d %H:%M:%S')] $*"; }
8
+
9
+ # add to `$PATH`
10
+ for __dir in "${HOME}/.local/bin" '/opt/homebrew/bin' '/opt/homebrew/opt/postgresql@17/bin'; do
11
+ if [ -d "${__dir}" ]; then
12
+ case ":${PATH}:" in
13
+ *:"$__dir":*) ;;
14
+ *)
15
+ export PATH="${__dir}:${PATH}"
16
+ ;;
17
+ esac
18
+ fi
19
+ done
20
+
21
+ # log file
22
+ __package_dir="${HOME}/work/python-utilities"
23
+ __log_file="${__package_dir}/.logs/test-yield-access"
24
+
25
+ # check if binaries are accessible
26
+ for __app in direnv just uv; do
27
+ if ! command -v "${__app}" >/dev/null 2>&1; then
28
+ echo_pid_date "ERROR: Command '${__app}' not found; exiting..." 2>&1 | tee -a "${__log_file}"
29
+ exit
30
+ fi
31
+ done
32
+
33
+ # enter package
34
+ echo_pid_date "Entering package directory '${__package_dir}'..." 2>&1 | tee -a "${__log_file}"
35
+ cd "${__package_dir}" || exit
36
+
37
+ # trim log
38
+ __threshold=$((10 * 1024 * 1024))
39
+ if [ -f "${__log_file}" ]; then
40
+ __log_size=$(wc -c <"${__log_file}")
41
+ if [ "${__log_size}" -gt "${__threshold}" ]; then
42
+ echo_pid_date "Truncating log file '${__log_file}' (log size = ${__log_size})..." 2>&1 | tee -a "${__log_file}"
43
+ __total_lines=$(wc -l <"${__log_file}")
44
+ __keep_lines=$((__total_lines / 2))
45
+ __tmp_log_file="${__log_file}.tmp.$$"
46
+ tail -n "${__keep_lines}" "${__log_file}" >"${__tmp_log_file}" && mv "${__tmp_log_file}" "${__log_file}"
47
+ fi
48
+ fi
49
+
50
+ # run the script
51
+ echo_pid_date "Running 'PYTHONPATH=src/tests/test_yield_access python -m script'..." 2>&1 | tee -a "${__log_file}"
52
+ __start="$(date +%s)"
53
+ PYTHONPATH=src/tests/test_yield_access direnv exec . python -m script "$*" 2>&1 | tee -a "${__log_file}"
54
+ __exit_code="$?"
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.147.3"
3
+ __version__ = "0.148.1"
@@ -35,10 +35,12 @@ from typing import (
35
35
  override,
36
36
  )
37
37
 
38
- from utilities.errors import ImpossibleCaseError
38
+ from utilities.errors import ImpossibleCaseError, is_instance_error
39
39
  from utilities.functions import ensure_int, ensure_not_none, to_bool
40
+ from utilities.logging import get_logger
40
41
  from utilities.random import SYSTEM_RANDOM
41
42
  from utilities.sentinel import Sentinel, sentinel
43
+ from utilities.warnings import suppress_warnings
42
44
  from utilities.whenever import get_now, round_date_or_date_time, to_nanoseconds
43
45
 
44
46
  if TYPE_CHECKING:
@@ -63,6 +65,8 @@ if TYPE_CHECKING:
63
65
  from utilities.types import (
64
66
  Coro,
65
67
  Delta,
68
+ ExceptionTypeLike,
69
+ LoggerOrName,
66
70
  MaybeCallableBool,
67
71
  MaybeType,
68
72
  SupportsKeysAndGetItem,
@@ -340,6 +344,20 @@ class EnhancedTaskGroup(TaskGroup):
340
344
  ##
341
345
 
342
346
 
347
+ def get_coroutine_name(func: Callable[[], Coro[Any]], /) -> str:
348
+ """Get the name of a coroutine, and then dispose of it gracefully."""
349
+ coro = func()
350
+ name = coro.__name__
351
+ with suppress_warnings(
352
+ message="coroutine '.*' was never awaited", category=RuntimeWarning
353
+ ):
354
+ del coro
355
+ return name
356
+
357
+
358
+ ##
359
+
360
+
343
361
  async def get_items[T](queue: Queue[T], /, *, max_size: int | None = None) -> list[T]:
344
362
  """Get items from a queue; if empty then wait."""
345
363
  try:
@@ -380,23 +398,34 @@ async def loop_until_succeed(
380
398
  func: Callable[[], Coro[None]],
381
399
  /,
382
400
  *,
383
- error: Callable[[Exception], None] | None = None,
401
+ logger: LoggerOrName | None = None,
402
+ errors: ExceptionTypeLike[Exception] | None = None,
384
403
  sleep: Delta | None = None,
385
- ) -> None:
404
+ ) -> bool:
386
405
  """Repeatedly call a coroutine until it succeeds."""
406
+ name = get_coroutine_name(func)
387
407
  while True:
388
408
  try:
389
- return await func()
390
- except Exception as err: # noqa: BLE001
391
- if error is not None:
392
- error(err)
409
+ await func()
410
+ except Exception as error: # noqa: BLE001
411
+ if logger is not None:
412
+ get_logger(logger=logger).error("Error running %r", name, exc_info=True)
393
413
  exc_type, exc_value, traceback = sys.exc_info()
394
414
  if (exc_type is None) or (exc_value is None): # pragma: no cover
395
415
  raise ImpossibleCaseError(
396
416
  case=[f"{exc_type=}", f"{exc_value=}"]
397
417
  ) from None
398
418
  sys.excepthook(exc_type, exc_value, traceback)
399
- await sleep_td(sleep)
419
+ if (errors is not None) and is_instance_error(error, errors):
420
+ return False
421
+ if sleep is not None:
422
+ if logger is not None:
423
+ get_logger(logger=logger).info("Sleeping for %s...", sleep)
424
+ await sleep_td(sleep)
425
+ if logger is not None:
426
+ get_logger(logger=logger).info("Retrying %r...", name)
427
+ else:
428
+ return True
400
429
 
401
430
 
402
431
  ##
@@ -522,6 +551,7 @@ __all__ = [
522
551
  "AsyncDict",
523
552
  "EnhancedTaskGroup",
524
553
  "StreamCommandOutput",
554
+ "get_coroutine_name",
525
555
  "get_items",
526
556
  "get_items_nowait",
527
557
  "loop_until_succeed",
@@ -4,7 +4,7 @@ from dataclasses import dataclass
4
4
  from typing import TYPE_CHECKING, assert_never, override
5
5
 
6
6
  if TYPE_CHECKING:
7
- from utilities.types import MaybeType
7
+ from utilities.types import ExceptionTypeLike, MaybeType
8
8
 
9
9
 
10
10
  @dataclass(kw_only=True, slots=True)
@@ -21,6 +21,20 @@ class ImpossibleCaseError(Exception):
21
21
  ##
22
22
 
23
23
 
24
+ def is_instance_error(
25
+ error: BaseException, class_or_tuple: ExceptionTypeLike[Exception], /
26
+ ) -> bool:
27
+ """Check if an instance relationship holds, allowing for groups."""
28
+ if isinstance(error, class_or_tuple):
29
+ return True
30
+ if not isinstance(error, BaseExceptionGroup):
31
+ return False
32
+ return any(is_instance_error(e, class_or_tuple) for e in error.exceptions)
33
+
34
+
35
+ ##
36
+
37
+
24
38
  def repr_error(error: MaybeType[BaseException], /) -> str:
25
39
  """Get a string representation of an error."""
26
40
  match error:
@@ -36,4 +50,4 @@ def repr_error(error: MaybeType[BaseException], /) -> str:
36
50
  assert_never(never)
37
51
 
38
52
 
39
- __all__ = ["ImpossibleCaseError", "repr_error"]
53
+ __all__ = ["ImpossibleCaseError", "is_instance_error", "repr_error"]
@@ -5,16 +5,14 @@ from dataclasses import dataclass
5
5
  from sys import maxsize
6
6
  from typing import TYPE_CHECKING, override
7
7
 
8
- from pottery import AIORedlock
8
+ from pottery import AIORedlock, ExtendUnlockedLock
9
9
  from pottery.exceptions import ReleaseUnlockedLock
10
10
  from redis.asyncio import Redis
11
11
 
12
12
  from utilities.asyncio import loop_until_succeed, sleep_td, timeout_td
13
13
  from utilities.contextlib import enhanced_async_context_manager
14
- from utilities.functools import partial
15
14
  from utilities.iterables import always_iterable
16
15
  from utilities.logging import get_logger
17
- from utilities.warnings import suppress_warnings
18
16
  from utilities.whenever import MILLISECOND, SECOND, to_seconds
19
17
 
20
18
  if TYPE_CHECKING:
@@ -72,10 +70,7 @@ async def try_yield_coroutine_looper(
72
70
  throttle=throttle,
73
71
  ) as lock:
74
72
  yield CoroutineLooper(lock=lock, logger=logger, sleep=sleep_error)
75
- except ( # skipif-ci-and-not-linux
76
- _YieldAccessUnableToAcquireLockError,
77
- _YieldAccessAcquiredUnlockedLockError,
78
- ) as error:
73
+ except _YieldAccessUnableToAcquireLockError as error: # skipif-ci-and-not-linux
79
74
  if logger is not None:
80
75
  get_logger(logger=logger).info("%s", error)
81
76
  async with nullcontext():
@@ -92,27 +87,14 @@ class CoroutineLooper:
92
87
 
93
88
  async def __call__[**P](
94
89
  self, func: Callable[P, Coro[None]], *args: P.args, **kwargs: P.kwargs
95
- ) -> None:
90
+ ) -> bool:
96
91
  def make_coro() -> Coro[None]:
97
92
  return func(*args, **kwargs)
98
93
 
99
- await loop_until_succeed(
100
- make_coro, error=partial(self._error, func=make_coro), sleep=self.sleep
94
+ return await loop_until_succeed(
95
+ make_coro, logger=self.logger, errors=ExtendUnlockedLock, sleep=self.sleep
101
96
  )
102
97
 
103
- def _error(self, error: Exception, /, *, func: Callable[[], Coro[None]]) -> None:
104
- _ = error
105
- if self.logger is not None:
106
- coro = func()
107
- name = coro.__name__ # skipif-ci-and-not-linux
108
- with suppress_warnings(
109
- message="coroutine '.*' was never awaited", category=RuntimeWarning
110
- ):
111
- del coro
112
- get_logger(logger=self.logger).error(
113
- "Error running %r", name, exc_info=True
114
- )
115
-
116
98
 
117
99
  ##
118
100
 
@@ -150,8 +132,6 @@ async def yield_access(
150
132
  lock = await _get_first_available_lock(
151
133
  key, locks, num=num, timeout=timeout_acquire, sleep=sleep
152
134
  )
153
- if (await lock.locked()) == 0.0: # pragma: no cover
154
- raise _YieldAccessAcquiredUnlockedLockError(key=lock.key)
155
135
  yield lock
156
136
  finally: # skipif-ci-and-not-linux
157
137
  await sleep_td(throttle)
@@ -175,9 +155,7 @@ async def _get_first_available_lock(
175
155
  )
176
156
  async with timeout_td(timeout, error=error): # skipif-ci-and-not-linux
177
157
  while True:
178
- if (
179
- (result := await _get_first_available_lock_if_any(locks)) is not None
180
- ) and (await result.locked() > 0.0):
158
+ if (result := await _get_first_available_lock_if_any(locks)) is not None:
181
159
  return result
182
160
  await sleep_td(sleep)
183
161
 
@@ -215,13 +193,6 @@ class _YieldAccessUnableToAcquireLockError(YieldAccessError):
215
193
  return f"Unable to acquire any 1 of {self.num} locks for {self.key!r} after {self.timeout}" # skipif-ci-and-not-linux
216
194
 
217
195
 
218
- @dataclass(kw_only=True, slots=True)
219
- class _YieldAccessAcquiredUnlockedLockError(YieldAccessError):
220
- @override
221
- def __str__(self) -> str:
222
- return f"Acquired an unlocked lock {self.key!r}" # pragma: no cover
223
-
224
-
225
196
  __all__ = [
226
197
  "CoroutineLooper",
227
198
  "YieldAccessError",
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -105,6 +105,10 @@ type MonthInt = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
105
105
  type EnumLike[E: Enum] = MaybeStr[E]
106
106
 
107
107
 
108
+ # errors
109
+ type ExceptionTypeLike[T: Exception] = type[T] | tuple[type[T], ...]
110
+
111
+
108
112
  # ipaddress
109
113
  IPv4AddressLike = MaybeStr[IPv4Address]
110
114
  IPv6AddressLike = MaybeStr[IPv6Address]
@@ -265,6 +269,7 @@ __all__ = [
265
269
  "Delta",
266
270
  "EnumLike",
267
271
  "ExcInfo",
272
+ "ExceptionTypeLike",
268
273
  "IPv4AddressLike",
269
274
  "IPv6AddressLike",
270
275
  "IterableHashable",