dycw-utilities 0.148.5__py3-none-any.whl → 0.174.12__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 (83) hide show
  1. dycw_utilities-0.174.12.dist-info/METADATA +41 -0
  2. dycw_utilities-0.174.12.dist-info/RECORD +104 -0
  3. dycw_utilities-0.174.12.dist-info/WHEEL +4 -0
  4. {dycw_utilities-0.148.5.dist-info → dycw_utilities-0.174.12.dist-info}/entry_points.txt +3 -0
  5. utilities/__init__.py +1 -1
  6. utilities/{eventkit.py → aeventkit.py} +12 -11
  7. utilities/altair.py +7 -6
  8. utilities/asyncio.py +113 -64
  9. utilities/atomicwrites.py +1 -1
  10. utilities/atools.py +64 -4
  11. utilities/cachetools.py +9 -6
  12. utilities/click.py +145 -49
  13. utilities/concurrent.py +1 -1
  14. utilities/contextlib.py +4 -2
  15. utilities/contextvars.py +20 -1
  16. utilities/cryptography.py +3 -3
  17. utilities/dataclasses.py +15 -28
  18. utilities/docker.py +292 -0
  19. utilities/enum.py +2 -2
  20. utilities/errors.py +1 -1
  21. utilities/fastapi.py +8 -3
  22. utilities/fpdf2.py +2 -2
  23. utilities/functions.py +20 -297
  24. utilities/git.py +19 -0
  25. utilities/grp.py +28 -0
  26. utilities/hypothesis.py +360 -78
  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 +297 -0
  41. utilities/platform.py +5 -5
  42. utilities/polars.py +932 -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 +2 -3
  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 +864 -0
  67. utilities/tempfile.py +62 -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/period.py +0 -237
  83. utilities/typed_settings.py +0 -144
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.3
2
+ Name: dycw-utilities
3
+ Version: 0.174.12
4
+ Author: Derek Wan
5
+ Author-email: Derek Wan <d.wan@icloud.com>
6
+ Requires-Dist: atomicwrites>=1.4.1,<1.5
7
+ Requires-Dist: typing-extensions>=4.15.0,<4.16
8
+ Requires-Dist: tzlocal>=5.3.1,<5.4
9
+ Requires-Dist: whenever>=0.9.4,<0.10
10
+ Requires-Dist: coloredlogs>=15.0.1,<15.1 ; extra == 'logging'
11
+ Requires-Dist: dycw-pytest-only>=2.1.1,<2.2 ; extra == 'test'
12
+ Requires-Dist: hypothesis>=6.148.8,<6.149 ; extra == 'test'
13
+ Requires-Dist: pytest>=9.0.2,<9.1 ; extra == 'test'
14
+ Requires-Dist: pytest-asyncio>=1.3.0,<1.4 ; extra == 'test'
15
+ Requires-Dist: pytest-cov>=7.0.0,<7.1 ; extra == 'test'
16
+ Requires-Dist: pytest-instafail>=0.5.0,<0.6 ; extra == 'test'
17
+ Requires-Dist: pytest-lazy-fixtures>=1.4.0,<1.5 ; extra == 'test'
18
+ Requires-Dist: pytest-randomly>=4.0.1,<4.1 ; extra == 'test'
19
+ Requires-Dist: pytest-regressions>=2.8.3,<2.9 ; extra == 'test'
20
+ Requires-Dist: pytest-repeat>=0.9.4,<0.10 ; extra == 'test'
21
+ Requires-Dist: pytest-rerunfailures>=16.1,<16.2 ; extra == 'test'
22
+ Requires-Dist: pytest-rng>=1.0.0,<1.1 ; extra == 'test'
23
+ Requires-Dist: pytest-timeout>=2.4.0,<2.5 ; extra == 'test'
24
+ Requires-Dist: pytest-xdist>=3.8.0,<3.9 ; extra == 'test'
25
+ Requires-Dist: testbook>=0.4.2,<0.5 ; extra == 'test'
26
+ Requires-Python: >=3.12
27
+ Provides-Extra: logging
28
+ Provides-Extra: test
29
+ Description-Content-Type: text/markdown
30
+
31
+ [![PyPI version](https://badge.fury.io/py/dycw-utilities.svg)](https://badge.fury.io/py/dycw-utilities)
32
+
33
+ # `dycw-utilities`
34
+
35
+ [All the Python functions I don't want to write twice.](https://github.com/nvim-lua/plenary.nvim)
36
+
37
+ ## Installation
38
+
39
+ - `pip install dycw-utilities`
40
+
41
+ or with [extras](https://github.com/dycw/python-utilities/blob/master/pyproject.toml).
@@ -0,0 +1,104 @@
1
+ utilities/__init__.py,sha256=VFU3kmpNxjyAnXuMdW-ygHCxl3CNmUA0rP-wy1y_uzg,61
2
+ utilities/aeventkit.py,sha256=OmDBhYGgbsKrB7cdC5FFpJHUatX9O76eTeKVVTksp2Y,12673
3
+ utilities/altair.py,sha256=rUK99g9x6CYDDfiZrf-aTx5fSRbL1Q8ctgKORowzXHg,9060
4
+ utilities/asyncio.py,sha256=aJySVxBY0gqsIYnoNmH7-1r8djKuf4vSsU69VCD08t8,16772
5
+ utilities/atomicwrites.py,sha256=tPo6r-Rypd9u99u66B9z86YBPpnLrlHtwox_8Z7T34Y,5790
6
+ utilities/atools.py,sha256=6neeCcgXxK2dlsc0xp15Za7nSucbCgFtAJepGI_-WXU,2549
7
+ utilities/cachetools.py,sha256=2S9LMHIunDYMIu8JGI7OLN04sQ7-xZGdEdP1Li0vksA,2775
8
+ utilities/click.py,sha256=-_B7V5arQopuVJA6emnwBlZGQvcnq7nq6IYc3cPX5FA,19204
9
+ utilities/concurrent.py,sha256=fHeW2SZ_TEMfFY0C8pyQI6aPlnecvx9x6SuUwBWj_JY,2853
10
+ utilities/contextlib.py,sha256=iP7R2tIm6ZsbfLD5ks6UKBYwj50e9gBI8AkpLN-chro,7476
11
+ utilities/contextvars.py,sha256=J8OhC7jqozAGYOCe2KUWysbPXNGe5JYz3HfaY_mIs08,883
12
+ utilities/cryptography.py,sha256=5PFrzsNUGHay91dFgYnDKwYprXxahrBqztmUqViRzBk,956
13
+ utilities/cvxpy.py,sha256=Rv1-fD-XYerosCavRF8Pohop2DBkU3AlFaGTfD8AEAA,13776
14
+ utilities/dataclasses.py,sha256=xbU3QN1GFy7RC6hIJRZIeUZm7YRlodrgEWmahWG6k2g,32465
15
+ utilities/docker.py,sha256=DBgSz-UPBDHk_XJLPNaNMkNym1kKK8l3Os8IqMwEyW8,7866
16
+ utilities/enum.py,sha256=5l6pwZD1cjSlVW4ss-zBPspWvrbrYrdtJWcg6f5_J5w,5781
17
+ utilities/errors.py,sha256=mFlDGSM0LI1jZ1pbqwLAH3ttLZ2JVIxyZLojw8tGVZU,1479
18
+ utilities/fastapi.py,sha256=TqyKvBjiMS594sXPjrz-KRTLMb3l3D3rZ1zAYV7GfOk,1454
19
+ utilities/fpdf2.py,sha256=dSiYz0FJTD2sQuxpxqFWwwIe2-p6Y7oTB9Tv0Jajit0,1866
20
+ utilities/functions.py,sha256=82qCAaPIB0JmZ5wsQurA3MTYl7fh8LHcoBFkxPs7Zeg,21478
21
+ utilities/functools.py,sha256=I00ru2gQPakZw2SHVeKIKXfTv741655s6HI0lUoE0D4,1552
22
+ utilities/getpass.py,sha256=DfN5UgMAtFCqS3dSfFHUfqIMZX2shXvwphOz_6J6f6A,103
23
+ utilities/git.py,sha256=U1RFvCTANGENgx9wVBDvllioqBQZM2ns12ivKhOsaO4,414
24
+ utilities/grp.py,sha256=1vV3gNR9dQsl1vtUtvC_2qgVdQzm7O8lLMSh56cTbeg,694
25
+ utilities/gzip.py,sha256=fkGP3KdsBfXlstodT4wtlp-PwNyUsogpbDCVVVGdsm4,781
26
+ utilities/hashlib.py,sha256=SVTgtguur0P4elppvzOBbLEjVM3Pea0eWB61yg2ilxo,309
27
+ utilities/http.py,sha256=TsavEfHlRtlLaeV21Z6KZh0qbPw-kvD1zsQdZ7Kep5Q,977
28
+ utilities/hypothesis.py,sha256=wk1HiNdBg7tGPEKLZ5uiNVbtlSZl58QJjlediYoSHkA,46753
29
+ utilities/importlib.py,sha256=mV1xT_O_zt_GnZZ36tl3xOmMaN_3jErDWY54fX39F6Y,429
30
+ utilities/inflect.py,sha256=v7YkOWSu8NAmVghPcf4F3YBZQoJCS47_DLf9jbfWIs0,581
31
+ utilities/ipython.py,sha256=V2oMYHvEKvlNBzxDXdLvKi48oUq2SclRg5xasjaXStw,763
32
+ utilities/iterables.py,sha256=t2TsW-K3rVlS6y4_tqcc1fk9RwJV-bi7G_VwduMABK0,42558
33
+ utilities/jinja2.py,sha256=JpNHMcyMJDguX8rBN4wEz-v4En7w6WHXvYJr4Xw-F0o,4691
34
+ utilities/json.py,sha256=-WcGtSsCr9Y42wHZzAMnfvU6ihAfVftylFfRUORaDFo,2102
35
+ utilities/jupyter.py,sha256=ft5JA7fBxXKzP-L9W8f2-wbF0QeYc_2uLQNFDVk4Z-M,2917
36
+ utilities/libcst.py,sha256=ngD4wxnR3Kh-RBVmU5l5ST7cuZLhMZwyMDjHZe5mhTs,5581
37
+ utilities/lightweight_charts.py,sha256=YM3ojBvJxuCSUBu_KrhFBmaMCvRPvupKC3qkm-UVZq4,2751
38
+ utilities/logging.py,sha256=mckcX_0Q5njVgnhtIoePcDHjYjTVELw7ExutiiDnzsU,18724
39
+ utilities/math.py,sha256=cevB-YyEYAzJTWtkAr7qeeu-hbxorDI3gMznXlmNQkw,26897
40
+ utilities/memory_profiler.py,sha256=XzN56jDCa5aqXS_DxEjb_K4L6aIWh_5zyKi6OhcIxw0,853
41
+ utilities/modules.py,sha256=iuvLluJya-hvl1Q25-Jk3dLgx2Es3ck4SjJiEkAlVTs,3195
42
+ utilities/more_itertools.py,sha256=syfIPhQF_WS-YiicdGe2h5F1G-Ld12Q2XsVduL2hA40,10908
43
+ utilities/numpy.py,sha256=Xn23sA2ZbVNqwUYEgNJD3XBYH6IbCri_WkHSNhg3NkY,26122
44
+ utilities/operator.py,sha256=C3NylZWGTVWRpwYHOPVhaLgRhw0DfpS4_XQ8KfPhBLQ,3613
45
+ utilities/optuna.py,sha256=C-fhWYiXHVPo1l8QctYkFJ4DyhbSrGorzP1dJb_qvd8,1933
46
+ utilities/orjson.py,sha256=T_0SlK811ysg46d3orvIPY3JpBa4FRMpP2wlPQo7-gU,41854
47
+ utilities/os.py,sha256=kjKKSQfnRqFTTZ315iavaaGd3gGuYNoSWlxVLCJjyQs,4852
48
+ utilities/parse.py,sha256=g7Qm9eBOIeDId2tGA021CIaeF6jp1TI8rx4srdvlyoo,17937
49
+ utilities/pathlib.py,sha256=EKZn-wWxH7MEWFrQGqHIoB-GJzyXeiEj8iDIgvkr8Wk,9325
50
+ utilities/permissions.py,sha256=u-1PMJGC8PDbZltR4SozBuKKX5yIubmXPuiKrBx3IKU,8943
51
+ utilities/pickle.py,sha256=MBT2xZCsv0pH868IXLGKnlcqNx2IRVKYNpRcqiQQqxw,653
52
+ utilities/platform.py,sha256=0pYO5v7L2sU5UN87zHhEEhTKsZ9NIEM8N6UCr0F7bLY,2778
53
+ utilities/polars.py,sha256=cNFBLWgOMUAp_Sz4xtlto17uZswZRrcfQYC95QKyaY4,87483
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=9HHwYgZQe6CRF0ekHQEFH05gmoP4Ne0V54RrtUNDfi4,10524
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=nKdVZAa88_aMo1a8V-EIHg_smZYdk1iDQBm50R3FlO4,4044
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=dIBguvLB0WepJKqgBxlzF6r8z9HW5JiGKLg-eslpuec,22549
85
+ utilities/tempfile.py,sha256=Lx6qa16lL1XVH6WdmD_G9vlN6gLI8nrIurxmsFkPKvg,3022
86
+ utilities/testbook.py,sha256=j1KmaVbrX9VrbeMgtPh5gk55myAsn3dyRUn7jGbPbRk,1294
87
+ utilities/text.py,sha256=7SvwcSR2l_5cOrm1samGnR4C-ZI6qyFLHLzSpO1zeHQ,13958
88
+ utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
89
+ utilities/timer.py,sha256=BGlwEVznx67scuLOUohyWJ4d5rTnwtk-IR4yLXFiNfo,2574
90
+ utilities/traceback.py,sha256=B_sc0TRUv-mGDnF-ek05nbqjmBiHr3-wvxliAqIF5hI,9608
91
+ utilities/types.py,sha256=UwxYajRnupKTCnzReaYWYqtKdpIPeSO-d97Wu4bLEq8,18878
92
+ utilities/typing.py,sha256=xuR8LxzjD-XlSftTM3TNvGVdQyV1mzpdwWdDMzWwCPE,25310
93
+ utilities/tzdata.py,sha256=fgNVj66yUbCSI_-vrRVzSD3gtf-L_8IEJEPjP_Jel5Y,266
94
+ utilities/tzlocal.py,sha256=KyCXEgCTjqGFx-389JdTuhMRUaT06U1RCMdWoED-qro,728
95
+ utilities/uuid.py,sha256=nQZs6tFX4mqtc2Ku3KqjloYCqwpTKeTj8eKwQwh3FQI,1572
96
+ utilities/version.py,sha256=ipBj5-WYY_nelp2uwFlApfWWCzTLzPwpovUi9x_OBMs,5085
97
+ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
98
+ utilities/whenever.py,sha256=F4ek0-OBWxHYrZdmoZt76N2RnNyKY5KrEHt7rqO4AQE,60183
99
+ utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
100
+ utilities/zoneinfo.py,sha256=tdIScrTB2-B-LH0ukb1HUXKooLknOfJNwHk10MuMYvA,3619
101
+ dycw_utilities-0.174.12.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
102
+ dycw_utilities-0.174.12.dist-info/entry_points.txt,sha256=ykGI1ArwOPHqm2g5Cqh3ENdMxEej_a_FcOUov5EM5Oc,155
103
+ dycw_utilities-0.174.12.dist-info/METADATA,sha256=Cf8kQqLOY8YDH8nG9Y7WcGf6xJFGxfDIMC1Amw13isc,1710
104
+ dycw_utilities-0.174.12.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.18
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -1,3 +1,6 @@
1
+ [console_scripts]
2
+
1
3
  [pytest11]
2
4
  pytest-randomly = utilities.pytest_plugins.pytest_randomly
3
5
  pytest-regressions = utilities.pytest_plugins.pytest_regressions
6
+
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.148.5"
3
+ __version__ = "0.174.12"
@@ -29,12 +29,12 @@ from eventkit import (
29
29
 
30
30
  from utilities.functions import apply_decorators
31
31
  from utilities.iterables import always_iterable
32
- from utilities.logging import get_logger
32
+ from utilities.logging import to_logger
33
33
 
34
34
  if TYPE_CHECKING:
35
35
  from collections.abc import Callable
36
36
 
37
- from utilities.types import Coro, LoggerOrName, MaybeCoro, MaybeIterable, TypeLike
37
+ from utilities.types import Coro, LoggerLike, MaybeCoro, MaybeIterable, TypeLike
38
38
 
39
39
 
40
40
  ##
@@ -47,7 +47,7 @@ def add_listener[E: Event, F: Callable](
47
47
  *,
48
48
  error: Callable[[Event, BaseException], MaybeCoro[None]] | None = None,
49
49
  ignore: TypeLike[BaseException] | None = None,
50
- logger: LoggerOrName | None = None,
50
+ logger: LoggerLike | None = None,
51
51
  decorators: MaybeIterable[Callable[[F], F]] | None = None,
52
52
  done: Callable[..., MaybeCoro[None]] | None = None,
53
53
  keep_ref: bool = False,
@@ -92,7 +92,7 @@ class LiftedEvent[F: Callable[..., MaybeCoro[None]]]:
92
92
  *,
93
93
  error: Callable[[Event, BaseException], MaybeCoro[None]] | None = None,
94
94
  ignore: TypeLike[BaseException] | None = None,
95
- logger: LoggerOrName | None = None,
95
+ logger: LoggerLike | None = None,
96
96
  decorators: MaybeIterable[Callable[[F2], F2]] | None = None,
97
97
  done: Callable[..., MaybeCoro[None]] | None = None,
98
98
  keep_ref: bool = False,
@@ -169,7 +169,8 @@ class LiftedEvent[F: Callable[..., MaybeCoro[None]]]:
169
169
  def __await__(self) -> Any:
170
170
  return self.event.__await__() # pragma: no cover
171
171
 
172
- __aiter__ = aiter
172
+ def __aiter__(self) -> Any:
173
+ return self.event.aiter() # pragma: no cover
173
174
 
174
175
  def __contains__(self, c: Any, /) -> bool:
175
176
  return self.event.__contains__(c) # pragma: no cover
@@ -255,7 +256,7 @@ class TypedEvent[F: Callable[..., MaybeCoro[None]]](Event):
255
256
  keep_ref: bool = False,
256
257
  *,
257
258
  ignore: TypeLike[BaseException] | None = None,
258
- logger: LoggerOrName | None = None,
259
+ logger: LoggerLike | None = None,
259
260
  decorators: MaybeIterable[Callable[[F2], F2]] | None = None,
260
261
  ) -> Self:
261
262
  lifted = lift_listener(
@@ -283,7 +284,7 @@ def lift_listener[F1: Callable[..., MaybeCoro[None]], F2: Callable](
283
284
  *,
284
285
  error: Callable[[Event, BaseException], MaybeCoro[None]] | None = None,
285
286
  ignore: TypeLike[BaseException] | None = None,
286
- logger: LoggerOrName | None = None,
287
+ logger: LoggerLike | None = None,
287
288
  decorators: MaybeIterable[Callable[[F2], F2]] | None = None,
288
289
  ) -> F1:
289
290
  match error, bool(iscoroutinefunction(listener)):
@@ -297,7 +298,7 @@ def lift_listener[F1: Callable[..., MaybeCoro[None]], F2: Callable](
297
298
  except Exception as exc: # noqa: BLE001
298
299
  if (ignore is not None) and isinstance(exc, ignore):
299
300
  return
300
- get_logger(logger=logger).exception("")
301
+ to_logger(logger).exception("")
301
302
 
302
303
  lifted = listener_no_error_sync
303
304
 
@@ -311,7 +312,7 @@ def lift_listener[F1: Callable[..., MaybeCoro[None]], F2: Callable](
311
312
  except Exception as exc: # noqa: BLE001
312
313
  if (ignore is not None) and isinstance(exc, ignore):
313
314
  return
314
- get_logger(logger=logger).exception("")
315
+ to_logger(logger).exception("")
315
316
 
316
317
  lifted = listener_no_error_async
317
318
  case _, _:
@@ -359,9 +360,9 @@ def lift_listener[F1: Callable[..., MaybeCoro[None]], F2: Callable](
359
360
  error_typed(event, exc)
360
361
 
361
362
  lifted = listener_have_error_async
362
- case _ as never:
363
+ case never:
363
364
  assert_never(never)
364
- case _ as never:
365
+ case never:
365
366
  assert_never(never)
366
367
 
367
368
  if decorators is not None:
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,
@@ -229,7 +229,8 @@ def plot_intraday_dataframe(
229
229
  )
230
230
 
231
231
  data4 = (
232
- data3.group_by("_date_index")
232
+ data3
233
+ .group_by("_date_index")
233
234
  .agg(
234
235
  col(f"_{datetime}_index").min().alias(f"{datetime}_index_min"),
235
236
  (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