asyncz 0.8.3__tar.gz → 0.10.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.3 → asyncz-0.10.0}/.gitignore +1 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/PKG-INFO +95 -8
- {asyncz-0.8.3 → asyncz-0.10.0}/README.md +87 -5
- asyncz-0.10.0/asyncz/__init__.py +1 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/datastructures.py +2 -2
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/enums.py +3 -3
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/events/base.py +2 -2
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/exceptions.py +6 -6
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/executors/asyncio.py +16 -17
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/executors/base.py +34 -37
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/executors/debug.py +7 -8
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/executors/pool.py +21 -9
- asyncz-0.10.0/asyncz/executors/types.py +62 -0
- asyncz-0.10.0/asyncz/schedulers/asgi.py +70 -0
- asyncz-0.10.0/asyncz/schedulers/asyncio.py +69 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/schedulers/base.py +238 -195
- asyncz-0.10.0/asyncz/schedulers/datastructures.py +9 -0
- asyncz-0.10.0/asyncz/schedulers/defaults.py +25 -0
- asyncz-0.10.0/asyncz/schedulers/types.py +369 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/schedulers/utils.py +1 -1
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/state.py +1 -1
- asyncz-0.10.0/asyncz/stores/base.py +72 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/stores/memory.py +22 -19
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/stores/mongo.py +35 -26
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/stores/redis.py +35 -32
- asyncz-0.10.0/asyncz/stores/sqlalchemy.py +173 -0
- asyncz-0.8.3/asyncz/stores/base.py → asyncz-0.10.0/asyncz/stores/types.py +20 -37
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/tasks/base.py +75 -98
- asyncz-0.10.0/asyncz/tasks/types.py +116 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/triggers/base.py +19 -28
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/triggers/combination.py +22 -18
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/triggers/cron/expressions.py +36 -37
- asyncz-0.10.0/asyncz/triggers/cron/fields.py +121 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/triggers/cron/trigger.py +12 -9
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/triggers/date.py +10 -11
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/triggers/interval.py +18 -15
- asyncz-0.10.0/asyncz/triggers/types.py +37 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/typing.py +1 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/utils.py +93 -37
- {asyncz-0.8.3 → asyncz-0.10.0}/pyproject.toml +37 -16
- asyncz-0.8.3/asyncz/__init__.py +0 -1
- asyncz-0.8.3/asyncz/_mapping.py +0 -33
- asyncz-0.8.3/asyncz/executors/types.py +0 -7
- asyncz-0.8.3/asyncz/schedulers/asyncio.py +0 -64
- asyncz-0.8.3/asyncz/schedulers/datastructures.py +0 -49
- asyncz-0.8.3/asyncz/schedulers/types.py +0 -3
- asyncz-0.8.3/asyncz/stores/types.py +0 -8
- asyncz-0.8.3/asyncz/tasks/types.py +0 -3
- asyncz-0.8.3/asyncz/triggers/cron/fields.py +0 -131
- asyncz-0.8.3/asyncz/triggers/types.py +0 -9
- {asyncz-0.8.3 → asyncz-0.10.0}/LICENSE +0 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/events/__init__.py +0 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/events/constants.py +0 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/executors/__init__.py +0 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/py.typed +0 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/schedulers/__init__.py +0 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/stores/__init__.py +0 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/tasks/__init__.py +0 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/triggers/__init__.py +0 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/triggers/cron/__init__.py +0 -0
- {asyncz-0.8.3 → asyncz-0.10.0}/asyncz/triggers/cron/constants.py +0 -0
- {asyncz-0.8.3 → asyncz-0.10.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.10.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
|
|
@@ -207,12 +212,94 @@ fun 😁🎉.
|
|
|
207
212
|
|
|
208
213
|
## ASGI support
|
|
209
214
|
|
|
210
|
-
Asyncz currently supports the [Esmerald framework](https://asyncz.dymmond.com/contrib/esmerald/
|
|
211
|
-
and brings some batteries that are currently used by
|
|
215
|
+
Asyncz currently supports ASGI, the [Esmerald framework](https://asyncz.dymmond.com/contrib/esmerald/)
|
|
216
|
+
and brings some batteries that are currently used by Esmerald 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
|
|
|
@@ -152,12 +152,94 @@ fun 😁🎉.
|
|
|
152
152
|
|
|
153
153
|
## ASGI support
|
|
154
154
|
|
|
155
|
-
Asyncz currently supports the [Esmerald framework](https://asyncz.dymmond.com/contrib/esmerald/
|
|
156
|
-
and brings some batteries that are currently used by
|
|
155
|
+
Asyncz currently supports ASGI, the [Esmerald framework](https://asyncz.dymmond.com/contrib/esmerald/)
|
|
156
|
+
and brings some batteries that are currently used by Esmerald 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.10.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: Optional[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: Optional[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,6 +1,6 @@
|
|
|
1
|
-
from
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import TYPE_CHECKING, Any, List, Set
|
|
2
3
|
|
|
3
|
-
from asyncz.exceptions import AsynczException
|
|
4
4
|
from asyncz.executors.base import BaseExecutor, run_coroutine_task, run_task
|
|
5
5
|
from asyncz.utils import iscoroutinefunction_partial
|
|
6
6
|
|
|
@@ -10,7 +10,8 @@ if TYPE_CHECKING:
|
|
|
10
10
|
|
|
11
11
|
class AsyncIOExecutor(BaseExecutor):
|
|
12
12
|
"""
|
|
13
|
-
Executor used for AsyncIO, typically can also be plugged into any ASGI framework as well,
|
|
13
|
+
Executor used for AsyncIO, typically can also be plugged into any ASGI framework as well,
|
|
14
|
+
for example, Esmerald, Starlite, FastAPI...
|
|
14
15
|
|
|
15
16
|
Runs the task in the default executor event loop.
|
|
16
17
|
|
|
@@ -19,36 +20,34 @@ class AsyncIOExecutor(BaseExecutor):
|
|
|
19
20
|
executor which is usually a thread pool.
|
|
20
21
|
"""
|
|
21
22
|
|
|
22
|
-
def start(self, scheduler: Any, alias: str):
|
|
23
|
+
def start(self, scheduler: Any, alias: str) -> None:
|
|
23
24
|
super().start(scheduler, alias)
|
|
24
25
|
self.event_loop = scheduler.event_loop
|
|
25
|
-
self.pending_futures = set()
|
|
26
|
+
self.pending_futures: Set[Any] = set()
|
|
26
27
|
|
|
27
|
-
def shutdown(self, wait: bool = True):
|
|
28
|
+
def shutdown(self, wait: bool = True) -> None:
|
|
28
29
|
for f in self.pending_futures:
|
|
29
30
|
if not f.done():
|
|
30
31
|
f.cancel()
|
|
31
32
|
|
|
32
33
|
self.pending_futures.clear()
|
|
33
34
|
|
|
34
|
-
def do_send_task(self, task: "TaskType", run_times:
|
|
35
|
-
|
|
35
|
+
def do_send_task(self, task: "TaskType", run_times: List[datetime]) -> None:
|
|
36
|
+
task_id = task.id
|
|
37
|
+
assert task_id is not None, "Cannot send decorator type task"
|
|
38
|
+
|
|
39
|
+
def callback(fn: Any) -> None:
|
|
36
40
|
self.pending_futures.discard(fn)
|
|
37
41
|
try:
|
|
38
42
|
events = fn.result()
|
|
39
43
|
except BaseException:
|
|
40
|
-
self.run_task_error(
|
|
44
|
+
self.run_task_error(task_id)
|
|
41
45
|
else:
|
|
42
|
-
self.run_task_success(
|
|
46
|
+
self.run_task_success(task_id, events)
|
|
43
47
|
|
|
44
48
|
if iscoroutinefunction_partial(task.fn):
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
fn = self.event_loop.create_task(coroutine)
|
|
48
|
-
else:
|
|
49
|
-
raise AsynczException(
|
|
50
|
-
detail="Executing coroutine based tasks is not supported with Trollius."
|
|
51
|
-
)
|
|
49
|
+
coroutine = run_coroutine_task(task, task.store_alias, run_times, self.logger) # type: ignore
|
|
50
|
+
fn = self.event_loop.create_task(coroutine)
|
|
52
51
|
else:
|
|
53
52
|
fn = self.event_loop.run_in_executor(
|
|
54
53
|
None, run_task, task, task.store_alias, run_times, self.logger
|
|
@@ -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,15 +53,16 @@ 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."
|
|
65
|
+
assert task.id is not None, "The task is in decorator mode."
|
|
64
66
|
with self.lock:
|
|
65
67
|
if self.instances[task.id] >= task.max_instances:
|
|
66
68
|
raise MaximumInstancesError(task.id, task.max_instances)
|
|
@@ -68,14 +70,7 @@ class BaseExecutor(BaseStateExtra, ABC):
|
|
|
68
70
|
self.do_send_task(task, run_times)
|
|
69
71
|
self.instances[task.id] += 1
|
|
70
72
|
|
|
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:
|
|
73
|
+
def run_task_success(self, task_id: str, events: List[TaskExecutionEvent]) -> None:
|
|
79
74
|
"""
|
|
80
75
|
Called by the executor with the list of generated events when the function run_task has
|
|
81
76
|
been successfully executed.
|
|
@@ -88,7 +83,7 @@ class BaseExecutor(BaseStateExtra, ABC):
|
|
|
88
83
|
for event in events or []:
|
|
89
84
|
self.scheduler.dispatch_event(event)
|
|
90
85
|
|
|
91
|
-
def run_task_error(self, task_id:
|
|
86
|
+
def run_task_error(self, task_id: str) -> None:
|
|
92
87
|
"""
|
|
93
88
|
Called by the executor with the exception if there is an error calling the run_task.
|
|
94
89
|
"""
|
|
@@ -104,8 +99,8 @@ def run_task(
|
|
|
104
99
|
task: "TaskType",
|
|
105
100
|
store_alias: str,
|
|
106
101
|
run_times: List[datetime],
|
|
107
|
-
_logger: Optional[
|
|
108
|
-
):
|
|
102
|
+
_logger: Optional[Any] = None,
|
|
103
|
+
) -> List[TaskExecutionEvent]:
|
|
109
104
|
"""
|
|
110
105
|
Called by executors to run the task. Returns a list of scheduler events to be dispatched by the
|
|
111
106
|
scheduler.
|
|
@@ -117,9 +112,10 @@ def run_task(
|
|
|
117
112
|
|
|
118
113
|
events = []
|
|
119
114
|
for run_time in run_times:
|
|
120
|
-
|
|
115
|
+
mistrigger_grace_time = task.mistrigger_grace_time
|
|
116
|
+
if mistrigger_grace_time is not None:
|
|
121
117
|
difference = datetime.now(tz.utc) - run_time
|
|
122
|
-
grace_time = timedelta(seconds=
|
|
118
|
+
grace_time = timedelta(seconds=mistrigger_grace_time)
|
|
123
119
|
if difference > grace_time:
|
|
124
120
|
events.append(
|
|
125
121
|
TaskExecutionEvent(
|
|
@@ -134,9 +130,9 @@ def run_task(
|
|
|
134
130
|
|
|
135
131
|
_logger.info(f'Running task "{task}" (scheduled at {run_time})')
|
|
136
132
|
try:
|
|
137
|
-
return_value = task.fn(*task.args, **task.kwargs)
|
|
138
|
-
except
|
|
139
|
-
|
|
133
|
+
return_value = cast(Callable[..., Any], task.fn)(*task.args, **task.kwargs)
|
|
134
|
+
except Exception as exc:
|
|
135
|
+
trace_back = sys.exc_info()[2]
|
|
140
136
|
formatted_trace_back = "".join(format_tb(trace_back))
|
|
141
137
|
events.append(
|
|
142
138
|
TaskExecutionEvent(
|
|
@@ -169,7 +165,7 @@ async def run_coroutine_task(
|
|
|
169
165
|
store_alias: str,
|
|
170
166
|
run_times: List[datetime],
|
|
171
167
|
_logger: Optional["Logger"] = None,
|
|
172
|
-
):
|
|
168
|
+
) -> List[TaskExecutionEvent]:
|
|
173
169
|
"""
|
|
174
170
|
Called by executors to run the task. Returns a list of scheduler events to be dispatched by the
|
|
175
171
|
scheduler.
|
|
@@ -177,13 +173,14 @@ async def run_coroutine_task(
|
|
|
177
173
|
The run task is made to run in async mode.
|
|
178
174
|
"""
|
|
179
175
|
if not _logger:
|
|
180
|
-
_logger = logger
|
|
176
|
+
_logger = logger # type: ignore
|
|
181
177
|
|
|
182
178
|
events = []
|
|
183
179
|
for run_time in run_times:
|
|
184
|
-
|
|
180
|
+
mistrigger_grace_time = task.mistrigger_grace_time
|
|
181
|
+
if mistrigger_grace_time is not None:
|
|
185
182
|
difference = datetime.now(tz.utc) - run_time
|
|
186
|
-
grace_time = timedelta(seconds=
|
|
183
|
+
grace_time = timedelta(seconds=mistrigger_grace_time)
|
|
187
184
|
if difference > grace_time:
|
|
188
185
|
events.append(
|
|
189
186
|
TaskExecutionEvent(
|
|
@@ -193,14 +190,14 @@ async def run_coroutine_task(
|
|
|
193
190
|
scheduled_run_time=run_time,
|
|
194
191
|
)
|
|
195
192
|
)
|
|
196
|
-
_logger.warning(f"Run time of task '{task}' was missed by {difference}")
|
|
193
|
+
_logger.warning(f"Run time of task '{task}' was missed by {difference}") # type: ignore
|
|
197
194
|
continue
|
|
198
195
|
|
|
199
|
-
_logger.info(f'Running task "{task}" (scheduled at {run_time})')
|
|
196
|
+
_logger.info(f'Running task "{task}" (scheduled at {run_time})') # type: ignore
|
|
200
197
|
try:
|
|
201
|
-
return_value = await task.fn(*task.args, **task.kwargs)
|
|
202
|
-
except
|
|
203
|
-
|
|
198
|
+
return_value = await cast(Callable[..., Any], task.fn)(*task.args, **task.kwargs)
|
|
199
|
+
except Exception as exc:
|
|
200
|
+
trace_back = sys.exc_info()[2]
|
|
204
201
|
formatted_trace_back = "".join(format_tb(trace_back))
|
|
205
202
|
events.append(
|
|
206
203
|
TaskExecutionEvent(
|
|
@@ -224,6 +221,6 @@ async def run_coroutine_task(
|
|
|
224
221
|
return_value=return_value,
|
|
225
222
|
)
|
|
226
223
|
)
|
|
227
|
-
_logger.info(f"Task '{task}' executed successfully")
|
|
224
|
+
_logger.info(f"Task '{task}' executed successfully") # type: ignore
|
|
228
225
|
|
|
229
226
|
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,12 @@ class DebugExecutor(BaseExecutor):
|
|
|
15
16
|
def do_send_task(
|
|
16
17
|
self,
|
|
17
18
|
task: "TaskType",
|
|
18
|
-
run_times:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
],
|
|
22
|
-
):
|
|
19
|
+
run_times: List[datetime],
|
|
20
|
+
) -> None:
|
|
21
|
+
assert task.id is not None, "Cannot send decorator type task"
|
|
23
22
|
try:
|
|
24
|
-
events = run_task(task, task.store_alias, run_times, self.logger)
|
|
25
|
-
except
|
|
23
|
+
events = run_task(task, task.store_alias, run_times, self.logger) # type: ignore
|
|
24
|
+
except Exception:
|
|
26
25
|
self.run_task_error(task.id)
|
|
27
26
|
else:
|
|
28
27
|
self.run_task_success(task.id, events)
|