dycw-utilities 0.166.30__py3-none-any.whl → 0.185.8__py3-none-any.whl

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 (96) hide show
  1. dycw_utilities-0.185.8.dist-info/METADATA +33 -0
  2. dycw_utilities-0.185.8.dist-info/RECORD +90 -0
  3. {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.185.8.dist-info}/WHEEL +1 -1
  4. {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.185.8.dist-info}/entry_points.txt +1 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +17 -10
  7. utilities/asyncio.py +50 -72
  8. utilities/atools.py +9 -11
  9. utilities/cachetools.py +16 -11
  10. utilities/click.py +76 -19
  11. utilities/concurrent.py +1 -1
  12. utilities/constants.py +492 -0
  13. utilities/contextlib.py +23 -30
  14. utilities/contextvars.py +1 -23
  15. utilities/core.py +2581 -0
  16. utilities/dataclasses.py +16 -119
  17. utilities/docker.py +387 -0
  18. utilities/enum.py +1 -1
  19. utilities/errors.py +2 -16
  20. utilities/fastapi.py +5 -5
  21. utilities/fpdf2.py +2 -1
  22. utilities/functions.py +34 -265
  23. utilities/http.py +2 -3
  24. utilities/hypothesis.py +84 -29
  25. utilities/importlib.py +17 -1
  26. utilities/iterables.py +39 -575
  27. utilities/jinja2.py +145 -0
  28. utilities/jupyter.py +5 -3
  29. utilities/libcst.py +1 -1
  30. utilities/lightweight_charts.py +4 -6
  31. utilities/logging.py +24 -24
  32. utilities/math.py +1 -36
  33. utilities/more_itertools.py +4 -6
  34. utilities/numpy.py +2 -1
  35. utilities/operator.py +2 -2
  36. utilities/orjson.py +42 -43
  37. utilities/os.py +4 -147
  38. utilities/packaging.py +129 -0
  39. utilities/parse.py +35 -15
  40. utilities/pathlib.py +3 -120
  41. utilities/platform.py +8 -90
  42. utilities/polars.py +38 -32
  43. utilities/postgres.py +37 -33
  44. utilities/pottery.py +20 -18
  45. utilities/pqdm.py +3 -4
  46. utilities/psutil.py +2 -3
  47. utilities/pydantic.py +25 -0
  48. utilities/pydantic_settings.py +87 -16
  49. utilities/pydantic_settings_sops.py +16 -3
  50. utilities/pyinstrument.py +4 -4
  51. utilities/pytest.py +96 -125
  52. utilities/pytest_plugins/pytest_regressions.py +2 -2
  53. utilities/pytest_regressions.py +32 -11
  54. utilities/random.py +2 -8
  55. utilities/redis.py +98 -94
  56. utilities/reprlib.py +11 -118
  57. utilities/shellingham.py +66 -0
  58. utilities/shutil.py +25 -0
  59. utilities/slack_sdk.py +13 -12
  60. utilities/sqlalchemy.py +57 -30
  61. utilities/sqlalchemy_polars.py +16 -25
  62. utilities/subprocess.py +2590 -0
  63. utilities/tabulate.py +32 -0
  64. utilities/testbook.py +8 -8
  65. utilities/text.py +24 -99
  66. utilities/throttle.py +159 -0
  67. utilities/time.py +18 -0
  68. utilities/timer.py +31 -14
  69. utilities/traceback.py +16 -23
  70. utilities/types.py +42 -2
  71. utilities/typing.py +26 -14
  72. utilities/uuid.py +1 -1
  73. utilities/version.py +202 -45
  74. utilities/whenever.py +53 -150
  75. dycw_utilities-0.166.30.dist-info/METADATA +0 -41
  76. dycw_utilities-0.166.30.dist-info/RECORD +0 -98
  77. dycw_utilities-0.166.30.dist-info/licenses/LICENSE +0 -21
  78. utilities/aeventkit.py +0 -388
  79. utilities/atomicwrites.py +0 -182
  80. utilities/cryptography.py +0 -41
  81. utilities/getpass.py +0 -8
  82. utilities/git.py +0 -19
  83. utilities/gzip.py +0 -31
  84. utilities/json.py +0 -70
  85. utilities/pickle.py +0 -25
  86. utilities/re.py +0 -156
  87. utilities/sentinel.py +0 -73
  88. utilities/socket.py +0 -8
  89. utilities/string.py +0 -20
  90. utilities/tempfile.py +0 -77
  91. utilities/typed_settings.py +0 -152
  92. utilities/tzdata.py +0 -11
  93. utilities/tzlocal.py +0 -28
  94. utilities/warnings.py +0 -65
  95. utilities/zipfile.py +0 -25
  96. utilities/zoneinfo.py +0 -133
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.3
2
+ Name: dycw-utilities
3
+ Version: 0.185.8
4
+ Summary: Miscellaneous Python utilities
5
+ Author: Derek Wan
6
+ Author-email: Derek Wan <d.wan@icloud.com>
7
+ Requires-Dist: typing-extensions>=4.15.0
8
+ Requires-Dist: tzdata>=2025.3
9
+ Requires-Dist: tzlocal>=5.3.1
10
+ Requires-Dist: whenever>=0.9.5
11
+ Requires-Dist: coloredlogs>=15.0.1 ; extra == 'logging'
12
+ Requires-Dist: dycw-pytest-only>=2.1.1 ; extra == 'test'
13
+ Requires-Dist: hypothesis>=6.150.2 ; extra == 'test'
14
+ Requires-Dist: pytest-asyncio>=1.3.0 ; extra == 'test'
15
+ Requires-Dist: pytest-cov>=7.0.0 ; extra == 'test'
16
+ Requires-Dist: pytest-instafail>=0.5.0 ; extra == 'test'
17
+ Requires-Dist: pytest-lazy-fixtures>=1.4.0 ; extra == 'test'
18
+ Requires-Dist: pytest-randomly>=4.0.1 ; extra == 'test'
19
+ Requires-Dist: pytest-regressions>=2.9.1 ; extra == 'test'
20
+ Requires-Dist: pytest-repeat>=0.9.4 ; extra == 'test'
21
+ Requires-Dist: pytest-rerunfailures>=16.1 ; extra == 'test'
22
+ Requires-Dist: pytest-rng>=1.0.0 ; extra == 'test'
23
+ Requires-Dist: pytest-timeout>=2.4.0 ; extra == 'test'
24
+ Requires-Dist: pytest-xdist>=3.8.0 ; extra == 'test'
25
+ Requires-Dist: pytest>=9.0.2 ; extra == 'test'
26
+ Requires-Python: >=3.12
27
+ Provides-Extra: logging
28
+ Provides-Extra: test
29
+ Description-Content-Type: text/markdown
30
+
31
+ # `python-utilities`
32
+
33
+ Miscellaneous Python utilities
@@ -0,0 +1,90 @@
1
+ utilities/__init__.py,sha256=f2LepwDrbt4lGZTQGsx6vT40UOEvW-BOkI7_SrElAds,60
2
+ utilities/altair.py,sha256=248uNvy7JkhGbmgn2yj0Um2Cd2kfEDCRMvUEKTxJjto,9032
3
+ utilities/asyncio.py,sha256=QX_RSCUEK6gMIbrVkJnR_ZjrXUW5hkWpvSXFDyBUz1s,16294
4
+ utilities/atools.py,sha256=Fi2gt4ylGJrwzzMvun1lV41NsoGFm2voHcp9yili9gc,2503
5
+ utilities/cachetools.py,sha256=p7aedtT0QahEU5DpR7xtliIApoS0hsDSdwMKmBF74pw,2820
6
+ utilities/click.py,sha256=fFPqQmCaB2bwcHG62X5c0smEyjoSqmBaJ0bodeaRF9Q,19205
7
+ utilities/concurrent.py,sha256=nzOPz2G21VKrpttgIE9RvbK0ngssS7vbbrauFcNNM3g,2856
8
+ utilities/constants.py,sha256=I7xXM7bFu2gsuG_a1zRMR_T7ZfPKo9-NCczo8X7eNHE,10965
9
+ utilities/contextlib.py,sha256=qBDWE9g5w_6By04X90CbfWWxnuX2xi8XGeptr8BpBIA,7482
10
+ utilities/contextvars.py,sha256=nuSzAoco5HqdfRIVKtsdXnUxFxeBoj8XI0-I71qses4,500
11
+ utilities/core.py,sha256=ehfrNF-Knl3FA1Ekd9LGJ2bvQzfpRv41uxbTry5FeCE,74792
12
+ utilities/cvxpy.py,sha256=Rv1-fD-XYerosCavRF8Pohop2DBkU3AlFaGTfD8AEAA,13776
13
+ utilities/dataclasses.py,sha256=XwAjYxdCnDQvtB34rnq79ZuKGgybl2qXVkYZU8CN03k,28917
14
+ utilities/docker.py,sha256=jqE1RIHLI6lkoJoaBaFID2juJFDeTlfsm3AOVY8qXAc,10660
15
+ utilities/enum.py,sha256=yrQQhFQCbfSF7oLu1Bz258CHn8na3ZK5F2FjvvAztU4,5776
16
+ utilities/errors.py,sha256=ceCdBp4WgvbRB0EpANFeLYFEYmvDerUsepDsFOiL3Oc,1040
17
+ utilities/fastapi.py,sha256=GLeM8pwFYPEn8LrBcSARxvpSmiqbjJY84rN5YreQRzk,1455
18
+ utilities/fpdf2.py,sha256=S5vrKFv-L5LP6tImcbngYtTa7OJJ4PNToCBrEV7-Dbw,1892
19
+ utilities/functions.py,sha256=QYfRRoRoJv7SgxKc4OdM9trubRqLbHGSb9mNGSpg_jc,15362
20
+ utilities/functools.py,sha256=I00ru2gQPakZw2SHVeKIKXfTv741655s6HI0lUoE0D4,1552
21
+ utilities/hashlib.py,sha256=SVTgtguur0P4elppvzOBbLEjVM3Pea0eWB61yg2ilxo,309
22
+ utilities/http.py,sha256=WcahTcKYRtZ04WXQoWt5EGCgFPcyHD3EJdlMfxvDt-0,946
23
+ utilities/hypothesis.py,sha256=Er8cA3K-4KwPLjRzzm6j-HTmkn5YXB6UweF3lOnaiIE,47160
24
+ utilities/importlib.py,sha256=SkVVtIjVC7bjJ36doXnmnmFiYe5tLbip4YAfYJj8Ycg,892
25
+ utilities/inflect.py,sha256=v7YkOWSu8NAmVghPcf4F3YBZQoJCS47_DLf9jbfWIs0,581
26
+ utilities/ipython.py,sha256=V2oMYHvEKvlNBzxDXdLvKi48oUq2SclRg5xasjaXStw,763
27
+ utilities/iterables.py,sha256=CDWPzYG_7fJHdcMZWYy1lI8-0oC6oEDdo3doBdzNjiE,27958
28
+ utilities/jinja2.py,sha256=nuGVbVEtvFzvcd9x1WRmN0OCDwGC_0-UOowzv3AQrqw,4567
29
+ utilities/jupyter.py,sha256=LQYq7oNw-eiY3xnAz-nEj6DFj0HXtq4a0n26qbDuTpE,2939
30
+ utilities/libcst.py,sha256=tcwn1yqwBd1O7TNRiD3Z2I7A__jem_F3CS1mmqBpAz4,5611
31
+ utilities/lightweight_charts.py,sha256=-rfCKBcJAfPTH6E-2podk33ag8gxLThUxWOdcE_s4KM,2660
32
+ utilities/logging.py,sha256=puR5c2L8XD0L19M7QxyBlvXj8a-lIaCyxt1FZ1fP54E,18789
33
+ utilities/math.py,sha256=-exzVZaKuOfPeMAZ24-5lVGD0tqbqjxcN1_hv8AytDY,26011
34
+ utilities/memory_profiler.py,sha256=XzN56jDCa5aqXS_DxEjb_K4L6aIWh_5zyKi6OhcIxw0,853
35
+ utilities/modules.py,sha256=iuvLluJya-hvl1Q25-Jk3dLgx2Es3ck4SjJiEkAlVTs,3195
36
+ utilities/more_itertools.py,sha256=dv2WQlKGXo34kFs1GvKjhXmRdJV3ljaKrLDC_1B83YQ,10834
37
+ utilities/numpy.py,sha256=i95XC2enKNC8ug1jkncLuq33Eqk3JBvXGHvROai1wIY,26148
38
+ utilities/operator.py,sha256=kI3VQSpH4gM68DunmC0Ir2mDiyP_BGUphcWFV1ckBm4,3601
39
+ utilities/optuna.py,sha256=C-fhWYiXHVPo1l8QctYkFJ4DyhbSrGorzP1dJb_qvd8,1933
40
+ utilities/orjson.py,sha256=0GWskBRlvRojMpKeJYe1Mkc0gqZy-vt0qNRQ-aMbo7U,42025
41
+ utilities/os.py,sha256=fJ97L-YgC2swNu44GMWHn9qMSCsyT0jQKDzZZAXXTgQ,781
42
+ utilities/packaging.py,sha256=ws9OIfyUMx130EK-u5ZVfQI6EOdkekeCRGLcoZt2VmQ,4293
43
+ utilities/parse.py,sha256=BMg75FXTftmLWYqye2iMjjS-H12I16KNg3tlppJ5n98,18195
44
+ utilities/pathlib.py,sha256=ZnEKDiKaaEAxUQ6gnE43OUwK3DM4enYeOClChCd7qV4,6101
45
+ utilities/platform.py,sha256=picjLyUsWMphI6rDzpSjCX8J9lpEFz9J43o-19TM52s,900
46
+ utilities/polars.py,sha256=WJ-Yj25adOT2e3VpXhOUvy_h5cz8yLQajWU5slWEUH4,87400
47
+ utilities/polars_ols.py,sha256=LNTFNLPuYW7fcAHymlbnams_DhitToblYvib3mhKbwI,5615
48
+ utilities/postgres.py,sha256=RQXBIaS54pObS0EFXJQAJ-Hl2H6fbilG7g20hOpt57A,12571
49
+ utilities/pottery.py,sha256=QJvT8I0A-7v5OdK7DSU7hzc2yKTJ9-wIhUNBkE62rlY,4015
50
+ utilities/pqdm.py,sha256=XcL7yZJ8TZOPOX9aQQOGhA_QdM6CI7QFOJde_R9eons,3059
51
+ utilities/psutil.py,sha256=yQIR1M7AtjNWaqGlAAHlAVv0OqsS6PwGBBUqCq9gohg,2054
52
+ utilities/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
+ utilities/pydantic.py,sha256=8wLNW3Hjor6xSJktILSRNfe4hyxSg5Xl-keWbBQ9qUk,656
54
+ utilities/pydantic_settings.py,sha256=UM5G5RAnfI4OApziM15Cqg2hqamNm1NnU12MclRjPX4,7564
55
+ utilities/pydantic_settings_sops.py,sha256=r7QhDHPfDMyexmi2krV0eIOl4Bco0MZxqyStapQyXPk,2307
56
+ utilities/pyinstrument.py,sha256=HtVYVNe7jJ2NuNvv4GyrDsx6lrLRa7BI7UtBWnypIwU,831
57
+ utilities/pytest.py,sha256=WkINIzo45omF2HDXbAFTDeMxDLs3iU2xtM1_Dcqe9TE,8339
58
+ utilities/pytest_plugins/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
59
+ utilities/pytest_plugins/pytest_randomly.py,sha256=B1qYVlExGOxTywq2r1SMi5o7btHLk2PNdY_b1p98dkE,409
60
+ utilities/pytest_plugins/pytest_regressions.py,sha256=p_omAAZCkiWLxydLS6LK-hIYn3GRbn-0IrcSpf36q4k,1649
61
+ utilities/pytest_regressions.py,sha256=T-T_2ZCDFm_nxyaEZy1zAw9-HGSAHRnPAH-TIZLTPNc,4910
62
+ utilities/random.py,sha256=DhBzzHoW0RS0rdfWaG0Ws9_Lw_p_doeHnlW9_7W0Pqs,4096
63
+ utilities/redis.py,sha256=mQuKx-cbSq5o2w-ITryiOXAv4YdHHHLrvd_qt0GMUJU,30346
64
+ utilities/reprlib.py,sha256=LYcIh5FbvRwL6ZPP-p1c34taQreBqDWipN5LP66yk4Y,1285
65
+ utilities/scipy.py,sha256=wZJM7fEgBAkLSYYvSmsg5ac-QuwAI0BGqHVetw1_Hb0,947
66
+ utilities/shellingham.py,sha256=lV_cAZHdUK3CC_NKQZuyZSepngr7u8CZZnph3galvaE,1712
67
+ utilities/shelve.py,sha256=4OzjQI6kGuUbJciqf535rwnao-_IBv66gsT6tRGiUt0,759
68
+ utilities/shutil.py,sha256=knJ7hx42FtIfGByxQZMcOSgQCDlaSony325g505ps3A,480
69
+ utilities/slack_sdk.py,sha256=zBM6NpxK3iIoWL-18UVxRavBbRked3PC7mrCUuvkAoY,2297
70
+ utilities/sqlalchemy.py,sha256=riSz1iod_TkUDuJlhgPz-DBn8zl1kWV8G83lgd_DNL0,37300
71
+ utilities/sqlalchemy_polars.py,sha256=jdEb9e6MR6PBbwFThLbMM7q0MeUqoqshChyeje6ia-0,13290
72
+ utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
73
+ utilities/subprocess.py,sha256=PjXK6Do5-uUMyIw1xLSa_DhvgzfmcRUHAVK6HkDqWxg,70961
74
+ utilities/tabulate.py,sha256=9lsjeMxb9lv2-dkHgl85Eln4Pu1RsWweylbxe5CNkAM,912
75
+ utilities/testbook.py,sha256=-A4hC0wNzBdT-biXSZpCI6dOgAilmSJOMArxS4Rhdag,1328
76
+ utilities/text.py,sha256=g4j7IkzArw1hJtSV0k8tqjYvOmvqjjYH369uYC3O9-Y,11817
77
+ utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
78
+ utilities/throttle.py,sha256=fAfsMMiMm0WRL_MnY2A-_slbhlcmuXij7JwDBzR_kaw,4994
79
+ utilities/time.py,sha256=oVvbpGFjgavzGn5Y8CevryQJR9uithmtyBMbPI-YcC8,376
80
+ utilities/timer.py,sha256=a9evZ_Ob8lMz9M6kWizFedVYmHO27bmvUa4m1kAKEKk,3119
81
+ utilities/traceback.py,sha256=wJWAT-8PVu4xwZ5f3OHUUF3o34YwTvQw74S_wkTGvXM,9519
82
+ utilities/types.py,sha256=n0TDfKJTzElnh2df7QdlSIcIGPzSSS0FJyRrcIDGRLc,19539
83
+ utilities/typing.py,sha256=zECFvJbzmUvzg3hn3CNnhtRr_AvGYypgYdiIQRZa2gU,25366
84
+ utilities/uuid.py,sha256=0jVYfTrh1mIrNHi8PNvwLne_DVn7XBmT5P3g7YcBTOQ,1573
85
+ utilities/version.py,sha256=YBhzHz-WpfXLByZr2eEisWsOyFOOfiYYtU3fxf4uDRA,9775
86
+ utilities/whenever.py,sha256=VJaQINRKJwFCVd9Q4zSkG_HvezbdaGxJNcgV6A-lS1Y,57445
87
+ dycw_utilities-0.185.8.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
88
+ dycw_utilities-0.185.8.dist-info/entry_points.txt,sha256=cOGtKeJI0KXLSV7MJ8Dhc2G8jPgDcBDm53MVNJU4ycI,136
89
+ dycw_utilities-0.185.8.dist-info/METADATA,sha256=Vf6EyD1EdCdKQLTzuWcvK3SGTF9kdIkSU_0DPqrhzhU,1281
90
+ dycw_utilities-0.185.8.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: uv 0.9.26
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,3 +1,4 @@
1
1
  [pytest11]
2
2
  pytest-randomly = utilities.pytest_plugins.pytest_randomly
3
3
  pytest-regressions = utilities.pytest_plugins.pytest_regressions
4
+
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.166.30"
3
+ __version__ = "0.185.8"
utilities/altair.py CHANGED
@@ -23,10 +23,13 @@ from altair import (
23
23
  )
24
24
  from altair.utils.schemapi import Undefined
25
25
 
26
- from utilities.atomicwrites import writer
26
+ from utilities.core import (
27
+ TemporaryDirectory,
28
+ always_iterable,
29
+ write_bytes,
30
+ yield_write_path,
31
+ )
27
32
  from utilities.functions import ensure_bytes, ensure_number
28
- from utilities.iterables import always_iterable
29
- from utilities.tempfile import TemporaryDirectory
30
33
 
31
34
  if TYPE_CHECKING:
32
35
  from polars import DataFrame
@@ -94,7 +97,8 @@ def plot_dataframes(
94
97
  # lines
95
98
  selection = selection_point(bind="legend", fields=[var_name], nearest=False)
96
99
  lines = [
97
- chart.mark_line(interpolate=interpolate)
100
+ chart
101
+ .mark_line(interpolate=interpolate)
98
102
  .encode(
99
103
  x=x_use,
100
104
  y=Y(value_name).scale(zero=False),
@@ -124,7 +128,8 @@ def plot_dataframes(
124
128
  else:
125
129
  tooltip_format_use = Undefined
126
130
  rules = [
127
- chart.transform_pivot(var_name, value=value_name, groupby=[x_use])
131
+ chart
132
+ .transform_pivot(var_name, value=value_name, groupby=[x_use])
128
133
  .mark_rule(color="gray")
129
134
  .encode(
130
135
  x=x_use,
@@ -143,7 +148,9 @@ def plot_dataframes(
143
148
  ]
144
149
  zoom = selection_interval(bind="scales", encodings=["x"])
145
150
  chart = (
146
- vconcat(*layers).add_params(zoom).resolve_scale(color="independent", x="shared")
151
+ vconcat_charts(*layers)
152
+ .add_params(zoom)
153
+ .resolve_scale(color="independent", x="shared")
147
154
  )
148
155
  if title is not None:
149
156
  chart = chart.properties(title=title)
@@ -227,7 +234,8 @@ def plot_intraday_dataframe(
227
234
  )
228
235
 
229
236
  data4 = (
230
- data3.group_by("_date_index")
237
+ data3
238
+ .group_by("_date_index")
231
239
  .agg(
232
240
  col(f"_{datetime}_index").min().alias(f"{datetime}_index_min"),
233
241
  (col(f"_{datetime}_index").max() + 1).alias(f"{datetime}_index_max"),
@@ -264,7 +272,7 @@ def save_chart(
264
272
  chart: _ChartLike, path: PathLike, /, *, overwrite: bool = False
265
273
  ) -> None:
266
274
  """Atomically save a chart to disk."""
267
- with writer(path, overwrite=overwrite) as temp:
275
+ with yield_write_path(path, overwrite=overwrite) as temp:
268
276
  chart.save(temp, format="png")
269
277
 
270
278
 
@@ -282,8 +290,7 @@ def save_charts_as_pdf(
282
290
  for chart, temp_path in zip(charts, temp_paths, strict=True):
283
291
  save_chart(chart, temp_path)
284
292
  data = ensure_bytes(convert(*temp_paths))
285
- with writer(path, overwrite=overwrite) as temp:
286
- _ = temp.write_bytes(data)
293
+ write_bytes(path, data, overwrite=overwrite)
287
294
 
288
295
 
289
296
  ##
utilities/asyncio.py CHANGED
@@ -10,7 +10,6 @@ from asyncio import (
10
10
  Task,
11
11
  TaskGroup,
12
12
  create_subprocess_shell,
13
- sleep,
14
13
  )
15
14
  from contextlib import (
16
15
  AbstractAsyncContextManager,
@@ -36,15 +35,12 @@ from typing import (
36
35
  override,
37
36
  )
38
37
 
39
- from utilities.functions import ensure_int, ensure_not_none
40
- from utilities.os import is_pytest
41
- from utilities.random import SYSTEM_RANDOM
42
- from utilities.reprlib import get_repr
43
- from utilities.sentinel import Sentinel, sentinel
38
+ from utilities.constants import SYSTEM_RANDOM, Sentinel, sentinel
39
+ from utilities.core import get_now, is_pytest, repr_
40
+ from utilities.functions import ensure_int, ensure_not_none, in_seconds
44
41
  from utilities.shelve import yield_shelf
45
42
  from utilities.text import to_bool
46
- from utilities.warnings import suppress_warnings
47
- from utilities.whenever import get_now, round_date_or_date_time, to_nanoseconds
43
+ from utilities.whenever import round_date_or_date_time
48
44
 
49
45
  if TYPE_CHECKING:
50
46
  from asyncio import _CoroutineLike
@@ -69,8 +65,8 @@ if TYPE_CHECKING:
69
65
 
70
66
  from utilities.shelve import _Flag
71
67
  from utilities.types import (
72
- Coro,
73
68
  Delta,
69
+ Duration,
74
70
  MaybeCallableBoolLike,
75
71
  MaybeType,
76
72
  PathLike,
@@ -229,7 +225,7 @@ class EnhancedTaskGroup(TaskGroup):
229
225
 
230
226
  _max_tasks: int | None
231
227
  _semaphore: Semaphore | None
232
- _timeout: Delta | None
228
+ _timeout: Duration | None
233
229
  _error: MaybeType[BaseException]
234
230
  _debug: MaybeCallableBoolLike
235
231
  _stack: AsyncExitStack
@@ -240,7 +236,7 @@ class EnhancedTaskGroup(TaskGroup):
240
236
  self,
241
237
  *,
242
238
  max_tasks: int | None = None,
243
- timeout: Delta | None = None,
239
+ timeout: Duration | None = None,
244
240
  error: MaybeType[BaseException] = TimeoutError,
245
241
  debug: MaybeCallableBoolLike = False,
246
242
  ) -> None:
@@ -342,7 +338,7 @@ class EnhancedTaskGroup(TaskGroup):
342
338
  return await coroutine
343
339
 
344
340
  async def _wrap_with_timeout[T](self, coroutine: _CoroutineLike[T], /) -> T:
345
- async with timeout_td(self._timeout, error=self._error):
341
+ async with timeout(self._timeout, error=self._error):
346
342
  return await coroutine
347
343
 
348
344
 
@@ -367,20 +363,6 @@ def chain_async[T](*iterables: Iterable[T] | AsyncIterable[T]) -> AsyncIterator[
367
363
  ##
368
364
 
369
365
 
370
- def get_coroutine_name(func: Callable[[], Coro[Any]], /) -> str:
371
- """Get the name of a coroutine, and then dispose of it gracefully."""
372
- coro = func()
373
- name = coro.__name__
374
- with suppress_warnings(
375
- message="coroutine '.*' was never awaited", category=RuntimeWarning
376
- ):
377
- del coro
378
- return name
379
-
380
-
381
- ##
382
-
383
-
384
366
  async def get_items[T](queue: Queue[T], /, *, max_size: int | None = None) -> list[T]:
385
367
  """Get items from a queue; if empty then wait."""
386
368
  try:
@@ -436,7 +418,7 @@ class OneAsyncError[T](Exception):
436
418
  class OneAsyncEmptyError[T](OneAsyncError[T]):
437
419
  @override
438
420
  def __str__(self) -> str:
439
- return f"Iterable(s) {get_repr(self.iterables)} must not be empty"
421
+ return f"Iterable(s) {repr_(self.iterables)} must not be empty"
440
422
 
441
423
 
442
424
  @dataclass(kw_only=True, slots=True)
@@ -446,7 +428,7 @@ class OneAsyncNonUniqueError[T](OneAsyncError):
446
428
 
447
429
  @override
448
430
  def __str__(self) -> str:
449
- return f"Iterable(s) {get_repr(self.iterables)} must contain exactly one item; got {self.first}, {self.second} and perhaps more"
431
+ return f"Iterable(s) {repr_(self.iterables)} must contain exactly one item; got {self.first}, {self.second} and perhaps more"
450
432
 
451
433
 
452
434
  ##
@@ -467,31 +449,29 @@ def put_items_nowait[T](items: Iterable[T], queue: Queue[T], /) -> None:
467
449
  ##
468
450
 
469
451
 
470
- async def sleep_max(
471
- sleep: Delta | None = None, /, *, random: Random = SYSTEM_RANDOM
472
- ) -> None:
473
- """Sleep which accepts deltas."""
474
- if sleep is None:
475
- return
476
- await asyncio.sleep(random.uniform(0.0, to_nanoseconds(sleep) / 1e9))
452
+ async def sleep(duration: Duration | None = None, /) -> None:
453
+ """Sleep which accepts durations."""
454
+ if duration is not None:
455
+ await asyncio.sleep(in_seconds(duration))
477
456
 
478
457
 
479
458
  ##
480
459
 
481
460
 
482
- async def sleep_rounded(delta: Delta, /) -> None:
483
- """Sleep until a rounded time."""
484
- await sleep_until(round_date_or_date_time(get_now(), delta, mode="ceil"))
461
+ async def sleep_max(
462
+ duration: Duration | None = None, /, *, random: Random = SYSTEM_RANDOM
463
+ ) -> None:
464
+ """Sleep up to a maximum duration."""
465
+ if duration is not None:
466
+ await sleep(random.uniform(0.0, in_seconds(duration)))
485
467
 
486
468
 
487
469
  ##
488
470
 
489
471
 
490
- async def sleep_td(delta: Delta | None = None, /) -> None:
491
- """Sleep which accepts deltas."""
492
- if delta is None:
493
- return
494
- await sleep(to_nanoseconds(delta) / 1e9)
472
+ async def sleep_rounded(delta: Delta, /) -> None:
473
+ """Sleep until a rounded time."""
474
+ await sleep_until(round_date_or_date_time(get_now(), delta, mode="ceil"))
495
475
 
496
476
 
497
477
  ##
@@ -499,7 +479,7 @@ async def sleep_td(delta: Delta | None = None, /) -> None:
499
479
 
500
480
  async def sleep_until(datetime: ZonedDateTime, /) -> None:
501
481
  """Sleep until a given time."""
502
- await sleep_td(datetime - get_now())
482
+ await sleep(datetime - get_now())
503
483
 
504
484
 
505
485
  ##
@@ -513,27 +493,21 @@ class StreamCommandOutput:
513
493
 
514
494
  @property
515
495
  def return_code(self) -> int:
516
- return ensure_int(self.process.returncode) # skipif-not-windows
496
+ return ensure_int(self.process.returncode)
517
497
 
518
498
 
519
499
  async def stream_command(cmd: str, /) -> StreamCommandOutput:
520
500
  """Run a shell command asynchronously and stream its output in real time."""
521
- process = await create_subprocess_shell( # skipif-not-windows
522
- cmd, stdout=PIPE, stderr=PIPE
523
- )
524
- proc_stdout = ensure_not_none( # skipif-not-windows
525
- process.stdout, desc="process.stdout"
526
- )
527
- proc_stderr = ensure_not_none( # skipif-not-windows
528
- process.stderr, desc="process.stderr"
529
- )
530
- ret_stdout = StringIO() # skipif-not-windows
531
- ret_stderr = StringIO() # skipif-not-windows
532
- async with TaskGroup() as tg: # skipif-not-windows
501
+ process = await create_subprocess_shell(cmd, stdout=PIPE, stderr=PIPE)
502
+ proc_stdout = ensure_not_none(process.stdout, desc="process.stdout")
503
+ proc_stderr = ensure_not_none(process.stderr, desc="process.stderr")
504
+ ret_stdout = StringIO()
505
+ ret_stderr = StringIO()
506
+ async with TaskGroup() as tg:
533
507
  _ = tg.create_task(_stream_one(proc_stdout, stdout, ret_stdout))
534
508
  _ = tg.create_task(_stream_one(proc_stderr, stderr, ret_stderr))
535
- _ = await process.wait() # skipif-not-windows
536
- return StreamCommandOutput( # skipif-not-windows
509
+ _ = await process.wait()
510
+ return StreamCommandOutput(
537
511
  process=process, stdout=ret_stdout.getvalue(), stderr=ret_stderr.getvalue()
538
512
  )
539
513
 
@@ -542,7 +516,7 @@ async def _stream_one(
542
516
  input_: StreamReader, out_stream: TextIO, ret_stream: StringIO, /
543
517
  ) -> None:
544
518
  """Asynchronously read from a stream and write to the target output stream."""
545
- while True: # skipif-not-windows
519
+ while True:
546
520
  line = await input_.readline()
547
521
  if not line:
548
522
  break
@@ -556,16 +530,21 @@ async def _stream_one(
556
530
 
557
531
 
558
532
  @asynccontextmanager
559
- async def timeout_td(
560
- timeout: Delta | None = None, /, *, error: MaybeType[BaseException] = TimeoutError
533
+ async def timeout(
534
+ duration: Duration | None = None,
535
+ /,
536
+ *,
537
+ error: MaybeType[BaseException] = TimeoutError,
561
538
  ) -> AsyncIterator[None]:
562
- """Timeout context manager which accepts deltas."""
563
- timeout_use = None if timeout is None else (to_nanoseconds(timeout) / 1e9)
564
- try:
565
- async with asyncio.timeout(timeout_use):
566
- yield
567
- except TimeoutError:
568
- raise error from None
539
+ """Timeout context manager which accepts durations."""
540
+ if duration is None:
541
+ yield
542
+ else:
543
+ try:
544
+ async with asyncio.timeout(in_seconds(duration)):
545
+ yield
546
+ except TimeoutError:
547
+ raise error from None
569
548
 
570
549
 
571
550
  ##
@@ -605,17 +584,16 @@ __all__ = [
605
584
  "OneAsyncNonUniqueError",
606
585
  "StreamCommandOutput",
607
586
  "chain_async",
608
- "get_coroutine_name",
609
587
  "get_items",
610
588
  "get_items_nowait",
611
589
  "one_async",
612
590
  "put_items",
613
591
  "put_items_nowait",
592
+ "sleep",
614
593
  "sleep_max",
615
594
  "sleep_rounded",
616
- "sleep_td",
617
595
  "sleep_until",
618
596
  "stream_command",
619
- "timeout_td",
597
+ "timeout",
620
598
  "yield_locked_shelf",
621
599
  ]
utilities/atools.py CHANGED
@@ -6,21 +6,21 @@ from pathlib import Path
6
6
  from typing import TYPE_CHECKING, Any, cast, overload
7
7
 
8
8
  import atools
9
- from whenever import TimeDelta
10
9
 
11
- from utilities.types import Coro, PathLike
10
+ from utilities.functions import in_seconds
11
+ from utilities.types import Coro, Duration, PathLike
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from atools._memoize_decorator import Keygen, Pickler
15
15
 
16
16
 
17
- type _Key[**P, T] = tuple[Callable[P, Coro[T]], TimeDelta]
17
+ type _Key[**P, T] = tuple[Callable[P, Coro[T]], Duration]
18
18
  _MEMOIZED_FUNCS: dict[_Key, Callable[..., Coro[Any]]] = {}
19
19
 
20
20
 
21
21
  async def call_memoized[**P, T](
22
22
  func: Callable[P, Coro[T]],
23
- refresh: TimeDelta | None = None,
23
+ refresh: Duration | None = None,
24
24
  /,
25
25
  *args: P.args,
26
26
  **kwargs: P.kwargs,
@@ -33,7 +33,7 @@ async def call_memoized[**P, T](
33
33
  try:
34
34
  memoized_func = _MEMOIZED_FUNCS[key]
35
35
  except KeyError:
36
- memoized_func = _MEMOIZED_FUNCS[(key)] = memoize(duration=refresh.in_seconds())(
36
+ memoized_func = _MEMOIZED_FUNCS[(key)] = memoize(duration=in_seconds(refresh))(
37
37
  func
38
38
  )
39
39
  return await memoized_func(*args, **kwargs)
@@ -48,7 +48,7 @@ def memoize[F: Callable[..., Coro[Any]]](
48
48
  /,
49
49
  *,
50
50
  db_path: PathLike | None = None,
51
- duration: float | TimeDelta | None = None,
51
+ duration: Duration | None = None,
52
52
  keygen: Keygen | None = None,
53
53
  pickler: Pickler | None = None,
54
54
  size: int | None = None,
@@ -59,7 +59,7 @@ def memoize[F: Callable[..., Coro[Any]]](
59
59
  /,
60
60
  *,
61
61
  db_path: PathLike | None = None,
62
- duration: float | TimeDelta | None = None,
62
+ duration: Duration | None = None,
63
63
  keygen: Keygen | None = None,
64
64
  pickler: Pickler | None = None,
65
65
  size: int | None = None,
@@ -69,7 +69,7 @@ def memoize[F: Callable[..., Coro[Any]]](
69
69
  /,
70
70
  *,
71
71
  db_path: PathLike | None = None,
72
- duration: float | TimeDelta | None = None,
72
+ duration: Duration | None = None,
73
73
  keygen: Keygen | None = None,
74
74
  pickler: Pickler | None = None,
75
75
  size: int | None = None,
@@ -87,9 +87,7 @@ def memoize[F: Callable[..., Coro[Any]]](
87
87
  return cast("Callable[[F], F]", result)
88
88
  return atools.memoize(
89
89
  db_path=None if db_path is None else Path(db_path),
90
- duration=duration.py_timedelta()
91
- if isinstance(duration, TimeDelta)
92
- else duration,
90
+ duration=None if duration is None else in_seconds(duration),
93
91
  keygen=keygen,
94
92
  pickler=pickler,
95
93
  size=size,
utilities/cachetools.py CHANGED
@@ -3,13 +3,15 @@ from __future__ import annotations
3
3
  from collections.abc import Callable, Hashable, Iterable, Iterator, MutableSet
4
4
  from math import inf
5
5
  from time import monotonic
6
- from typing import TYPE_CHECKING, Any, override
6
+ from typing import TYPE_CHECKING, Any, cast, override
7
7
 
8
8
  import cachetools
9
9
  from cachetools.func import ttl_cache
10
10
 
11
+ from utilities.functions import in_seconds
12
+
11
13
  if TYPE_CHECKING:
12
- from whenever import TimeDelta
14
+ from utilities.types import Duration
13
15
 
14
16
 
15
17
  class TTLCache[K: Hashable, V](cachetools.TTLCache[K, V]):
@@ -19,13 +21,13 @@ class TTLCache[K: Hashable, V](cachetools.TTLCache[K, V]):
19
21
  self,
20
22
  *,
21
23
  max_size: int | None = None,
22
- max_duration: TimeDelta | None = None,
24
+ max_duration: Duration | None = None,
23
25
  timer: Callable[[], float] = monotonic,
24
26
  get_size_of: Callable[[Any], int] | None = None,
25
27
  ) -> None:
26
28
  super().__init__(
27
29
  maxsize=inf if max_size is None else max_size,
28
- ttl=inf if max_duration is None else max_duration.in_seconds(),
30
+ ttl=inf if max_duration is None else in_seconds(max_duration),
29
31
  timer=timer,
30
32
  getsizeof=get_size_of,
31
33
  )
@@ -46,7 +48,7 @@ class TTLSet[T: Hashable](MutableSet[T]):
46
48
  /,
47
49
  *,
48
50
  max_size: int | None = None,
49
- max_duration: TimeDelta | None = None,
51
+ max_duration: Duration | None = None,
50
52
  timer: Callable[[], float] = monotonic,
51
53
  get_size_of: Callable[[Any], int] | None = None,
52
54
  ) -> None:
@@ -95,16 +97,19 @@ class TTLSet[T: Hashable](MutableSet[T]):
95
97
  def cache[F: Callable](
96
98
  *,
97
99
  max_size: int | None = None,
98
- max_duration: TimeDelta | None = None,
100
+ max_duration: Duration | None = None,
99
101
  timer: Callable[[], float] = monotonic,
100
102
  typed_: bool = False,
101
103
  ) -> Callable[[F], F]:
102
104
  """Decorate a function with `max_size` and/or `ttl` settings."""
103
- return ttl_cache(
104
- maxsize=inf if max_size is None else max_size,
105
- ttl=inf if max_duration is None else max_duration.in_seconds(),
106
- timer=timer,
107
- typed=typed_,
105
+ return cast(
106
+ "F",
107
+ ttl_cache(
108
+ maxsize=max_size,
109
+ ttl=inf if max_duration is None else in_seconds(max_duration),
110
+ timer=timer,
111
+ typed=typed_,
112
+ ),
108
113
  )
109
114
 
110
115