dycw-utilities 0.125.10__py3-none-any.whl → 0.125.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.
- {dycw_utilities-0.125.10.dist-info → dycw_utilities-0.125.12.dist-info}/METADATA +30 -30
- {dycw_utilities-0.125.10.dist-info → dycw_utilities-0.125.12.dist-info}/RECORD +6 -6
- utilities/__init__.py +1 -1
- utilities/asyncio.py +453 -9
- {dycw_utilities-0.125.10.dist-info → dycw_utilities-0.125.12.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.125.10.dist-info → dycw_utilities-0.125.12.dist-info}/licenses/LICENSE +0 -0
@@ -1,16 +1,16 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: dycw-utilities
|
3
|
-
Version: 0.125.
|
3
|
+
Version: 0.125.12
|
4
4
|
Author-email: Derek Wan <d.wan@icloud.com>
|
5
5
|
License-File: LICENSE
|
6
6
|
Requires-Python: >=3.12
|
7
7
|
Requires-Dist: typing-extensions<4.14,>=4.13.1
|
8
8
|
Provides-Extra: test
|
9
|
-
Requires-Dist: hypothesis<6.132,>=6.131.
|
9
|
+
Requires-Dist: hypothesis<6.132,>=6.131.30; extra == 'test'
|
10
10
|
Requires-Dist: pytest-asyncio<1.1,>=1.0.0; extra == 'test'
|
11
11
|
Requires-Dist: pytest-cov<6.2,>=6.1.1; extra == 'test'
|
12
12
|
Requires-Dist: pytest-instafail<0.6,>=0.5.0; extra == 'test'
|
13
|
-
Requires-Dist: pytest-lazy-fixtures<1.2,>=1.1.
|
13
|
+
Requires-Dist: pytest-lazy-fixtures<1.2,>=1.1.4; extra == 'test'
|
14
14
|
Requires-Dist: pytest-only<2.2,>=2.1.2; extra == 'test'
|
15
15
|
Requires-Dist: pytest-randomly<3.17,>=3.16.0; extra == 'test'
|
16
16
|
Requires-Dist: pytest-regressions<2.8,>=2.7.0; extra == 'test'
|
@@ -25,8 +25,8 @@ Requires-Dist: altair<5.6,>=5.5.0; extra == 'zzz-test-altair'
|
|
25
25
|
Requires-Dist: atomicwrites<1.5,>=1.4.1; extra == 'zzz-test-altair'
|
26
26
|
Requires-Dist: img2pdf<0.7,>=0.6.0; extra == 'zzz-test-altair'
|
27
27
|
Requires-Dist: polars-lts-cpu<1.31,>=1.30.0; extra == 'zzz-test-altair'
|
28
|
-
Requires-Dist: vl-convert-python<1.
|
29
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
28
|
+
Requires-Dist: vl-convert-python<1.9,>=1.8.0; extra == 'zzz-test-altair'
|
29
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-altair'
|
30
30
|
Provides-Extra: zzz-test-asyncio
|
31
31
|
Provides-Extra: zzz-test-atomicwrites
|
32
32
|
Requires-Dist: atomicwrites<1.5,>=1.4.1; extra == 'zzz-test-atomicwrites'
|
@@ -37,7 +37,7 @@ Requires-Dist: cachetools<5.6,>=5.5.2; extra == 'zzz-test-cachetools'
|
|
37
37
|
Provides-Extra: zzz-test-click
|
38
38
|
Requires-Dist: click<8.3,>=8.2.1; extra == 'zzz-test-click'
|
39
39
|
Requires-Dist: sqlalchemy<2.1,>=2.0.41; extra == 'zzz-test-click'
|
40
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
40
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-click'
|
41
41
|
Provides-Extra: zzz-test-contextlib
|
42
42
|
Provides-Extra: zzz-test-contextvars
|
43
43
|
Provides-Extra: zzz-test-cryptography
|
@@ -47,10 +47,10 @@ Requires-Dist: cvxpy<1.7,>=1.6.5; extra == 'zzz-test-cvxpy'
|
|
47
47
|
Provides-Extra: zzz-test-dataclasses
|
48
48
|
Requires-Dist: orjson<3.11,>=3.10.15; extra == 'zzz-test-dataclasses'
|
49
49
|
Requires-Dist: polars-lts-cpu<1.31,>=1.30.0; extra == 'zzz-test-dataclasses'
|
50
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
50
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-dataclasses'
|
51
51
|
Provides-Extra: zzz-test-datetime
|
52
52
|
Requires-Dist: tzlocal<5.4,>=5.3.1; extra == 'zzz-test-datetime'
|
53
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
53
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-datetime'
|
54
54
|
Provides-Extra: zzz-test-enum
|
55
55
|
Provides-Extra: zzz-test-errors
|
56
56
|
Provides-Extra: zzz-test-eventkit
|
@@ -69,29 +69,29 @@ Provides-Extra: zzz-test-git
|
|
69
69
|
Provides-Extra: zzz-test-hashlib
|
70
70
|
Requires-Dist: orjson<3.11,>=3.10.15; extra == 'zzz-test-hashlib'
|
71
71
|
Requires-Dist: polars-lts-cpu<1.31,>=1.30.0; extra == 'zzz-test-hashlib'
|
72
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
72
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-hashlib'
|
73
73
|
Provides-Extra: zzz-test-http
|
74
74
|
Requires-Dist: atomicwrites<1.5,>=1.4.1; extra == 'zzz-test-http'
|
75
75
|
Requires-Dist: orjson<3.11,>=3.10.18; extra == 'zzz-test-http'
|
76
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
76
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-http'
|
77
77
|
Provides-Extra: zzz-test-hypothesis
|
78
78
|
Requires-Dist: aiosqlite<0.22,>=0.21.0; extra == 'zzz-test-hypothesis'
|
79
79
|
Requires-Dist: asyncpg<0.31,>=0.30.0; extra == 'zzz-test-hypothesis'
|
80
80
|
Requires-Dist: greenlet<3.3,>=3.2.0; extra == 'zzz-test-hypothesis'
|
81
|
-
Requires-Dist: hypothesis<6.132,>=6.131.
|
81
|
+
Requires-Dist: hypothesis<6.132,>=6.131.30; extra == 'zzz-test-hypothesis'
|
82
82
|
Requires-Dist: luigi<3.7,>=3.6.0; extra == 'zzz-test-hypothesis'
|
83
83
|
Requires-Dist: numpy<2.3,>=2.2.6; extra == 'zzz-test-hypothesis'
|
84
84
|
Requires-Dist: pathvalidate<3.3,>=3.2.3; extra == 'zzz-test-hypothesis'
|
85
|
-
Requires-Dist: redis<6.
|
85
|
+
Requires-Dist: redis<6.3,>=6.2.0; extra == 'zzz-test-hypothesis'
|
86
86
|
Requires-Dist: sqlalchemy<2.1,>=2.0.41; extra == 'zzz-test-hypothesis'
|
87
87
|
Requires-Dist: tenacity<9.0,>=8.5.0; extra == 'zzz-test-hypothesis'
|
88
88
|
Requires-Dist: tzlocal<5.4,>=5.3.1; extra == 'zzz-test-hypothesis'
|
89
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
89
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-hypothesis'
|
90
90
|
Provides-Extra: zzz-test-ipython
|
91
91
|
Requires-Dist: ipython<9.1,>=9.0.1; extra == 'zzz-test-ipython'
|
92
92
|
Provides-Extra: zzz-test-iterables
|
93
93
|
Requires-Dist: polars-lts-cpu<1.31,>=1.30.0; extra == 'zzz-test-iterables'
|
94
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
94
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-iterables'
|
95
95
|
Provides-Extra: zzz-test-jupyter
|
96
96
|
Requires-Dist: jupyterlab<4.3,>=4.2.0; extra == 'zzz-test-jupyter'
|
97
97
|
Requires-Dist: pandas<2.3,>=2.2.2; extra == 'zzz-test-jupyter'
|
@@ -103,12 +103,12 @@ Requires-Dist: concurrent-log-handler<0.10,>=0.9.26; extra == 'zzz-test-logging'
|
|
103
103
|
Requires-Dist: rich<14.1,>=14.0.0; extra == 'zzz-test-logging'
|
104
104
|
Requires-Dist: tomlkit<0.14,>=0.13.2; extra == 'zzz-test-logging'
|
105
105
|
Requires-Dist: tzlocal<5.4,>=5.3.1; extra == 'zzz-test-logging'
|
106
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
106
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-logging'
|
107
107
|
Provides-Extra: zzz-test-loguru
|
108
108
|
Requires-Dist: loguru<0.8,>=0.7.3; extra == 'zzz-test-loguru'
|
109
109
|
Provides-Extra: zzz-test-luigi
|
110
110
|
Requires-Dist: luigi<3.7,>=3.6.0; extra == 'zzz-test-luigi'
|
111
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
111
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-luigi'
|
112
112
|
Provides-Extra: zzz-test-math
|
113
113
|
Requires-Dist: numpy<2.3,>=2.2.6; extra == 'zzz-test-math'
|
114
114
|
Provides-Extra: zzz-test-memory-profiler
|
@@ -120,7 +120,7 @@ Provides-Extra: zzz-test-numpy
|
|
120
120
|
Requires-Dist: numpy<2.3,>=2.2.6; extra == 'zzz-test-numpy'
|
121
121
|
Provides-Extra: zzz-test-operator
|
122
122
|
Requires-Dist: polars-lts-cpu<1.31,>=1.30.0; extra == 'zzz-test-operator'
|
123
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
123
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-operator'
|
124
124
|
Provides-Extra: zzz-test-optuna
|
125
125
|
Requires-Dist: optuna<4.4,>=4.3.0; extra == 'zzz-test-optuna'
|
126
126
|
Provides-Extra: zzz-test-orjson
|
@@ -128,7 +128,7 @@ Requires-Dist: orjson<3.11,>=3.10.15; extra == 'zzz-test-orjson'
|
|
128
128
|
Requires-Dist: polars-lts-cpu<1.31,>=1.30.0; extra == 'zzz-test-orjson'
|
129
129
|
Requires-Dist: rich<14.1,>=14.0.0; extra == 'zzz-test-orjson'
|
130
130
|
Requires-Dist: tzlocal<5.4,>=5.3.1; extra == 'zzz-test-orjson'
|
131
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
131
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-orjson'
|
132
132
|
Provides-Extra: zzz-test-os
|
133
133
|
Provides-Extra: zzz-test-pathlib
|
134
134
|
Provides-Extra: zzz-test-pickle
|
@@ -136,7 +136,7 @@ Requires-Dist: atomicwrites<1.5,>=1.4.1; extra == 'zzz-test-pickle'
|
|
136
136
|
Provides-Extra: zzz-test-platform
|
137
137
|
Provides-Extra: zzz-test-polars
|
138
138
|
Requires-Dist: polars-lts-cpu<1.31,>=1.30.0; extra == 'zzz-test-polars'
|
139
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
139
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-polars'
|
140
140
|
Provides-Extra: zzz-test-pqdm
|
141
141
|
Requires-Dist: pqdm<0.3,>=0.2.0; extra == 'zzz-test-pqdm'
|
142
142
|
Provides-Extra: zzz-test-pydantic
|
@@ -151,22 +151,22 @@ Requires-Dist: pyrsistent<0.21,>=0.20.0; extra == 'zzz-test-pyrsistent'
|
|
151
151
|
Provides-Extra: zzz-test-pytest
|
152
152
|
Requires-Dist: atomicwrites<1.5,>=1.4.1; extra == 'zzz-test-pytest'
|
153
153
|
Requires-Dist: orjson<3.11,>=3.10.18; extra == 'zzz-test-pytest'
|
154
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
154
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-pytest'
|
155
155
|
Provides-Extra: zzz-test-pytest-regressions
|
156
156
|
Requires-Dist: pytest-regressions<2.8,>=2.7.0; extra == 'zzz-test-pytest-regressions'
|
157
157
|
Provides-Extra: zzz-test-python-dotenv
|
158
158
|
Requires-Dist: python-dotenv<1.2,>=1.1.0; extra == 'zzz-test-python-dotenv'
|
159
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
159
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-python-dotenv'
|
160
160
|
Provides-Extra: zzz-test-random
|
161
161
|
Provides-Extra: zzz-test-re
|
162
162
|
Provides-Extra: zzz-test-redis
|
163
163
|
Requires-Dist: orjson<3.11,>=3.10.15; extra == 'zzz-test-redis'
|
164
164
|
Requires-Dist: polars-lts-cpu<1.31,>=1.30.0; extra == 'zzz-test-redis'
|
165
|
-
Requires-Dist: redis<6.
|
165
|
+
Requires-Dist: redis<6.3,>=6.2.0; extra == 'zzz-test-redis'
|
166
166
|
Requires-Dist: rich<14.1,>=14.0.0; extra == 'zzz-test-redis'
|
167
167
|
Requires-Dist: tenacity<9.0,>=8.5.0; extra == 'zzz-test-redis'
|
168
168
|
Requires-Dist: tzlocal<5.4,>=5.3.1; extra == 'zzz-test-redis'
|
169
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
169
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-redis'
|
170
170
|
Provides-Extra: zzz-test-rich
|
171
171
|
Requires-Dist: rich<14.1,>=14.0.0; extra == 'zzz-test-rich'
|
172
172
|
Provides-Extra: zzz-test-scipy
|
@@ -174,7 +174,7 @@ Requires-Dist: scipy<1.16,>=1.15.3; extra == 'zzz-test-scipy'
|
|
174
174
|
Provides-Extra: zzz-test-sentinel
|
175
175
|
Provides-Extra: zzz-test-shelve
|
176
176
|
Provides-Extra: zzz-test-slack-sdk
|
177
|
-
Requires-Dist: aiohttp<3.12.
|
177
|
+
Requires-Dist: aiohttp<3.12.5,>=3.12.4; extra == 'zzz-test-slack-sdk'
|
178
178
|
Requires-Dist: slack-sdk<3.36,>=3.35.0; extra == 'zzz-test-slack-sdk'
|
179
179
|
Provides-Extra: zzz-test-socket
|
180
180
|
Provides-Extra: zzz-test-sqlalchemy
|
@@ -192,7 +192,7 @@ Requires-Dist: nest-asyncio<1.7,>=1.6.0; extra == 'zzz-test-sqlalchemy-polars'
|
|
192
192
|
Requires-Dist: polars-lts-cpu<1.31,>=1.30.0; extra == 'zzz-test-sqlalchemy-polars'
|
193
193
|
Requires-Dist: sqlalchemy<2.1,>=2.0.41; extra == 'zzz-test-sqlalchemy-polars'
|
194
194
|
Requires-Dist: tenacity<9.0,>=8.5.0; extra == 'zzz-test-sqlalchemy-polars'
|
195
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
195
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-sqlalchemy-polars'
|
196
196
|
Provides-Extra: zzz-test-streamlit
|
197
197
|
Requires-Dist: streamlit<1.46,>=1.45.0; extra == 'zzz-test-streamlit'
|
198
198
|
Provides-Extra: zzz-test-sys
|
@@ -200,7 +200,7 @@ Requires-Dist: atomicwrites<1.5,>=1.4.1; extra == 'zzz-test-sys'
|
|
200
200
|
Requires-Dist: rich<14.1,>=14.0.0; extra == 'zzz-test-sys'
|
201
201
|
Requires-Dist: tomlkit<0.14,>=0.13.2; extra == 'zzz-test-sys'
|
202
202
|
Requires-Dist: tzlocal<5.4,>=5.3.1; extra == 'zzz-test-sys'
|
203
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
203
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-sys'
|
204
204
|
Provides-Extra: zzz-test-tempfile
|
205
205
|
Provides-Extra: zzz-test-tenacity
|
206
206
|
Requires-Dist: tenacity<9.0,>=8.5.0; extra == 'zzz-test-tenacity'
|
@@ -211,11 +211,11 @@ Provides-Extra: zzz-test-traceback
|
|
211
211
|
Requires-Dist: rich<14.1,>=14.0.0; extra == 'zzz-test-traceback'
|
212
212
|
Requires-Dist: tomlkit<0.14,>=0.13.2; extra == 'zzz-test-traceback'
|
213
213
|
Requires-Dist: tzlocal<5.4,>=5.3.1; extra == 'zzz-test-traceback'
|
214
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
214
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-traceback'
|
215
215
|
Provides-Extra: zzz-test-types
|
216
216
|
Provides-Extra: zzz-test-typing
|
217
217
|
Requires-Dist: polars-lts-cpu<1.31,>=1.30.0; extra == 'zzz-test-typing'
|
218
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
218
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-typing'
|
219
219
|
Provides-Extra: zzz-test-tzlocal
|
220
220
|
Requires-Dist: tzlocal<5.4,>=5.3.1; extra == 'zzz-test-tzlocal'
|
221
221
|
Provides-Extra: zzz-test-uuid
|
@@ -223,11 +223,11 @@ Provides-Extra: zzz-test-version
|
|
223
223
|
Requires-Dist: tomlkit<0.14,>=0.13.2; extra == 'zzz-test-version'
|
224
224
|
Provides-Extra: zzz-test-warnings
|
225
225
|
Provides-Extra: zzz-test-whenever
|
226
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
226
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-whenever'
|
227
227
|
Provides-Extra: zzz-test-zipfile
|
228
228
|
Provides-Extra: zzz-test-zoneinfo
|
229
229
|
Requires-Dist: tzdata<2025.3,>=2025.2; extra == 'zzz-test-zoneinfo'
|
230
|
-
Requires-Dist: whenever<0.9,>=0.8.
|
230
|
+
Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-zoneinfo'
|
231
231
|
Description-Content-Type: text/markdown
|
232
232
|
|
233
233
|
[](https://badge.fury.io/py/dycw-utilities)
|
@@ -1,6 +1,6 @@
|
|
1
|
-
utilities/__init__.py,sha256=
|
1
|
+
utilities/__init__.py,sha256=eu0r7qVMzjFJgG7Su5IuWbXETZTCy5nXfNr7kwpH_yw,61
|
2
2
|
utilities/altair.py,sha256=Gpja-flOo-Db0PIPJLJsgzAlXWoKUjPU1qY-DQ829ek,9156
|
3
|
-
utilities/asyncio.py,sha256=
|
3
|
+
utilities/asyncio.py,sha256=G7CaIcR-oANVjKWIH7KFMHpPZqL1CkwCV6FBT8vrkKA,46941
|
4
4
|
utilities/atomicwrites.py,sha256=geFjn9Pwn-tTrtoGjDDxWli9NqbYfy3gGL6ZBctiqSo,5393
|
5
5
|
utilities/atools.py,sha256=IYMuFSFGSKyuQmqD6v5IUtDlz8PPw0Sr87Cub_gRU3M,1168
|
6
6
|
utilities/cachetools.py,sha256=C1zqOg7BYz0IfQFK8e3qaDDgEZxDpo47F15RTfJM37Q,2910
|
@@ -88,7 +88,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
|
|
88
88
|
utilities/whenever.py,sha256=jS31ZAY5OMxFxLja_Yo5Fidi87Pd-GoVZ7Vi_teqVDA,16743
|
89
89
|
utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
|
90
90
|
utilities/zoneinfo.py,sha256=-5j7IQ9nb7gR43rdgA7ms05im-XuqhAk9EJnQBXxCoQ,1874
|
91
|
-
dycw_utilities-0.125.
|
92
|
-
dycw_utilities-0.125.
|
93
|
-
dycw_utilities-0.125.
|
94
|
-
dycw_utilities-0.125.
|
91
|
+
dycw_utilities-0.125.12.dist-info/METADATA,sha256=9JECEW7LQjwHf5sjb53N4xApsa3V9RImfG__u2LDN4E,12852
|
92
|
+
dycw_utilities-0.125.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
93
|
+
dycw_utilities-0.125.12.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
|
94
|
+
dycw_utilities-0.125.12.dist-info/RECORD,,
|
utilities/__init__.py
CHANGED
utilities/asyncio.py
CHANGED
@@ -29,7 +29,7 @@ from contextlib import (
|
|
29
29
|
from dataclasses import dataclass, field
|
30
30
|
from io import StringIO
|
31
31
|
from itertools import chain
|
32
|
-
from logging import getLogger
|
32
|
+
from logging import DEBUG, Logger, getLogger
|
33
33
|
from subprocess import PIPE
|
34
34
|
from sys import stderr, stdout
|
35
35
|
from typing import (
|
@@ -47,6 +47,7 @@ from typing import (
|
|
47
47
|
|
48
48
|
from typing_extensions import deprecated
|
49
49
|
|
50
|
+
from utilities.dataclasses import replace_non_sentinel
|
50
51
|
from utilities.datetime import (
|
51
52
|
MINUTE,
|
52
53
|
SECOND,
|
@@ -610,12 +611,9 @@ class InfiniteQueueLooper(InfiniteLooper[THashable], Generic[THashable, _T]):
|
|
610
611
|
"""An infinite loop which processes a queue."""
|
611
612
|
|
612
613
|
_await_upon_aenter: bool = field(default=False, init=False, repr=False)
|
613
|
-
_queue: EnhancedQueue[_T] = field(
|
614
|
-
|
615
|
-
|
616
|
-
def __post_init__(self) -> None:
|
617
|
-
super().__post_init__()
|
618
|
-
self._queue = EnhancedQueue()
|
614
|
+
_queue: EnhancedQueue[_T] = field(
|
615
|
+
default_factory=EnhancedQueue, init=False, repr=False
|
616
|
+
)
|
619
617
|
|
620
618
|
def __len__(self) -> int:
|
621
619
|
return self._queue.qsize()
|
@@ -623,8 +621,8 @@ class InfiniteQueueLooper(InfiniteLooper[THashable], Generic[THashable, _T]):
|
|
623
621
|
@override
|
624
622
|
async def _core(self) -> None:
|
625
623
|
"""Run the core part of the loop."""
|
626
|
-
|
627
|
-
|
624
|
+
if self.empty():
|
625
|
+
return
|
628
626
|
await self._process_queue()
|
629
627
|
|
630
628
|
@abstractmethod
|
@@ -643,6 +641,10 @@ class InfiniteQueueLooper(InfiniteLooper[THashable], Generic[THashable, _T]):
|
|
643
641
|
"""Put items into the queue at the end without blocking."""
|
644
642
|
self._queue.put_right_nowait(*items) # pragma: no cover
|
645
643
|
|
644
|
+
def qsize(self) -> int:
|
645
|
+
"""Get the number of items in the queue."""
|
646
|
+
return self._queue.qsize()
|
647
|
+
|
646
648
|
async def run_until_empty(self, *, stop: bool = False) -> None:
|
647
649
|
"""Run until the queue is empty."""
|
648
650
|
while not self.empty():
|
@@ -654,6 +656,445 @@ class InfiniteQueueLooper(InfiniteLooper[THashable], Generic[THashable, _T]):
|
|
654
656
|
##
|
655
657
|
|
656
658
|
|
659
|
+
@dataclass(kw_only=True, slots=True)
|
660
|
+
class LooperError(Exception): ...
|
661
|
+
|
662
|
+
|
663
|
+
@dataclass(kw_only=True, slots=True)
|
664
|
+
class LooperTimeoutError(LooperError):
|
665
|
+
duration: Duration | None = None
|
666
|
+
|
667
|
+
@override
|
668
|
+
def __str__(self) -> str:
|
669
|
+
return "Timeout" if self.duration is None else f"Timeout after {self.duration}"
|
670
|
+
|
671
|
+
|
672
|
+
@dataclass(kw_only=True, slots=True)
|
673
|
+
class _LooperNoTaskError(LooperError):
|
674
|
+
looper: Looper
|
675
|
+
|
676
|
+
@override
|
677
|
+
def __str__(self) -> str:
|
678
|
+
return f"{self.looper} has no running task"
|
679
|
+
|
680
|
+
|
681
|
+
@dataclass(kw_only=True, unsafe_hash=True)
|
682
|
+
class Looper(Generic[_T]):
|
683
|
+
"""A looper of a core coroutine, handling errors."""
|
684
|
+
|
685
|
+
auto_start: bool = field(default=False, repr=False)
|
686
|
+
freq: Duration = field(default=SECOND, repr=False)
|
687
|
+
backoff: Duration = field(default=10 * SECOND, repr=False)
|
688
|
+
logger: str | None = field(default=None, repr=False)
|
689
|
+
timeout: Duration | None = field(default=None, repr=False)
|
690
|
+
timeout_error: type[Exception] = field(default=LooperTimeoutError, repr=False)
|
691
|
+
# settings
|
692
|
+
_backoff: float = field(init=False, repr=False)
|
693
|
+
_debug: bool = field(default=False, repr=False)
|
694
|
+
_freq: float = field(init=False, repr=False)
|
695
|
+
# counts
|
696
|
+
_entries: int = field(default=0, init=False, repr=False)
|
697
|
+
_core_attempts: int = field(default=0, init=False, repr=False)
|
698
|
+
_core_successes: int = field(default=0, init=False, repr=False)
|
699
|
+
_core_failures: int = field(default=0, init=False, repr=False)
|
700
|
+
_initialization_attempts: int = field(default=0, init=False, repr=False)
|
701
|
+
_initialization_successes: int = field(default=0, init=False, repr=False)
|
702
|
+
_initialization_failures: int = field(default=0, init=False, repr=False)
|
703
|
+
_tear_down_attempts: int = field(default=0, init=False, repr=False)
|
704
|
+
_tear_down_successes: int = field(default=0, init=False, repr=False)
|
705
|
+
_tear_down_failures: int = field(default=0, init=False, repr=False)
|
706
|
+
_restart_attempts: int = field(default=0, init=False, repr=False)
|
707
|
+
_restart_successes: int = field(default=0, init=False, repr=False)
|
708
|
+
_restart_failures: int = field(default=0, init=False, repr=False)
|
709
|
+
_stops: int = field(default=0, init=False, repr=False)
|
710
|
+
# flags
|
711
|
+
_is_entered: Event = field(default_factory=Event, init=False, repr=False)
|
712
|
+
_is_initialized: Event = field(default_factory=Event, init=False, repr=False)
|
713
|
+
_is_initializing: Event = field(default_factory=Event, init=False, repr=False)
|
714
|
+
_is_pending_restart: Event = field(default_factory=Event, init=False, repr=False)
|
715
|
+
_is_pending_stop: Event = field(default_factory=Event, init=False, repr=False)
|
716
|
+
_is_pending_stop_when_empty: Event = field(
|
717
|
+
default_factory=Event, init=False, repr=False
|
718
|
+
)
|
719
|
+
_is_stopped: Event = field(default_factory=Event, init=False, repr=False)
|
720
|
+
_is_tearing_down: Event = field(default_factory=Event, init=False, repr=False)
|
721
|
+
# internal objects
|
722
|
+
_logger: Logger = field(init=False, repr=False, hash=False)
|
723
|
+
_queue: EnhancedQueue[_T] = field(
|
724
|
+
default_factory=EnhancedQueue, init=False, repr=False, hash=False
|
725
|
+
)
|
726
|
+
_stack: AsyncExitStack = field(
|
727
|
+
default_factory=AsyncExitStack, init=False, repr=False, hash=False
|
728
|
+
)
|
729
|
+
_task: Task[None] | None = field(default=None, init=False, repr=False, hash=False)
|
730
|
+
|
731
|
+
def __post_init__(self) -> None:
|
732
|
+
self._backoff = datetime_duration_to_float(self.backoff)
|
733
|
+
self._freq = datetime_duration_to_float(self.freq)
|
734
|
+
self._logger = getLogger(name=self.logger)
|
735
|
+
self._logger.setLevel(DEBUG)
|
736
|
+
|
737
|
+
async def __aenter__(self) -> Self:
|
738
|
+
"""Enter the context manager."""
|
739
|
+
match self._is_entered.is_set():
|
740
|
+
case True:
|
741
|
+
_ = self._debug and self._logger.debug("%s: already entered", self)
|
742
|
+
case False:
|
743
|
+
_ = self._debug and self._logger.debug("%s: entering context...", self)
|
744
|
+
self._is_entered.set()
|
745
|
+
self._entries += 1
|
746
|
+
self._task = create_task(self.run_looper())
|
747
|
+
for looper in self._yield_sub_loopers():
|
748
|
+
_ = self._debug and self._logger.debug(
|
749
|
+
"%s: adding sub-looper %s", self, looper
|
750
|
+
)
|
751
|
+
if not looper.auto_start:
|
752
|
+
self._logger.warning(
|
753
|
+
"%s: changing sub-looper %s to auto-start...", self, looper
|
754
|
+
)
|
755
|
+
looper.auto_start = True
|
756
|
+
_ = await self._stack.enter_async_context(looper)
|
757
|
+
if self.auto_start:
|
758
|
+
_ = self._debug and self._logger.debug("%s: auto-starting...", self)
|
759
|
+
with suppress(self.timeout_error):
|
760
|
+
await self._task
|
761
|
+
case _ as never:
|
762
|
+
assert_never(never)
|
763
|
+
return self
|
764
|
+
|
765
|
+
async def __aexit__(
|
766
|
+
self,
|
767
|
+
exc_type: type[BaseException] | None = None,
|
768
|
+
exc_value: BaseException | None = None,
|
769
|
+
traceback: TracebackType | None = None,
|
770
|
+
) -> None:
|
771
|
+
"""Exit the context manager."""
|
772
|
+
match self._is_entered.is_set():
|
773
|
+
case True:
|
774
|
+
_ = self._debug and self._logger.debug("%s: exiting context...", self)
|
775
|
+
self._is_entered.clear()
|
776
|
+
if (
|
777
|
+
(exc_type is not None)
|
778
|
+
and (exc_value is not None)
|
779
|
+
and (traceback is not None)
|
780
|
+
):
|
781
|
+
_ = self._debug and self._logger.warning(
|
782
|
+
"%s: encountered %s whilst in context",
|
783
|
+
self,
|
784
|
+
repr_error(exc_value),
|
785
|
+
)
|
786
|
+
_ = await self._stack.__aexit__(exc_type, exc_value, traceback)
|
787
|
+
await self.stop()
|
788
|
+
case False:
|
789
|
+
_ = self._debug and self._logger.debug("%s: already exited", self)
|
790
|
+
case _ as never:
|
791
|
+
assert_never(never)
|
792
|
+
|
793
|
+
def __await__(self) -> Any:
|
794
|
+
match self._task:
|
795
|
+
case None:
|
796
|
+
raise _LooperNoTaskError(looper=self)
|
797
|
+
case Task() as task:
|
798
|
+
return task.__await__()
|
799
|
+
case _ as never:
|
800
|
+
self._logger.warning( # pragma: no cover
|
801
|
+
"Got %s of type %s", self._task, type(self._task)
|
802
|
+
)
|
803
|
+
assert_never(never)
|
804
|
+
|
805
|
+
def __len__(self) -> int:
|
806
|
+
return self._queue.qsize()
|
807
|
+
|
808
|
+
async def core(self) -> None:
|
809
|
+
"""Core part of running the looper."""
|
810
|
+
|
811
|
+
def empty(self) -> bool:
|
812
|
+
"""Check if the queue is empty."""
|
813
|
+
return self._queue.empty()
|
814
|
+
|
815
|
+
def get_left_nowait(self) -> _T:
|
816
|
+
"""Remove and return an item from the start of the queue without blocking."""
|
817
|
+
return self._queue.get_left_nowait()
|
818
|
+
|
819
|
+
def get_right_nowait(self) -> _T:
|
820
|
+
"""Remove and return an item from the end of the queue without blocking."""
|
821
|
+
return self._queue.get_right_nowait()
|
822
|
+
|
823
|
+
async def initialize(self) -> Exception | None:
|
824
|
+
"""Initialize the looper."""
|
825
|
+
match self._is_initializing.is_set():
|
826
|
+
case True:
|
827
|
+
_ = self._debug and self._logger.debug("%s: already initializing", self)
|
828
|
+
return None
|
829
|
+
case False:
|
830
|
+
_ = self._debug and self._logger.debug("%s: initializing...", self)
|
831
|
+
self._is_initializing.set()
|
832
|
+
self._is_initialized.clear()
|
833
|
+
self._initialization_attempts += 1
|
834
|
+
try:
|
835
|
+
await self._initialize_core()
|
836
|
+
except Exception as error: # noqa: BLE001
|
837
|
+
_ = self._logger.warning(
|
838
|
+
"%s: encountered %s whilst initializing",
|
839
|
+
self,
|
840
|
+
repr_error(error),
|
841
|
+
)
|
842
|
+
self._initialization_failures += 1
|
843
|
+
ret = error
|
844
|
+
else:
|
845
|
+
_ = self._debug and self._logger.debug(
|
846
|
+
"%s: finished initializing", self
|
847
|
+
)
|
848
|
+
self._is_initialized.set()
|
849
|
+
self._initialization_successes += 1
|
850
|
+
ret = None
|
851
|
+
finally:
|
852
|
+
self._is_initializing.clear()
|
853
|
+
return ret
|
854
|
+
case _ as never:
|
855
|
+
assert_never(never)
|
856
|
+
|
857
|
+
async def _initialize_core(self) -> None:
|
858
|
+
"""Core part of initializing the looper."""
|
859
|
+
|
860
|
+
def put_left_nowait(self, *items: _T) -> None:
|
861
|
+
"""Put items into the queue at the start without blocking."""
|
862
|
+
self._queue.put_left_nowait(*items)
|
863
|
+
|
864
|
+
def put_right_nowait(self, *items: _T) -> None:
|
865
|
+
"""Put items into the queue at the end without blocking."""
|
866
|
+
self._queue.put_right_nowait(*items)
|
867
|
+
|
868
|
+
def qsize(self) -> int:
|
869
|
+
"""Get the number of items in the queue."""
|
870
|
+
return self._queue.qsize()
|
871
|
+
|
872
|
+
def replace(
|
873
|
+
self,
|
874
|
+
*,
|
875
|
+
auto_start: bool | Sentinel = sentinel,
|
876
|
+
freq: Duration | Sentinel = sentinel,
|
877
|
+
backoff: Duration | Sentinel = sentinel,
|
878
|
+
logger: str | None | Sentinel = sentinel,
|
879
|
+
timeout: Duration | None | Sentinel = sentinel,
|
880
|
+
) -> Self:
|
881
|
+
"""Replace elements of the looper."""
|
882
|
+
return replace_non_sentinel(
|
883
|
+
self,
|
884
|
+
auto_start=auto_start,
|
885
|
+
freq=freq,
|
886
|
+
backoff=backoff,
|
887
|
+
logger=logger,
|
888
|
+
timeout=timeout,
|
889
|
+
)
|
890
|
+
|
891
|
+
def request_restart(self) -> None:
|
892
|
+
"""Request the looper to restart."""
|
893
|
+
match self._is_pending_restart.is_set():
|
894
|
+
case True:
|
895
|
+
_ = self._debug and self._logger.debug(
|
896
|
+
"%s: already requested restart", self
|
897
|
+
)
|
898
|
+
case False:
|
899
|
+
_ = self._debug and self._logger.debug(
|
900
|
+
"%s: requesting restart...", self
|
901
|
+
)
|
902
|
+
self._is_pending_restart.set()
|
903
|
+
case _ as never:
|
904
|
+
assert_never(never)
|
905
|
+
|
906
|
+
def request_stop(self) -> None:
|
907
|
+
"""Request the looper to stop."""
|
908
|
+
match self._is_pending_stop.is_set():
|
909
|
+
case True:
|
910
|
+
_ = self._debug and self._logger.debug(
|
911
|
+
"%s: already requested stop", self
|
912
|
+
)
|
913
|
+
case False:
|
914
|
+
_ = self._debug and self._logger.debug("%s: requesting stop...", self)
|
915
|
+
self._is_pending_stop.set()
|
916
|
+
case _ as never:
|
917
|
+
assert_never(never)
|
918
|
+
|
919
|
+
def request_stop_when_empty(self) -> None:
|
920
|
+
"""Request the looper to stop when the queue is empty."""
|
921
|
+
match self._is_pending_stop_when_empty.is_set():
|
922
|
+
case True:
|
923
|
+
_ = self._debug and self._logger.debug(
|
924
|
+
"%s: already requested stop when empty", self
|
925
|
+
)
|
926
|
+
case False:
|
927
|
+
_ = self._debug and self._logger.debug(
|
928
|
+
"%s: requesting stop when empty...", self
|
929
|
+
)
|
930
|
+
self._is_pending_stop_when_empty.set()
|
931
|
+
case _ as never:
|
932
|
+
assert_never(never)
|
933
|
+
|
934
|
+
async def restart(self) -> None:
|
935
|
+
"""Restart the looper."""
|
936
|
+
_ = self._debug and self._logger.debug("%s: restarting...", self)
|
937
|
+
self._is_pending_restart.clear()
|
938
|
+
self._restart_attempts += 1
|
939
|
+
tear_down = await self.tear_down()
|
940
|
+
initialization = await self.initialize()
|
941
|
+
match tear_down, initialization:
|
942
|
+
case None, None:
|
943
|
+
_ = self._debug and self._logger.debug("%s: finished restarting", self)
|
944
|
+
self._restart_successes += 1
|
945
|
+
case Exception(), None:
|
946
|
+
_ = self._logger.warning(
|
947
|
+
"%s: encountered %s whilst restarting, during tear down",
|
948
|
+
self,
|
949
|
+
repr_error(tear_down),
|
950
|
+
)
|
951
|
+
self._restart_failures += 1
|
952
|
+
case None, Exception():
|
953
|
+
_ = self._logger.warning(
|
954
|
+
"%s: encountered %s whilst restarting, during initialization",
|
955
|
+
self,
|
956
|
+
repr_error(initialization),
|
957
|
+
)
|
958
|
+
self._restart_failures += 1
|
959
|
+
case Exception(), Exception():
|
960
|
+
_ = self._logger.warning(
|
961
|
+
"%s: encountered %s (tear down) and then %s (initialization) whilst restarting",
|
962
|
+
self,
|
963
|
+
repr_error(tear_down),
|
964
|
+
repr_error(initialization),
|
965
|
+
)
|
966
|
+
self._restart_failures += 1
|
967
|
+
case _ as never:
|
968
|
+
assert_never(never)
|
969
|
+
|
970
|
+
async def run_looper(self) -> None:
|
971
|
+
"""Run the looper."""
|
972
|
+
async with timeout_dur(duration=self.timeout, error=self.timeout_error):
|
973
|
+
while True:
|
974
|
+
if self._is_stopped.is_set():
|
975
|
+
_ = self._debug and self._logger.debug("%s: stopped", self)
|
976
|
+
return
|
977
|
+
if (self._is_pending_stop.is_set()) or (
|
978
|
+
self._is_pending_stop_when_empty.is_set() and self.empty()
|
979
|
+
):
|
980
|
+
await self.stop()
|
981
|
+
elif self._is_pending_restart.is_set():
|
982
|
+
await self.restart()
|
983
|
+
elif not self._is_initialized.is_set():
|
984
|
+
_ = await self.initialize()
|
985
|
+
else:
|
986
|
+
_ = self._debug and self._logger.debug("%s: running core...", self)
|
987
|
+
self._core_attempts += 1
|
988
|
+
try:
|
989
|
+
await self.core()
|
990
|
+
except Exception as error: # noqa: BLE001
|
991
|
+
_ = self._logger.warning(
|
992
|
+
"%s: encountered %s whilst running core...",
|
993
|
+
self,
|
994
|
+
repr_error(error),
|
995
|
+
)
|
996
|
+
self._core_failures += 1
|
997
|
+
self.request_restart()
|
998
|
+
await sleep(self._backoff)
|
999
|
+
else:
|
1000
|
+
self._core_successes += 1
|
1001
|
+
await sleep(self._freq)
|
1002
|
+
|
1003
|
+
@property
|
1004
|
+
def stats(self) -> _LooperStats:
|
1005
|
+
"""Return the statistics."""
|
1006
|
+
return _LooperStats(
|
1007
|
+
entries=self._entries,
|
1008
|
+
core_attempts=self._core_attempts,
|
1009
|
+
core_successes=self._core_successes,
|
1010
|
+
core_failures=self._core_failures,
|
1011
|
+
initialization_attempts=self._initialization_attempts,
|
1012
|
+
initialization_successes=self._initialization_successes,
|
1013
|
+
initialization_failures=self._initialization_failures,
|
1014
|
+
tear_down_attempts=self._tear_down_attempts,
|
1015
|
+
tear_down_successes=self._tear_down_successes,
|
1016
|
+
tear_down_failures=self._tear_down_failures,
|
1017
|
+
restart_attempts=self._restart_attempts,
|
1018
|
+
restart_successes=self._restart_successes,
|
1019
|
+
restart_failures=self._restart_failures,
|
1020
|
+
stops=self._stops,
|
1021
|
+
)
|
1022
|
+
|
1023
|
+
async def stop(self) -> None:
|
1024
|
+
"""Stop the looper."""
|
1025
|
+
match self._is_stopped.is_set():
|
1026
|
+
case True:
|
1027
|
+
_ = self._debug and self._logger.debug("%s: already stopped", self)
|
1028
|
+
case False:
|
1029
|
+
_ = self._debug and self._logger.debug("%s: stopping...", self)
|
1030
|
+
self._is_pending_stop.clear()
|
1031
|
+
self._is_stopped.set()
|
1032
|
+
self._stops += 1
|
1033
|
+
_ = self._debug and self._logger.debug("%s: stopped", self)
|
1034
|
+
case _ as never:
|
1035
|
+
assert_never(never)
|
1036
|
+
|
1037
|
+
async def tear_down(self) -> Exception | None:
|
1038
|
+
"""Tear down the looper."""
|
1039
|
+
match self._is_tearing_down.is_set():
|
1040
|
+
case True:
|
1041
|
+
_ = self._debug and self._logger.debug("%s: already tearing down", self)
|
1042
|
+
return None
|
1043
|
+
case False:
|
1044
|
+
_ = self._debug and self._logger.debug("%s: tearing down...", self)
|
1045
|
+
self._is_tearing_down.set()
|
1046
|
+
self._tear_down_attempts += 1
|
1047
|
+
try:
|
1048
|
+
await self._tear_down_core()
|
1049
|
+
except Exception as error: # noqa: BLE001
|
1050
|
+
_ = self._logger.warning(
|
1051
|
+
"%s: encountered %s whilst tearing down",
|
1052
|
+
self,
|
1053
|
+
repr_error(error),
|
1054
|
+
)
|
1055
|
+
self._tear_down_failures += 1
|
1056
|
+
ret = error
|
1057
|
+
else:
|
1058
|
+
_ = self._debug and self._logger.debug(
|
1059
|
+
"%s: finished tearing down", self
|
1060
|
+
)
|
1061
|
+
self._tear_down_successes += 1
|
1062
|
+
ret = None
|
1063
|
+
finally:
|
1064
|
+
self._is_tearing_down.clear()
|
1065
|
+
return ret
|
1066
|
+
case _ as never:
|
1067
|
+
assert_never(never)
|
1068
|
+
|
1069
|
+
async def _tear_down_core(self) -> None:
|
1070
|
+
"""Core part of tearing down the looper."""
|
1071
|
+
|
1072
|
+
def _yield_sub_loopers(self) -> Iterator[Looper]:
|
1073
|
+
"""Yield all sub-loopers."""
|
1074
|
+
yield from []
|
1075
|
+
|
1076
|
+
|
1077
|
+
@dataclass(kw_only=True, slots=True)
|
1078
|
+
class _LooperStats:
|
1079
|
+
entries: int = 0
|
1080
|
+
core_attempts: int = 0
|
1081
|
+
core_successes: int = 0
|
1082
|
+
core_failures: int = 0
|
1083
|
+
initialization_attempts: int = 0
|
1084
|
+
initialization_successes: int = 0
|
1085
|
+
initialization_failures: int = 0
|
1086
|
+
tear_down_attempts: int = 0
|
1087
|
+
tear_down_successes: int = 0
|
1088
|
+
tear_down_failures: int = 0
|
1089
|
+
restart_attempts: int = 0
|
1090
|
+
restart_successes: int = 0
|
1091
|
+
restart_failures: int = 0
|
1092
|
+
stops: int = 0
|
1093
|
+
|
1094
|
+
|
1095
|
+
##
|
1096
|
+
|
1097
|
+
|
657
1098
|
class UniquePriorityQueue(PriorityQueue[tuple[TSupportsRichComparison, THashable]]):
|
658
1099
|
"""Priority queue with unique tasks."""
|
659
1100
|
|
@@ -878,6 +1319,9 @@ __all__ = [
|
|
878
1319
|
"InfiniteLooper",
|
879
1320
|
"InfiniteLooperError",
|
880
1321
|
"InfiniteQueueLooper",
|
1322
|
+
"Looper",
|
1323
|
+
"LooperError",
|
1324
|
+
"LooperTimeoutError",
|
881
1325
|
"StreamCommandOutput",
|
882
1326
|
"UniquePriorityQueue",
|
883
1327
|
"UniqueQueue",
|
File without changes
|
File without changes
|