dycw-utilities 0.146.2__py3-none-any.whl → 0.178.1__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 (89) hide show
  1. dycw_utilities-0.178.1.dist-info/METADATA +34 -0
  2. dycw_utilities-0.178.1.dist-info/RECORD +105 -0
  3. dycw_utilities-0.178.1.dist-info/WHEEL +4 -0
  4. {dycw_utilities-0.146.2.dist-info → dycw_utilities-0.178.1.dist-info}/entry_points.txt +1 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +10 -7
  7. utilities/asyncio.py +129 -50
  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 +387 -0
  18. utilities/enum.py +2 -2
  19. utilities/errors.py +17 -3
  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 +33 -58
  29. utilities/jinja2.py +148 -0
  30. utilities/json.py +1 -1
  31. utilities/libcst.py +7 -7
  32. utilities/logging.py +131 -93
  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/packaging.py +115 -0
  39. utilities/parse.py +2 -2
  40. utilities/pathlib.py +66 -34
  41. utilities/permissions.py +298 -0
  42. utilities/platform.py +5 -4
  43. utilities/polars.py +934 -420
  44. utilities/polars_ols.py +1 -1
  45. utilities/postgres.py +317 -153
  46. utilities/pottery.py +10 -86
  47. utilities/pqdm.py +3 -3
  48. utilities/pwd.py +28 -0
  49. utilities/pydantic.py +4 -51
  50. utilities/pydantic_settings.py +240 -0
  51. utilities/pydantic_settings_sops.py +76 -0
  52. utilities/pyinstrument.py +5 -5
  53. utilities/pytest.py +100 -126
  54. utilities/pytest_plugins/pytest_randomly.py +1 -1
  55. utilities/pytest_plugins/pytest_regressions.py +7 -3
  56. utilities/pytest_regressions.py +27 -8
  57. utilities/random.py +11 -6
  58. utilities/re.py +1 -1
  59. utilities/redis.py +101 -64
  60. utilities/sentinel.py +10 -0
  61. utilities/shelve.py +4 -1
  62. utilities/shutil.py +25 -0
  63. utilities/slack_sdk.py +9 -4
  64. utilities/sqlalchemy.py +422 -352
  65. utilities/sqlalchemy_polars.py +28 -52
  66. utilities/string.py +1 -1
  67. utilities/subprocess.py +1977 -0
  68. utilities/tempfile.py +112 -4
  69. utilities/testbook.py +50 -0
  70. utilities/text.py +174 -42
  71. utilities/throttle.py +158 -0
  72. utilities/timer.py +2 -2
  73. utilities/traceback.py +59 -38
  74. utilities/types.py +68 -22
  75. utilities/typing.py +479 -19
  76. utilities/uuid.py +42 -5
  77. utilities/version.py +27 -26
  78. utilities/whenever.py +663 -178
  79. utilities/zoneinfo.py +80 -22
  80. dycw_utilities-0.146.2.dist-info/METADATA +0 -41
  81. dycw_utilities-0.146.2.dist-info/RECORD +0 -99
  82. dycw_utilities-0.146.2.dist-info/WHEEL +0 -4
  83. dycw_utilities-0.146.2.dist-info/licenses/LICENSE +0 -21
  84. utilities/aiolimiter.py +0 -25
  85. utilities/eventkit.py +0 -388
  86. utilities/period.py +0 -237
  87. utilities/python_dotenv.py +0 -101
  88. utilities/streamlit.py +0 -105
  89. utilities/typed_settings.py +0 -144
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.3
2
+ Name: dycw-utilities
3
+ Version: 0.178.1
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,<2
8
+ Requires-Dist: typing-extensions>=4.15.0,<5
9
+ Requires-Dist: tzlocal>=5.3.1,<6
10
+ Requires-Dist: whenever>=0.9.4,<1
11
+ Requires-Dist: coloredlogs>=15.0.1,<16 ; extra == 'logging'
12
+ Requires-Dist: dycw-pytest-only>=2.1.1,<3 ; extra == 'test'
13
+ Requires-Dist: hypothesis>=6.150.0,<7 ; extra == 'test'
14
+ Requires-Dist: pytest>=9.0.2,<10 ; extra == 'test'
15
+ Requires-Dist: pytest-asyncio>=1.3.0,<2 ; extra == 'test'
16
+ Requires-Dist: pytest-cov>=7.0.0,<8 ; extra == 'test'
17
+ Requires-Dist: pytest-instafail>=0.5.0,<1 ; extra == 'test'
18
+ Requires-Dist: pytest-lazy-fixtures>=1.4.0,<2 ; extra == 'test'
19
+ Requires-Dist: pytest-randomly>=4.0.1,<5 ; extra == 'test'
20
+ Requires-Dist: pytest-regressions>=2.8.3,<3 ; extra == 'test'
21
+ Requires-Dist: pytest-repeat>=0.9.4,<1 ; extra == 'test'
22
+ Requires-Dist: pytest-rerunfailures>=16.1,<17 ; extra == 'test'
23
+ Requires-Dist: pytest-rng>=1.0.0,<2 ; extra == 'test'
24
+ Requires-Dist: pytest-timeout>=2.4.0,<3 ; extra == 'test'
25
+ Requires-Dist: pytest-xdist>=3.8.0,<4 ; extra == 'test'
26
+ Requires-Dist: testbook>=0.4.2,<1 ; 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,105 @@
1
+ utilities/__init__.py,sha256=UPRGb4cG5FkpFHiF05BtsrvJN4xEBqAOklkZld8UbEc,60
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=_cUgyU3207VH8e6IdQ88GNssvOVSkvEsUx95LiaayjY,10635
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/packaging.py,sha256=z_3v2ofEkqIMLhiEvP8zvQUaUFIqY6d-9XK_ZfsZTb0,3723
48
+ utilities/parse.py,sha256=g7Qm9eBOIeDId2tGA021CIaeF6jp1TI8rx4srdvlyoo,17937
49
+ utilities/pathlib.py,sha256=N4Ip8R9eCM-6GfvxUJ3T9oQIle2C2P52F-13BCFRdTg,9345
50
+ utilities/permissions.py,sha256=vLXlWztSVYffbrxptne7ksj6dU1HLekm4fEvS4ny_4Q,8944
51
+ utilities/pickle.py,sha256=MBT2xZCsv0pH868IXLGKnlcqNx2IRVKYNpRcqiQQqxw,653
52
+ utilities/platform.py,sha256=R3ldt2-DlI7la9ng6Rxt1CThd2lL0Ai2tC0TbabtCC0,2800
53
+ utilities/polars.py,sha256=JPzN4UqQDC7R4IXsIuXEIXRiwHSrkiSZcD8UOfwGPuE,87535
54
+ utilities/polars_ols.py,sha256=LNTFNLPuYW7fcAHymlbnams_DhitToblYvib3mhKbwI,5615
55
+ utilities/postgres.py,sha256=g3tEwTI8TdmiCbRME61ffQ0xaibdpXPu8mJOOHvjPKc,12532
56
+ utilities/pottery.py,sha256=nA0SsF9irvfC0tk68YAr08tuL9lGRSlBKihSx7Ibk84,3963
57
+ utilities/pqdm.py,sha256=idv2seRVP2f6NeSfpeEnT5A-tQezaHZKDyeu16g2-0E,3091
58
+ utilities/psutil.py,sha256=KUlu4lrUw9Zg1V7ZGetpWpGb9DB8l_SSDWGbANFNCPU,2104
59
+ utilities/pwd.py,sha256=pzlLkBfSTZsCVkMrAxEmuEoNmm-6goklfAygVmOEZqs,707
60
+ utilities/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
+ utilities/pydantic.py,sha256=z_PnYXN_UNNLQwo_XF7cr4GDcf5wicvscD6aFYVOEvA,238
62
+ utilities/pydantic_settings.py,sha256=53tQTpHFtA6UIuzHKzauZtW_bSRwL5lQnNwTWeO4Fjw,7608
63
+ utilities/pydantic_settings_sops.py,sha256=9Ou6Cx6PiYOU49vtkKqqW1Sdp_i3WlVyg8KkUUKNliM,2310
64
+ utilities/pyinstrument.py,sha256=hnXaL-4HE7wWBI5JKaPfYTpsrXe68YiuZKahHV0VJn8,841
65
+ utilities/pytest.py,sha256=p8fjpiBuMUbaH0DnpM0j5BxnPLtH3uiMEkBKAjpM3Ss,6481
66
+ utilities/pytest_plugins/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
67
+ utilities/pytest_plugins/pytest_randomly.py,sha256=B1qYVlExGOxTywq2r1SMi5o7btHLk2PNdY_b1p98dkE,409
68
+ utilities/pytest_plugins/pytest_regressions.py,sha256=mnHYBfdprz50UGVkVzV1bZERZN5CFfoF8YbokGxdFwU,1639
69
+ utilities/pytest_regressions.py,sha256=tJxW38u-zpoyjW1N4zogBx4V_07r-ibDInddcEUyXmc,4763
70
+ utilities/random.py,sha256=hZlH4gnAtoaofWswuJYjcygejrY8db4CzP-z_adO2Mo,4165
71
+ utilities/re.py,sha256=S4h-DLL6ScMPqjboZ_uQ1BVTJajrqV06r_81D--_HCE,4573
72
+ utilities/redis.py,sha256=gybjqKea33Jy50n4dHTS14JdquqHaJqHF2dixQljYWQ,30172
73
+ utilities/reprlib.py,sha256=ssYTcBW-TeRh3fhCJv57sopTZHF5FrPyyUg9yp5XBlo,3953
74
+ utilities/scipy.py,sha256=wZJM7fEgBAkLSYYvSmsg5ac-QuwAI0BGqHVetw1_Hb0,947
75
+ utilities/sentinel.py,sha256=A_p5jX2K0Yc5XBfoYHyBLqHsEWzE1ByOdDuzzA2pZnE,1434
76
+ utilities/shelve.py,sha256=4OzjQI6kGuUbJciqf535rwnao-_IBv66gsT6tRGiUt0,759
77
+ utilities/shutil.py,sha256=knJ7hx42FtIfGByxQZMcOSgQCDlaSony325g505ps3A,480
78
+ utilities/slack_sdk.py,sha256=76-DYtcGiUhEvl-voMamc5OjfF7Y7nCq54Bys1arqzw,2233
79
+ utilities/socket.py,sha256=K77vfREvzoVTrpYKo6MZakol0EYu2q1sWJnnZqL0So0,118
80
+ utilities/sqlalchemy.py,sha256=HQYpd7LFxdTF5WYVWYtCJeEBI71EJm7ytvCGyAH9B-U,37163
81
+ utilities/sqlalchemy_polars.py,sha256=JCGhB37raSR7fqeWV5dTsciRTMVzIdVT9YSqKT0piT0,13370
82
+ utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
83
+ utilities/string.py,sha256=shmBK87zZwzGyixuNuXCiUbqzfeZ9xlrFwz6JTaRvDk,582
84
+ utilities/subprocess.py,sha256=eGRRQF-OXDy2kMSy7dv4fUzcPizvRgGBxYvPh-QVqZo,53980
85
+ utilities/tempfile.py,sha256=4kRGd4hyINDX4hpcYMtwzDcd-4IKjpTWT0MnjnKD4hE,4221
86
+ utilities/testbook.py,sha256=dy9ID7GoFitMTCi4XBDTE4_mJm9uLTPyGgGrkJ1yuec,1304
87
+ utilities/text.py,sha256=UKW6xtF4bb0i-Gu1jctJvvQmeXk6U6UYNqoEnCLdaOo,14102
88
+ utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
89
+ utilities/throttle.py,sha256=zVyqSdFGIAUQUj4dk-Zh8SFiuX0DUmRWMsn-qwSYk3g,4911
90
+ utilities/timer.py,sha256=BGlwEVznx67scuLOUohyWJ4d5rTnwtk-IR4yLXFiNfo,2574
91
+ utilities/traceback.py,sha256=B_sc0TRUv-mGDnF-ek05nbqjmBiHr3-wvxliAqIF5hI,9608
92
+ utilities/types.py,sha256=UwxYajRnupKTCnzReaYWYqtKdpIPeSO-d97Wu4bLEq8,18878
93
+ utilities/typing.py,sha256=xuR8LxzjD-XlSftTM3TNvGVdQyV1mzpdwWdDMzWwCPE,25310
94
+ utilities/tzdata.py,sha256=fgNVj66yUbCSI_-vrRVzSD3gtf-L_8IEJEPjP_Jel5Y,266
95
+ utilities/tzlocal.py,sha256=KyCXEgCTjqGFx-389JdTuhMRUaT06U1RCMdWoED-qro,728
96
+ utilities/uuid.py,sha256=nQZs6tFX4mqtc2Ku3KqjloYCqwpTKeTj8eKwQwh3FQI,1572
97
+ utilities/version.py,sha256=ipBj5-WYY_nelp2uwFlApfWWCzTLzPwpovUi9x_OBMs,5085
98
+ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
99
+ utilities/whenever.py,sha256=F4ek0-OBWxHYrZdmoZt76N2RnNyKY5KrEHt7rqO4AQE,60183
100
+ utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
101
+ utilities/zoneinfo.py,sha256=tdIScrTB2-B-LH0ukb1HUXKooLknOfJNwHk10MuMYvA,3619
102
+ dycw_utilities-0.178.1.dist-info/WHEEL,sha256=KSLUh82mDPEPk0Bx0ScXlWL64bc8KmzIPNcpQZFV-6E,79
103
+ dycw_utilities-0.178.1.dist-info/entry_points.txt,sha256=cOGtKeJI0KXLSV7MJ8Dhc2G8jPgDcBDm53MVNJU4ycI,136
104
+ dycw_utilities-0.178.1.dist-info/METADATA,sha256=Brf1aK3mbaluoqDK93fH9FVQ09bBfP6JDIx85DCRa0M,1398
105
+ dycw_utilities-0.178.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.22
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.146.2"
3
+ __version__ = "0.178.1"
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,20 +31,26 @@ 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
39
- from utilities.functions import ensure_int, ensure_not_none, to_bool
39
+ from utilities.functions import ensure_int, ensure_not_none
40
+ from utilities.os import is_pytest
40
41
  from utilities.random import SYSTEM_RANDOM
42
+ from utilities.reprlib import get_repr
41
43
  from utilities.sentinel import Sentinel, sentinel
44
+ from utilities.shelve import yield_shelf
45
+ from utilities.text import to_bool
46
+ from utilities.warnings import suppress_warnings
42
47
  from utilities.whenever import get_now, round_date_or_date_time, to_nanoseconds
43
48
 
44
49
  if TYPE_CHECKING:
45
50
  from asyncio import _CoroutineLike
46
51
  from asyncio.subprocess import Process
47
52
  from collections.abc import (
53
+ AsyncIterable,
48
54
  AsyncIterator,
49
55
  Callable,
50
56
  ItemsView,
@@ -56,15 +62,18 @@ if TYPE_CHECKING:
56
62
  )
57
63
  from contextvars import Context
58
64
  from random import Random
65
+ from shelve import Shelf
59
66
  from types import TracebackType
60
67
 
61
68
  from whenever import ZonedDateTime
62
69
 
70
+ from utilities.shelve import _Flag
63
71
  from utilities.types import (
64
72
  Coro,
65
73
  Delta,
66
- MaybeCallableBool,
74
+ MaybeCallableBoolLike,
67
75
  MaybeType,
76
+ PathLike,
68
77
  SupportsKeysAndGetItem,
69
78
  )
70
79
 
@@ -222,7 +231,7 @@ class EnhancedTaskGroup(TaskGroup):
222
231
  _semaphore: Semaphore | None
223
232
  _timeout: Delta | None
224
233
  _error: MaybeType[BaseException]
225
- _debug: MaybeCallableBool
234
+ _debug: MaybeCallableBoolLike
226
235
  _stack: AsyncExitStack
227
236
  _timeout_cm: _AsyncGeneratorContextManager[None] | None
228
237
 
@@ -233,7 +242,7 @@ class EnhancedTaskGroup(TaskGroup):
233
242
  max_tasks: int | None = None,
234
243
  timeout: Delta | None = None,
235
244
  error: MaybeType[BaseException] = TimeoutError,
236
- debug: MaybeCallableBool = False,
245
+ debug: MaybeCallableBoolLike = False,
237
246
  ) -> None:
238
247
  super().__init__()
239
248
  self._max_tasks = max_tasks
@@ -266,7 +275,7 @@ class EnhancedTaskGroup(TaskGroup):
266
275
  _ = await super().__aexit__(et, exc, tb)
267
276
  case False:
268
277
  _ = await super().__aexit__(et, exc, tb)
269
- case _ as never:
278
+ case never:
270
279
  assert_never(never)
271
280
 
272
281
  @override
@@ -303,7 +312,7 @@ class EnhancedTaskGroup(TaskGroup):
303
312
  self.create_task(make_coro(*args, **kwargs))
304
313
  for _ in range(self._max_tasks)
305
314
  ]
306
- case _ as never:
315
+ case never:
307
316
  assert_never(never)
308
317
 
309
318
  async def run_or_create_task[T](
@@ -318,11 +327,11 @@ class EnhancedTaskGroup(TaskGroup):
318
327
  return await coro
319
328
  case False:
320
329
  return self.create_task(coro, name=name, context=context)
321
- case _ as never:
330
+ case never:
322
331
  assert_never(never)
323
332
 
324
333
  def _is_debug(self) -> bool:
325
- return to_bool(bool_=self._debug) or (
334
+ return to_bool(self._debug) or (
326
335
  (self._max_tasks is not None) and (self._max_tasks <= 0)
327
336
  )
328
337
 
@@ -340,13 +349,43 @@ class EnhancedTaskGroup(TaskGroup):
340
349
  ##
341
350
 
342
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
+
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
+
343
384
  async def get_items[T](queue: Queue[T], /, *, max_size: int | None = None) -> list[T]:
344
385
  """Get items from a queue; if empty then wait."""
345
386
  try:
346
387
  items = [await queue.get()]
347
388
  except RuntimeError as error: # pragma: no cover
348
- from utilities.pytest import is_pytest
349
-
350
389
  if (not is_pytest()) or (error.args[0] != "Event loop is closed"):
351
390
  raise
352
391
  return []
@@ -376,27 +415,38 @@ def get_items_nowait[T](queue: Queue[T], /, *, max_size: int | None = None) -> l
376
415
  ##
377
416
 
378
417
 
379
- async def loop_until_succeed(
380
- func: Callable[[], Coro[None]],
381
- /,
382
- *,
383
- error: Callable[[Exception], None] | None = None,
384
- sleep: Delta | None = None,
385
- ) -> None:
386
- """Repeatedly call a coroutine until it succeeds."""
387
- while True:
388
- try:
389
- return await func()
390
- except Exception as err: # noqa: BLE001
391
- if error is not None:
392
- error(err)
393
- exc_type, exc_value, traceback = sys.exc_info()
394
- if (exc_type is None) or (exc_value is None): # pragma: no cover
395
- raise ImpossibleCaseError(
396
- case=[f"{exc_type=}", f"{exc_value=}"]
397
- ) from None
398
- sys.excepthook(exc_type, exc_value, traceback)
399
- await sleep_td(sleep)
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"
400
450
 
401
451
 
402
452
  ##
@@ -463,27 +513,21 @@ class StreamCommandOutput:
463
513
 
464
514
  @property
465
515
  def return_code(self) -> int:
466
- return ensure_int(self.process.returncode) # skipif-not-windows
516
+ return ensure_int(self.process.returncode)
467
517
 
468
518
 
469
519
  async def stream_command(cmd: str, /) -> StreamCommandOutput:
470
520
  """Run a shell command asynchronously and stream its output in real time."""
471
- process = await create_subprocess_shell( # skipif-not-windows
472
- cmd, stdout=PIPE, stderr=PIPE
473
- )
474
- proc_stdout = ensure_not_none( # skipif-not-windows
475
- process.stdout, desc="process.stdout"
476
- )
477
- proc_stderr = ensure_not_none( # skipif-not-windows
478
- process.stderr, desc="process.stderr"
479
- )
480
- ret_stdout = StringIO() # skipif-not-windows
481
- ret_stderr = StringIO() # skipif-not-windows
482
- 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:
483
527
  _ = tg.create_task(_stream_one(proc_stdout, stdout, ret_stdout))
484
528
  _ = tg.create_task(_stream_one(proc_stderr, stderr, ret_stderr))
485
- _ = await process.wait() # skipif-not-windows
486
- return StreamCommandOutput( # skipif-not-windows
529
+ _ = await process.wait()
530
+ return StreamCommandOutput(
487
531
  process=process, stdout=ret_stdout.getvalue(), stderr=ret_stderr.getvalue()
488
532
  )
489
533
 
@@ -492,7 +536,7 @@ async def _stream_one(
492
536
  input_: StreamReader, out_stream: TextIO, ret_stream: StringIO, /
493
537
  ) -> None:
494
538
  """Asynchronously read from a stream and write to the target output stream."""
495
- while True: # skipif-not-windows
539
+ while True:
496
540
  line = await input_.readline()
497
541
  if not line:
498
542
  break
@@ -518,13 +562,47 @@ async def timeout_td(
518
562
  raise error from None
519
563
 
520
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
+
521
594
  __all__ = [
522
595
  "AsyncDict",
523
596
  "EnhancedTaskGroup",
597
+ "OneAsyncEmptyError",
598
+ "OneAsyncError",
599
+ "OneAsyncNonUniqueError",
524
600
  "StreamCommandOutput",
601
+ "chain_async",
602
+ "get_coroutine_name",
525
603
  "get_items",
526
604
  "get_items_nowait",
527
- "loop_until_succeed",
605
+ "one_async",
528
606
  "put_items",
529
607
  "put_items_nowait",
530
608
  "sleep_max",
@@ -533,4 +611,5 @@ __all__ = [
533
611
  "sleep_until",
534
612
  "stream_command",
535
613
  "timeout_td",
614
+ "yield_locked_shelf",
536
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