dycw-utilities 0.148.5__py3-none-any.whl → 0.175.31__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.

Potentially problematic release.


This version of dycw-utilities might be problematic. Click here for more details.

Files changed (84) hide show
  1. dycw_utilities-0.175.31.dist-info/METADATA +34 -0
  2. dycw_utilities-0.175.31.dist-info/RECORD +103 -0
  3. dycw_utilities-0.175.31.dist-info/WHEEL +4 -0
  4. {dycw_utilities-0.148.5.dist-info → dycw_utilities-0.175.31.dist-info}/entry_points.txt +1 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +10 -7
  7. utilities/asyncio.py +113 -64
  8. utilities/atomicwrites.py +1 -1
  9. utilities/atools.py +64 -4
  10. utilities/cachetools.py +9 -6
  11. utilities/click.py +144 -49
  12. utilities/concurrent.py +1 -1
  13. utilities/contextlib.py +4 -2
  14. utilities/contextvars.py +20 -1
  15. utilities/cryptography.py +3 -3
  16. utilities/dataclasses.py +15 -28
  17. utilities/docker.py +381 -0
  18. utilities/enum.py +2 -2
  19. utilities/errors.py +1 -1
  20. utilities/fastapi.py +8 -3
  21. utilities/fpdf2.py +2 -2
  22. utilities/functions.py +20 -297
  23. utilities/git.py +19 -0
  24. utilities/grp.py +28 -0
  25. utilities/hypothesis.py +361 -79
  26. utilities/importlib.py +17 -1
  27. utilities/inflect.py +1 -1
  28. utilities/iterables.py +12 -58
  29. utilities/jinja2.py +148 -0
  30. utilities/json.py +1 -1
  31. utilities/libcst.py +7 -7
  32. utilities/logging.py +74 -85
  33. utilities/math.py +8 -4
  34. utilities/more_itertools.py +4 -6
  35. utilities/operator.py +1 -1
  36. utilities/orjson.py +86 -34
  37. utilities/os.py +49 -2
  38. utilities/parse.py +2 -2
  39. utilities/pathlib.py +66 -34
  40. utilities/permissions.py +298 -0
  41. utilities/platform.py +4 -4
  42. utilities/polars.py +934 -420
  43. utilities/polars_ols.py +1 -1
  44. utilities/postgres.py +296 -174
  45. utilities/pottery.py +8 -73
  46. utilities/pqdm.py +3 -3
  47. utilities/pwd.py +28 -0
  48. utilities/pydantic.py +11 -0
  49. utilities/pydantic_settings.py +240 -0
  50. utilities/pydantic_settings_sops.py +76 -0
  51. utilities/pyinstrument.py +5 -5
  52. utilities/pytest.py +155 -46
  53. utilities/pytest_plugins/pytest_randomly.py +1 -1
  54. utilities/pytest_plugins/pytest_regressions.py +7 -3
  55. utilities/pytest_regressions.py +27 -8
  56. utilities/random.py +11 -6
  57. utilities/re.py +1 -1
  58. utilities/redis.py +101 -64
  59. utilities/sentinel.py +10 -0
  60. utilities/shelve.py +4 -1
  61. utilities/shutil.py +25 -0
  62. utilities/slack_sdk.py +8 -3
  63. utilities/sqlalchemy.py +422 -352
  64. utilities/sqlalchemy_polars.py +28 -52
  65. utilities/string.py +1 -1
  66. utilities/subprocess.py +1947 -0
  67. utilities/tempfile.py +95 -4
  68. utilities/testbook.py +50 -0
  69. utilities/text.py +165 -42
  70. utilities/timer.py +2 -2
  71. utilities/traceback.py +46 -36
  72. utilities/types.py +62 -23
  73. utilities/typing.py +479 -19
  74. utilities/uuid.py +42 -5
  75. utilities/version.py +27 -26
  76. utilities/whenever.py +661 -151
  77. utilities/zoneinfo.py +80 -22
  78. dycw_utilities-0.148.5.dist-info/METADATA +0 -41
  79. dycw_utilities-0.148.5.dist-info/RECORD +0 -95
  80. dycw_utilities-0.148.5.dist-info/WHEEL +0 -4
  81. dycw_utilities-0.148.5.dist-info/licenses/LICENSE +0 -21
  82. utilities/eventkit.py +0 -388
  83. utilities/period.py +0 -237
  84. utilities/typed_settings.py +0 -144
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.3
2
+ Name: dycw-utilities
3
+ Version: 0.175.31
4
+ Summary: Miscellaneous Python utilities
5
+ Author: Derek Wan
6
+ Author-email: Derek Wan <d.wan@icloud.com>
7
+ Requires-Dist: atomicwrites>=1.4.1,<1.5
8
+ Requires-Dist: typing-extensions>=4.15.0,<4.16
9
+ Requires-Dist: tzlocal>=5.3.1,<5.4
10
+ Requires-Dist: whenever>=0.9.4,<0.10
11
+ Requires-Dist: coloredlogs>=15.0.1,<15.1 ; extra == 'logging'
12
+ Requires-Dist: dycw-pytest-only>=2.1.1,<2.2 ; extra == 'test'
13
+ Requires-Dist: hypothesis>=6.148.8,<6.149 ; extra == 'test'
14
+ Requires-Dist: pytest>=9.0.2,<9.1 ; extra == 'test'
15
+ Requires-Dist: pytest-asyncio>=1.3.0,<1.4 ; extra == 'test'
16
+ Requires-Dist: pytest-cov>=7.0.0,<7.1 ; extra == 'test'
17
+ Requires-Dist: pytest-instafail>=0.5.0,<0.6 ; extra == 'test'
18
+ Requires-Dist: pytest-lazy-fixtures>=1.4.0,<1.5 ; extra == 'test'
19
+ Requires-Dist: pytest-randomly>=4.0.1,<4.1 ; extra == 'test'
20
+ Requires-Dist: pytest-regressions>=2.8.3,<2.9 ; extra == 'test'
21
+ Requires-Dist: pytest-repeat>=0.9.4,<0.10 ; extra == 'test'
22
+ Requires-Dist: pytest-rerunfailures>=16.1,<16.2 ; extra == 'test'
23
+ Requires-Dist: pytest-rng>=1.0.0,<1.1 ; extra == 'test'
24
+ Requires-Dist: pytest-timeout>=2.4.0,<2.5 ; extra == 'test'
25
+ Requires-Dist: pytest-xdist>=3.8.0,<3.9 ; extra == 'test'
26
+ Requires-Dist: testbook>=0.4.2,<0.5 ; extra == 'test'
27
+ Requires-Python: >=3.12
28
+ Provides-Extra: logging
29
+ Provides-Extra: test
30
+ Description-Content-Type: text/markdown
31
+
32
+ # `python-utilities`
33
+
34
+ Miscellaneous Python utilities
@@ -0,0 +1,103 @@
1
+ utilities/__init__.py,sha256=y-0IecU_rQ9B_0aVstJ6gMlcyWn2-Hw_v-RcSXW8pWY,61
2
+ utilities/altair.py,sha256=TLfRFbG9HwG7SLXoJ-v0r-t49ZaGgTQZD82cpjVi4vs,9085
3
+ utilities/asyncio.py,sha256=aJySVxBY0gqsIYnoNmH7-1r8djKuf4vSsU69VCD08t8,16772
4
+ utilities/atomicwrites.py,sha256=tPo6r-Rypd9u99u66B9z86YBPpnLrlHtwox_8Z7T34Y,5790
5
+ utilities/atools.py,sha256=6neeCcgXxK2dlsc0xp15Za7nSucbCgFtAJepGI_-WXU,2549
6
+ utilities/cachetools.py,sha256=2S9LMHIunDYMIu8JGI7OLN04sQ7-xZGdEdP1Li0vksA,2775
7
+ utilities/click.py,sha256=ScLzBLoBp8Si5YjgB18A0IVMAR-r4sGUnVfJbAaru98,19191
8
+ utilities/concurrent.py,sha256=fHeW2SZ_TEMfFY0C8pyQI6aPlnecvx9x6SuUwBWj_JY,2853
9
+ utilities/contextlib.py,sha256=iP7R2tIm6ZsbfLD5ks6UKBYwj50e9gBI8AkpLN-chro,7476
10
+ utilities/contextvars.py,sha256=J8OhC7jqozAGYOCe2KUWysbPXNGe5JYz3HfaY_mIs08,883
11
+ utilities/cryptography.py,sha256=5PFrzsNUGHay91dFgYnDKwYprXxahrBqztmUqViRzBk,956
12
+ utilities/cvxpy.py,sha256=Rv1-fD-XYerosCavRF8Pohop2DBkU3AlFaGTfD8AEAA,13776
13
+ utilities/dataclasses.py,sha256=xbU3QN1GFy7RC6hIJRZIeUZm7YRlodrgEWmahWG6k2g,32465
14
+ utilities/docker.py,sha256=IOOdxAqfJfy1SXwy3e4VaAR9ag42W0HTcNcQz7DJPF8,10501
15
+ utilities/enum.py,sha256=5l6pwZD1cjSlVW4ss-zBPspWvrbrYrdtJWcg6f5_J5w,5781
16
+ utilities/errors.py,sha256=mFlDGSM0LI1jZ1pbqwLAH3ttLZ2JVIxyZLojw8tGVZU,1479
17
+ utilities/fastapi.py,sha256=TqyKvBjiMS594sXPjrz-KRTLMb3l3D3rZ1zAYV7GfOk,1454
18
+ utilities/fpdf2.py,sha256=dSiYz0FJTD2sQuxpxqFWwwIe2-p6Y7oTB9Tv0Jajit0,1866
19
+ utilities/functions.py,sha256=18Zda7nTloARdcEudH8YJ4e13xAdWShAGhPNN4w2Gyc,21498
20
+ utilities/functools.py,sha256=I00ru2gQPakZw2SHVeKIKXfTv741655s6HI0lUoE0D4,1552
21
+ utilities/getpass.py,sha256=DfN5UgMAtFCqS3dSfFHUfqIMZX2shXvwphOz_6J6f6A,103
22
+ utilities/git.py,sha256=U1RFvCTANGENgx9wVBDvllioqBQZM2ns12ivKhOsaO4,414
23
+ utilities/grp.py,sha256=1vV3gNR9dQsl1vtUtvC_2qgVdQzm7O8lLMSh56cTbeg,694
24
+ utilities/gzip.py,sha256=fkGP3KdsBfXlstodT4wtlp-PwNyUsogpbDCVVVGdsm4,781
25
+ utilities/hashlib.py,sha256=SVTgtguur0P4elppvzOBbLEjVM3Pea0eWB61yg2ilxo,309
26
+ utilities/http.py,sha256=TsavEfHlRtlLaeV21Z6KZh0qbPw-kvD1zsQdZ7Kep5Q,977
27
+ utilities/hypothesis.py,sha256=NUu30pl5kjL3tzo-m8SMRwTqLAmTWK-_Sau2NemJcQo,46773
28
+ utilities/importlib.py,sha256=SkVVtIjVC7bjJ36doXnmnmFiYe5tLbip4YAfYJj8Ycg,892
29
+ utilities/inflect.py,sha256=v7YkOWSu8NAmVghPcf4F3YBZQoJCS47_DLf9jbfWIs0,581
30
+ utilities/ipython.py,sha256=V2oMYHvEKvlNBzxDXdLvKi48oUq2SclRg5xasjaXStw,763
31
+ utilities/iterables.py,sha256=t2TsW-K3rVlS6y4_tqcc1fk9RwJV-bi7G_VwduMABK0,42558
32
+ utilities/jinja2.py,sha256=JpNHMcyMJDguX8rBN4wEz-v4En7w6WHXvYJr4Xw-F0o,4691
33
+ utilities/json.py,sha256=-WcGtSsCr9Y42wHZzAMnfvU6ihAfVftylFfRUORaDFo,2102
34
+ utilities/jupyter.py,sha256=ft5JA7fBxXKzP-L9W8f2-wbF0QeYc_2uLQNFDVk4Z-M,2917
35
+ utilities/libcst.py,sha256=ngD4wxnR3Kh-RBVmU5l5ST7cuZLhMZwyMDjHZe5mhTs,5581
36
+ utilities/lightweight_charts.py,sha256=YM3ojBvJxuCSUBu_KrhFBmaMCvRPvupKC3qkm-UVZq4,2751
37
+ utilities/logging.py,sha256=mckcX_0Q5njVgnhtIoePcDHjYjTVELw7ExutiiDnzsU,18724
38
+ utilities/math.py,sha256=cevB-YyEYAzJTWtkAr7qeeu-hbxorDI3gMznXlmNQkw,26897
39
+ utilities/memory_profiler.py,sha256=XzN56jDCa5aqXS_DxEjb_K4L6aIWh_5zyKi6OhcIxw0,853
40
+ utilities/modules.py,sha256=iuvLluJya-hvl1Q25-Jk3dLgx2Es3ck4SjJiEkAlVTs,3195
41
+ utilities/more_itertools.py,sha256=syfIPhQF_WS-YiicdGe2h5F1G-Ld12Q2XsVduL2hA40,10908
42
+ utilities/numpy.py,sha256=Xn23sA2ZbVNqwUYEgNJD3XBYH6IbCri_WkHSNhg3NkY,26122
43
+ utilities/operator.py,sha256=C3NylZWGTVWRpwYHOPVhaLgRhw0DfpS4_XQ8KfPhBLQ,3613
44
+ utilities/optuna.py,sha256=C-fhWYiXHVPo1l8QctYkFJ4DyhbSrGorzP1dJb_qvd8,1933
45
+ utilities/orjson.py,sha256=T_0SlK811ysg46d3orvIPY3JpBa4FRMpP2wlPQo7-gU,41854
46
+ utilities/os.py,sha256=kjKKSQfnRqFTTZ315iavaaGd3gGuYNoSWlxVLCJjyQs,4852
47
+ utilities/parse.py,sha256=g7Qm9eBOIeDId2tGA021CIaeF6jp1TI8rx4srdvlyoo,17937
48
+ utilities/pathlib.py,sha256=N4Ip8R9eCM-6GfvxUJ3T9oQIle2C2P52F-13BCFRdTg,9345
49
+ utilities/permissions.py,sha256=vLXlWztSVYffbrxptne7ksj6dU1HLekm4fEvS4ny_4Q,8944
50
+ utilities/pickle.py,sha256=MBT2xZCsv0pH868IXLGKnlcqNx2IRVKYNpRcqiQQqxw,653
51
+ utilities/platform.py,sha256=R3ldt2-DlI7la9ng6Rxt1CThd2lL0Ai2tC0TbabtCC0,2800
52
+ utilities/polars.py,sha256=JPzN4UqQDC7R4IXsIuXEIXRiwHSrkiSZcD8UOfwGPuE,87535
53
+ utilities/polars_ols.py,sha256=LNTFNLPuYW7fcAHymlbnams_DhitToblYvib3mhKbwI,5615
54
+ utilities/postgres.py,sha256=g3tEwTI8TdmiCbRME61ffQ0xaibdpXPu8mJOOHvjPKc,12532
55
+ utilities/pottery.py,sha256=nA0SsF9irvfC0tk68YAr08tuL9lGRSlBKihSx7Ibk84,3963
56
+ utilities/pqdm.py,sha256=idv2seRVP2f6NeSfpeEnT5A-tQezaHZKDyeu16g2-0E,3091
57
+ utilities/psutil.py,sha256=KUlu4lrUw9Zg1V7ZGetpWpGb9DB8l_SSDWGbANFNCPU,2104
58
+ utilities/pwd.py,sha256=pzlLkBfSTZsCVkMrAxEmuEoNmm-6goklfAygVmOEZqs,707
59
+ utilities/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
+ utilities/pydantic.py,sha256=z_PnYXN_UNNLQwo_XF7cr4GDcf5wicvscD6aFYVOEvA,238
61
+ utilities/pydantic_settings.py,sha256=53tQTpHFtA6UIuzHKzauZtW_bSRwL5lQnNwTWeO4Fjw,7608
62
+ utilities/pydantic_settings_sops.py,sha256=9Ou6Cx6PiYOU49vtkKqqW1Sdp_i3WlVyg8KkUUKNliM,2310
63
+ utilities/pyinstrument.py,sha256=hnXaL-4HE7wWBI5JKaPfYTpsrXe68YiuZKahHV0VJn8,841
64
+ utilities/pytest.py,sha256=9HHwYgZQe6CRF0ekHQEFH05gmoP4Ne0V54RrtUNDfi4,10524
65
+ utilities/pytest_plugins/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
66
+ utilities/pytest_plugins/pytest_randomly.py,sha256=B1qYVlExGOxTywq2r1SMi5o7btHLk2PNdY_b1p98dkE,409
67
+ utilities/pytest_plugins/pytest_regressions.py,sha256=mnHYBfdprz50UGVkVzV1bZERZN5CFfoF8YbokGxdFwU,1639
68
+ utilities/pytest_regressions.py,sha256=tJxW38u-zpoyjW1N4zogBx4V_07r-ibDInddcEUyXmc,4763
69
+ utilities/random.py,sha256=hZlH4gnAtoaofWswuJYjcygejrY8db4CzP-z_adO2Mo,4165
70
+ utilities/re.py,sha256=S4h-DLL6ScMPqjboZ_uQ1BVTJajrqV06r_81D--_HCE,4573
71
+ utilities/redis.py,sha256=gybjqKea33Jy50n4dHTS14JdquqHaJqHF2dixQljYWQ,30172
72
+ utilities/reprlib.py,sha256=ssYTcBW-TeRh3fhCJv57sopTZHF5FrPyyUg9yp5XBlo,3953
73
+ utilities/scipy.py,sha256=wZJM7fEgBAkLSYYvSmsg5ac-QuwAI0BGqHVetw1_Hb0,947
74
+ utilities/sentinel.py,sha256=A_p5jX2K0Yc5XBfoYHyBLqHsEWzE1ByOdDuzzA2pZnE,1434
75
+ utilities/shelve.py,sha256=4OzjQI6kGuUbJciqf535rwnao-_IBv66gsT6tRGiUt0,759
76
+ utilities/shutil.py,sha256=knJ7hx42FtIfGByxQZMcOSgQCDlaSony325g505ps3A,480
77
+ utilities/slack_sdk.py,sha256=76-DYtcGiUhEvl-voMamc5OjfF7Y7nCq54Bys1arqzw,2233
78
+ utilities/socket.py,sha256=K77vfREvzoVTrpYKo6MZakol0EYu2q1sWJnnZqL0So0,118
79
+ utilities/sqlalchemy.py,sha256=HQYpd7LFxdTF5WYVWYtCJeEBI71EJm7ytvCGyAH9B-U,37163
80
+ utilities/sqlalchemy_polars.py,sha256=JCGhB37raSR7fqeWV5dTsciRTMVzIdVT9YSqKT0piT0,13370
81
+ utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
82
+ utilities/string.py,sha256=shmBK87zZwzGyixuNuXCiUbqzfeZ9xlrFwz6JTaRvDk,582
83
+ utilities/subprocess.py,sha256=ei0OD73S0WX7ZqHCQp9UB3hY9OWbAUVIrLoz9niIfQQ,53031
84
+ utilities/tempfile.py,sha256=a3_M1QyxGZql_VcGkBOQBeWbbkItjgkfIpVyzU1UAic,3843
85
+ utilities/testbook.py,sha256=j1KmaVbrX9VrbeMgtPh5gk55myAsn3dyRUn7jGbPbRk,1294
86
+ utilities/text.py,sha256=7SvwcSR2l_5cOrm1samGnR4C-ZI6qyFLHLzSpO1zeHQ,13958
87
+ utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
88
+ utilities/timer.py,sha256=BGlwEVznx67scuLOUohyWJ4d5rTnwtk-IR4yLXFiNfo,2574
89
+ utilities/traceback.py,sha256=B_sc0TRUv-mGDnF-ek05nbqjmBiHr3-wvxliAqIF5hI,9608
90
+ utilities/types.py,sha256=UwxYajRnupKTCnzReaYWYqtKdpIPeSO-d97Wu4bLEq8,18878
91
+ utilities/typing.py,sha256=xuR8LxzjD-XlSftTM3TNvGVdQyV1mzpdwWdDMzWwCPE,25310
92
+ utilities/tzdata.py,sha256=fgNVj66yUbCSI_-vrRVzSD3gtf-L_8IEJEPjP_Jel5Y,266
93
+ utilities/tzlocal.py,sha256=KyCXEgCTjqGFx-389JdTuhMRUaT06U1RCMdWoED-qro,728
94
+ utilities/uuid.py,sha256=nQZs6tFX4mqtc2Ku3KqjloYCqwpTKeTj8eKwQwh3FQI,1572
95
+ utilities/version.py,sha256=ipBj5-WYY_nelp2uwFlApfWWCzTLzPwpovUi9x_OBMs,5085
96
+ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
97
+ utilities/whenever.py,sha256=F4ek0-OBWxHYrZdmoZt76N2RnNyKY5KrEHt7rqO4AQE,60183
98
+ utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
99
+ utilities/zoneinfo.py,sha256=tdIScrTB2-B-LH0ukb1HUXKooLknOfJNwHk10MuMYvA,3619
100
+ dycw_utilities-0.175.31.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
101
+ dycw_utilities-0.175.31.dist-info/entry_points.txt,sha256=cOGtKeJI0KXLSV7MJ8Dhc2G8jPgDcBDm53MVNJU4ycI,136
102
+ dycw_utilities-0.175.31.dist-info/METADATA,sha256=WX68a6sQoOamTeIZsrXvDdI_VKItf-Jm3Cam7p1jR_U,1443
103
+ dycw_utilities-0.175.31.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.21
3
+ Root-Is-Purelib: true
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.148.5"
3
+ __version__ = "0.175.31"
utilities/altair.py CHANGED
@@ -29,8 +29,6 @@ from utilities.iterables import always_iterable
29
29
  from utilities.tempfile import TemporaryDirectory
30
30
 
31
31
  if TYPE_CHECKING:
32
- from collections.abc import Sequence
33
-
34
32
  from polars import DataFrame
35
33
 
36
34
  from utilities.types import PathLike
@@ -43,7 +41,7 @@ _WIDTH = 800
43
41
 
44
42
  @dataclass(kw_only=True, slots=True)
45
43
  class _PlotDataFramesSpec:
46
- y: Sequence[str]
44
+ y: list[str]
47
45
  height: int = _HEIGHT
48
46
 
49
47
 
@@ -96,7 +94,8 @@ def plot_dataframes(
96
94
  # lines
97
95
  selection = selection_point(bind="legend", fields=[var_name], nearest=False)
98
96
  lines = [
99
- chart.mark_line(interpolate=interpolate)
97
+ chart
98
+ .mark_line(interpolate=interpolate)
100
99
  .encode(
101
100
  x=x_use,
102
101
  y=Y(value_name).scale(zero=False),
@@ -126,7 +125,8 @@ def plot_dataframes(
126
125
  else:
127
126
  tooltip_format_use = Undefined
128
127
  rules = [
129
- chart.transform_pivot(var_name, value=value_name, groupby=[x_use])
128
+ chart
129
+ .transform_pivot(var_name, value=value_name, groupby=[x_use])
130
130
  .mark_rule(color="gray")
131
131
  .encode(
132
132
  x=x_use,
@@ -145,7 +145,9 @@ def plot_dataframes(
145
145
  ]
146
146
  zoom = selection_interval(bind="scales", encodings=["x"])
147
147
  chart = (
148
- vconcat(*layers).add_params(zoom).resolve_scale(color="independent", x="shared")
148
+ vconcat_charts(*layers)
149
+ .add_params(zoom)
150
+ .resolve_scale(color="independent", x="shared")
149
151
  )
150
152
  if title is not None:
151
153
  chart = chart.properties(title=title)
@@ -229,7 +231,8 @@ def plot_intraday_dataframe(
229
231
  )
230
232
 
231
233
  data4 = (
232
- data3.group_by("_date_index")
234
+ data3
235
+ .group_by("_date_index")
233
236
  .agg(
234
237
  col(f"_{datetime}_index").min().alias(f"{datetime}_index_min"),
235
238
  (col(f"_{datetime}_index").max() + 1).alias(f"{datetime}_index_max"),
utilities/asyncio.py CHANGED
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import sys
5
4
  from asyncio import (
6
5
  Lock,
7
6
  Queue,
@@ -22,6 +21,7 @@ from contextlib import (
22
21
  )
23
22
  from dataclasses import dataclass
24
23
  from io import StringIO
24
+ from pathlib import Path
25
25
  from subprocess import PIPE
26
26
  from sys import stderr, stdout
27
27
  from typing import (
@@ -31,15 +31,18 @@ from typing import (
31
31
  Self,
32
32
  TextIO,
33
33
  assert_never,
34
+ cast,
34
35
  overload,
35
36
  override,
36
37
  )
37
38
 
38
- from utilities.errors import ImpossibleCaseError, is_instance_error
39
- from utilities.functions import ensure_int, ensure_not_none, to_bool
40
- from utilities.logging import get_logger
39
+ from utilities.functions import ensure_int, ensure_not_none
40
+ from utilities.os import is_pytest
41
41
  from utilities.random import SYSTEM_RANDOM
42
+ from utilities.reprlib import get_repr
42
43
  from utilities.sentinel import Sentinel, sentinel
44
+ from utilities.shelve import yield_shelf
45
+ from utilities.text import to_bool
43
46
  from utilities.warnings import suppress_warnings
44
47
  from utilities.whenever import get_now, round_date_or_date_time, to_nanoseconds
45
48
 
@@ -47,6 +50,7 @@ if TYPE_CHECKING:
47
50
  from asyncio import _CoroutineLike
48
51
  from asyncio.subprocess import Process
49
52
  from collections.abc import (
53
+ AsyncIterable,
50
54
  AsyncIterator,
51
55
  Callable,
52
56
  ItemsView,
@@ -58,17 +62,18 @@ if TYPE_CHECKING:
58
62
  )
59
63
  from contextvars import Context
60
64
  from random import Random
65
+ from shelve import Shelf
61
66
  from types import TracebackType
62
67
 
63
68
  from whenever import ZonedDateTime
64
69
 
70
+ from utilities.shelve import _Flag
65
71
  from utilities.types import (
66
72
  Coro,
67
73
  Delta,
68
- ExceptionTypeLike,
69
- LoggerOrName,
70
- MaybeCallableBool,
74
+ MaybeCallableBoolLike,
71
75
  MaybeType,
76
+ PathLike,
72
77
  SupportsKeysAndGetItem,
73
78
  )
74
79
 
@@ -226,7 +231,7 @@ class EnhancedTaskGroup(TaskGroup):
226
231
  _semaphore: Semaphore | None
227
232
  _timeout: Delta | None
228
233
  _error: MaybeType[BaseException]
229
- _debug: MaybeCallableBool
234
+ _debug: MaybeCallableBoolLike
230
235
  _stack: AsyncExitStack
231
236
  _timeout_cm: _AsyncGeneratorContextManager[None] | None
232
237
 
@@ -237,7 +242,7 @@ class EnhancedTaskGroup(TaskGroup):
237
242
  max_tasks: int | None = None,
238
243
  timeout: Delta | None = None,
239
244
  error: MaybeType[BaseException] = TimeoutError,
240
- debug: MaybeCallableBool = False,
245
+ debug: MaybeCallableBoolLike = False,
241
246
  ) -> None:
242
247
  super().__init__()
243
248
  self._max_tasks = max_tasks
@@ -270,7 +275,7 @@ class EnhancedTaskGroup(TaskGroup):
270
275
  _ = await super().__aexit__(et, exc, tb)
271
276
  case False:
272
277
  _ = await super().__aexit__(et, exc, tb)
273
- case _ as never:
278
+ case never:
274
279
  assert_never(never)
275
280
 
276
281
  @override
@@ -307,7 +312,7 @@ class EnhancedTaskGroup(TaskGroup):
307
312
  self.create_task(make_coro(*args, **kwargs))
308
313
  for _ in range(self._max_tasks)
309
314
  ]
310
- case _ as never:
315
+ case never:
311
316
  assert_never(never)
312
317
 
313
318
  async def run_or_create_task[T](
@@ -322,11 +327,11 @@ class EnhancedTaskGroup(TaskGroup):
322
327
  return await coro
323
328
  case False:
324
329
  return self.create_task(coro, name=name, context=context)
325
- case _ as never:
330
+ case never:
326
331
  assert_never(never)
327
332
 
328
333
  def _is_debug(self) -> bool:
329
- return to_bool(bool_=self._debug) or (
334
+ return to_bool(self._debug) or (
330
335
  (self._max_tasks is not None) and (self._max_tasks <= 0)
331
336
  )
332
337
 
@@ -344,6 +349,24 @@ class EnhancedTaskGroup(TaskGroup):
344
349
  ##
345
350
 
346
351
 
352
+ def chain_async[T](*iterables: Iterable[T] | AsyncIterable[T]) -> AsyncIterator[T]:
353
+ """Asynchronous version of `chain`."""
354
+
355
+ async def iterator() -> AsyncIterator[T]:
356
+ for it in iterables:
357
+ try:
358
+ async for item in cast("AsyncIterable[T]", it):
359
+ yield item
360
+ except TypeError:
361
+ for item in cast("Iterable[T]", it):
362
+ yield item
363
+
364
+ return iterator()
365
+
366
+
367
+ ##
368
+
369
+
347
370
  def get_coroutine_name(func: Callable[[], Coro[Any]], /) -> str:
348
371
  """Get the name of a coroutine, and then dispose of it gracefully."""
349
372
  coro = func()
@@ -363,8 +386,6 @@ async def get_items[T](queue: Queue[T], /, *, max_size: int | None = None) -> li
363
386
  try:
364
387
  items = [await queue.get()]
365
388
  except RuntimeError as error: # pragma: no cover
366
- from utilities.pytest import is_pytest
367
-
368
389
  if (not is_pytest()) or (error.args[0] != "Event loop is closed"):
369
390
  raise
370
391
  return []
@@ -394,38 +415,38 @@ def get_items_nowait[T](queue: Queue[T], /, *, max_size: int | None = None) -> l
394
415
  ##
395
416
 
396
417
 
397
- async def loop_until_succeed(
398
- func: Callable[[], Coro[None]],
399
- /,
400
- *,
401
- logger: LoggerOrName | None = None,
402
- errors: ExceptionTypeLike[Exception] | None = None,
403
- sleep: Delta | None = None,
404
- ) -> bool:
405
- """Repeatedly call a coroutine until it succeeds."""
406
- name = get_coroutine_name(func)
407
- while True:
408
- try:
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)
413
- exc_type, exc_value, traceback = sys.exc_info()
414
- if (exc_type is None) or (exc_value is None): # pragma: no cover
415
- raise ImpossibleCaseError(
416
- case=[f"{exc_type=}", f"{exc_value=}"]
417
- ) from None
418
- sys.excepthook(exc_type, exc_value, traceback)
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
418
+ async def one_async[T](*iterables: Iterable[T] | AsyncIterable[T]) -> T:
419
+ """Asynchronous version of `one`."""
420
+ result: T | Sentinel = sentinel
421
+ async for item in chain_async(*iterables):
422
+ if not isinstance(result, Sentinel):
423
+ raise OneAsyncNonUniqueError(iterables=iterables, first=result, second=item)
424
+ result = item
425
+ if isinstance(result, Sentinel):
426
+ raise OneAsyncEmptyError(iterables=iterables)
427
+ return result
428
+
429
+
430
+ @dataclass(kw_only=True, slots=True)
431
+ class OneAsyncError[T](Exception):
432
+ iterables: tuple[Iterable[T] | AsyncIterable[T], ...]
433
+
434
+
435
+ @dataclass(kw_only=True, slots=True)
436
+ class OneAsyncEmptyError[T](OneAsyncError[T]):
437
+ @override
438
+ def __str__(self) -> str:
439
+ return f"Iterable(s) {get_repr(self.iterables)} must not be empty"
440
+
441
+
442
+ @dataclass(kw_only=True, slots=True)
443
+ class OneAsyncNonUniqueError[T](OneAsyncError):
444
+ first: T
445
+ second: T
446
+
447
+ @override
448
+ 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"
429
450
 
430
451
 
431
452
  ##
@@ -492,27 +513,21 @@ class StreamCommandOutput:
492
513
 
493
514
  @property
494
515
  def return_code(self) -> int:
495
- return ensure_int(self.process.returncode) # skipif-not-windows
516
+ return ensure_int(self.process.returncode)
496
517
 
497
518
 
498
519
  async def stream_command(cmd: str, /) -> StreamCommandOutput:
499
520
  """Run a shell command asynchronously and stream its output in real time."""
500
- process = await create_subprocess_shell( # skipif-not-windows
501
- cmd, stdout=PIPE, stderr=PIPE
502
- )
503
- proc_stdout = ensure_not_none( # skipif-not-windows
504
- process.stdout, desc="process.stdout"
505
- )
506
- proc_stderr = ensure_not_none( # skipif-not-windows
507
- process.stderr, desc="process.stderr"
508
- )
509
- ret_stdout = StringIO() # skipif-not-windows
510
- ret_stderr = StringIO() # skipif-not-windows
511
- async with TaskGroup() as tg: # skipif-not-windows
521
+ process = await create_subprocess_shell(cmd, stdout=PIPE, stderr=PIPE)
522
+ proc_stdout = ensure_not_none(process.stdout, desc="process.stdout")
523
+ proc_stderr = ensure_not_none(process.stderr, desc="process.stderr")
524
+ ret_stdout = StringIO()
525
+ ret_stderr = StringIO()
526
+ async with TaskGroup() as tg:
512
527
  _ = tg.create_task(_stream_one(proc_stdout, stdout, ret_stdout))
513
528
  _ = tg.create_task(_stream_one(proc_stderr, stderr, ret_stderr))
514
- _ = await process.wait() # skipif-not-windows
515
- return StreamCommandOutput( # skipif-not-windows
529
+ _ = await process.wait()
530
+ return StreamCommandOutput(
516
531
  process=process, stdout=ret_stdout.getvalue(), stderr=ret_stderr.getvalue()
517
532
  )
518
533
 
@@ -521,7 +536,7 @@ async def _stream_one(
521
536
  input_: StreamReader, out_stream: TextIO, ret_stream: StringIO, /
522
537
  ) -> None:
523
538
  """Asynchronously read from a stream and write to the target output stream."""
524
- while True: # skipif-not-windows
539
+ while True:
525
540
  line = await input_.readline()
526
541
  if not line:
527
542
  break
@@ -547,14 +562,47 @@ async def timeout_td(
547
562
  raise error from None
548
563
 
549
564
 
565
+ ##
566
+
567
+
568
+ _LOCKS: AsyncDict[Path, Lock] = AsyncDict()
569
+
570
+
571
+ @asynccontextmanager
572
+ async def yield_locked_shelf(
573
+ path: PathLike,
574
+ /,
575
+ *,
576
+ flag: _Flag = "c",
577
+ protocol: int | None = None,
578
+ writeback: bool = False,
579
+ ) -> AsyncIterator[Shelf[Any]]:
580
+ """Yield a shelf, behind a lock."""
581
+ path = Path(path)
582
+ try:
583
+ lock = _LOCKS[path]
584
+ except KeyError:
585
+ lock = Lock()
586
+ await _LOCKS.set(path, lock)
587
+ async with lock:
588
+ with yield_shelf(
589
+ path, flag=flag, protocol=protocol, writeback=writeback
590
+ ) as shelf:
591
+ yield shelf
592
+
593
+
550
594
  __all__ = [
551
595
  "AsyncDict",
552
596
  "EnhancedTaskGroup",
597
+ "OneAsyncEmptyError",
598
+ "OneAsyncError",
599
+ "OneAsyncNonUniqueError",
553
600
  "StreamCommandOutput",
601
+ "chain_async",
554
602
  "get_coroutine_name",
555
603
  "get_items",
556
604
  "get_items_nowait",
557
- "loop_until_succeed",
605
+ "one_async",
558
606
  "put_items",
559
607
  "put_items_nowait",
560
608
  "sleep_max",
@@ -563,4 +611,5 @@ __all__ = [
563
611
  "sleep_until",
564
612
  "stream_command",
565
613
  "timeout_td",
614
+ "yield_locked_shelf",
566
615
  ]
utilities/atomicwrites.py CHANGED
@@ -57,7 +57,7 @@ def move(
57
57
  raise ImpossibleCaseError(
58
58
  case=[f"{source.is_file()=}", f"{source.is_dir()=}"]
59
59
  )
60
- case _ as never:
60
+ case never:
61
61
  assert_never(never)
62
62
 
63
63
 
utilities/atools.py CHANGED
@@ -1,14 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Callable
4
- from typing import TYPE_CHECKING, Any
4
+ from functools import partial
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Any, cast, overload
5
7
 
6
- from atools import memoize
8
+ import atools
9
+ from whenever import TimeDelta
7
10
 
8
- from utilities.types import Coro
11
+ from utilities.types import Coro, PathLike
9
12
 
10
13
  if TYPE_CHECKING:
11
- from whenever import TimeDelta
14
+ from atools._memoize_decorator import Keygen, Pickler
12
15
 
13
16
 
14
17
  type _Key[**P, T] = tuple[Callable[P, Coro[T]], TimeDelta]
@@ -36,4 +39,61 @@ async def call_memoized[**P, T](
36
39
  return await memoized_func(*args, **kwargs)
37
40
 
38
41
 
42
+ ##
43
+
44
+
45
+ @overload
46
+ def memoize[F: Callable[..., Coro[Any]]](
47
+ func: F,
48
+ /,
49
+ *,
50
+ db_path: PathLike | None = None,
51
+ duration: float | TimeDelta | None = None,
52
+ keygen: Keygen | None = None,
53
+ pickler: Pickler | None = None,
54
+ size: int | None = None,
55
+ ) -> F: ...
56
+ @overload
57
+ def memoize[F: Callable[..., Coro[Any]]](
58
+ func: None = None,
59
+ /,
60
+ *,
61
+ db_path: PathLike | None = None,
62
+ duration: float | TimeDelta | None = None,
63
+ keygen: Keygen | None = None,
64
+ pickler: Pickler | None = None,
65
+ size: int | None = None,
66
+ ) -> Callable[[F], F]: ...
67
+ def memoize[F: Callable[..., Coro[Any]]](
68
+ func: F | None = None,
69
+ /,
70
+ *,
71
+ db_path: PathLike | None = None,
72
+ duration: float | TimeDelta | None = None,
73
+ keygen: Keygen | None = None,
74
+ pickler: Pickler | None = None,
75
+ size: int | None = None,
76
+ ) -> F | Callable[[F], F]:
77
+ """Memoize a function."""
78
+ if func is None:
79
+ result = partial(
80
+ memoize,
81
+ db_path=db_path,
82
+ duration=duration,
83
+ keygen=keygen,
84
+ pickler=pickler,
85
+ size=size,
86
+ )
87
+ return cast("Callable[[F], F]", result)
88
+ return atools.memoize(
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,
93
+ keygen=keygen,
94
+ pickler=pickler,
95
+ size=size,
96
+ )(func)
97
+
98
+
39
99
  __all__ = ["call_memoized"]
utilities/cachetools.py CHANGED
@@ -3,7 +3,7 @@ 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
@@ -100,11 +100,14 @@ def cache[F: Callable](
100
100
  typed_: bool = False,
101
101
  ) -> Callable[[F], F]:
102
102
  """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_,
103
+ return cast(
104
+ "F",
105
+ ttl_cache(
106
+ maxsize=max_size,
107
+ ttl=inf if max_duration is None else max_duration.in_seconds(),
108
+ timer=timer,
109
+ typed=typed_,
110
+ ),
108
111
  )
109
112
 
110
113