asyncz 0.8.2__tar.gz → 0.9.0__tar.gz
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.
- {asyncz-0.8.2 → asyncz-0.9.0}/.gitignore +1 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/PKG-INFO +93 -6
- {asyncz-0.8.2 → asyncz-0.9.0}/README.md +85 -3
- asyncz-0.9.0/asyncz/__init__.py +1 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/datastructures.py +2 -2
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/enums.py +3 -3
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/events/base.py +2 -2
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/exceptions.py +6 -6
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/executors/asyncio.py +10 -8
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/executors/base.py +28 -36
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/executors/debug.py +6 -8
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/executors/pool.py +4 -4
- asyncz-0.9.0/asyncz/executors/types.py +62 -0
- asyncz-0.9.0/asyncz/schedulers/asgi.py +69 -0
- asyncz-0.9.0/asyncz/schedulers/asyncio.py +69 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/schedulers/base.py +169 -112
- asyncz-0.9.0/asyncz/schedulers/datastructures.py +12 -0
- asyncz-0.9.0/asyncz/schedulers/defaults.py +25 -0
- asyncz-0.9.0/asyncz/schedulers/types.py +400 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/schedulers/utils.py +1 -1
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/state.py +1 -1
- asyncz-0.9.0/asyncz/stores/base.py +72 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/stores/memory.py +14 -13
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/stores/mongo.py +29 -20
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/stores/redis.py +32 -31
- asyncz-0.9.0/asyncz/stores/sqlalchemy.py +173 -0
- asyncz-0.8.2/asyncz/stores/base.py → asyncz-0.9.0/asyncz/stores/types.py +20 -37
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/tasks/base.py +46 -45
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/triggers/base.py +19 -28
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/triggers/combination.py +22 -18
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/triggers/cron/expressions.py +36 -37
- asyncz-0.9.0/asyncz/triggers/cron/fields.py +121 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/triggers/cron/trigger.py +12 -9
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/triggers/date.py +5 -8
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/triggers/interval.py +18 -15
- asyncz-0.9.0/asyncz/triggers/types.py +36 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/typing.py +1 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/utils.py +98 -38
- {asyncz-0.8.2 → asyncz-0.9.0}/pyproject.toml +36 -15
- asyncz-0.8.2/asyncz/__init__.py +0 -1
- asyncz-0.8.2/asyncz/_mapping.py +0 -33
- asyncz-0.8.2/asyncz/contrib/esmerald/__init__.py +0 -0
- asyncz-0.8.2/asyncz/contrib/esmerald/decorator.py +0 -40
- asyncz-0.8.2/asyncz/contrib/esmerald/scheduler.py +0 -217
- asyncz-0.8.2/asyncz/executors/types.py +0 -7
- asyncz-0.8.2/asyncz/schedulers/asyncio.py +0 -64
- asyncz-0.8.2/asyncz/schedulers/datastructures.py +0 -49
- asyncz-0.8.2/asyncz/schedulers/types.py +0 -3
- asyncz-0.8.2/asyncz/stores/__init__.py +0 -0
- asyncz-0.8.2/asyncz/stores/types.py +0 -8
- asyncz-0.8.2/asyncz/triggers/cron/fields.py +0 -131
- asyncz-0.8.2/asyncz/triggers/types.py +0 -9
- {asyncz-0.8.2 → asyncz-0.9.0}/LICENSE +0 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/events/__init__.py +0 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/events/constants.py +0 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/executors/__init__.py +0 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/py.typed +0 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/schedulers/__init__.py +0 -0
- {asyncz-0.8.2/asyncz/contrib → asyncz-0.9.0/asyncz/stores}/__init__.py +0 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/tasks/__init__.py +0 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/tasks/types.py +0 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/triggers/__init__.py +0 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/triggers/cron/__init__.py +0 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/triggers/cron/constants.py +0 -0
- {asyncz-0.8.2 → asyncz-0.9.0}/asyncz/triggers/cron/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: asyncz
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: The scheduler that nobody wants but every application needs.
|
|
5
5
|
Project-URL: Homepage, https://github.com/dymmond/asyncz
|
|
6
6
|
Project-URL: Documentation, https://asyncz.dymmond.com/
|
|
@@ -10,7 +10,7 @@ Project-URL: Source, https://github.com/dymmond/asyncz
|
|
|
10
10
|
Author-email: Tiago Silva <tiago.arasilva@gmail.com>
|
|
11
11
|
License-Expression: MIT
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Keywords:
|
|
13
|
+
Keywords: apscheduler,asgi,asyncz,cron,fastapi,framework,pydantic,scheduler,starlette
|
|
14
14
|
Classifier: Development Status :: 4 - Beta
|
|
15
15
|
Classifier: Environment :: Web Environment
|
|
16
16
|
Classifier: Framework :: AnyIO
|
|
@@ -38,19 +38,24 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
38
38
|
Classifier: Typing :: Typed
|
|
39
39
|
Requires-Python: >=3.8
|
|
40
40
|
Requires-Dist: backports-zoneinfo; python_version < '3.9'
|
|
41
|
+
Requires-Dist: cryptography
|
|
41
42
|
Requires-Dist: loguru<0.8.0,>=0.7.0
|
|
42
43
|
Requires-Dist: pydantic<3.0.0,>=2.5.3
|
|
43
44
|
Requires-Dist: tzlocal<6.0,>=4.0
|
|
44
45
|
Provides-Extra: testing
|
|
45
46
|
Requires-Dist: esmerald; extra == 'testing'
|
|
47
|
+
Requires-Dist: httpx; extra == 'testing'
|
|
48
|
+
Requires-Dist: lilya; extra == 'testing'
|
|
46
49
|
Requires-Dist: mypy<2.0.0,>=0.982; extra == 'testing'
|
|
47
50
|
Requires-Dist: pymongo<5.0.0,>=4.3.3; extra == 'testing'
|
|
48
|
-
Requires-Dist: pytest-asyncio<1.0.0,>=0.
|
|
51
|
+
Requires-Dist: pytest-asyncio<1.0.0,>=0.23.0; extra == 'testing'
|
|
49
52
|
Requires-Dist: pytest-cov<6.0.0,>=2.12.0; extra == 'testing'
|
|
50
53
|
Requires-Dist: pytest-loguru<1,>=0.2.0; extra == 'testing'
|
|
51
54
|
Requires-Dist: pytest<9.0.0,>=7.1.3; extra == 'testing'
|
|
52
55
|
Requires-Dist: pytz>=2022.6; extra == 'testing'
|
|
53
56
|
Requires-Dist: redis<6.0.0,>=4.4.0; extra == 'testing'
|
|
57
|
+
Requires-Dist: sqlalchemy; extra == 'testing'
|
|
58
|
+
Requires-Dist: starlette; extra == 'testing'
|
|
54
59
|
Description-Content-Type: text/markdown
|
|
55
60
|
|
|
56
61
|
# Asyncz
|
|
@@ -210,9 +215,91 @@ fun 😁🎉.
|
|
|
210
215
|
Asyncz currently supports the [Esmerald framework](https://asyncz.dymmond.com/contrib/esmerald/index.md)
|
|
211
216
|
and brings some batteries that are currently used by the framework and leveraging Asyncz.
|
|
212
217
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from asyncz.schedulers import AsyncIOScheduler
|
|
221
|
+
...
|
|
222
|
+
|
|
223
|
+
# handle_lifespan is optional, set to True if you don't want to pass it down because the underlying app doesn't support it
|
|
224
|
+
# this is true for django
|
|
225
|
+
application = AsyncIOScheduler().asgi(application, handle_lifespan=False)
|
|
226
|
+
# or more simple (please do not use both together)
|
|
227
|
+
application = AsyncIOScheduler().asgi()(application)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Using with lilya:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from asyncz.schedulers import AsyncIOScheduler
|
|
234
|
+
|
|
235
|
+
# Lilya middleware doesn't pass lifespan events
|
|
236
|
+
|
|
237
|
+
app = AsyncIOScheduler().asgi(Lilya(
|
|
238
|
+
routes=[...],
|
|
239
|
+
))
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Or manually:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from asyncz.schedulers import AsyncIOScheduler
|
|
247
|
+
|
|
248
|
+
scheduler = AsyncIOScheduler()
|
|
249
|
+
|
|
250
|
+
app = Lilya(
|
|
251
|
+
routes=[...],
|
|
252
|
+
on_startup=[scheduler.start],
|
|
253
|
+
on_shutdown=[scheduler.shutdown],
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
## Contextmanager support
|
|
260
|
+
|
|
261
|
+
Use as sync contextmanager
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
from asyncz.schedulers import AsyncIOScheduler
|
|
265
|
+
|
|
266
|
+
with AsyncIOScheduler() as scheduler:
|
|
267
|
+
...
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Use as async contextmanager
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
from asyncz.schedulers import AsyncIOScheduler
|
|
274
|
+
|
|
275
|
+
async with AsyncIOScheduler() as scheduler:
|
|
276
|
+
...
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
For using with lifespan of starlette:
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
from asyncz.schedulers import AsyncIOScheduler
|
|
283
|
+
|
|
284
|
+
async lifespan(app):
|
|
285
|
+
with AsyncIOScheduler() as scheduler:
|
|
286
|
+
yield
|
|
287
|
+
# or yield a state
|
|
288
|
+
app = Starlette(
|
|
289
|
+
lifespan=lifespan,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Security
|
|
295
|
+
|
|
296
|
+
You should use store encryption for security reasons.
|
|
297
|
+
|
|
298
|
+
All standard stores except MemoryStore support the environment variable `ASYNCZ_STORE_ENCRYPTION_KEY`.
|
|
299
|
+
If set and non-empty the hash of the value is used for AESGCM encrypting the elements before sending them
|
|
300
|
+
to the store.
|
|
301
|
+
This way store entries are encrypted and authentificated so there is no security hole.
|
|
302
|
+
This is highly recommended! Because if someone can inject store entries he can execute code.
|
|
216
303
|
|
|
217
304
|
## Sponsors
|
|
218
305
|
|
|
@@ -155,9 +155,91 @@ fun 😁🎉.
|
|
|
155
155
|
Asyncz currently supports the [Esmerald framework](https://asyncz.dymmond.com/contrib/esmerald/index.md)
|
|
156
156
|
and brings some batteries that are currently used by the framework and leveraging Asyncz.
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from asyncz.schedulers import AsyncIOScheduler
|
|
161
|
+
...
|
|
162
|
+
|
|
163
|
+
# handle_lifespan is optional, set to True if you don't want to pass it down because the underlying app doesn't support it
|
|
164
|
+
# this is true for django
|
|
165
|
+
application = AsyncIOScheduler().asgi(application, handle_lifespan=False)
|
|
166
|
+
# or more simple (please do not use both together)
|
|
167
|
+
application = AsyncIOScheduler().asgi()(application)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Using with lilya:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from asyncz.schedulers import AsyncIOScheduler
|
|
174
|
+
|
|
175
|
+
# Lilya middleware doesn't pass lifespan events
|
|
176
|
+
|
|
177
|
+
app = AsyncIOScheduler().asgi(Lilya(
|
|
178
|
+
routes=[...],
|
|
179
|
+
))
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Or manually:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from asyncz.schedulers import AsyncIOScheduler
|
|
187
|
+
|
|
188
|
+
scheduler = AsyncIOScheduler()
|
|
189
|
+
|
|
190
|
+
app = Lilya(
|
|
191
|
+
routes=[...],
|
|
192
|
+
on_startup=[scheduler.start],
|
|
193
|
+
on_shutdown=[scheduler.shutdown],
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
## Contextmanager support
|
|
200
|
+
|
|
201
|
+
Use as sync contextmanager
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from asyncz.schedulers import AsyncIOScheduler
|
|
205
|
+
|
|
206
|
+
with AsyncIOScheduler() as scheduler:
|
|
207
|
+
...
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Use as async contextmanager
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from asyncz.schedulers import AsyncIOScheduler
|
|
214
|
+
|
|
215
|
+
async with AsyncIOScheduler() as scheduler:
|
|
216
|
+
...
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
For using with lifespan of starlette:
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from asyncz.schedulers import AsyncIOScheduler
|
|
223
|
+
|
|
224
|
+
async lifespan(app):
|
|
225
|
+
with AsyncIOScheduler() as scheduler:
|
|
226
|
+
yield
|
|
227
|
+
# or yield a state
|
|
228
|
+
app = Starlette(
|
|
229
|
+
lifespan=lifespan,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Security
|
|
235
|
+
|
|
236
|
+
You should use store encryption for security reasons.
|
|
237
|
+
|
|
238
|
+
All standard stores except MemoryStore support the environment variable `ASYNCZ_STORE_ENCRYPTION_KEY`.
|
|
239
|
+
If set and non-empty the hash of the value is used for AESGCM encrypting the elements before sending them
|
|
240
|
+
to the store.
|
|
241
|
+
This way store entries are encrypted and authentificated so there is no security hole.
|
|
242
|
+
This is highly recommended! Because if someone can inject store entries he can execute code.
|
|
161
243
|
|
|
162
244
|
## Sponsors
|
|
163
245
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.9.0"
|
|
@@ -57,7 +57,7 @@ class CronState(BaseDatastructureState):
|
|
|
57
57
|
jitter: Optional[int] = None
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
class TaskState(BaseDatastructureState):
|
|
60
|
+
class TaskState(BaseDatastructureState): # type: ignore
|
|
61
61
|
id: Optional[str] = None
|
|
62
62
|
name: Optional[str] = None
|
|
63
63
|
fn: Optional[Union[Callable[..., Any], str]] = None
|
|
@@ -69,7 +69,7 @@ class TaskState(BaseDatastructureState):
|
|
|
69
69
|
executor: Optional[str] = None
|
|
70
70
|
mistrigger_grace_time: Optional[int] = None
|
|
71
71
|
max_instances: Optional[int] = None
|
|
72
|
-
next_run_time: Optional[datetime]
|
|
72
|
+
next_run_time: Optional[datetime] = None
|
|
73
73
|
scheduler: Optional[Any] = None
|
|
74
74
|
store_alias: Optional[str] = None
|
|
75
75
|
store: Optional[Union[str, StoreType]] = None
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
from enum import Enum
|
|
1
|
+
from enum import Enum, IntEnum
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
class SchedulerState(
|
|
4
|
+
class SchedulerState(IntEnum):
|
|
5
5
|
STATE_STOPPED = 0
|
|
6
6
|
STATE_RUNNING = 1
|
|
7
7
|
STATE_PAUSED = 2
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class PluginInstance(Enum):
|
|
10
|
+
class PluginInstance(str, Enum):
|
|
11
11
|
EXECUTOR = "executor"
|
|
12
12
|
STORE = "store"
|
|
@@ -30,7 +30,7 @@ class BaseLookupError(LookupError):
|
|
|
30
30
|
|
|
31
31
|
detail = "Not found."
|
|
32
32
|
|
|
33
|
-
def __init__(self, detail: Optional[str] = None):
|
|
33
|
+
def __init__(self, detail: Optional[str] = None) -> None:
|
|
34
34
|
if not detail:
|
|
35
35
|
detail = self.detail
|
|
36
36
|
super().__init__(detail)
|
|
@@ -47,7 +47,7 @@ class AsynczLookupError(BaseLookupError):
|
|
|
47
47
|
class SchedulerLookupError(BaseLookupError):
|
|
48
48
|
detail = "No schedule with the id {schedule_id} has been found."
|
|
49
49
|
|
|
50
|
-
def __init__(self, schedule_id: str):
|
|
50
|
+
def __init__(self, schedule_id: str) -> None:
|
|
51
51
|
detail = self.detail.format(schedule_id=schedule_id)
|
|
52
52
|
super().__init__(detail=detail)
|
|
53
53
|
|
|
@@ -55,7 +55,7 @@ class SchedulerLookupError(BaseLookupError):
|
|
|
55
55
|
class TaskLookupError(BaseLookupError):
|
|
56
56
|
detail = "No task with the id {task_id} has been found."
|
|
57
57
|
|
|
58
|
-
def __init__(self, task_id: str):
|
|
58
|
+
def __init__(self, task_id: str) -> None:
|
|
59
59
|
detail = self.detail.format(task_id=task_id)
|
|
60
60
|
super().__init__(detail=detail)
|
|
61
61
|
|
|
@@ -63,7 +63,7 @@ class TaskLookupError(BaseLookupError):
|
|
|
63
63
|
class ConflictError(KeyError):
|
|
64
64
|
detail = "This data store already contains a schedule with the identifier {schedule_id}."
|
|
65
65
|
|
|
66
|
-
def __init__(self, schedule_id: str):
|
|
66
|
+
def __init__(self, schedule_id: str) -> None:
|
|
67
67
|
detail = self.detail.format(schedule_id=schedule_id)
|
|
68
68
|
super().__init__(detail)
|
|
69
69
|
|
|
@@ -71,7 +71,7 @@ class ConflictError(KeyError):
|
|
|
71
71
|
class ConflictIdError(KeyError):
|
|
72
72
|
detail = "Task identifier ({task_id}) conflicts with an existing task."
|
|
73
73
|
|
|
74
|
-
def __init__(self, task_id: str):
|
|
74
|
+
def __init__(self, task_id: str) -> None:
|
|
75
75
|
detail = self.detail.format(task_id=task_id)
|
|
76
76
|
super().__init__(detail)
|
|
77
77
|
|
|
@@ -83,7 +83,7 @@ class MaxInterationsReached(AsynczException):
|
|
|
83
83
|
class MaximumInstancesError(AsynczException):
|
|
84
84
|
detail = "The task by the id of {id} reached its maximum number of instances {total}."
|
|
85
85
|
|
|
86
|
-
def __init__(self, _id: Union[UUID, str, int], total: int):
|
|
86
|
+
def __init__(self, _id: Union[UUID, str, int], total: int) -> None:
|
|
87
87
|
detail = self.detail.format(id=_id, total=total)
|
|
88
88
|
super().__init__(detail=detail)
|
|
89
89
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import TYPE_CHECKING, Any, List, Set
|
|
2
3
|
|
|
3
4
|
from asyncz.exceptions import AsynczException
|
|
4
5
|
from asyncz.executors.base import BaseExecutor, run_coroutine_task, run_task
|
|
@@ -10,7 +11,8 @@ if TYPE_CHECKING:
|
|
|
10
11
|
|
|
11
12
|
class AsyncIOExecutor(BaseExecutor):
|
|
12
13
|
"""
|
|
13
|
-
Executor used for AsyncIO, typically can also be plugged into any ASGI framework as well,
|
|
14
|
+
Executor used for AsyncIO, typically can also be plugged into any ASGI framework as well,
|
|
15
|
+
for example, Esmerald, Starlite, FastAPI...
|
|
14
16
|
|
|
15
17
|
Runs the task in the default executor event loop.
|
|
16
18
|
|
|
@@ -19,20 +21,20 @@ class AsyncIOExecutor(BaseExecutor):
|
|
|
19
21
|
executor which is usually a thread pool.
|
|
20
22
|
"""
|
|
21
23
|
|
|
22
|
-
def start(self, scheduler: Any, alias: str):
|
|
24
|
+
def start(self, scheduler: Any, alias: str) -> None:
|
|
23
25
|
super().start(scheduler, alias)
|
|
24
26
|
self.event_loop = scheduler.event_loop
|
|
25
|
-
self.pending_futures = set()
|
|
27
|
+
self.pending_futures: Set[Any] = set()
|
|
26
28
|
|
|
27
|
-
def shutdown(self, wait: bool = True):
|
|
29
|
+
def shutdown(self, wait: bool = True) -> None:
|
|
28
30
|
for f in self.pending_futures:
|
|
29
31
|
if not f.done():
|
|
30
32
|
f.cancel()
|
|
31
33
|
|
|
32
34
|
self.pending_futures.clear()
|
|
33
35
|
|
|
34
|
-
def do_send_task(self, task: "TaskType", run_times:
|
|
35
|
-
def callback(fn):
|
|
36
|
+
def do_send_task(self, task: "TaskType", run_times: List[datetime]) -> None:
|
|
37
|
+
def callback(fn: Any) -> None:
|
|
36
38
|
self.pending_futures.discard(fn)
|
|
37
39
|
try:
|
|
38
40
|
events = fn.result()
|
|
@@ -43,7 +45,7 @@ class AsyncIOExecutor(BaseExecutor):
|
|
|
43
45
|
|
|
44
46
|
if iscoroutinefunction_partial(task.fn):
|
|
45
47
|
if run_coroutine_task is not None:
|
|
46
|
-
coroutine = run_coroutine_task(task, task.store_alias, run_times, self.logger)
|
|
48
|
+
coroutine = run_coroutine_task(task, task.store_alias, run_times, self.logger) # type: ignore
|
|
47
49
|
fn = self.event_loop.create_task(coroutine)
|
|
48
50
|
else:
|
|
49
51
|
raise AsynczException(
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import traceback
|
|
3
|
-
from abc import ABC, abstractmethod
|
|
4
3
|
from collections import defaultdict
|
|
5
4
|
from datetime import datetime, timedelta
|
|
6
5
|
from datetime import timezone as tz
|
|
6
|
+
from threading import RLock
|
|
7
7
|
from traceback import format_tb
|
|
8
|
-
from typing import TYPE_CHECKING, Any, List, Optional,
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, cast
|
|
9
9
|
|
|
10
10
|
from loguru import logger
|
|
11
11
|
from loguru._logger import Logger
|
|
@@ -13,24 +13,25 @@ from loguru._logger import Logger
|
|
|
13
13
|
from asyncz.events import TaskExecutionEvent
|
|
14
14
|
from asyncz.events.constants import TASK_ERROR, TASK_EXECUTED, TASK_MISSED
|
|
15
15
|
from asyncz.exceptions import MaximumInstancesError
|
|
16
|
+
from asyncz.executors.types import ExecutorType
|
|
16
17
|
from asyncz.state import BaseStateExtra
|
|
17
18
|
|
|
18
19
|
if TYPE_CHECKING:
|
|
19
20
|
from asyncz.tasks.types import TaskType
|
|
20
21
|
|
|
21
22
|
|
|
22
|
-
class BaseExecutor(BaseStateExtra,
|
|
23
|
+
class BaseExecutor(BaseStateExtra, ExecutorType):
|
|
23
24
|
"""
|
|
24
25
|
Base model for the executors. It defines the interface for all the executors used by the Asyncz.
|
|
25
26
|
|
|
26
27
|
Asyncz uses loguru for its logging as it is more descriptive and intuitive.
|
|
27
28
|
"""
|
|
28
29
|
|
|
29
|
-
def __init__(self, **kwargs):
|
|
30
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
30
31
|
super().__init__(**kwargs)
|
|
31
|
-
self.instances = defaultdict(lambda: 0)
|
|
32
|
+
self.instances: Dict[str, int] = defaultdict(lambda: 0)
|
|
32
33
|
|
|
33
|
-
def start(self, scheduler: Any, alias: str):
|
|
34
|
+
def start(self, scheduler: Any, alias: str) -> None:
|
|
34
35
|
"""
|
|
35
36
|
Called by the scheduler when the scheduler is being started or when the executor is being
|
|
36
37
|
added to an already running scheduler.
|
|
@@ -40,11 +41,11 @@ class BaseExecutor(BaseStateExtra, ABC):
|
|
|
40
41
|
alias - The alias of this executor as it was assigned to the scheduler.
|
|
41
42
|
"""
|
|
42
43
|
self.scheduler = scheduler
|
|
43
|
-
self.lock = scheduler.create_lock()
|
|
44
|
-
self.logger = logger
|
|
44
|
+
self.lock: RLock = scheduler.create_lock()
|
|
45
|
+
self.logger: Any = logger
|
|
45
46
|
self.logger.bind(logger_name=f"asyncz.executors.{alias}")
|
|
46
47
|
|
|
47
|
-
def shutdown(self, wait: bool = True):
|
|
48
|
+
def shutdown(self, wait: bool = True) -> None:
|
|
48
49
|
"""
|
|
49
50
|
Shuts down the executor.
|
|
50
51
|
|
|
@@ -52,12 +53,12 @@ class BaseExecutor(BaseStateExtra, ABC):
|
|
|
52
53
|
wait - Boolean indicating to wait until all submitted tasks have been executed.
|
|
53
54
|
"""
|
|
54
55
|
|
|
55
|
-
def send_task(self, task: "TaskType", run_times: List[datetime]):
|
|
56
|
+
def send_task(self, task: "TaskType", run_times: List[datetime]) -> None:
|
|
56
57
|
"""
|
|
57
58
|
Sends the task for execution.
|
|
58
59
|
|
|
59
60
|
Args:
|
|
60
|
-
task: A Task
|
|
61
|
+
task: A Task instance to execute.
|
|
61
62
|
run_times: A list of datetimes specifying when the task should have been run.
|
|
62
63
|
"""
|
|
63
64
|
assert self.lock is not None, "This executor has not been started yet."
|
|
@@ -68,14 +69,7 @@ class BaseExecutor(BaseStateExtra, ABC):
|
|
|
68
69
|
self.do_send_task(task, run_times)
|
|
69
70
|
self.instances[task.id] += 1
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
def do_send_task(self, task: "TaskType", run_times: List[datetime]) -> Any:
|
|
73
|
-
"""
|
|
74
|
-
Executes the actual task of scheduling `run_task` to be called.
|
|
75
|
-
"""
|
|
76
|
-
...
|
|
77
|
-
|
|
78
|
-
def run_task_success(self, task_id: Union[str, int], events: List[int]) -> Any:
|
|
72
|
+
def run_task_success(self, task_id: str, events: List[TaskExecutionEvent]) -> None:
|
|
79
73
|
"""
|
|
80
74
|
Called by the executor with the list of generated events when the function run_task has
|
|
81
75
|
been successfully executed.
|
|
@@ -88,7 +82,7 @@ class BaseExecutor(BaseStateExtra, ABC):
|
|
|
88
82
|
for event in events or []:
|
|
89
83
|
self.scheduler.dispatch_event(event)
|
|
90
84
|
|
|
91
|
-
def run_task_error(self, task_id:
|
|
85
|
+
def run_task_error(self, task_id: str) -> None:
|
|
92
86
|
"""
|
|
93
87
|
Called by the executor with the exception if there is an error calling the run_task.
|
|
94
88
|
"""
|
|
@@ -97,17 +91,15 @@ class BaseExecutor(BaseStateExtra, ABC):
|
|
|
97
91
|
if self.instances[task_id] == 0:
|
|
98
92
|
del self.instances[task_id]
|
|
99
93
|
|
|
100
|
-
self.logger.opt(exception=True).error(
|
|
101
|
-
f"Error running task {task_id}", exc_info=True
|
|
102
|
-
)
|
|
94
|
+
self.logger.opt(exception=True).error(f"Error running task {task_id}", exc_info=True)
|
|
103
95
|
|
|
104
96
|
|
|
105
97
|
def run_task(
|
|
106
98
|
task: "TaskType",
|
|
107
99
|
store_alias: str,
|
|
108
100
|
run_times: List[datetime],
|
|
109
|
-
_logger: Optional[
|
|
110
|
-
):
|
|
101
|
+
_logger: Optional[Any] = None,
|
|
102
|
+
) -> List[TaskExecutionEvent]:
|
|
111
103
|
"""
|
|
112
104
|
Called by executors to run the task. Returns a list of scheduler events to be dispatched by the
|
|
113
105
|
scheduler.
|
|
@@ -136,9 +128,9 @@ def run_task(
|
|
|
136
128
|
|
|
137
129
|
_logger.info(f'Running task "{task}" (scheduled at {run_time})')
|
|
138
130
|
try:
|
|
139
|
-
return_value = task.fn(*task.args, **task.kwargs)
|
|
140
|
-
except
|
|
141
|
-
|
|
131
|
+
return_value = cast(Callable[..., Any], task.fn)(*task.args, **task.kwargs)
|
|
132
|
+
except Exception as exc:
|
|
133
|
+
trace_back = sys.exc_info()[2]
|
|
142
134
|
formatted_trace_back = "".join(format_tb(trace_back))
|
|
143
135
|
events.append(
|
|
144
136
|
TaskExecutionEvent(
|
|
@@ -171,7 +163,7 @@ async def run_coroutine_task(
|
|
|
171
163
|
store_alias: str,
|
|
172
164
|
run_times: List[datetime],
|
|
173
165
|
_logger: Optional["Logger"] = None,
|
|
174
|
-
):
|
|
166
|
+
) -> List[TaskExecutionEvent]:
|
|
175
167
|
"""
|
|
176
168
|
Called by executors to run the task. Returns a list of scheduler events to be dispatched by the
|
|
177
169
|
scheduler.
|
|
@@ -179,7 +171,7 @@ async def run_coroutine_task(
|
|
|
179
171
|
The run task is made to run in async mode.
|
|
180
172
|
"""
|
|
181
173
|
if not _logger:
|
|
182
|
-
_logger = logger
|
|
174
|
+
_logger = logger # type: ignore
|
|
183
175
|
|
|
184
176
|
events = []
|
|
185
177
|
for run_time in run_times:
|
|
@@ -195,14 +187,14 @@ async def run_coroutine_task(
|
|
|
195
187
|
scheduled_run_time=run_time,
|
|
196
188
|
)
|
|
197
189
|
)
|
|
198
|
-
_logger.warning(f"Run time of task '{task}' was missed by {difference}")
|
|
190
|
+
_logger.warning(f"Run time of task '{task}' was missed by {difference}") # type: ignore
|
|
199
191
|
continue
|
|
200
192
|
|
|
201
|
-
_logger.info(f'Running task "{task}" (scheduled at {run_time})')
|
|
193
|
+
_logger.info(f'Running task "{task}" (scheduled at {run_time})') # type: ignore
|
|
202
194
|
try:
|
|
203
|
-
return_value = await task.fn(*task.args, **task.kwargs)
|
|
204
|
-
except
|
|
205
|
-
|
|
195
|
+
return_value = await cast(Callable[..., Any], task.fn)(*task.args, **task.kwargs)
|
|
196
|
+
except Exception as exc:
|
|
197
|
+
trace_back = sys.exc_info()[2]
|
|
206
198
|
formatted_trace_back = "".join(format_tb(trace_back))
|
|
207
199
|
events.append(
|
|
208
200
|
TaskExecutionEvent(
|
|
@@ -226,6 +218,6 @@ async def run_coroutine_task(
|
|
|
226
218
|
return_value=return_value,
|
|
227
219
|
)
|
|
228
220
|
)
|
|
229
|
-
_logger.info(f"Task '{task}' executed successfully")
|
|
221
|
+
_logger.info(f"Task '{task}' executed successfully") # type: ignore
|
|
230
222
|
|
|
231
223
|
return events
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import TYPE_CHECKING, List
|
|
2
3
|
|
|
3
4
|
from asyncz.executors.base import BaseExecutor, run_task
|
|
4
5
|
|
|
@@ -15,14 +16,11 @@ class DebugExecutor(BaseExecutor):
|
|
|
15
16
|
def do_send_task(
|
|
16
17
|
self,
|
|
17
18
|
task: "TaskType",
|
|
18
|
-
run_times:
|
|
19
|
-
|
|
20
|
-
str,
|
|
21
|
-
],
|
|
22
|
-
):
|
|
19
|
+
run_times: List[datetime],
|
|
20
|
+
) -> None:
|
|
23
21
|
try:
|
|
24
|
-
events = run_task(task, task.store_alias, run_times, self.logger)
|
|
25
|
-
except
|
|
22
|
+
events = run_task(task, task.store_alias, run_times, self.logger) # type: ignore
|
|
23
|
+
except Exception:
|
|
26
24
|
self.run_task_error(task.id)
|
|
27
25
|
else:
|
|
28
26
|
self.run_task_success(task.id, events)
|
|
@@ -16,12 +16,12 @@ class BasePoolExecutor(BaseExecutor):
|
|
|
16
16
|
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True, populate_by_name=True)
|
|
17
17
|
|
|
18
18
|
@abstractmethod
|
|
19
|
-
def __init__(self, pool: Any, **kwargs: Any):
|
|
19
|
+
def __init__(self, pool: Any, **kwargs: Any) -> None:
|
|
20
20
|
super().__init__(**kwargs)
|
|
21
21
|
self.pool = pool
|
|
22
22
|
|
|
23
23
|
def do_send_task(self, task: "TaskType", run_times: List[datetime]) -> Any:
|
|
24
|
-
def callback(fn):
|
|
24
|
+
def callback(fn: Any) -> None:
|
|
25
25
|
exc, _ = (
|
|
26
26
|
fn.exception_info()
|
|
27
27
|
if hasattr(fn, "exception_info")
|
|
@@ -41,7 +41,7 @@ class BasePoolExecutor(BaseExecutor):
|
|
|
41
41
|
|
|
42
42
|
fn.add_done_callback(callback)
|
|
43
43
|
|
|
44
|
-
def shutdown(self, wait=True):
|
|
44
|
+
def shutdown(self, wait: bool = True) -> None:
|
|
45
45
|
self.pool.shutdown(wait)
|
|
46
46
|
|
|
47
47
|
|
|
@@ -70,7 +70,7 @@ class ProcessPoolExecutor(BasePoolExecutor):
|
|
|
70
70
|
ProcessPoolExecutor constructor.
|
|
71
71
|
"""
|
|
72
72
|
|
|
73
|
-
def __init__(self, max_workers: int = 10, pool_kwargs: Optional[Any] = None):
|
|
73
|
+
def __init__(self, max_workers: int = 10, pool_kwargs: Optional[Any] = None) -> None:
|
|
74
74
|
pool_kwargs = pool_kwargs or {}
|
|
75
75
|
pool = concurrent.futures.ProcessPoolExecutor(int(max_workers), **pool_kwargs)
|
|
76
76
|
super().__init__(pool)
|