schedex 0.1.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.
- schedex-0.1.0/LICENSE +24 -0
- schedex-0.1.0/PKG-INFO +190 -0
- schedex-0.1.0/README.md +150 -0
- schedex-0.1.0/pyproject.toml +69 -0
- schedex-0.1.0/schedex/__init__.py +35 -0
- schedex-0.1.0/schedex/context.py +40 -0
- schedex-0.1.0/schedex/contrib/__init__.py +0 -0
- schedex-0.1.0/schedex/contrib/dishka.py +53 -0
- schedex-0.1.0/schedex/contrib/opentelemetry.py +213 -0
- schedex-0.1.0/schedex/contrib/psycopg.py +114 -0
- schedex-0.1.0/schedex/contrib/sqlalchemy/__init__.py +16 -0
- schedex-0.1.0/schedex/contrib/sqlalchemy/lock.py +811 -0
- schedex-0.1.0/schedex/contrib/sqlalchemy/scheduler.py +130 -0
- schedex-0.1.0/schedex/contrib/sqlalchemy/storage.py +158 -0
- schedex-0.1.0/schedex/contrib/sqlalchemy/tables.py +113 -0
- schedex-0.1.0/schedex/eventbus.py +174 -0
- schedex-0.1.0/schedex/executor/__init__.py +2 -0
- schedex-0.1.0/schedex/executor/task_fetcher.py +87 -0
- schedex-0.1.0/schedex/executor/worker_pool.py +295 -0
- schedex-0.1.0/schedex/guard.py +210 -0
- schedex-0.1.0/schedex/lock.py +130 -0
- schedex-0.1.0/schedex/mem/__init__.py +1 -0
- schedex-0.1.0/schedex/mem/eventbus.py +68 -0
- schedex-0.1.0/schedex/metadata.py +9 -0
- schedex-0.1.0/schedex/py.typed +0 -0
- schedex-0.1.0/schedex/runtime/__init__.py +1 -0
- schedex-0.1.0/schedex/runtime/select.py +140 -0
- schedex-0.1.0/schedex/schedule/__init__.py +3 -0
- schedex-0.1.0/schedex/schedule/base.py +22 -0
- schedex-0.1.0/schedex/schedule/combined.py +20 -0
- schedex-0.1.0/schedex/schedule/cron.py +28 -0
- schedex-0.1.0/schedex/schedule/periodic.py +200 -0
- schedex-0.1.0/schedex/scheduler.py +286 -0
- schedex-0.1.0/schedex/serializer/__init__.py +1 -0
- schedex-0.1.0/schedex/serializer/json.py +85 -0
- schedex-0.1.0/schedex/serializer/serializable.py +86 -0
- schedex-0.1.0/schedex/storage.py +194 -0
- schedex-0.1.0/schedex/task.py +209 -0
schedex-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org>
|
schedex-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: schedex
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Distributed persistent extendable job scheduling library
|
|
5
|
+
License-Expression: Unlicense
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: schedex,async,scheduler,persistent,distributed
|
|
8
|
+
Author: Dmitry Pershin
|
|
9
|
+
Author-email: dapper1291@gmail.com
|
|
10
|
+
Requires-Python: >=3.12,<4.0
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Classifier: Topic :: Utilities
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Provides-Extra: asyncmy
|
|
23
|
+
Provides-Extra: cron
|
|
24
|
+
Provides-Extra: dishka
|
|
25
|
+
Provides-Extra: opentelemetry
|
|
26
|
+
Provides-Extra: psycopg
|
|
27
|
+
Provides-Extra: sqlalchemy
|
|
28
|
+
Requires-Dist: asyncmy (>=0.2.14) ; extra == "asyncmy"
|
|
29
|
+
Requires-Dist: crontools (>=0.1.6) ; extra == "cron"
|
|
30
|
+
Requires-Dist: dishka (>=1.10.1) ; extra == "dishka"
|
|
31
|
+
Requires-Dist: msgspec (>=0.21.1)
|
|
32
|
+
Requires-Dist: opentelemetry-api (>=1.44.0) ; extra == "opentelemetry"
|
|
33
|
+
Requires-Dist: psycopg[pool] (>=3.3.4) ; extra == "psycopg"
|
|
34
|
+
Requires-Dist: sqlalchemy[asyncio] (>=2.0.52) ; extra == "sqlalchemy"
|
|
35
|
+
Project-URL: Documentation, https://github.com/dapper91/schedex
|
|
36
|
+
Project-URL: Homepage, https://github.com/dapper91/schedex
|
|
37
|
+
Project-URL: Repository, https://github.com/dapper91/schedex
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# schedex
|
|
41
|
+
|
|
42
|
+
Distributed persistent extendable job scheduling library
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```commandline
|
|
47
|
+
pip3 install schedex
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Optional dependencies**:
|
|
51
|
+
|
|
52
|
+
- **cron**: crontab based schedules
|
|
53
|
+
- **sqlalchemy**: [sqlalchemy](https://www.sqlalchemy.org/) backed scheduler
|
|
54
|
+
- **psycopg**: [psycopg](https://www.psycopg.org/psycopg3/docs/) backed scheduler and event broker (through LISTEN/NOTIFY mechanism)
|
|
55
|
+
- **asyncmy**: mysql([asyncmy](https://github.com/long2ice/asyncmy)) backed scheduler
|
|
56
|
+
- **pymongo**: mongodb([pymongo](https://pymongo.readthedocs.io/en/stable/index.html)) backed scheduler
|
|
57
|
+
- **opentelemetry**: telemetry compatible with [opentelemetry](https://opentelemetry.io/) standard
|
|
58
|
+
- **dishka**: dependency injection with [dishka](https://dishka.readthedocs.io/) library
|
|
59
|
+
|
|
60
|
+
## Overview
|
|
61
|
+
|
|
62
|
+
`Schedex` is a modern strictly-typed distributed persistent backend agnostic job scheduling library.
|
|
63
|
+
|
|
64
|
+
### Features
|
|
65
|
+
|
|
66
|
+
- **Database agnostic** nature allows to use it with any relation database (PostgreSQL, MySql, MsSql, MariaDB, Percona etc.)
|
|
67
|
+
or NoSql one (MongoDB, Redis, etc.) which let you pick up the best solution suitable for your requirements.
|
|
68
|
+
|
|
69
|
+
- The library is aimed to real-world projects so that it supports essential features required in production like
|
|
70
|
+
**transactional updates** - may be necessary if your application logic requires scheduling multiple jobs or
|
|
71
|
+
cancel one job and schedule another or modify your domain data and schedule a job atomically.
|
|
72
|
+
|
|
73
|
+
- For data serialization `schedex` uses [msgspec](https://msgspec.dev/) library which make it **serialization format agnostic** (JSON, MsgPack etc.)
|
|
74
|
+
|
|
75
|
+
- **Strongly typed** codebase helps typecheckers like mypy or pyright to validate your code for type correctness
|
|
76
|
+
and fix more bugs on development stage.
|
|
77
|
+
|
|
78
|
+
- The library supports **locking mechanism customization** so that you can choose the one that is more suitable
|
|
79
|
+
in your environment. It supports select-for-update locks (with relational databases), leasing locks; but you can
|
|
80
|
+
implement your own one if it is necessary.
|
|
81
|
+
|
|
82
|
+
### Why yet another scheduler?
|
|
83
|
+
|
|
84
|
+
There are some libraries that occupy the same niche. But they have some disadvantages
|
|
85
|
+
which motivated to write `schedex` instead.
|
|
86
|
+
|
|
87
|
+
#### [procrastinate](https://procrastinate.readthedocs.io)
|
|
88
|
+
|
|
89
|
+
Mature and well tested library with great documentation and support. But it is tied to PostgreSQL which
|
|
90
|
+
doesn't allow to use it with different databases. No transaction support make it useless when the business
|
|
91
|
+
logic requires strong consistency.
|
|
92
|
+
|
|
93
|
+
#### [apscheduler](https://apscheduler.readthedocs.io)
|
|
94
|
+
|
|
95
|
+
Mature and well tested database agnostic library, but has some disadvantages: it is not strictly typed which
|
|
96
|
+
leads to many bugs in runtime; only one locking mechanism supported (leasing locks), transactional modifications
|
|
97
|
+
are not supported which makes it problematic to use the library in strong consistency required applications.
|
|
98
|
+
|
|
99
|
+
## Quickstart
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import datetime as dt
|
|
103
|
+
from typing import Any, MutableMapping
|
|
104
|
+
|
|
105
|
+
import psycopg_pool as ppg_pool
|
|
106
|
+
import sqlalchemy.ext.asyncio as aiosa
|
|
107
|
+
|
|
108
|
+
import schedex as sx
|
|
109
|
+
import schedex.contrib.psycopg as sxpg
|
|
110
|
+
import schedex.contrib.sqlalchemy as sxsa
|
|
111
|
+
|
|
112
|
+
AppState = MutableMapping[str, Any]
|
|
113
|
+
TaskLock = sxsa.SqlAlchemySfuTaskLock
|
|
114
|
+
Context = sx.Context[AppState, TaskLock]
|
|
115
|
+
|
|
116
|
+
tasks = sx.TaskRegistry[AppState, TaskLock]()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@tasks.task(name="print")
|
|
120
|
+
async def print_message(context: Context, message: str, /, *, end: str = "\n") -> None:
|
|
121
|
+
print(message, end=end)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
async def start_scheduler() -> None:
|
|
125
|
+
db_url = "postgresql+psycopg://user:password@localhost:5432/myapp"
|
|
126
|
+
db_engine = aiosa.create_async_engine(db_url)
|
|
127
|
+
|
|
128
|
+
async with db_engine.begin() as conn:
|
|
129
|
+
await conn.run_sync(sxsa.BaseModel.metadata.create_all)
|
|
130
|
+
|
|
131
|
+
async with ppg_pool.AsyncConnectionPool(db_url) as pg_pool:
|
|
132
|
+
event_sender = sxpg.PsycopgEventSender(pg_pool)
|
|
133
|
+
|
|
134
|
+
async with sxsa.SqlAlchemyTransactionalScheduler(
|
|
135
|
+
aiosa.async_sessionmaker(db_engine),
|
|
136
|
+
schedule_type=sx.PeriodicSchedule,
|
|
137
|
+
event_sender=event_sender,
|
|
138
|
+
graceful_timeout=0.1,
|
|
139
|
+
) as scheduler:
|
|
140
|
+
await scheduler.add_job(
|
|
141
|
+
sx.Job(
|
|
142
|
+
sx.PeriodicSchedule.after(dt.timedelta(seconds=0)),
|
|
143
|
+
print_message.with_params("let's get started"),
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
async with scheduler.transactional() as scheduler_tx:
|
|
148
|
+
await scheduler_tx.add_job(
|
|
149
|
+
sx.Job(
|
|
150
|
+
sx.PeriodicSchedule.periodically(
|
|
151
|
+
start_at=dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=0),
|
|
152
|
+
delay=dt.timedelta(seconds=2),
|
|
153
|
+
).with_max_count(5),
|
|
154
|
+
print_message.with_params("hello", end=" "),
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
await scheduler_tx.add_job(
|
|
158
|
+
sx.Job(
|
|
159
|
+
sx.PeriodicSchedule.periodically(
|
|
160
|
+
start_at=dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=1),
|
|
161
|
+
delay=dt.timedelta(seconds=2),
|
|
162
|
+
).with_max_count(5),
|
|
163
|
+
print_message.with_params("world!!!"),
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
async def start_executor() -> None:
|
|
169
|
+
db_url = "postgresql+psycopg://user:password@localhost:5432/myapp"
|
|
170
|
+
db_engine = aiosa.create_async_engine(db_url)
|
|
171
|
+
async with db_engine.begin() as conn:
|
|
172
|
+
await conn.run_sync(sxsa.BaseModel.metadata.create_all)
|
|
173
|
+
|
|
174
|
+
app_state: AppState = {}
|
|
175
|
+
|
|
176
|
+
async with ppg_pool.AsyncConnectionPool(db_url) as pg_pool:
|
|
177
|
+
executor = sx.WorkerPoolExecutor(
|
|
178
|
+
max_workers=3,
|
|
179
|
+
state=app_state,
|
|
180
|
+
polling_interval=dt.timedelta(seconds=15),
|
|
181
|
+
schedule_type=sx.PeriodicSchedule,
|
|
182
|
+
task_registry=tasks,
|
|
183
|
+
lock_manager=sxsa.SqlAlchemySfuLockManager(db_engine, sxpg.PsycopgEventSender(pg_pool)),
|
|
184
|
+
event_receiver=sxpg.PsycopgEventReceiver(pg_pool),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
await executor.run()
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
|
schedex-0.1.0/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# schedex
|
|
2
|
+
|
|
3
|
+
Distributed persistent extendable job scheduling library
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```commandline
|
|
8
|
+
pip3 install schedex
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Optional dependencies**:
|
|
12
|
+
|
|
13
|
+
- **cron**: crontab based schedules
|
|
14
|
+
- **sqlalchemy**: [sqlalchemy](https://www.sqlalchemy.org/) backed scheduler
|
|
15
|
+
- **psycopg**: [psycopg](https://www.psycopg.org/psycopg3/docs/) backed scheduler and event broker (through LISTEN/NOTIFY mechanism)
|
|
16
|
+
- **asyncmy**: mysql([asyncmy](https://github.com/long2ice/asyncmy)) backed scheduler
|
|
17
|
+
- **pymongo**: mongodb([pymongo](https://pymongo.readthedocs.io/en/stable/index.html)) backed scheduler
|
|
18
|
+
- **opentelemetry**: telemetry compatible with [opentelemetry](https://opentelemetry.io/) standard
|
|
19
|
+
- **dishka**: dependency injection with [dishka](https://dishka.readthedocs.io/) library
|
|
20
|
+
|
|
21
|
+
## Overview
|
|
22
|
+
|
|
23
|
+
`Schedex` is a modern strictly-typed distributed persistent backend agnostic job scheduling library.
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
- **Database agnostic** nature allows to use it with any relation database (PostgreSQL, MySql, MsSql, MariaDB, Percona etc.)
|
|
28
|
+
or NoSql one (MongoDB, Redis, etc.) which let you pick up the best solution suitable for your requirements.
|
|
29
|
+
|
|
30
|
+
- The library is aimed to real-world projects so that it supports essential features required in production like
|
|
31
|
+
**transactional updates** - may be necessary if your application logic requires scheduling multiple jobs or
|
|
32
|
+
cancel one job and schedule another or modify your domain data and schedule a job atomically.
|
|
33
|
+
|
|
34
|
+
- For data serialization `schedex` uses [msgspec](https://msgspec.dev/) library which make it **serialization format agnostic** (JSON, MsgPack etc.)
|
|
35
|
+
|
|
36
|
+
- **Strongly typed** codebase helps typecheckers like mypy or pyright to validate your code for type correctness
|
|
37
|
+
and fix more bugs on development stage.
|
|
38
|
+
|
|
39
|
+
- The library supports **locking mechanism customization** so that you can choose the one that is more suitable
|
|
40
|
+
in your environment. It supports select-for-update locks (with relational databases), leasing locks; but you can
|
|
41
|
+
implement your own one if it is necessary.
|
|
42
|
+
|
|
43
|
+
### Why yet another scheduler?
|
|
44
|
+
|
|
45
|
+
There are some libraries that occupy the same niche. But they have some disadvantages
|
|
46
|
+
which motivated to write `schedex` instead.
|
|
47
|
+
|
|
48
|
+
#### [procrastinate](https://procrastinate.readthedocs.io)
|
|
49
|
+
|
|
50
|
+
Mature and well tested library with great documentation and support. But it is tied to PostgreSQL which
|
|
51
|
+
doesn't allow to use it with different databases. No transaction support make it useless when the business
|
|
52
|
+
logic requires strong consistency.
|
|
53
|
+
|
|
54
|
+
#### [apscheduler](https://apscheduler.readthedocs.io)
|
|
55
|
+
|
|
56
|
+
Mature and well tested database agnostic library, but has some disadvantages: it is not strictly typed which
|
|
57
|
+
leads to many bugs in runtime; only one locking mechanism supported (leasing locks), transactional modifications
|
|
58
|
+
are not supported which makes it problematic to use the library in strong consistency required applications.
|
|
59
|
+
|
|
60
|
+
## Quickstart
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import datetime as dt
|
|
64
|
+
from typing import Any, MutableMapping
|
|
65
|
+
|
|
66
|
+
import psycopg_pool as ppg_pool
|
|
67
|
+
import sqlalchemy.ext.asyncio as aiosa
|
|
68
|
+
|
|
69
|
+
import schedex as sx
|
|
70
|
+
import schedex.contrib.psycopg as sxpg
|
|
71
|
+
import schedex.contrib.sqlalchemy as sxsa
|
|
72
|
+
|
|
73
|
+
AppState = MutableMapping[str, Any]
|
|
74
|
+
TaskLock = sxsa.SqlAlchemySfuTaskLock
|
|
75
|
+
Context = sx.Context[AppState, TaskLock]
|
|
76
|
+
|
|
77
|
+
tasks = sx.TaskRegistry[AppState, TaskLock]()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@tasks.task(name="print")
|
|
81
|
+
async def print_message(context: Context, message: str, /, *, end: str = "\n") -> None:
|
|
82
|
+
print(message, end=end)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def start_scheduler() -> None:
|
|
86
|
+
db_url = "postgresql+psycopg://user:password@localhost:5432/myapp"
|
|
87
|
+
db_engine = aiosa.create_async_engine(db_url)
|
|
88
|
+
|
|
89
|
+
async with db_engine.begin() as conn:
|
|
90
|
+
await conn.run_sync(sxsa.BaseModel.metadata.create_all)
|
|
91
|
+
|
|
92
|
+
async with ppg_pool.AsyncConnectionPool(db_url) as pg_pool:
|
|
93
|
+
event_sender = sxpg.PsycopgEventSender(pg_pool)
|
|
94
|
+
|
|
95
|
+
async with sxsa.SqlAlchemyTransactionalScheduler(
|
|
96
|
+
aiosa.async_sessionmaker(db_engine),
|
|
97
|
+
schedule_type=sx.PeriodicSchedule,
|
|
98
|
+
event_sender=event_sender,
|
|
99
|
+
graceful_timeout=0.1,
|
|
100
|
+
) as scheduler:
|
|
101
|
+
await scheduler.add_job(
|
|
102
|
+
sx.Job(
|
|
103
|
+
sx.PeriodicSchedule.after(dt.timedelta(seconds=0)),
|
|
104
|
+
print_message.with_params("let's get started"),
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
async with scheduler.transactional() as scheduler_tx:
|
|
109
|
+
await scheduler_tx.add_job(
|
|
110
|
+
sx.Job(
|
|
111
|
+
sx.PeriodicSchedule.periodically(
|
|
112
|
+
start_at=dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=0),
|
|
113
|
+
delay=dt.timedelta(seconds=2),
|
|
114
|
+
).with_max_count(5),
|
|
115
|
+
print_message.with_params("hello", end=" "),
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
await scheduler_tx.add_job(
|
|
119
|
+
sx.Job(
|
|
120
|
+
sx.PeriodicSchedule.periodically(
|
|
121
|
+
start_at=dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=1),
|
|
122
|
+
delay=dt.timedelta(seconds=2),
|
|
123
|
+
).with_max_count(5),
|
|
124
|
+
print_message.with_params("world!!!"),
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def start_executor() -> None:
|
|
130
|
+
db_url = "postgresql+psycopg://user:password@localhost:5432/myapp"
|
|
131
|
+
db_engine = aiosa.create_async_engine(db_url)
|
|
132
|
+
async with db_engine.begin() as conn:
|
|
133
|
+
await conn.run_sync(sxsa.BaseModel.metadata.create_all)
|
|
134
|
+
|
|
135
|
+
app_state: AppState = {}
|
|
136
|
+
|
|
137
|
+
async with ppg_pool.AsyncConnectionPool(db_url) as pg_pool:
|
|
138
|
+
executor = sx.WorkerPoolExecutor(
|
|
139
|
+
max_workers=3,
|
|
140
|
+
state=app_state,
|
|
141
|
+
polling_interval=dt.timedelta(seconds=15),
|
|
142
|
+
schedule_type=sx.PeriodicSchedule,
|
|
143
|
+
task_registry=tasks,
|
|
144
|
+
lock_manager=sxsa.SqlAlchemySfuLockManager(db_engine, sxpg.PsycopgEventSender(pg_pool)),
|
|
145
|
+
event_receiver=sxpg.PsycopgEventReceiver(pg_pool),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
await executor.run()
|
|
149
|
+
|
|
150
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "schedex"
|
|
3
|
+
description = "Distributed persistent extendable job scheduling library"
|
|
4
|
+
version = "0.1.0"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Unlicense"
|
|
7
|
+
authors = [
|
|
8
|
+
{name = "Dmitry Pershin", email = "dapper1291@gmail.com"}
|
|
9
|
+
]
|
|
10
|
+
keywords = ["schedex", "async", "scheduler", "persistent", "distributed"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Natural Language :: English",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Topic :: Software Development :: Libraries",
|
|
17
|
+
"Topic :: Utilities",
|
|
18
|
+
"Programming Language :: Python",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
requires-python = ">=3.12,<4.0"
|
|
26
|
+
dependencies = [
|
|
27
|
+
"msgspec>=0.21.1",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/dapper91/schedex"
|
|
32
|
+
Repository = "https://github.com/dapper91/schedex"
|
|
33
|
+
Documentation = "https://github.com/dapper91/schedex"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
cron = [
|
|
38
|
+
"crontools>=0.1.6",
|
|
39
|
+
]
|
|
40
|
+
sqlalchemy = [
|
|
41
|
+
"sqlalchemy[asyncio]>=2.0.52",
|
|
42
|
+
]
|
|
43
|
+
psycopg = [
|
|
44
|
+
"psycopg[pool]>=3.3.4",
|
|
45
|
+
]
|
|
46
|
+
asyncmy = [
|
|
47
|
+
"asyncmy>=0.2.14",
|
|
48
|
+
]
|
|
49
|
+
dishka = [
|
|
50
|
+
"dishka>=1.10.1",
|
|
51
|
+
]
|
|
52
|
+
opentelemetry = [
|
|
53
|
+
"opentelemetry-api>=1.44.0",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[dependency-groups]
|
|
57
|
+
dev = [
|
|
58
|
+
"mypy>=2.3.0",
|
|
59
|
+
"polyfactory>=3.3.0",
|
|
60
|
+
"pytest-asyncio>=1.4.0",
|
|
61
|
+
"pytest>=9.1.1",
|
|
62
|
+
"sqlalchemy-utils>=0.42.1",
|
|
63
|
+
"time-machine>=3.5.0",
|
|
64
|
+
"pytest-timeout>=2.4.0",
|
|
65
|
+
"pytest-lazy-fixtures>=1.4.1",
|
|
66
|
+
]
|
|
67
|
+
linters = [
|
|
68
|
+
"pre-commit>=4.6.2",
|
|
69
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from . import mem
|
|
2
|
+
from .context import Context, TaskInfo
|
|
3
|
+
from .eventbus import BufferedEventSender, Event, EventKind, EventReceiver, EventSender
|
|
4
|
+
from .executor import (
|
|
5
|
+
ExecutorMiddleware,
|
|
6
|
+
ExecutorMiddlewareWrappedFunc,
|
|
7
|
+
TaskFetcher,
|
|
8
|
+
Worker,
|
|
9
|
+
WorkerPool,
|
|
10
|
+
WorkerPoolExecutor,
|
|
11
|
+
)
|
|
12
|
+
from .lock import EventManagerMixin, JobLock, Lock, LockManager, TaskLock
|
|
13
|
+
from .schedule import CombinedSchedule, PeriodicSchedule, Schedule
|
|
14
|
+
from .scheduler import (
|
|
15
|
+
Job,
|
|
16
|
+
OwnedSchedulerTransaction,
|
|
17
|
+
Scheduler,
|
|
18
|
+
SchedulerMiddleware,
|
|
19
|
+
SchedulerMiddlewareWrappedFunc,
|
|
20
|
+
SchedulerTransaction,
|
|
21
|
+
TransactionalScheduler,
|
|
22
|
+
)
|
|
23
|
+
from .serializer import Serializable
|
|
24
|
+
from .storage import (
|
|
25
|
+
JobManager,
|
|
26
|
+
JobStatus,
|
|
27
|
+
OwnedTransaction,
|
|
28
|
+
StoredJob,
|
|
29
|
+
StoredTask,
|
|
30
|
+
TaskManager,
|
|
31
|
+
TaskStatus,
|
|
32
|
+
Transaction,
|
|
33
|
+
TransactionManager,
|
|
34
|
+
)
|
|
35
|
+
from .task import BoundTask, Task, TaskRegistry
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import dataclasses as dc
|
|
2
|
+
import datetime as dt
|
|
3
|
+
|
|
4
|
+
from .metadata import Metadata
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dc.dataclass(frozen=True)
|
|
8
|
+
class TaskInfo:
|
|
9
|
+
"""
|
|
10
|
+
Executing task information.
|
|
11
|
+
|
|
12
|
+
:param id: task identifier
|
|
13
|
+
:param job_id: job identifier which spawned the task
|
|
14
|
+
:param sequence_number: task sequence number (unique within a job)
|
|
15
|
+
:param created_at: task creation time
|
|
16
|
+
:param attempts: number of task execution attempts
|
|
17
|
+
:param meta: task metadata
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
id: str
|
|
21
|
+
job_id: str
|
|
22
|
+
sequence_number: int
|
|
23
|
+
created_at: dt.datetime
|
|
24
|
+
attempts: int
|
|
25
|
+
meta: Metadata
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dc.dataclass(frozen=True)
|
|
29
|
+
class Context[StT, LkT]:
|
|
30
|
+
"""
|
|
31
|
+
Task context.
|
|
32
|
+
|
|
33
|
+
:param state: application state
|
|
34
|
+
:param task: executing task
|
|
35
|
+
:param lock: executing task lock
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
state: StT
|
|
39
|
+
task: TaskInfo
|
|
40
|
+
lock: LkT
|
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from collections import ChainMap
|
|
2
|
+
from typing import Any, Callable, Concatenate, MutableMapping, Never
|
|
3
|
+
|
|
4
|
+
import dishka
|
|
5
|
+
from dishka.integrations.base import wrap_injection
|
|
6
|
+
|
|
7
|
+
from schedex import Context, ExecutorMiddlewareWrappedFunc, Task
|
|
8
|
+
|
|
9
|
+
CONTAINER_KEY = "dishka_container"
|
|
10
|
+
|
|
11
|
+
AppState = MutableMapping[str, Any]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InjectedType:
|
|
15
|
+
def __getattribute__(self, item: Any) -> Never:
|
|
16
|
+
raise AssertionError("argument is not injected")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Injected: Any = InjectedType()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def inject[StT: AppState, **P, R](
|
|
23
|
+
func: Callable[Concatenate[Context[StT, Any], P], R],
|
|
24
|
+
) -> Callable[Concatenate[Context[StT, Any], P], R]:
|
|
25
|
+
"""
|
|
26
|
+
Function decorator injecting dependencies into the function arguments.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
return wrap_injection(
|
|
30
|
+
func=func,
|
|
31
|
+
is_async=True,
|
|
32
|
+
container_getter=lambda args, kwargs: args[0].state[CONTAINER_KEY],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def request_container_middleware(
|
|
37
|
+
context: Context[AppState, Any],
|
|
38
|
+
task: Task[Context[AppState, Any], Any, Any],
|
|
39
|
+
/,
|
|
40
|
+
*,
|
|
41
|
+
wrapped: ExecutorMiddlewareWrappedFunc[Context[AppState, Any]],
|
|
42
|
+
) -> None:
|
|
43
|
+
"""
|
|
44
|
+
A middleware that initialize request-scoped di container.
|
|
45
|
+
Works only with MutableMapping application state.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
container: dishka.AsyncContainer = context.state[CONTAINER_KEY]
|
|
49
|
+
|
|
50
|
+
async with container(scope=dishka.Scope.REQUEST) as request_container:
|
|
51
|
+
request_context = ChainMap({CONTAINER_KEY: request_container}, context.state)
|
|
52
|
+
context = Context(state=request_context, task=context.task, lock=context.lock)
|
|
53
|
+
return await wrapped(context, task)
|