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.
Files changed (38) hide show
  1. schedex-0.1.0/LICENSE +24 -0
  2. schedex-0.1.0/PKG-INFO +190 -0
  3. schedex-0.1.0/README.md +150 -0
  4. schedex-0.1.0/pyproject.toml +69 -0
  5. schedex-0.1.0/schedex/__init__.py +35 -0
  6. schedex-0.1.0/schedex/context.py +40 -0
  7. schedex-0.1.0/schedex/contrib/__init__.py +0 -0
  8. schedex-0.1.0/schedex/contrib/dishka.py +53 -0
  9. schedex-0.1.0/schedex/contrib/opentelemetry.py +213 -0
  10. schedex-0.1.0/schedex/contrib/psycopg.py +114 -0
  11. schedex-0.1.0/schedex/contrib/sqlalchemy/__init__.py +16 -0
  12. schedex-0.1.0/schedex/contrib/sqlalchemy/lock.py +811 -0
  13. schedex-0.1.0/schedex/contrib/sqlalchemy/scheduler.py +130 -0
  14. schedex-0.1.0/schedex/contrib/sqlalchemy/storage.py +158 -0
  15. schedex-0.1.0/schedex/contrib/sqlalchemy/tables.py +113 -0
  16. schedex-0.1.0/schedex/eventbus.py +174 -0
  17. schedex-0.1.0/schedex/executor/__init__.py +2 -0
  18. schedex-0.1.0/schedex/executor/task_fetcher.py +87 -0
  19. schedex-0.1.0/schedex/executor/worker_pool.py +295 -0
  20. schedex-0.1.0/schedex/guard.py +210 -0
  21. schedex-0.1.0/schedex/lock.py +130 -0
  22. schedex-0.1.0/schedex/mem/__init__.py +1 -0
  23. schedex-0.1.0/schedex/mem/eventbus.py +68 -0
  24. schedex-0.1.0/schedex/metadata.py +9 -0
  25. schedex-0.1.0/schedex/py.typed +0 -0
  26. schedex-0.1.0/schedex/runtime/__init__.py +1 -0
  27. schedex-0.1.0/schedex/runtime/select.py +140 -0
  28. schedex-0.1.0/schedex/schedule/__init__.py +3 -0
  29. schedex-0.1.0/schedex/schedule/base.py +22 -0
  30. schedex-0.1.0/schedex/schedule/combined.py +20 -0
  31. schedex-0.1.0/schedex/schedule/cron.py +28 -0
  32. schedex-0.1.0/schedex/schedule/periodic.py +200 -0
  33. schedex-0.1.0/schedex/scheduler.py +286 -0
  34. schedex-0.1.0/schedex/serializer/__init__.py +1 -0
  35. schedex-0.1.0/schedex/serializer/json.py +85 -0
  36. schedex-0.1.0/schedex/serializer/serializable.py +86 -0
  37. schedex-0.1.0/schedex/storage.py +194 -0
  38. 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
+
@@ -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)