dycw-utilities 0.153.15__tar.gz → 0.154.0__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 (212) hide show
  1. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/PKG-INFO +1 -1
  2. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/pyproject.toml +2 -2
  3. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/conftest.py +2 -8
  4. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_iterables.py +0 -14
  5. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_logging.py +8 -47
  6. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_sqlalchemy.py +2 -2
  7. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_traceback.py +16 -3
  8. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_whenever.py +6 -3
  9. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/__init__.py +1 -1
  10. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/fpdf2.py +2 -2
  11. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/iterables.py +1 -21
  12. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/logging.py +8 -49
  13. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/pyinstrument.py +2 -4
  14. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/traceback.py +24 -16
  15. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/whenever.py +8 -4
  16. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/.gitignore +0 -0
  17. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/LICENSE +0 -0
  18. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/README.md +0 -0
  19. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/__init__.py +0 -0
  20. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/modules/__init__.py +0 -0
  21. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/modules/package_missing/__init__.py +0 -0
  22. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/modules/package_missing/module.py +0 -0
  23. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/modules/package_with/__init__.py +0 -0
  24. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/modules/package_with/outer_1.py +0 -0
  25. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/modules/package_with/outer_2.py +0 -0
  26. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/modules/package_with/subpackage/__init__.py +0 -0
  27. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/modules/package_with/subpackage/inner_1.py +0 -0
  28. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/modules/package_with/subpackage/inner_2.py +0 -0
  29. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/modules/package_with/subpackage/inner_3.py +0 -0
  30. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/modules/package_without/__init__.py +0 -0
  31. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/modules/package_without/module_1.py +0 -0
  32. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/modules/package_without/module_2.py +0 -0
  33. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/modules/standalone.py +0 -0
  34. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/modules/with_imports.py +0 -0
  35. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/regressions/test_pytest_regressions/TestMultipleRegressionFixtures__test_main__obj.json +0 -0
  36. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/regressions/test_pytest_regressions/TestMultipleRegressionFixtures__test_main__series.json +0 -0
  37. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_int.json +0 -0
  38. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_literal__false.json +0 -0
  39. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_literal__true.json +0 -0
  40. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_nested.json +0 -0
  41. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/regressions/test_pytest_regressions/TestPolarsRegressionFixture__test_dataframe.json +0 -0
  42. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/regressions/test_pytest_regressions/TestPolarsRegressionFixture__test_series.json +0 -0
  43. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_altair.py +0 -0
  44. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_asyncio.py +0 -0
  45. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_atomicwrites.py +0 -0
  46. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_atools.py +0 -0
  47. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_cachetools.py +0 -0
  48. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_click.py +0 -0
  49. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_concurrent.py +0 -0
  50. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_contextlib.py +0 -0
  51. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_contextvars.py +0 -0
  52. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_cryptography.py +0 -0
  53. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_cvxpy.py +0 -0
  54. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_dataclasses.py +0 -0
  55. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_enum.py +0 -0
  56. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_errors.py +0 -0
  57. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_eventkit.py +0 -0
  58. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_fastapi.py +0 -0
  59. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_fpdf2.py +0 -0
  60. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_functions.py +0 -0
  61. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_functools.py +0 -0
  62. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_getpass.py +0 -0
  63. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_gzip.py +0 -0
  64. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_hashlib.py +0 -0
  65. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_http.py +0 -0
  66. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_hypothesis.py +0 -0
  67. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_importlib.py +0 -0
  68. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_inflect.py +0 -0
  69. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_ipython.py +0 -0
  70. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_json.py +0 -0
  71. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_jupyter.py +0 -0
  72. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_libcst.py +0 -0
  73. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_lightweight_charts.py +0 -0
  74. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_math.py +0 -0
  75. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_memory_profiler.py +0 -0
  76. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_modules.py +0 -0
  77. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_more_itertools.py +0 -0
  78. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_numpy.py +0 -0
  79. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_objects/__init__.py +0 -0
  80. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_objects/objects.py +0 -0
  81. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_operator.py +0 -0
  82. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_optuna.py +0 -0
  83. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_orjson.py +0 -0
  84. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_os.py +0 -0
  85. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_parse.py +0 -0
  86. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_pathlib.py +0 -0
  87. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_pickle.py +0 -0
  88. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_platform.py +0 -0
  89. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_polars.py +0 -0
  90. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_polars_ols.py +0 -0
  91. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_postgres.py +0 -0
  92. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_pottery.py +0 -0
  93. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_pqdm.py +0 -0
  94. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_psutil.py +0 -0
  95. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_pyinstrument.py +0 -0
  96. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_pytest.py +0 -0
  97. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_pytest_randomly.py +0 -0
  98. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_pytest_regressions.py +0 -0
  99. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_random.py +0 -0
  100. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_re.py +0 -0
  101. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_redis.py +0 -0
  102. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_reprlib.py +0 -0
  103. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_scipy.py +0 -0
  104. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_sentinel.py +0 -0
  105. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_shelve.py +0 -0
  106. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_slack_sdk.py +0 -0
  107. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_socket.py +0 -0
  108. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_sqlalchemy_polars.py +0 -0
  109. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_statsmodels.py +0 -0
  110. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_string.py +0 -0
  111. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_tempfile.py +0 -0
  112. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_text.py +0 -0
  113. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_threading.py +0 -0
  114. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_timer.py +0 -0
  115. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_typed_settings.py +0 -0
  116. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_types.py +0 -0
  117. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_typing.py +0 -0
  118. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_typing_funcs/__init__.py +0 -0
  119. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_typing_funcs/no_future.py +0 -0
  120. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_typing_funcs/with_future.py +0 -0
  121. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_tzdata.py +0 -0
  122. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_tzlocal.py +0 -0
  123. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_uuid.py +0 -0
  124. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_version.py +0 -0
  125. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_warnings.py +0 -0
  126. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_yield_access/__init__.py +0 -0
  127. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_yield_access/script.py +0 -0
  128. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_yield_access/script.sh +0 -0
  129. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_zipfile.py +0 -0
  130. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/tests/test_zoneinfo.py +0 -0
  131. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/altair.py +0 -0
  132. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/asyncio.py +0 -0
  133. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/atomicwrites.py +0 -0
  134. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/atools.py +0 -0
  135. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/cachetools.py +0 -0
  136. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/click.py +0 -0
  137. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/concurrent.py +0 -0
  138. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/contextlib.py +0 -0
  139. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/contextvars.py +0 -0
  140. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/cryptography.py +0 -0
  141. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/cvxpy.py +0 -0
  142. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/dataclasses.py +0 -0
  143. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/enum.py +0 -0
  144. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/errors.py +0 -0
  145. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/eventkit.py +0 -0
  146. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/fastapi.py +0 -0
  147. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/functions.py +0 -0
  148. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/functools.py +0 -0
  149. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/getpass.py +0 -0
  150. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/gzip.py +0 -0
  151. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/hashlib.py +0 -0
  152. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/http.py +0 -0
  153. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/hypothesis.py +0 -0
  154. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/importlib.py +0 -0
  155. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/inflect.py +0 -0
  156. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/ipython.py +0 -0
  157. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/json.py +0 -0
  158. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/jupyter.py +0 -0
  159. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/libcst.py +0 -0
  160. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/lightweight_charts.py +0 -0
  161. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/math.py +0 -0
  162. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/memory_profiler.py +0 -0
  163. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/modules.py +0 -0
  164. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/more_itertools.py +0 -0
  165. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/numpy.py +0 -0
  166. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/operator.py +0 -0
  167. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/optuna.py +0 -0
  168. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/orjson.py +0 -0
  169. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/os.py +0 -0
  170. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/parse.py +0 -0
  171. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/pathlib.py +0 -0
  172. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/pickle.py +0 -0
  173. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/platform.py +0 -0
  174. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/polars.py +0 -0
  175. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/polars_ols.py +0 -0
  176. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/postgres.py +0 -0
  177. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/pottery.py +0 -0
  178. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/pqdm.py +0 -0
  179. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/psutil.py +0 -0
  180. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/py.typed +0 -0
  181. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/pytest.py +0 -0
  182. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/pytest_plugins/__init__.py +0 -0
  183. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/pytest_plugins/pytest_randomly.py +0 -0
  184. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/pytest_plugins/pytest_regressions.py +0 -0
  185. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/pytest_regressions.py +0 -0
  186. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/random.py +0 -0
  187. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/re.py +0 -0
  188. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/redis.py +0 -0
  189. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/reprlib.py +0 -0
  190. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/scipy.py +0 -0
  191. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/sentinel.py +0 -0
  192. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/shelve.py +0 -0
  193. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/slack_sdk.py +0 -0
  194. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/socket.py +0 -0
  195. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/sqlalchemy.py +0 -0
  196. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/sqlalchemy_polars.py +0 -0
  197. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/statsmodels.py +0 -0
  198. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/string.py +0 -0
  199. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/tempfile.py +0 -0
  200. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/text.py +0 -0
  201. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/threading.py +0 -0
  202. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/timer.py +0 -0
  203. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/typed_settings.py +0 -0
  204. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/types.py +0 -0
  205. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/typing.py +0 -0
  206. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/tzdata.py +0 -0
  207. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/tzlocal.py +0 -0
  208. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/uuid.py +0 -0
  209. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/version.py +0 -0
  210. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/warnings.py +0 -0
  211. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/zipfile.py +0 -0
  212. {dycw_utilities-0.153.15 → dycw_utilities-0.154.0}/src/utilities/zoneinfo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.153.15
3
+ Version: 0.154.0
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -229,7 +229,7 @@ dependencies = [
229
229
  name = "dycw-utilities"
230
230
  readme = "README.md"
231
231
  requires-python = ">= 3.12"
232
- version = "0.153.15"
232
+ version = "0.154.0"
233
233
 
234
234
  [project.entry-points.pytest11]
235
235
  pytest-randomly = "utilities.pytest_plugins.pytest_randomly"
@@ -262,7 +262,7 @@ test = [
262
262
  # bump-my-version
263
263
  [tool.bumpversion]
264
264
  allow_dirty = true
265
- current_version = "0.153.15"
265
+ current_version = "0.154.0"
266
266
 
267
267
  [[tool.bumpversion.files]]
268
268
  filename = "src/utilities/__init__.py"
@@ -7,13 +7,11 @@ from typing import TYPE_CHECKING
7
7
 
8
8
  from hypothesis import HealthCheck
9
9
  from pytest import fixture, mark, param, skip
10
- from whenever import PlainDateTime
11
10
 
12
11
  from utilities.contextlib import enhanced_context_manager
13
12
  from utilities.platform import IS_MAC, IS_NOT_LINUX, IS_WINDOWS
14
13
  from utilities.re import ExtractGroupError, extract_group
15
- from utilities.tzlocal import LOCAL_TIME_ZONE_NAME
16
- from utilities.whenever import MINUTE, get_now
14
+ from utilities.whenever import MINUTE, get_now, parse_plain_local
17
15
 
18
16
  if TYPE_CHECKING:
19
17
  from collections.abc import AsyncIterator, Iterator, Sequence
@@ -174,11 +172,7 @@ def _is_to_drop(table: str, /) -> bool:
174
172
  datetime_str = extract_group(r"^(\d{8}T\d{6})_", table)
175
173
  except ExtractGroupError:
176
174
  return True
177
- datetime = PlainDateTime.parse_common_iso(datetime_str).assume_tz(
178
- LOCAL_TIME_ZONE_NAME
179
- )
180
- now = get_now()
181
- return (now - datetime) >= MINUTE
175
+ return (get_now() - parse_plain_local(datetime_str)) >= MINUTE
182
176
 
183
177
 
184
178
  def _select_tables() -> TextClause:
@@ -94,7 +94,6 @@ from utilities.iterables import (
94
94
  enumerate_with_edge,
95
95
  expanding_window,
96
96
  filter_include_and_exclude,
97
- group_consecutive_integers,
98
97
  groupby_lists,
99
98
  hashable_to_iterable,
100
99
  is_iterable,
@@ -117,7 +116,6 @@ from utilities.iterables import (
117
116
  sum_mappings,
118
117
  take,
119
118
  transpose,
120
- ungroup_consecutive_integers,
121
119
  unique_everseen,
122
120
  )
123
121
  from utilities.sentinel import Sentinel, sentinel
@@ -745,18 +743,6 @@ class TestFilterIncludeAndExclude:
745
743
  assert result == expected
746
744
 
747
745
 
748
- class TestGroupAndUngroupConsecutiveIntegers:
749
- @given(xs=lists(integers(), unique=True).map(sorted))
750
- def test_main(self, *, xs: list[int]) -> None:
751
- result = list(ungroup_consecutive_integers(group_consecutive_integers(xs)))
752
- assert result == xs
753
-
754
- def test_example(self) -> None:
755
- result = list(group_consecutive_integers([1, 2, 3, 6, 7, 10, 11, 12]))
756
- expected = [(1, 3), (6, 7), (10, 12)]
757
- assert result == expected
758
-
759
-
760
746
  class TestGroupbyLists:
761
747
  iterable: ClassVar[str] = "AAAABBBCCDAABB"
762
748
 
@@ -8,20 +8,13 @@ from re import search
8
8
  from typing import TYPE_CHECKING, Any, cast
9
9
 
10
10
  from hypothesis import given
11
- from hypothesis.strategies import booleans, integers, none, sampled_from
11
+ from hypothesis.strategies import booleans, integers
12
12
  from pytest import LogCaptureFixture, mark, param, raises
13
13
 
14
14
  from tests.conftest import SKIPIF_CI_AND_WINDOWS
15
- from utilities.hypothesis import (
16
- assume_does_not_raise,
17
- pairs,
18
- temp_paths,
19
- text_ascii,
20
- zoned_datetimes,
21
- )
15
+ from utilities.hypothesis import pairs, temp_paths, text_ascii, zoned_datetimes
22
16
  from utilities.iterables import one
23
17
  from utilities.logging import (
24
- FilterForKeyError,
25
18
  GetLoggingLevelNumberError,
26
19
  SizeAndTimeRotatingFileHandler,
27
20
  _compute_rollover_actions,
@@ -29,7 +22,6 @@ from utilities.logging import (
29
22
  _RotatingLogFile,
30
23
  add_filters,
31
24
  basic_config,
32
- filter_for_key,
33
25
  get_format_str,
34
26
  get_formatter,
35
27
  get_logging_level_number,
@@ -39,7 +31,7 @@ from utilities.logging import (
39
31
  from utilities.text import unique_str
40
32
  from utilities.types import LogLevel
41
33
  from utilities.typing import get_args
42
- from utilities.whenever import format_compact, get_now, to_local_plain
34
+ from utilities.whenever import get_now, to_local_plain
43
35
 
44
36
  if TYPE_CHECKING:
45
37
  from collections.abc import Mapping
@@ -196,7 +188,7 @@ class TestComputeRolloverActions:
196
188
 
197
189
  await sleep(1)
198
190
  tmp_path.joinpath("log.txt").touch()
199
- now = format_compact(to_local_plain(get_now()))
191
+ now = to_local_plain(get_now())
200
192
  tmp_path.joinpath(f"log.99__{now}__{now}.txt").touch()
201
193
  actions = _compute_rollover_actions(tmp_path, "log", ".txt")
202
194
  assert len(actions.deletions) == 2
@@ -209,33 +201,6 @@ class TestComputeRolloverActions:
209
201
  )
210
202
 
211
203
 
212
- class TestFilterForKey:
213
- @given(key=text_ascii(), value=booleans() | none(), default=booleans())
214
- def test_main(self, *, key: str, value: bool | None, default: bool) -> None:
215
- logger = getLogger(unique_str())
216
- logger.addHandler(handler := StreamHandler(buffer := StringIO()))
217
- with assume_does_not_raise(FilterForKeyError):
218
- filter_ = filter_for_key(key, default=default)
219
- add_filters(handler, filter_)
220
- match value:
221
- case bool():
222
- logger.warning("message", extra={key: value})
223
- expected = value
224
- case None:
225
- logger.warning("message")
226
- expected = default
227
- result = buffer.getvalue() != ""
228
- assert result is expected
229
-
230
- def test_sunder(self) -> None:
231
- _ = filter_for_key("_key")
232
-
233
- @given(key=sampled_from(["msg", "__dunder__"]))
234
- def test_error(self, *, key: str) -> None:
235
- with raises(FilterForKeyError, match="Invalid key: '.*'"):
236
- _ = filter_for_key(key)
237
-
238
-
239
204
  class TestGetFormatStr:
240
205
  @mark.parametrize("prefix", [param(">"), param(None)])
241
206
  @mark.parametrize("hostname", [param(True), param(False)])
@@ -313,7 +278,7 @@ class TestRotatingLogFile:
313
278
  def test_from_path_with_index_and_end(
314
279
  self, *, index: int, end: ZonedDateTime
315
280
  ) -> None:
316
- path = Path(f"log.{index}__{format_compact(to_local_plain(end))}.txt")
281
+ path = Path(f"log.{index}__{to_local_plain(end)}.txt")
317
282
  result = _RotatingLogFile.from_path(path, "log", ".txt")
318
283
  assert result is not None
319
284
  assert result.stem == "log"
@@ -327,9 +292,7 @@ class TestRotatingLogFile:
327
292
  self, *, index: int, datetimes: tuple[ZonedDateTime, ZonedDateTime]
328
293
  ) -> None:
329
294
  start, end = datetimes
330
- path = Path(
331
- f"log.{index}__{format_compact(to_local_plain(start))}__{format_compact(to_local_plain(end))}.txt"
332
- )
295
+ path = Path(f"log.{index}__{to_local_plain(start)}__{to_local_plain(end)}.txt")
333
296
  result = _RotatingLogFile.from_path(path, "log", ".txt")
334
297
  assert result is not None
335
298
  assert result.stem == "log"
@@ -359,9 +322,7 @@ class TestRotatingLogFile:
359
322
  file = _RotatingLogFile(
360
323
  directory=root, stem="log", suffix=".txt", index=index, end=end
361
324
  )
362
- assert file.path == root.joinpath(
363
- f"log.{index}__{format_compact(to_local_plain(end))}.txt"
364
- )
325
+ assert file.path == root.joinpath(f"log.{index}__{to_local_plain(end)}.txt")
365
326
 
366
327
  @given(
367
328
  root=temp_paths(),
@@ -376,7 +337,7 @@ class TestRotatingLogFile:
376
337
  directory=root, stem="log", suffix=".txt", index=index, start=start, end=end
377
338
  )
378
339
  assert file.path == root.joinpath(
379
- f"log.{index}__{format_compact(to_local_plain(start))}__{format_compact(to_local_plain(end))}.txt"
340
+ f"log.{index}__{to_local_plain(start)}__{to_local_plain(end)}.txt"
380
341
  )
381
342
 
382
343
 
@@ -99,7 +99,7 @@ from utilities.sqlalchemy import (
99
99
  )
100
100
  from utilities.text import strip_and_dedent
101
101
  from utilities.typing import get_args, get_literal_elements
102
- from utilities.whenever import MILLISECOND, format_compact, get_now, to_local_plain
102
+ from utilities.whenever import MILLISECOND, get_now, to_local_plain
103
103
 
104
104
  if TYPE_CHECKING:
105
105
  from collections.abc import Callable, Iterator
@@ -111,7 +111,7 @@ if TYPE_CHECKING:
111
111
  def _table_names() -> str:
112
112
  """Generate at unique string."""
113
113
  key = str(uuid4()).replace("-", "")
114
- return f"{format_compact(to_local_plain(get_now()))}_{key}"
114
+ return f"{to_local_plain(get_now())}_{key}"
115
115
 
116
116
 
117
117
  @overload
@@ -8,20 +8,24 @@ from typing import TYPE_CHECKING
8
8
 
9
9
  from hypothesis import given
10
10
  from hypothesis.strategies import sampled_from
11
- from pytest import CaptureFixture, raises
11
+ from pytest import CaptureFixture, mark, param, raises
12
12
 
13
13
  from utilities.iterables import one
14
14
  from utilities.traceback import (
15
15
  MakeExceptHookError,
16
+ _make_except_hook_purge,
16
17
  _path_to_dots,
17
18
  format_exception_stack,
18
19
  make_except_hook,
19
20
  )
20
21
  from utilities.tzlocal import LOCAL_TIME_ZONE_NAME
22
+ from utilities.whenever import SECOND, get_now, to_local_plain
21
23
 
22
24
  if TYPE_CHECKING:
23
25
  from collections.abc import Iterable
24
26
 
27
+ from utilities.types import Delta
28
+
25
29
 
26
30
  class TestFormatExceptionStack:
27
31
  @classmethod
@@ -93,8 +97,9 @@ class TestMakeExceptHook:
93
97
  hook(exc_type, exc_val, traceback)
94
98
  assert capsys.readouterr() != ""
95
99
 
96
- def test_file(self, *, tmp_path: Path) -> None:
97
- hook = make_except_hook(path=tmp_path)
100
+ @mark.parametrize("path_max_age", [param(SECOND), param(None)])
101
+ def test_path(self, *, tmp_path: Path, path_max_age: Delta | None) -> None:
102
+ hook = make_except_hook(path=tmp_path, path_max_age=path_max_age)
98
103
  try:
99
104
  _ = 1 / 0
100
105
  except ZeroDivisionError:
@@ -109,6 +114,14 @@ class TestMakeExceptHook:
109
114
  with raises(MakeExceptHookError, match="No exception to log"):
110
115
  hook(exc_type, exc_val, traceback)
111
116
 
117
+ def test_purge(self, *, tmp_path: Path) -> None:
118
+ now = get_now()
119
+ path = tmp_path.joinpath(to_local_plain(now - 2 * SECOND)).with_suffix(".txt")
120
+ path.touch()
121
+ assert len(list(tmp_path.iterdir())) == 1
122
+ _make_except_hook_purge(tmp_path, SECOND)
123
+ assert len(list(tmp_path.iterdir())) == 0
124
+
112
125
 
113
126
  class TestPathToDots:
114
127
  @given(
@@ -122,6 +122,7 @@ from utilities.whenever import (
122
122
  get_today_local,
123
123
  mean_datetime,
124
124
  min_max_date,
125
+ parse_plain_local,
125
126
  round_date_or_date_time,
126
127
  sub_year_month,
127
128
  to_date,
@@ -1016,11 +1017,13 @@ class TestToHours:
1016
1017
  _ = to_hours(delta)
1017
1018
 
1018
1019
 
1019
- class TestToLocalPlain:
1020
+ class TestToLocalPlainAndParsePlainLocal:
1020
1021
  @given(date_time=zoned_datetimes())
1021
1022
  def test_main(self, *, date_time: ZonedDateTime) -> None:
1022
- result = to_local_plain(date_time)
1023
- assert isinstance(result, PlainDateTime)
1023
+ text = to_local_plain(date_time)
1024
+ assert isinstance(text, str)
1025
+ parsed = parse_plain_local(text)
1026
+ assert abs(parsed - date_time) <= SECOND
1024
1027
 
1025
1028
 
1026
1029
  class TestToMicroseconds:
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.153.15"
3
+ __version__ = "0.154.0"
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, override
6
6
  from fpdf import FPDF
7
7
  from fpdf.enums import XPos, YPos
8
8
 
9
- from utilities.whenever import format_compact, get_now, to_local_plain
9
+ from utilities.whenever import get_now, to_local_plain
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from collections.abc import Iterator
@@ -47,7 +47,7 @@ def yield_pdf(*, header: str | None = None) -> Iterator[_BasePDF]:
47
47
  def footer(self) -> None:
48
48
  self.set_y(-15)
49
49
  self.set_font(family="Helvetica", style="I", size=8)
50
- page_no, now = (self.page_no(), format_compact(to_local_plain(get_now())))
50
+ page_no, now = (self.page_no(), to_local_plain(get_now()))
51
51
  text = f"page {page_no}/{{}}; {now}"
52
52
  _ = self.cell(
53
53
  w=0,
@@ -18,7 +18,7 @@ from enum import Enum
18
18
  from functools import cmp_to_key, partial, reduce
19
19
  from itertools import accumulate, chain, groupby, islice, pairwise, product
20
20
  from math import isnan
21
- from operator import add, itemgetter, or_
21
+ from operator import add, or_
22
22
  from typing import (
23
23
  TYPE_CHECKING,
24
24
  Any,
@@ -821,24 +821,6 @@ def filter_include_and_exclude[T, U](
821
821
  ##
822
822
 
823
823
 
824
- def group_consecutive_integers(iterable: Iterable[int], /) -> Iterable[tuple[int, int]]:
825
- """Group consecutive integers."""
826
- integers = sorted(iterable)
827
- for _, group in groupby(enumerate(integers), key=lambda x: x[1] - x[0]):
828
- as_list = list(map(itemgetter(1), group))
829
- yield as_list[0], as_list[-1]
830
-
831
-
832
- def ungroup_consecutive_integers(
833
- iterable: Iterable[tuple[int, int]], /
834
- ) -> Iterable[int]:
835
- """Ungroup consecutive integers."""
836
- return chain.from_iterable(range(start, end + 1) for start, end in iterable)
837
-
838
-
839
- ##
840
-
841
-
842
824
  @overload
843
825
  def groupby_lists[T](
844
826
  iterable: Iterable[T], /, *, key: None = None
@@ -1504,7 +1486,6 @@ __all__ = [
1504
1486
  "enumerate_with_edge",
1505
1487
  "expanding_window",
1506
1488
  "filter_include_and_exclude",
1507
- "group_consecutive_integers",
1508
1489
  "groupby_lists",
1509
1490
  "hashable_to_iterable",
1510
1491
  "is_iterable",
@@ -1527,6 +1508,5 @@ __all__ = [
1527
1508
  "sum_mappings",
1528
1509
  "take",
1529
1510
  "transpose",
1530
- "ungroup_consecutive_integers",
1531
1511
  "unique_everseen",
1532
1512
  ]
@@ -31,7 +31,7 @@ from typing import (
31
31
  override,
32
32
  )
33
33
 
34
- from whenever import PlainDateTime, ZonedDateTime
34
+ from whenever import ZonedDateTime
35
35
 
36
36
  from utilities.atomicwrites import move_many
37
37
  from utilities.dataclasses import replace_non_sentinel
@@ -45,16 +45,15 @@ from utilities.re import (
45
45
  extract_groups,
46
46
  )
47
47
  from utilities.sentinel import Sentinel, sentinel
48
- from utilities.tzlocal import LOCAL_TIME_ZONE_NAME
49
48
  from utilities.whenever import (
50
49
  WheneverLogRecord,
51
- format_compact,
52
50
  get_now_local,
51
+ parse_plain_local,
53
52
  to_local_plain,
54
53
  )
55
54
 
56
55
  if TYPE_CHECKING:
57
- from collections.abc import Callable, Iterable, Mapping
56
+ from collections.abc import Iterable, Mapping
58
57
  from datetime import time
59
58
  from logging import _FilterType
60
59
 
@@ -151,42 +150,6 @@ def basic_config(
151
150
  ##
152
151
 
153
152
 
154
- def filter_for_key(
155
- key: str, /, *, default: bool = False
156
- ) -> Callable[[LogRecord], bool]:
157
- """Make a filter for a given attribute."""
158
- if (key in _FILTER_FOR_KEY_BLACKLIST) or key.startswith("__"):
159
- raise FilterForKeyError(key=key)
160
-
161
- def filter_(record: LogRecord, /) -> bool:
162
- try:
163
- value = getattr(record, key)
164
- except AttributeError:
165
- return default
166
- return bool(value)
167
-
168
- return filter_
169
-
170
-
171
- # fmt: off
172
- _FILTER_FOR_KEY_BLACKLIST = {
173
- "args", "created", "exc_info", "exc_text", "filename", "funcName", "getMessage", "levelname", "levelno", "lineno", "module", "msecs", "msg", "name", "pathname", "process", "processName", "relativeCreated", "stack_info", "taskName", "thread", "threadName"
174
- }
175
- # fmt: on
176
-
177
-
178
- @dataclass(kw_only=True, slots=True)
179
- class FilterForKeyError(Exception):
180
- key: str
181
-
182
- @override
183
- def __str__(self) -> str:
184
- return f"Invalid key: {self.key!r}"
185
-
186
-
187
- ##
188
-
189
-
190
153
  def get_format_str(*, prefix: str | None = None, hostname: bool = False) -> str:
191
154
  """Generate a format string."""
192
155
  parts: list[str] = [
@@ -535,10 +498,8 @@ class _RotatingLogFile:
535
498
  stem=stem,
536
499
  suffix=suffix,
537
500
  index=int(index),
538
- start=PlainDateTime.parse_common_iso(start).assume_tz(
539
- LOCAL_TIME_ZONE_NAME
540
- ),
541
- end=PlainDateTime.parse_common_iso(end).assume_tz(LOCAL_TIME_ZONE_NAME),
501
+ start=parse_plain_local(start),
502
+ end=parse_plain_local(end),
542
503
  )
543
504
  try:
544
505
  index, end = extract_groups(patterns.pattern2, path.name)
@@ -550,7 +511,7 @@ class _RotatingLogFile:
550
511
  stem=stem,
551
512
  suffix=suffix,
552
513
  index=int(index),
553
- end=PlainDateTime.parse_common_iso(end).assume_tz(LOCAL_TIME_ZONE_NAME),
514
+ end=parse_plain_local(end),
554
515
  )
555
516
  try:
556
517
  index = extract_group(patterns.pattern1, path.name)
@@ -571,9 +532,9 @@ class _RotatingLogFile:
571
532
  case int() as index, None, None:
572
533
  tail = str(index)
573
534
  case int() as index, None, ZonedDateTime() as end:
574
- tail = f"{index}__{format_compact(to_local_plain(end))}"
535
+ tail = f"{index}__{to_local_plain(end)}"
575
536
  case int() as index, ZonedDateTime() as start, ZonedDateTime() as end:
576
- tail = f"{index}__{format_compact(to_local_plain(start))}__{format_compact(to_local_plain(end))}"
537
+ tail = f"{index}__{to_local_plain(start)}__{to_local_plain(end)}"
577
538
  case _: # pragma: no cover
578
539
  raise ImpossibleCaseError(
579
540
  case=[f"{self.index=}", f"{self.start=}", f"{self.end=}"]
@@ -626,12 +587,10 @@ def to_logger(logger: LoggerLike | None = None, /) -> Logger:
626
587
 
627
588
 
628
589
  __all__ = [
629
- "FilterForKeyError",
630
590
  "GetLoggingLevelNumberError",
631
591
  "SizeAndTimeRotatingFileHandler",
632
592
  "add_filters",
633
593
  "basic_config",
634
- "filter_for_key",
635
594
  "get_format_str",
636
595
  "get_logging_level_number",
637
596
  "setup_logging",
@@ -8,7 +8,7 @@ from pyinstrument.profiler import Profiler
8
8
 
9
9
  from utilities.atomicwrites import writer
10
10
  from utilities.pathlib import to_path
11
- from utilities.whenever import format_compact, get_now, to_local_plain
11
+ from utilities.whenever import get_now, to_local_plain
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from collections.abc import Iterator
@@ -21,9 +21,7 @@ def profile(path: MaybeCallablePathLike = Path.cwd, /) -> Iterator[None]:
21
21
  """Profile the contents of a block."""
22
22
  with Profiler() as profiler:
23
23
  yield
24
- filename = to_path(path).joinpath(
25
- f"profile__{format_compact(to_local_plain(get_now()))}.html"
26
- )
24
+ filename = to_path(path).joinpath(f"profile__{to_local_plain(get_now())}.html")
27
25
  with writer(filename) as temp:
28
26
  _ = temp.write_text(profiler.output_html())
29
27
 
@@ -33,6 +33,7 @@ from utilities.whenever import (
33
33
  format_compact,
34
34
  get_now,
35
35
  get_now_local,
36
+ parse_plain_local,
36
37
  to_local_plain,
37
38
  to_zoned_date_time,
38
39
  )
@@ -43,6 +44,7 @@ if TYPE_CHECKING:
43
44
  from types import TracebackType
44
45
 
45
46
  from utilities.types import (
47
+ Delta,
46
48
  MaybeCallableBoolLike,
47
49
  MaybeCallablePathLike,
48
50
  MaybeCallableZonedDateTimeLike,
@@ -95,16 +97,10 @@ def _yield_header_lines(
95
97
  ) -> Iterator[str]:
96
98
  """Yield the header lines."""
97
99
  now = get_now_local()
98
- start_use = to_zoned_date_time(start)
99
100
  yield f"Date/time | {format_compact(now)}"
100
- if start_use is None:
101
- start_str = ""
102
- else:
103
- start_str = format_compact(start_use.to_tz(LOCAL_TIME_ZONE_NAME))
104
- yield f"Started | {start_str}"
105
- delta = None if start_use is None else (now - start_use)
106
- delta_str = "" if delta is None else delta.format_common_iso()
107
- yield f"Duration | {delta_str}"
101
+ start_use = to_zoned_date_time(start).to_tz(LOCAL_TIME_ZONE_NAME)
102
+ yield f"Started | {format_compact(start_use)}"
103
+ yield f"Duration | {(now - start_use).format_common_iso()}"
108
104
  yield f"User | {getuser()}"
109
105
  yield f"Host | {gethostname()}"
110
106
  yield f"Process ID | {getpid()}"
@@ -205,6 +201,7 @@ def make_except_hook(
205
201
  start: MaybeCallableZonedDateTimeLike = get_now,
206
202
  version: MaybeCallableVersionLike | None = None,
207
203
  path: MaybeCallablePathLike | None = None,
204
+ path_max_age: Delta | None = None,
208
205
  max_width: int = RICH_MAX_WIDTH,
209
206
  indent_size: int = RICH_INDENT_SIZE,
210
207
  max_length: int | None = RICH_MAX_LENGTH,
@@ -222,6 +219,7 @@ def make_except_hook(
222
219
  start=start,
223
220
  version=version,
224
221
  path=path,
222
+ path_max_age=path_max_age,
225
223
  max_width=max_width,
226
224
  indent_size=indent_size,
227
225
  max_length=max_length,
@@ -242,6 +240,7 @@ def _make_except_hook_inner(
242
240
  start: MaybeCallableZonedDateTimeLike = get_now,
243
241
  version: MaybeCallableVersionLike | None = None,
244
242
  path: MaybeCallablePathLike | None = None,
243
+ path_max_age: Delta | None = None,
245
244
  max_width: int = RICH_MAX_WIDTH,
246
245
  indent_size: int = RICH_INDENT_SIZE,
247
246
  max_length: int | None = RICH_MAX_LENGTH,
@@ -258,11 +257,8 @@ def _make_except_hook_inner(
258
257
  slim = format_exception_stack(exc_val, header=True, start=start, version=version)
259
258
  _ = sys.stderr.write(f"{slim}\n") # don't 'from sys import stderr'
260
259
  if path is not None:
261
- path = (
262
- to_path(path)
263
- .joinpath(format_compact(to_local_plain(get_now())))
264
- .with_suffix(".txt")
265
- )
260
+ path = to_path(path)
261
+ path_log = path.joinpath(to_local_plain(get_now())).with_suffix(".txt")
266
262
  full = format_exception_stack(
267
263
  exc_val,
268
264
  header=True,
@@ -276,8 +272,10 @@ def _make_except_hook_inner(
276
272
  max_depth=max_depth,
277
273
  expand_all=expand_all,
278
274
  )
279
- with writer(path, overwrite=True) as temp:
275
+ with writer(path_log, overwrite=True) as temp:
280
276
  _ = temp.write_text(full)
277
+ if path_max_age is not None:
278
+ _make_except_hook_purge(path, path_max_age)
281
279
  if slack_url is not None: # pragma: no cover
282
280
  from utilities.slack_sdk import SendToSlackError, send_to_slack
283
281
 
@@ -285,13 +283,23 @@ def _make_except_hook_inner(
285
283
  send_to_slack(slack_url, f"```{slim}```")
286
284
  except SendToSlackError as error:
287
285
  _ = stderr.write(f"{error}\n")
288
-
289
286
  if to_bool(pudb): # pragma: no cover
290
287
  from pudb import post_mortem
291
288
 
292
289
  post_mortem(tb=traceback, e_type=exc_type, e_value=exc_val)
293
290
 
294
291
 
292
+ def _make_except_hook_purge(path: PathLike, max_age: Delta, /) -> None:
293
+ threshold = get_now() - max_age
294
+ paths = {
295
+ p
296
+ for p in Path(path).iterdir()
297
+ if p.is_file() and (parse_plain_local(p.stem) <= threshold)
298
+ }
299
+ for p in paths:
300
+ p.unlink(missing_ok=True)
301
+
302
+
295
303
  @dataclass(kw_only=True, slots=True)
296
304
  class MakeExceptHookError(Exception):
297
305
  @override
@@ -52,8 +52,6 @@ if TYPE_CHECKING:
52
52
  TimeZoneLike,
53
53
  )
54
54
 
55
- # type vars
56
-
57
55
 
58
56
  # bounds
59
57
 
@@ -1008,9 +1006,14 @@ class _ToHoursNanosecondsError(ToHoursError):
1008
1006
  ##
1009
1007
 
1010
1008
 
1011
- def to_local_plain(date_time: ZonedDateTime, /) -> PlainDateTime:
1009
+ def to_local_plain(date_time: ZonedDateTime, /) -> str:
1012
1010
  """Convert a datetime to its local/plain variant."""
1013
- return date_time.to_tz(LOCAL_TIME_ZONE_NAME).to_plain()
1011
+ return format_compact(date_time.to_tz(LOCAL_TIME_ZONE_NAME).to_plain())
1012
+
1013
+
1014
+ def parse_plain_local(text: str, /) -> ZonedDateTime:
1015
+ """Parse a plain, local datetime."""
1016
+ return PlainDateTime.parse_common_iso(text).assume_tz(LOCAL_TIME_ZONE_NAME)
1014
1017
 
1015
1018
 
1016
1019
  ##
@@ -1967,6 +1970,7 @@ __all__ = [
1967
1970
  "get_today_local",
1968
1971
  "mean_datetime",
1969
1972
  "min_max_date",
1973
+ "parse_plain_local",
1970
1974
  "round_date_or_date_time",
1971
1975
  "sub_year_month",
1972
1976
  "to_date",