asyncz 0.5.0__tar.gz → 0.7.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.5.0 → asyncz-0.7.0}/PKG-INFO +44 -66
- {asyncz-0.5.0 → asyncz-0.7.0}/README.md +25 -25
- asyncz-0.7.0/asyncz/__init__.py +1 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/contrib/esmerald/decorator.py +2 -2
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/contrib/esmerald/scheduler.py +13 -13
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/executors/base.py +6 -4
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/schedulers/base.py +51 -23
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/stores/redis.py +15 -8
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/triggers/cron/fields.py +1 -1
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/triggers/interval.py +14 -3
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/utils.py +18 -20
- {asyncz-0.5.0 → asyncz-0.7.0}/pyproject.toml +44 -46
- asyncz-0.5.0/asyncz/__init__.py +0 -1
- {asyncz-0.5.0 → asyncz-0.7.0}/.gitignore +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/LICENSE +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/_mapping.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/contrib/__init__.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/contrib/esmerald/__init__.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/datastructures.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/enums.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/events/__init__.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/events/base.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/events/constants.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/exceptions.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/executors/__init__.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/executors/asyncio.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/executors/debug.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/executors/pool.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/executors/types.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/py.typed +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/schedulers/__init__.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/schedulers/asyncio.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/schedulers/datastructures.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/schedulers/types.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/schedulers/utils.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/state.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/stores/__init__.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/stores/base.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/stores/memory.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/stores/mongo.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/stores/types.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/tasks/__init__.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/tasks/base.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/tasks/types.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/triggers/__init__.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/triggers/base.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/triggers/combination.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/triggers/cron/__init__.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/triggers/cron/constants.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/triggers/cron/expressions.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/triggers/cron/trigger.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/triggers/cron/types.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/triggers/date.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/triggers/types.py +0 -0
- {asyncz-0.5.0 → asyncz-0.7.0}/asyncz/typing.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: asyncz
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: The scheduler that nobody wants but every application needs.
|
|
5
|
-
Project-URL: Homepage, https://github.com/
|
|
6
|
-
Project-URL: Documentation, https://asyncz.
|
|
7
|
-
Project-URL: Changelog, https://asyncz.
|
|
5
|
+
Project-URL: Homepage, https://github.com/dymmond/asyncz
|
|
6
|
+
Project-URL: Documentation, https://asyncz.dymmond.com/
|
|
7
|
+
Project-URL: Changelog, https://asyncz.dymmond.com/release-notes/
|
|
8
8
|
Project-URL: Funding, https://github.com/sponsors/tarsil
|
|
9
|
-
Project-URL: Source, https://github.com/
|
|
9
|
+
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
|
|
@@ -37,42 +37,20 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
|
37
37
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
38
38
|
Classifier: Typing :: Typed
|
|
39
39
|
Requires-Python: >=3.8
|
|
40
|
-
Requires-Dist:
|
|
40
|
+
Requires-Dist: backports-zoneinfo; python_version < '3.9'
|
|
41
41
|
Requires-Dist: loguru<0.8.0,>=0.7.0
|
|
42
|
-
Requires-Dist: pydantic<3.0.0,>=2.
|
|
43
|
-
Requires-Dist:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
Requires-Dist:
|
|
47
|
-
Requires-Dist:
|
|
48
|
-
Requires-Dist:
|
|
49
|
-
|
|
50
|
-
Requires-Dist:
|
|
51
|
-
Requires-Dist:
|
|
52
|
-
Requires-Dist:
|
|
53
|
-
Requires-Dist:
|
|
54
|
-
Requires-Dist: isort<6.0.0,>=5.0.6; extra == 'dev'
|
|
55
|
-
Requires-Dist: mypy<2.0.0,>=0.982; extra == 'dev'
|
|
56
|
-
Requires-Dist: pre-commit<3.0.0,>=2.17.0; extra == 'dev'
|
|
57
|
-
Requires-Dist: watchfiles<0.20.0,>=0.16.1; extra == 'dev'
|
|
58
|
-
Provides-Extra: doc
|
|
59
|
-
Requires-Dist: mdx-include<2.0.0,>=1.4.2; extra == 'doc'
|
|
60
|
-
Requires-Dist: mkautodoc<0.3.0,>=0.2.0; extra == 'doc'
|
|
61
|
-
Requires-Dist: mkdocs-markdownextradata-plugin<0.3.0,>=0.2.5; extra == 'doc'
|
|
62
|
-
Requires-Dist: mkdocs-material<10.0.0,>=9.0.13; extra == 'doc'
|
|
63
|
-
Requires-Dist: mkdocs<2.0.0,>=1.1.2; extra == 'doc'
|
|
64
|
-
Requires-Dist: mkdocstrings<0.30.0,>=0.20.0; extra == 'doc'
|
|
65
|
-
Requires-Dist: pyyaml<7.0.0,>=6.0; extra == 'doc'
|
|
66
|
-
Provides-Extra: test
|
|
67
|
-
Requires-Dist: mock>=4.0.3; extra == 'test'
|
|
68
|
-
Requires-Dist: pymongo<5.0.0,>=4.3.3; extra == 'test'
|
|
69
|
-
Requires-Dist: pytest-asyncio<1.0.0,>=0.19.0; extra == 'test'
|
|
70
|
-
Requires-Dist: pytest-cov<5.0.0,>=2.12.0; extra == 'test'
|
|
71
|
-
Requires-Dist: pytest-loguru<1,>=0.2.0; extra == 'test'
|
|
72
|
-
Requires-Dist: pytest<8.0.0,>=7.1.3; extra == 'test'
|
|
73
|
-
Requires-Dist: redis<5.0.0,>=4.4.0; extra == 'test'
|
|
74
|
-
Requires-Dist: requests<3.0.0,>=2.27.0; extra == 'test'
|
|
75
|
-
Requires-Dist: ruff<1.0.0,>=0.0.256; extra == 'test'
|
|
42
|
+
Requires-Dist: pydantic<3.0.0,>=2.5.3
|
|
43
|
+
Requires-Dist: tzlocal<6.0,>=4.0
|
|
44
|
+
Provides-Extra: testing
|
|
45
|
+
Requires-Dist: esmerald; extra == 'testing'
|
|
46
|
+
Requires-Dist: mypy<2.0.0,>=0.982; extra == 'testing'
|
|
47
|
+
Requires-Dist: pymongo<5.0.0,>=4.3.3; extra == 'testing'
|
|
48
|
+
Requires-Dist: pytest-asyncio<1.0.0,>=0.19.0; extra == 'testing'
|
|
49
|
+
Requires-Dist: pytest-cov<6.0.0,>=2.12.0; extra == 'testing'
|
|
50
|
+
Requires-Dist: pytest-loguru<1,>=0.2.0; extra == 'testing'
|
|
51
|
+
Requires-Dist: pytest<9.0.0,>=7.1.3; extra == 'testing'
|
|
52
|
+
Requires-Dist: pytz>=2022.6; extra == 'testing'
|
|
53
|
+
Requires-Dist: redis<6.0.0,>=4.4.0; extra == 'testing'
|
|
76
54
|
Description-Content-Type: text/markdown
|
|
77
55
|
|
|
78
56
|
# Asyncz
|
|
@@ -86,8 +64,8 @@ Description-Content-Type: text/markdown
|
|
|
86
64
|
</p>
|
|
87
65
|
|
|
88
66
|
<p align="center">
|
|
89
|
-
<a href="https://github.com/
|
|
90
|
-
<img src="https://github.com/
|
|
67
|
+
<a href="https://github.com/dymmond/asyncz/workflows/Test%20Suite/badge.svg?event=push&branch=main" target="_blank">
|
|
68
|
+
<img src="https://github.com/dymmond/asyncz/workflows/Test%20Suite/badge.svg?event=push&branch=main" alt="Test Suite">
|
|
91
69
|
</a>
|
|
92
70
|
|
|
93
71
|
<a href="https://pypi.org/project/asyncz" target="_blank">
|
|
@@ -101,9 +79,9 @@ Description-Content-Type: text/markdown
|
|
|
101
79
|
|
|
102
80
|
---
|
|
103
81
|
|
|
104
|
-
**Documentation**: [https://asyncz.
|
|
82
|
+
**Documentation**: [https://asyncz.dymmond.com](https://asyncz.dymmond.com) 📚
|
|
105
83
|
|
|
106
|
-
**Source Code**: [https://github.com/
|
|
84
|
+
**Source Code**: [https://github.com/dymmond/asyncz](https://github.com/dymmond/asyncz)
|
|
107
85
|
|
|
108
86
|
---
|
|
109
87
|
|
|
@@ -145,16 +123,16 @@ completely indepent from any framework as well as inside ASGI frameworks such as
|
|
|
145
123
|
|
|
146
124
|
Asyncz comes with special support to [Esmerald](https://esmerald.dymmond.com) for the simple reason that the author is
|
|
147
125
|
the same but it can be added more support. If you are interested in adding support to your favourite frameworks then
|
|
148
|
-
see the [contributing](https://asyncz.
|
|
126
|
+
see the [contributing](https://asyncz.dymmond.com/contributing.md) section.
|
|
149
127
|
|
|
150
128
|
## Concepts
|
|
151
129
|
|
|
152
130
|
Like APScheduler, Asyncz also brings four kinds of components:
|
|
153
131
|
|
|
154
|
-
* [Schedulers](https://asyncz.
|
|
155
|
-
* [Triggers](https://asyncz.
|
|
156
|
-
* [Stores](https://asyncz.
|
|
157
|
-
* [Executors](https://asyncz.
|
|
132
|
+
* [Schedulers](https://asyncz.dymmond.com/schedulers.md)
|
|
133
|
+
* [Triggers](https://asyncz.dymmond.com/triggers.md)
|
|
134
|
+
* [Stores](https://asyncz.dymmond.com/stores.md)
|
|
135
|
+
* [Executors](https://asyncz.dymmond.com/executors.md)
|
|
158
136
|
|
|
159
137
|
## Requirements
|
|
160
138
|
|
|
@@ -173,28 +151,28 @@ $ pip install asyncz
|
|
|
173
151
|
|
|
174
152
|
## The right decisions
|
|
175
153
|
|
|
176
|
-
How do you know if you are choosing the right [scheduler](https://asyncz.
|
|
177
|
-
[triggers](https://asyncz.
|
|
178
|
-
and [executors](https://asyncz.
|
|
154
|
+
How do you know if you are choosing the right [scheduler](https://asyncz.dymmond.com/schedulers.md),
|
|
155
|
+
[triggers](https://asyncz.dymmond.com/triggers.md), [stores](https://asyncz.dymmond.com/stores.md)
|
|
156
|
+
and [executors](https://asyncz.dymmond.com/executors.md)?
|
|
179
157
|
|
|
180
158
|
Well, Asyncz is intentionally designed for specific systems and already helps you out with some of
|
|
181
159
|
those questions.
|
|
182
160
|
|
|
183
|
-
* **Schedulers** - Natively only supports the [AsyncIOScheduler](https://asyncz.
|
|
161
|
+
* **Schedulers** - Natively only supports the [AsyncIOScheduler](https://asyncz.dymmond.com/schedulers.md#asyncioscheduler).
|
|
184
162
|
* **Triggers** - Here it will depend of the periocidity of our tasks. Example:
|
|
185
|
-
* [CronTrigger](https://asyncz.
|
|
163
|
+
* [CronTrigger](https://asyncz.dymmond.com/triggers.md#crontrigger) - UNIX like cron and gives you the same feeling as
|
|
186
164
|
scheduling a task on a native UNIX like based system.
|
|
187
|
-
* [DateTrigger](https://asyncz.
|
|
165
|
+
* [DateTrigger](https://asyncz.dymmond.com/triggers.md#datetrigger) - When you need to run a task once on a specific
|
|
188
166
|
point of time.
|
|
189
|
-
* [IntervalTrigger](https://asyncz.
|
|
167
|
+
* [IntervalTrigger](https://asyncz.dymmond.com/triggers.md#intervaltrigger) - When you need to run tasks in specific
|
|
190
168
|
intervals of time.
|
|
191
|
-
* [OrTrigger](https://asyncz.
|
|
169
|
+
* [OrTrigger](https://asyncz.dymmond.com/triggers.md#ortrigger)/[AndTrigger](https://asyncz.dymmond.com/triggers.md#andtrigger) - If you would
|
|
192
170
|
like to combine more than one trigger (cron, interval and date) together.
|
|
193
|
-
* **Stores** - Natively only supports [redis](https://asyncz.
|
|
194
|
-
[mongo](https://asyncz.
|
|
195
|
-
* **Executors** - Natively only supports [AsyncIOExecutor](https://asyncz.
|
|
196
|
-
[ThreadPoolExecutor](https://asyncz.
|
|
197
|
-
[ProcessPoolExecutor](https://asyncz.
|
|
171
|
+
* **Stores** - Natively only supports [redis](https://asyncz.dymmond.com/stores.md#redisstore),
|
|
172
|
+
[mongo](https://asyncz.dymmond.com/stores.md#mongodbstore) and [memory](https://asyncz.dymmond.com/stores.md#memorystore).
|
|
173
|
+
* **Executors** - Natively only supports [AsyncIOExecutor](https://asyncz.dymmond.com/executors.md#asyncioexecutor),
|
|
174
|
+
[ThreadPoolExecutor](https://asyncz.dymmond.com/executors.md#threadpoolexecutor) and
|
|
175
|
+
[ProcessPoolExecutor](https://asyncz.dymmond.com/executors.md#processpoolexecutor).
|
|
198
176
|
|
|
199
177
|
Sometimes having a lot of options makes the decision making very hard and Asyncz is intentionally
|
|
200
178
|
designed and driven to simplify and for specific use cases but is not limited to those. In every
|
|
@@ -221,7 +199,7 @@ scheduler = AsyncIOScheduler()
|
|
|
221
199
|
```
|
|
222
200
|
|
|
223
201
|
Initialize the rest of the application after the `scheduler` initialisation.
|
|
224
|
-
More [details](https://asyncz.
|
|
202
|
+
More [details](https://asyncz.dymmond.com/schedulers.md) can be found with more thorough explanations.
|
|
225
203
|
|
|
226
204
|
This is in simple terms and in a nutshell how to start with Asyncz quickly. For more information,
|
|
227
205
|
details and examples how to leverage Asyncz simply navigate through the documentation and have
|
|
@@ -229,11 +207,11 @@ fun 😁🎉.
|
|
|
229
207
|
|
|
230
208
|
## ASGI support
|
|
231
209
|
|
|
232
|
-
Asyncz currently supports the [Esmerald framework](https://asyncz.
|
|
210
|
+
Asyncz currently supports the [Esmerald framework](https://asyncz.dymmond.com/contrib/esmerald/index.md)
|
|
233
211
|
and brings some batteries that are currently used by the framework and leveraging Asyncz.
|
|
234
212
|
|
|
235
213
|
If you wish to have support to any other framework such as FastAPI, Starlite, Starlette or
|
|
236
|
-
literally any other, check the [contributing](https://asyncz.
|
|
214
|
+
literally any other, check the [contributing](https://asyncz.dymmond.com/contributing.md) section and see how you can
|
|
237
215
|
do it.
|
|
238
216
|
|
|
239
217
|
## Sponsors
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<a href="https://github.com/
|
|
13
|
-
<img src="https://github.com/
|
|
12
|
+
<a href="https://github.com/dymmond/asyncz/workflows/Test%20Suite/badge.svg?event=push&branch=main" target="_blank">
|
|
13
|
+
<img src="https://github.com/dymmond/asyncz/workflows/Test%20Suite/badge.svg?event=push&branch=main" alt="Test Suite">
|
|
14
14
|
</a>
|
|
15
15
|
|
|
16
16
|
<a href="https://pypi.org/project/asyncz" target="_blank">
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
-
**Documentation**: [https://asyncz.
|
|
27
|
+
**Documentation**: [https://asyncz.dymmond.com](https://asyncz.dymmond.com) 📚
|
|
28
28
|
|
|
29
|
-
**Source Code**: [https://github.com/
|
|
29
|
+
**Source Code**: [https://github.com/dymmond/asyncz](https://github.com/dymmond/asyncz)
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
@@ -68,16 +68,16 @@ completely indepent from any framework as well as inside ASGI frameworks such as
|
|
|
68
68
|
|
|
69
69
|
Asyncz comes with special support to [Esmerald](https://esmerald.dymmond.com) for the simple reason that the author is
|
|
70
70
|
the same but it can be added more support. If you are interested in adding support to your favourite frameworks then
|
|
71
|
-
see the [contributing](https://asyncz.
|
|
71
|
+
see the [contributing](https://asyncz.dymmond.com/contributing.md) section.
|
|
72
72
|
|
|
73
73
|
## Concepts
|
|
74
74
|
|
|
75
75
|
Like APScheduler, Asyncz also brings four kinds of components:
|
|
76
76
|
|
|
77
|
-
* [Schedulers](https://asyncz.
|
|
78
|
-
* [Triggers](https://asyncz.
|
|
79
|
-
* [Stores](https://asyncz.
|
|
80
|
-
* [Executors](https://asyncz.
|
|
77
|
+
* [Schedulers](https://asyncz.dymmond.com/schedulers.md)
|
|
78
|
+
* [Triggers](https://asyncz.dymmond.com/triggers.md)
|
|
79
|
+
* [Stores](https://asyncz.dymmond.com/stores.md)
|
|
80
|
+
* [Executors](https://asyncz.dymmond.com/executors.md)
|
|
81
81
|
|
|
82
82
|
## Requirements
|
|
83
83
|
|
|
@@ -96,28 +96,28 @@ $ pip install asyncz
|
|
|
96
96
|
|
|
97
97
|
## The right decisions
|
|
98
98
|
|
|
99
|
-
How do you know if you are choosing the right [scheduler](https://asyncz.
|
|
100
|
-
[triggers](https://asyncz.
|
|
101
|
-
and [executors](https://asyncz.
|
|
99
|
+
How do you know if you are choosing the right [scheduler](https://asyncz.dymmond.com/schedulers.md),
|
|
100
|
+
[triggers](https://asyncz.dymmond.com/triggers.md), [stores](https://asyncz.dymmond.com/stores.md)
|
|
101
|
+
and [executors](https://asyncz.dymmond.com/executors.md)?
|
|
102
102
|
|
|
103
103
|
Well, Asyncz is intentionally designed for specific systems and already helps you out with some of
|
|
104
104
|
those questions.
|
|
105
105
|
|
|
106
|
-
* **Schedulers** - Natively only supports the [AsyncIOScheduler](https://asyncz.
|
|
106
|
+
* **Schedulers** - Natively only supports the [AsyncIOScheduler](https://asyncz.dymmond.com/schedulers.md#asyncioscheduler).
|
|
107
107
|
* **Triggers** - Here it will depend of the periocidity of our tasks. Example:
|
|
108
|
-
* [CronTrigger](https://asyncz.
|
|
108
|
+
* [CronTrigger](https://asyncz.dymmond.com/triggers.md#crontrigger) - UNIX like cron and gives you the same feeling as
|
|
109
109
|
scheduling a task on a native UNIX like based system.
|
|
110
|
-
* [DateTrigger](https://asyncz.
|
|
110
|
+
* [DateTrigger](https://asyncz.dymmond.com/triggers.md#datetrigger) - When you need to run a task once on a specific
|
|
111
111
|
point of time.
|
|
112
|
-
* [IntervalTrigger](https://asyncz.
|
|
112
|
+
* [IntervalTrigger](https://asyncz.dymmond.com/triggers.md#intervaltrigger) - When you need to run tasks in specific
|
|
113
113
|
intervals of time.
|
|
114
|
-
* [OrTrigger](https://asyncz.
|
|
114
|
+
* [OrTrigger](https://asyncz.dymmond.com/triggers.md#ortrigger)/[AndTrigger](https://asyncz.dymmond.com/triggers.md#andtrigger) - If you would
|
|
115
115
|
like to combine more than one trigger (cron, interval and date) together.
|
|
116
|
-
* **Stores** - Natively only supports [redis](https://asyncz.
|
|
117
|
-
[mongo](https://asyncz.
|
|
118
|
-
* **Executors** - Natively only supports [AsyncIOExecutor](https://asyncz.
|
|
119
|
-
[ThreadPoolExecutor](https://asyncz.
|
|
120
|
-
[ProcessPoolExecutor](https://asyncz.
|
|
116
|
+
* **Stores** - Natively only supports [redis](https://asyncz.dymmond.com/stores.md#redisstore),
|
|
117
|
+
[mongo](https://asyncz.dymmond.com/stores.md#mongodbstore) and [memory](https://asyncz.dymmond.com/stores.md#memorystore).
|
|
118
|
+
* **Executors** - Natively only supports [AsyncIOExecutor](https://asyncz.dymmond.com/executors.md#asyncioexecutor),
|
|
119
|
+
[ThreadPoolExecutor](https://asyncz.dymmond.com/executors.md#threadpoolexecutor) and
|
|
120
|
+
[ProcessPoolExecutor](https://asyncz.dymmond.com/executors.md#processpoolexecutor).
|
|
121
121
|
|
|
122
122
|
Sometimes having a lot of options makes the decision making very hard and Asyncz is intentionally
|
|
123
123
|
designed and driven to simplify and for specific use cases but is not limited to those. In every
|
|
@@ -144,7 +144,7 @@ scheduler = AsyncIOScheduler()
|
|
|
144
144
|
```
|
|
145
145
|
|
|
146
146
|
Initialize the rest of the application after the `scheduler` initialisation.
|
|
147
|
-
More [details](https://asyncz.
|
|
147
|
+
More [details](https://asyncz.dymmond.com/schedulers.md) can be found with more thorough explanations.
|
|
148
148
|
|
|
149
149
|
This is in simple terms and in a nutshell how to start with Asyncz quickly. For more information,
|
|
150
150
|
details and examples how to leverage Asyncz simply navigate through the documentation and have
|
|
@@ -152,11 +152,11 @@ fun 😁🎉.
|
|
|
152
152
|
|
|
153
153
|
## ASGI support
|
|
154
154
|
|
|
155
|
-
Asyncz currently supports the [Esmerald framework](https://asyncz.
|
|
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
158
|
If you wish to have support to any other framework such as FastAPI, Starlite, Starlette or
|
|
159
|
-
literally any other, check the [contributing](https://asyncz.
|
|
159
|
+
literally any other, check the [contributing](https://asyncz.dymmond.com/contributing.md) section and see how you can
|
|
160
160
|
do it.
|
|
161
161
|
|
|
162
162
|
## Sponsors
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.7.0"
|
|
@@ -14,7 +14,7 @@ class scheduler(Task):
|
|
|
14
14
|
id: Optional[str] = None,
|
|
15
15
|
mistrigger_grace_time: Optional[int] = None,
|
|
16
16
|
coalesce: Optional[bool] = None,
|
|
17
|
-
|
|
17
|
+
max_instances: Optional[int] = None,
|
|
18
18
|
next_run_time: Optional[datetime] = None,
|
|
19
19
|
store: Optional[str] = "default",
|
|
20
20
|
executor: Optional[str] = "default",
|
|
@@ -29,7 +29,7 @@ class scheduler(Task):
|
|
|
29
29
|
id=id,
|
|
30
30
|
mistrigger_grace_time=mistrigger_grace_time,
|
|
31
31
|
coalesce=coalesce,
|
|
32
|
-
|
|
32
|
+
max_instances=max_instances,
|
|
33
33
|
next_run_time=next_run_time,
|
|
34
34
|
store=store,
|
|
35
35
|
executor=executor,
|
|
@@ -18,7 +18,6 @@ except ImportError:
|
|
|
18
18
|
|
|
19
19
|
if TYPE_CHECKING:
|
|
20
20
|
from esmerald.applications import Esmerald
|
|
21
|
-
from pydantic.typing import AnyCallable
|
|
22
21
|
|
|
23
22
|
|
|
24
23
|
class EsmeraldScheduler:
|
|
@@ -93,14 +92,15 @@ class EsmeraldScheduler:
|
|
|
93
92
|
"""
|
|
94
93
|
Registers the scheduler events in the Esmerald application.
|
|
95
94
|
"""
|
|
95
|
+
if app.on_startup is not None:
|
|
96
|
+
app.on_startup.append(self.handler.start)
|
|
97
|
+
else:
|
|
98
|
+
app.on_startup = [self.handler.start]
|
|
96
99
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
@app.on_event("shutdown")
|
|
102
|
-
def stop_scheduler() -> None:
|
|
103
|
-
self.handler.shutdown()
|
|
100
|
+
if app.on_shutdown is not None:
|
|
101
|
+
app.on_shutdown.append(self.handler.shutdown)
|
|
102
|
+
else:
|
|
103
|
+
app.on_shutdown = [self.handler.shutdown]
|
|
104
104
|
|
|
105
105
|
def get_scheduler(
|
|
106
106
|
self,
|
|
@@ -145,7 +145,7 @@ class Task:
|
|
|
145
145
|
id: Optional[str] = None,
|
|
146
146
|
mistrigger_grace_time: Optional[int] = None,
|
|
147
147
|
coalesce: Optional[bool] = None,
|
|
148
|
-
|
|
148
|
+
max_instances: Optional[int] = None,
|
|
149
149
|
next_run_time: Optional[datetime] = None,
|
|
150
150
|
store: Optional[str] = "default",
|
|
151
151
|
executor: Optional[str] = "default",
|
|
@@ -163,7 +163,7 @@ class Task:
|
|
|
163
163
|
mistrigger_grace_time: Seconds after the designated runtime that the task is still allowed to be run (or None to allow the task to run no
|
|
164
164
|
matter how late it is).
|
|
165
165
|
coalesce: Run once instead of many times if the scheduler determines that the task should be run more than once in succession.
|
|
166
|
-
|
|
166
|
+
max_instances: Maximum number of concurrently running instances allowed for this task.
|
|
167
167
|
next_run_time: When to first run the task, regardless of the trigger (pass None to add the task as paused).
|
|
168
168
|
store: Alias of the task store to store the task in.
|
|
169
169
|
executor: Alias of the executor to run the task with.
|
|
@@ -178,7 +178,7 @@ class Task:
|
|
|
178
178
|
self.id = id
|
|
179
179
|
self.mistrigger_grace_time = mistrigger_grace_time or undefined
|
|
180
180
|
self.coalesce = coalesce or undefined
|
|
181
|
-
self.
|
|
181
|
+
self.max_instances = max_instances or undefined
|
|
182
182
|
self.next_run_time = next_run_time or undefined
|
|
183
183
|
self.store = store
|
|
184
184
|
self.executor = executor
|
|
@@ -199,7 +199,7 @@ class Task:
|
|
|
199
199
|
name=self.name,
|
|
200
200
|
mistrigger_grace_time=self.mistrigger_grace_time,
|
|
201
201
|
coalesce=self.coalesce,
|
|
202
|
-
max_instances=self.
|
|
202
|
+
max_instances=self.max_instances,
|
|
203
203
|
next_run_time=self.next_run_time,
|
|
204
204
|
store=self.store,
|
|
205
205
|
executor=self.executor,
|
|
@@ -208,7 +208,7 @@ class Task:
|
|
|
208
208
|
except Exception as e:
|
|
209
209
|
raise ImproperlyConfigured(str(e)) from e
|
|
210
210
|
|
|
211
|
-
def __call__(self, fn:
|
|
211
|
+
def __call__(self, fn: Any) -> None:
|
|
212
212
|
"""
|
|
213
213
|
Tricking the object into think it's being instantiated by in reality
|
|
214
214
|
is just returning itself.
|
|
@@ -3,12 +3,12 @@ import traceback
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from collections import defaultdict
|
|
5
5
|
from datetime import datetime, timedelta
|
|
6
|
+
from datetime import timezone as tz
|
|
6
7
|
from traceback import format_tb
|
|
7
8
|
from typing import TYPE_CHECKING, Any, List, Optional, Union
|
|
8
9
|
|
|
9
10
|
from loguru import logger
|
|
10
11
|
from loguru._logger import Logger
|
|
11
|
-
from pytz import utc
|
|
12
12
|
|
|
13
13
|
from asyncz.events import TaskExecutionEvent
|
|
14
14
|
from asyncz.events.constants import TASK_ERROR, TASK_EXECUTED, TASK_MISSED
|
|
@@ -97,7 +97,9 @@ class BaseExecutor(BaseStateExtra, ABC):
|
|
|
97
97
|
if self.instances[task_id] == 0:
|
|
98
98
|
del self.instances[task_id]
|
|
99
99
|
|
|
100
|
-
self.logger.opt(exception=True).error(
|
|
100
|
+
self.logger.opt(exception=True).error(
|
|
101
|
+
f"Error running task {task_id}", exc_info=True
|
|
102
|
+
)
|
|
101
103
|
|
|
102
104
|
|
|
103
105
|
def run_task(
|
|
@@ -118,7 +120,7 @@ def run_task(
|
|
|
118
120
|
events = []
|
|
119
121
|
for run_time in run_times:
|
|
120
122
|
if getattr(task, "mistrigger_grace_time", None) is not None:
|
|
121
|
-
difference = datetime.now(utc) - run_time
|
|
123
|
+
difference = datetime.now(tz.utc) - run_time
|
|
122
124
|
grace_time = timedelta(seconds=task.mistrigger_grace_time)
|
|
123
125
|
if difference > grace_time:
|
|
124
126
|
events.append(
|
|
@@ -182,7 +184,7 @@ async def run_coroutine_task(
|
|
|
182
184
|
events = []
|
|
183
185
|
for run_time in run_times:
|
|
184
186
|
if getattr(task, "mistrigger_grace_time", None) is not None:
|
|
185
|
-
difference = datetime.now(utc) - run_time
|
|
187
|
+
difference = datetime.now(tz.utc) - run_time
|
|
186
188
|
grace_time = timedelta(seconds=task.mistrigger_grace_time)
|
|
187
189
|
if difference > grace_time:
|
|
188
190
|
events.append(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
from abc import ABC, abstractmethod
|
|
3
|
+
from collections.abc import MutableMapping
|
|
3
4
|
from datetime import datetime, timedelta
|
|
4
5
|
from importlib import import_module
|
|
5
6
|
from threading import RLock
|
|
@@ -9,6 +10,7 @@ from loguru import logger
|
|
|
9
10
|
from tzlocal import get_localzone
|
|
10
11
|
|
|
11
12
|
from asyncz._mapping import AsynczObjectMapping
|
|
13
|
+
from asyncz.enums import PluginInstance, SchedulerState
|
|
12
14
|
from asyncz.events.base import SchedulerEvent, TaskEvent, TaskSubmissionEvent
|
|
13
15
|
from asyncz.events.constants import (
|
|
14
16
|
ALL_EVENTS,
|
|
@@ -36,21 +38,21 @@ from asyncz.exceptions import (
|
|
|
36
38
|
)
|
|
37
39
|
from asyncz.executors.base import BaseExecutor
|
|
38
40
|
from asyncz.executors.pool import ThreadPoolExecutor
|
|
41
|
+
from asyncz.schedulers.datastructures import TaskDefaultStruct
|
|
42
|
+
from asyncz.state import BaseStateExtra
|
|
39
43
|
from asyncz.stores.base import BaseStore
|
|
40
44
|
from asyncz.stores.memory import MemoryStore
|
|
41
45
|
from asyncz.tasks import Task
|
|
42
46
|
from asyncz.triggers.base import BaseTrigger
|
|
43
47
|
from asyncz.typing import undefined
|
|
44
|
-
from asyncz.utils import
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
from asyncz.schedulers.datastructures import TaskDefaultStruct
|
|
53
|
-
from asyncz.state import BaseStateExtra
|
|
48
|
+
from asyncz.utils import (
|
|
49
|
+
TIMEOUT_MAX,
|
|
50
|
+
maybe_ref,
|
|
51
|
+
timedelta_seconds,
|
|
52
|
+
to_bool,
|
|
53
|
+
to_int,
|
|
54
|
+
to_timezone,
|
|
55
|
+
)
|
|
54
56
|
|
|
55
57
|
DictAny = Dict[Any, Any]
|
|
56
58
|
|
|
@@ -179,7 +181,9 @@ class BaseScheduler(BaseStateExtra, ABC):
|
|
|
179
181
|
self.real_add_task(task, store_alias, replace_existing)
|
|
180
182
|
del self.pending_tasks[:]
|
|
181
183
|
|
|
182
|
-
self.state =
|
|
184
|
+
self.state = (
|
|
185
|
+
SchedulerState.STATE_PAUSED if paused else SchedulerState.STATE_RUNNING
|
|
186
|
+
)
|
|
183
187
|
self.logger.info("Scheduler started.")
|
|
184
188
|
self.dispatch_event(SchedulerEvent(code=SCHEDULER_START))
|
|
185
189
|
|
|
@@ -281,7 +285,9 @@ class BaseScheduler(BaseStateExtra, ABC):
|
|
|
281
285
|
|
|
282
286
|
self.dispatch_event(SchedulerEvent(code=EXECUTOR_REMOVED, alias=alias))
|
|
283
287
|
|
|
284
|
-
def add_store(
|
|
288
|
+
def add_store(
|
|
289
|
+
self, store: "StoreType", alias: str = "default", **store_options: Any
|
|
290
|
+
):
|
|
285
291
|
"""
|
|
286
292
|
Adds a task store to this scheduler.
|
|
287
293
|
|
|
@@ -418,7 +424,9 @@ class BaseScheduler(BaseStateExtra, ABC):
|
|
|
418
424
|
"max_instances": max_instances,
|
|
419
425
|
"next_run_time": next_run_time,
|
|
420
426
|
}
|
|
421
|
-
task_kwargs = {
|
|
427
|
+
task_kwargs = {
|
|
428
|
+
key: value for key, value in task_struct.items() if value is not undefined
|
|
429
|
+
}
|
|
422
430
|
task = Task(self, **task_kwargs)
|
|
423
431
|
|
|
424
432
|
with self.store_lock:
|
|
@@ -532,9 +540,13 @@ class BaseScheduler(BaseStateExtra, ABC):
|
|
|
532
540
|
trigger = self.create_trigger(trigger, trigger_args)
|
|
533
541
|
now = datetime.now(self.timezone)
|
|
534
542
|
next_run_time = trigger.get_next_trigger_time(None, now)
|
|
535
|
-
return self.update_task(
|
|
543
|
+
return self.update_task(
|
|
544
|
+
task_id, store, trigger=trigger, next_run_time=next_run_time
|
|
545
|
+
)
|
|
536
546
|
|
|
537
|
-
def pause_task(
|
|
547
|
+
def pause_task(
|
|
548
|
+
self, task_id: Union[int, str], store: Optional[str] = None
|
|
549
|
+
) -> "TaskType":
|
|
538
550
|
"""
|
|
539
551
|
Causes the given task not to be executed until it is explicitly resumed.
|
|
540
552
|
|
|
@@ -588,7 +600,9 @@ class BaseScheduler(BaseStateExtra, ABC):
|
|
|
588
600
|
|
|
589
601
|
return tasks
|
|
590
602
|
|
|
591
|
-
def get_task(
|
|
603
|
+
def get_task(
|
|
604
|
+
self, task_id: str, store: Optional[str] = None
|
|
605
|
+
) -> Union["TaskType", None]:
|
|
592
606
|
"""
|
|
593
607
|
Returms the Task that matches the given task_id.
|
|
594
608
|
|
|
@@ -827,7 +841,9 @@ class BaseScheduler(BaseStateExtra, ABC):
|
|
|
827
841
|
# Calculate the next run time if there is none defined
|
|
828
842
|
if not getattr(task, "next_run_time", None):
|
|
829
843
|
now = datetime.now(self.timezone)
|
|
830
|
-
replacements["next_run_time"] = task.trigger.get_next_trigger_time(
|
|
844
|
+
replacements["next_run_time"] = task.trigger.get_next_trigger_time(
|
|
845
|
+
None, now
|
|
846
|
+
)
|
|
831
847
|
|
|
832
848
|
# Apply replacements
|
|
833
849
|
task._update(**replacements)
|
|
@@ -860,7 +876,9 @@ class BaseScheduler(BaseStateExtra, ABC):
|
|
|
860
876
|
try:
|
|
861
877
|
module_path, class_name = module_name.rsplit(":", 1)
|
|
862
878
|
except ValueError as err:
|
|
863
|
-
raise ImportError(
|
|
879
|
+
raise ImportError(
|
|
880
|
+
"%s doesn't look like a module path" % module_name
|
|
881
|
+
) from err
|
|
864
882
|
|
|
865
883
|
module = import_module(module_path)
|
|
866
884
|
|
|
@@ -892,7 +910,9 @@ class BaseScheduler(BaseStateExtra, ABC):
|
|
|
892
910
|
f"The {format(_type)} entry point does not point to a {format(_type)} class."
|
|
893
911
|
) from None
|
|
894
912
|
else:
|
|
895
|
-
raise LookupError(
|
|
913
|
+
raise LookupError(
|
|
914
|
+
f"No {_type} by the name '{alias}' was found."
|
|
915
|
+
) from None
|
|
896
916
|
|
|
897
917
|
return plugin_cls(**constructor_args)
|
|
898
918
|
|
|
@@ -944,7 +964,9 @@ class BaseScheduler(BaseStateExtra, ABC):
|
|
|
944
964
|
self.logger.warning(
|
|
945
965
|
f"Error getting due tasks from the store {store_alias}: {e}."
|
|
946
966
|
)
|
|
947
|
-
retry_wakeup_time = now + timedelta(
|
|
967
|
+
retry_wakeup_time = now + timedelta(
|
|
968
|
+
seconds=self.store_retry_interval
|
|
969
|
+
)
|
|
948
970
|
if not next_wakeup_time or next_wakeup_time > retry_wakeup_time:
|
|
949
971
|
next_wakeup_time = retry_wakeup_time
|
|
950
972
|
continue
|
|
@@ -960,7 +982,9 @@ class BaseScheduler(BaseStateExtra, ABC):
|
|
|
960
982
|
continue
|
|
961
983
|
|
|
962
984
|
run_times = task.get_run_times(now)
|
|
963
|
-
run_times =
|
|
985
|
+
run_times = (
|
|
986
|
+
run_times[-1:] if run_times and task.coalesce else run_times
|
|
987
|
+
)
|
|
964
988
|
|
|
965
989
|
if run_times:
|
|
966
990
|
try:
|
|
@@ -990,7 +1014,9 @@ class BaseScheduler(BaseStateExtra, ABC):
|
|
|
990
1014
|
)
|
|
991
1015
|
events.append(event)
|
|
992
1016
|
|
|
993
|
-
next_run = task.trigger.get_next_trigger_time(
|
|
1017
|
+
next_run = task.trigger.get_next_trigger_time(
|
|
1018
|
+
run_times[-1], now
|
|
1019
|
+
)
|
|
994
1020
|
if next_run:
|
|
995
1021
|
task._update(next_run_time=next_run)
|
|
996
1022
|
store.update_task(task)
|
|
@@ -1013,7 +1039,9 @@ class BaseScheduler(BaseStateExtra, ABC):
|
|
|
1013
1039
|
wait_seconds = None
|
|
1014
1040
|
self.logger.debug("No tasks. Waiting until task is added.")
|
|
1015
1041
|
else:
|
|
1016
|
-
wait_seconds = min(
|
|
1042
|
+
wait_seconds = min(
|
|
1043
|
+
max(timedelta_seconds(next_wakeup_time - now), 0), TIMEOUT_MAX
|
|
1044
|
+
)
|
|
1017
1045
|
self.logger.debug(
|
|
1018
1046
|
f"Next wakeup is due at {next_wakeup_time} (in {wait_seconds} seconds)."
|
|
1019
1047
|
)
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import pickle
|
|
2
2
|
from datetime import datetime
|
|
3
|
+
from datetime import timezone as tz
|
|
3
4
|
from typing import Any, List, Optional, Union
|
|
4
5
|
|
|
5
|
-
from pytz import utc
|
|
6
|
-
|
|
7
6
|
from asyncz.exceptions import AsynczException, ConflictIdError, TaskLookupError
|
|
8
7
|
from asyncz.stores.base import BaseStore
|
|
9
8
|
from asyncz.tasks import Task
|
|
@@ -79,7 +78,9 @@ class RedisStore(BaseStore):
|
|
|
79
78
|
try:
|
|
80
79
|
tasks.append(self.rebuild_task(state))
|
|
81
80
|
except BaseException:
|
|
82
|
-
self.logger.exception(
|
|
81
|
+
self.logger.exception(
|
|
82
|
+
f"Unable to restore task '{task_id}'. Removing it..."
|
|
83
|
+
)
|
|
83
84
|
failed_task_ids.append(task_id)
|
|
84
85
|
|
|
85
86
|
if failed_task_ids:
|
|
@@ -98,7 +99,7 @@ class RedisStore(BaseStore):
|
|
|
98
99
|
def get_all_tasks(self) -> List["TaskType"]:
|
|
99
100
|
states = self.redis.hgetall(self.tasks_key)
|
|
100
101
|
tasks = self.rebuild_tasks(states.items())
|
|
101
|
-
paused_sort_key = datetime(9999, 12, 31, tzinfo=utc)
|
|
102
|
+
paused_sort_key = datetime(9999, 12, 31, tzinfo=tz.utc)
|
|
102
103
|
return sorted(tasks, key=lambda task: task.next_run_time or paused_sort_key)
|
|
103
104
|
|
|
104
105
|
def add_task(self, task: "TaskType"):
|
|
@@ -108,12 +109,15 @@ class RedisStore(BaseStore):
|
|
|
108
109
|
with self.redis.pipeline() as pipe:
|
|
109
110
|
pipe.multi()
|
|
110
111
|
pipe.hset(
|
|
111
|
-
self.tasks_key,
|
|
112
|
+
self.tasks_key,
|
|
113
|
+
task.id,
|
|
114
|
+
pickle.dumps(task.__getstate__(), self.pickle_protocol),
|
|
112
115
|
)
|
|
113
116
|
|
|
114
117
|
if task.next_run_time:
|
|
115
118
|
pipe.zadd(
|
|
116
|
-
self.run_times_key,
|
|
119
|
+
self.run_times_key,
|
|
120
|
+
{task.id: datetime_to_utc_timestamp(task.next_run_time)},
|
|
117
121
|
)
|
|
118
122
|
pipe.execute()
|
|
119
123
|
|
|
@@ -123,11 +127,14 @@ class RedisStore(BaseStore):
|
|
|
123
127
|
|
|
124
128
|
with self.redis.pipeline() as pipe:
|
|
125
129
|
pipe.hset(
|
|
126
|
-
self.tasks_key,
|
|
130
|
+
self.tasks_key,
|
|
131
|
+
task.id,
|
|
132
|
+
pickle.dumps(task.__getstate__(), self.pickle_protocol),
|
|
127
133
|
)
|
|
128
134
|
if task.next_run_time:
|
|
129
135
|
pipe.zadd(
|
|
130
|
-
self.run_times_key,
|
|
136
|
+
self.run_times_key,
|
|
137
|
+
{task.id: datetime_to_utc_timestamp(task.next_run_time)},
|
|
131
138
|
)
|
|
132
139
|
else:
|
|
133
140
|
pipe.zrem(self.run_times_key, task.id)
|
|
@@ -6,7 +6,13 @@ from tzlocal import get_localzone
|
|
|
6
6
|
|
|
7
7
|
from asyncz.datastructures import IntervalState
|
|
8
8
|
from asyncz.triggers.base import BaseTrigger
|
|
9
|
-
from asyncz.utils import
|
|
9
|
+
from asyncz.utils import (
|
|
10
|
+
datetime_repr,
|
|
11
|
+
normalize,
|
|
12
|
+
timedelta_seconds,
|
|
13
|
+
to_datetime,
|
|
14
|
+
to_timezone,
|
|
15
|
+
)
|
|
10
16
|
|
|
11
17
|
|
|
12
18
|
class IntervalTrigger(BaseTrigger):
|
|
@@ -73,7 +79,9 @@ class IntervalTrigger(BaseTrigger):
|
|
|
73
79
|
next_trigger_time = self.start_at
|
|
74
80
|
else:
|
|
75
81
|
time_difference_seconds = timedelta_seconds(now - self.start_at)
|
|
76
|
-
next_interval_number = int(
|
|
82
|
+
next_interval_number = int(
|
|
83
|
+
ceil(time_difference_seconds / self.interval_size)
|
|
84
|
+
)
|
|
77
85
|
next_trigger_time = self.start_at + self.interval * next_interval_number
|
|
78
86
|
|
|
79
87
|
if self.jitter is not None:
|
|
@@ -98,7 +106,10 @@ class IntervalTrigger(BaseTrigger):
|
|
|
98
106
|
return f"interval[{self.interval}]"
|
|
99
107
|
|
|
100
108
|
def __repr__(self) -> str:
|
|
101
|
-
options = [
|
|
109
|
+
options = [
|
|
110
|
+
"interval=%r" % self.interval,
|
|
111
|
+
"start_at=%r" % datetime_repr(self.start_at),
|
|
112
|
+
]
|
|
102
113
|
if self.end_at:
|
|
103
114
|
options.append("end_at=%r" % datetime_repr(self.end_at))
|
|
104
115
|
if self.jitter:
|
|
@@ -3,17 +3,20 @@ import re
|
|
|
3
3
|
from asyncio import iscoroutinefunction
|
|
4
4
|
from calendar import timegm
|
|
5
5
|
from datetime import date, datetime, time, timedelta, tzinfo
|
|
6
|
+
from datetime import timezone as dttz
|
|
6
7
|
from functools import partial
|
|
7
8
|
from typing import Any, Callable, Union
|
|
8
9
|
|
|
9
|
-
from pytz import FixedOffset, timezone, utc
|
|
10
|
-
|
|
11
10
|
from asyncz.exceptions import AsynczException, AsynczLookupError
|
|
12
11
|
|
|
13
12
|
try:
|
|
14
13
|
from threading import TIMEOUT_MAX
|
|
15
14
|
except ImportError:
|
|
16
15
|
TIMEOUT_MAX = 4294967
|
|
16
|
+
try:
|
|
17
|
+
from zoneinfo import ZoneInfo
|
|
18
|
+
except ImportError:
|
|
19
|
+
from backports.zoneinfo import ZoneInfo
|
|
17
20
|
|
|
18
21
|
BOOL_VALIDATION = {
|
|
19
22
|
"true": ["true", "yes", "on", "y", "t", "1", True],
|
|
@@ -61,22 +64,13 @@ def to_bool(value: str) -> bool:
|
|
|
61
64
|
return False
|
|
62
65
|
|
|
63
66
|
|
|
64
|
-
def to_timezone(value: Any) ->
|
|
67
|
+
def to_timezone(value: Any) -> tzinfo:
|
|
65
68
|
"""
|
|
66
69
|
Converts a value to timezone object.
|
|
67
70
|
"""
|
|
68
71
|
if isinstance(value, str):
|
|
69
|
-
return
|
|
72
|
+
return ZoneInfo(value)
|
|
70
73
|
if isinstance(value, tzinfo):
|
|
71
|
-
if not hasattr(value, "localize") or not hasattr(value, "normalize"):
|
|
72
|
-
raise TypeError("Only timezones from the pytz library are supported.")
|
|
73
|
-
if value.tzname(None) == "local":
|
|
74
|
-
raise ValueError(
|
|
75
|
-
"Unable to determine the name of the local timezone -- you must explicitly "
|
|
76
|
-
"specify the name of the local timezone. Please refrain from using timezones like "
|
|
77
|
-
"BST to prevent problems with daylight saving time. Instead, use a locale based "
|
|
78
|
-
"timezone name (such as Europe/London)."
|
|
79
|
-
)
|
|
80
74
|
return value
|
|
81
75
|
if value is not None:
|
|
82
76
|
raise TypeError("Expected tzinfo, got %s instead" % value.__class__.__name__)
|
|
@@ -109,11 +103,11 @@ def to_datetime(
|
|
|
109
103
|
tzname = values.pop("timezone")
|
|
110
104
|
|
|
111
105
|
if tzname == "Z":
|
|
112
|
-
tz = utc
|
|
106
|
+
tz = dttz.utc
|
|
113
107
|
elif tzname:
|
|
114
108
|
hours, minutes = (int(x) for x in tzname[1:].split(":"))
|
|
115
109
|
sign = 1 if tzname[0] == "+" else -1
|
|
116
|
-
tz =
|
|
110
|
+
tz = dttz(sign * timedelta(hours=hours, minutes=minutes))
|
|
117
111
|
|
|
118
112
|
values = {k: int(v or 0) for k, v in values.items()}
|
|
119
113
|
_datetime = datetime(**values) # type: ignore
|
|
@@ -132,7 +126,7 @@ def to_datetime(
|
|
|
132
126
|
)
|
|
133
127
|
|
|
134
128
|
if isinstance(tz, str):
|
|
135
|
-
tz =
|
|
129
|
+
tz = ZoneInfo(tz)
|
|
136
130
|
|
|
137
131
|
return localize(_datetime, tz)
|
|
138
132
|
|
|
@@ -150,7 +144,7 @@ def utc_timestamp_to_datetime(timestamp: Union[int, float]) -> datetime:
|
|
|
150
144
|
Converts the given timestamp to a datetime instance.
|
|
151
145
|
"""
|
|
152
146
|
if timestamp is not None:
|
|
153
|
-
return datetime.fromtimestamp(timestamp, utc)
|
|
147
|
+
return datetime.fromtimestamp(timestamp, dttz.utc)
|
|
154
148
|
|
|
155
149
|
|
|
156
150
|
def timedelta_seconds(delta: Any) -> Any:
|
|
@@ -196,7 +190,9 @@ def get_callable_name(func: Any) -> Any:
|
|
|
196
190
|
return func.__name__
|
|
197
191
|
return func.__class__.__name__
|
|
198
192
|
|
|
199
|
-
raise TypeError(
|
|
193
|
+
raise TypeError(
|
|
194
|
+
"Unable to determine a name for %r -- maybe it is not a callable?" % func
|
|
195
|
+
)
|
|
200
196
|
|
|
201
197
|
|
|
202
198
|
def obj_to_ref(obj: Any) -> str:
|
|
@@ -320,7 +316,8 @@ def check_callable_args(func: Callable[..., Any], args: Any, kwargs: Any) -> Non
|
|
|
320
316
|
|
|
321
317
|
if unsatisfied_args:
|
|
322
318
|
raise ValueError(
|
|
323
|
-
"The following arguments have not been supplied: %s"
|
|
319
|
+
"The following arguments have not been supplied: %s"
|
|
320
|
+
% ", ".join(unsatisfied_args)
|
|
324
321
|
)
|
|
325
322
|
|
|
326
323
|
if unsatisfied_kwargs:
|
|
@@ -332,7 +329,8 @@ def check_callable_args(func: Callable[..., Any], args: Any, kwargs: Any) -> Non
|
|
|
332
329
|
if not has_varargs and unmatched_args:
|
|
333
330
|
raise ValueError(
|
|
334
331
|
"The list of positional arguments is longer than the target callable can handle "
|
|
335
|
-
"(allowed: %d, given in args: %d)"
|
|
332
|
+
"(allowed: %d, given in args: %d)"
|
|
333
|
+
% (len(args) - len(unmatched_args), len(args))
|
|
336
334
|
)
|
|
337
335
|
|
|
338
336
|
if not has_var_kwargs and unmatched_kwargs:
|
|
@@ -39,11 +39,10 @@ classifiers = [
|
|
|
39
39
|
"Topic :: Internet :: WWW/HTTP",
|
|
40
40
|
]
|
|
41
41
|
dependencies = [
|
|
42
|
-
"asyncio>=3.4.3,<4.0.0",
|
|
43
42
|
"loguru>=0.7.0,<0.8.0",
|
|
44
|
-
"pydantic>=2.
|
|
45
|
-
"
|
|
46
|
-
"
|
|
43
|
+
"pydantic>=2.5.3,<3.0.0",
|
|
44
|
+
"tzlocal>=4.0,<6.0",
|
|
45
|
+
'backports.zoneinfo;python_version<"3.9"',
|
|
47
46
|
]
|
|
48
47
|
keywords = [
|
|
49
48
|
"api",
|
|
@@ -66,38 +65,47 @@ keywords = [
|
|
|
66
65
|
]
|
|
67
66
|
|
|
68
67
|
[project.urls]
|
|
69
|
-
Homepage = "https://github.com/
|
|
70
|
-
Documentation = "https://asyncz.
|
|
71
|
-
Changelog = "https://asyncz.
|
|
68
|
+
Homepage = "https://github.com/dymmond/asyncz"
|
|
69
|
+
Documentation = "https://asyncz.dymmond.com/"
|
|
70
|
+
Changelog = "https://asyncz.dymmond.com/release-notes/"
|
|
72
71
|
Funding = "https://github.com/sponsors/tarsil"
|
|
73
|
-
Source = "https://github.com/
|
|
72
|
+
Source = "https://github.com/dymmond/asyncz"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
[tool.hatch.envs.default]
|
|
76
|
+
dependencies = ["ruff", "pre-commit>=2.17.0,<3.0.0", "twine"]
|
|
74
77
|
|
|
75
78
|
[project.optional-dependencies]
|
|
76
|
-
|
|
77
|
-
# "esmerald==0.4.2",
|
|
78
|
-
"mock>=4.0.3",
|
|
79
|
+
testing = [
|
|
79
80
|
"pymongo>=4.3.3,<5.0.0",
|
|
80
|
-
"pytest
|
|
81
|
-
"pytest-cov >=2.12.0,<
|
|
81
|
+
"pytest>=7.1.3,<9.0.0",
|
|
82
|
+
"pytest-cov >=2.12.0,<6.0.0",
|
|
82
83
|
"pytest-asyncio >=0.19.0,<1.0.0",
|
|
83
84
|
"pytest-loguru>=0.2.0,<1",
|
|
84
|
-
"redis>=4.4.0,<
|
|
85
|
-
"requests >=2.27.0,<3.0.0",
|
|
86
|
-
"ruff>=0.0.256,<1.0.0",
|
|
87
|
-
]
|
|
88
|
-
|
|
89
|
-
dev = [
|
|
90
|
-
"autoflake >=1.4.0,<3.0.0",
|
|
91
|
-
"black>=22.10.0,<23.0.0",
|
|
92
|
-
"flake8>=3.8.3,<7.0.0",
|
|
93
|
-
"ipdb>=0.13.13",
|
|
94
|
-
"isort>=5.0.6,<6.0.0",
|
|
85
|
+
"redis>=4.4.0,<6.0.0",
|
|
95
86
|
"mypy>=0.982,<2.0.0",
|
|
96
|
-
"
|
|
97
|
-
"
|
|
87
|
+
"esmerald",
|
|
88
|
+
"pytz>=2022.6",
|
|
98
89
|
]
|
|
99
90
|
|
|
100
|
-
|
|
91
|
+
[tool.hatch.envs.default.scripts]
|
|
92
|
+
clean_pyc = "find . -type f -name \"*.pyc\" -delete"
|
|
93
|
+
clean_pycache = "find . -type d -name \"*__pycache__*\" -delete"
|
|
94
|
+
lint = "ruff check --fix --line-length 99 asyncz tests docs_src {args}"
|
|
95
|
+
build_with_check = "hatch build; twine check dist/*"
|
|
96
|
+
|
|
97
|
+
[tool.hatch.envs.test]
|
|
98
|
+
dependencies = ["asyncz[testing]"]
|
|
99
|
+
|
|
100
|
+
[tool.hatch.envs.test.scripts]
|
|
101
|
+
# needs docker services running
|
|
102
|
+
test = "pytest --disable-pytest-warnings -s -vv {args}"
|
|
103
|
+
coverage = "pytest --cov=asyncz --cov=tests --cov-report=term-missing:skip-covered --cov-report=html tests {args}"
|
|
104
|
+
check_types = "mypy -p asyncz"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
[tool.hatch.envs.docs]
|
|
108
|
+
dependencies = [
|
|
101
109
|
"mkautodoc >=0.2.0,<0.3.0",
|
|
102
110
|
"mkdocs >=1.1.2,<2.0.0",
|
|
103
111
|
"mkdocs-material >=9.0.13,<10.0.0",
|
|
@@ -106,16 +114,13 @@ doc = [
|
|
|
106
114
|
"mkdocstrings>=0.20.0,<0.30.0",
|
|
107
115
|
"pyyaml >=6.0,<7.0.0",
|
|
108
116
|
]
|
|
109
|
-
|
|
110
|
-
|
|
117
|
+
[tool.hatch.envs.docs.scripts]
|
|
118
|
+
build = "mkdocs build"
|
|
119
|
+
serve = "mkdocs serve --dev-addr localhost:8000"
|
|
111
120
|
|
|
112
121
|
[tool.hatch.version]
|
|
113
122
|
path = "asyncz/__init__.py"
|
|
114
123
|
|
|
115
|
-
[tool.isort]
|
|
116
|
-
profile = "black"
|
|
117
|
-
known_third_party = ["asyncz", "pydantic", "starlette"]
|
|
118
|
-
|
|
119
124
|
[tool.mypy]
|
|
120
125
|
warn_unused_configs = true
|
|
121
126
|
warn_unreachable = true
|
|
@@ -130,22 +135,15 @@ disable_error_code = "attr-defined"
|
|
|
130
135
|
warn_unused_ignores = true
|
|
131
136
|
warn_redundant_casts = true
|
|
132
137
|
|
|
133
|
-
[tool.ruff]
|
|
134
|
-
select = [
|
|
135
|
-
|
|
136
|
-
"W", # pycodestyle warnings
|
|
137
|
-
"F", # pyflakes
|
|
138
|
-
"C", # flake8-comprehensions
|
|
139
|
-
"B", # flake8-bugbear
|
|
140
|
-
]
|
|
141
|
-
ignore = [
|
|
142
|
-
"E501", # line too long, handled by black
|
|
143
|
-
"B008", # do not perform function calls in argument defaults
|
|
144
|
-
"C901", # too complex
|
|
145
|
-
]
|
|
138
|
+
[tool.ruff.lint]
|
|
139
|
+
select = ["E", "W", "F", "C", "B", "I"]
|
|
140
|
+
ignore = ["E501", "B008", "C901", "B026"]
|
|
146
141
|
|
|
147
142
|
exclude = ["docs_src/*"]
|
|
148
143
|
|
|
144
|
+
[tool.ruff.lint.isort]
|
|
145
|
+
known-third-party = ["esmerald", "pydantic", "starlette"]
|
|
146
|
+
|
|
149
147
|
[[tool.mypy.overrides]]
|
|
150
148
|
module = "asyncz.tests.*"
|
|
151
149
|
ignore_missing_imports = true
|
asyncz-0.5.0/asyncz/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.5.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|