asyncz 0.14.2__tar.gz → 0.15.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 (118) hide show
  1. {asyncz-0.14.2 → asyncz-0.15.0}/.gitignore +2 -1
  2. asyncz-0.15.0/PKG-INFO +218 -0
  3. asyncz-0.15.0/README.md +145 -0
  4. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/__init__.py +1 -1
  5. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/cli/commands/list.py +60 -9
  6. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/cli/commands/pause.py +5 -17
  7. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/cli/commands/resume.py +8 -27
  8. asyncz-0.15.0/asyncz/cli/commands/run.py +64 -0
  9. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/cli/types.py +1 -1
  10. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/cli/utils.py +1 -1
  11. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/conf/global_settings.py +32 -0
  12. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/application.py +1 -1
  13. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/controllers/_helpers.py +138 -164
  14. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/controllers/home.py +17 -2
  15. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/controllers/logs.py +9 -4
  16. asyncz-0.15.0/asyncz/contrib/dashboard/controllers/tasks.py +330 -0
  17. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/logs/handler.py +19 -6
  18. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/mixins.py +9 -0
  19. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/templates/index.html +27 -3
  20. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/templates/tasks/_table.html +19 -12
  21. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/templates/tasks/tasks.html +56 -13
  22. asyncz-0.15.0/asyncz/enums.py +29 -0
  23. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/executors/asyncio.py +1 -1
  24. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/executors/base.py +3 -5
  25. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/executors/pool.py +0 -1
  26. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/schedulers/base.py +235 -36
  27. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/schedulers/defaults.py +1 -0
  28. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/schedulers/types.py +44 -0
  29. asyncz-0.15.0/asyncz/tasks/__init__.py +4 -0
  30. asyncz-0.15.0/asyncz/tasks/inspection.py +44 -0
  31. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/tasks/types.py +83 -1
  32. {asyncz-0.14.2 → asyncz-0.15.0}/pyproject.toml +42 -70
  33. asyncz-0.14.2/PKG-INFO +0 -326
  34. asyncz-0.14.2/README.md +0 -247
  35. asyncz-0.14.2/asyncz/cli/commands/run.py +0 -107
  36. asyncz-0.14.2/asyncz/contrib/dashboard/controllers/tasks.py +0 -589
  37. asyncz-0.14.2/asyncz/enums.py +0 -12
  38. asyncz-0.14.2/asyncz/tasks/__init__.py +0 -3
  39. {asyncz-0.14.2 → asyncz-0.15.0}/LICENSE +0 -0
  40. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/cli/__init__.py +0 -0
  41. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/cli/_parsers.py +0 -0
  42. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/cli/app.py +0 -0
  43. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/cli/bootstrap_loader.py +0 -0
  44. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/cli/commands/__init__.py +0 -0
  45. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/cli/commands/add.py +0 -0
  46. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/cli/commands/remove.py +0 -0
  47. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/cli/commands/start.py +0 -0
  48. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/cli/loader.py +0 -0
  49. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/conf/__init__.py +0 -0
  50. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/__init__.py +0 -0
  51. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/__init__.py +0 -0
  52. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/admin/__init__.py +0 -0
  53. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/admin/backends.py +0 -0
  54. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/admin/core.py +0 -0
  55. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/admin/middleware/__init__.py +0 -0
  56. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/admin/middleware/auth_gate.py +0 -0
  57. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/admin/middleware/forward_root_path.py +0 -0
  58. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/admin/protocols.py +0 -0
  59. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/config.py +0 -0
  60. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/controllers/__init__.py +0 -0
  61. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/engine.py +0 -0
  62. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/logs/__init__.py +0 -0
  63. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/logs/storage.py +0 -0
  64. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/messages.py +0 -0
  65. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/statics/css/asyncz.css +0 -0
  66. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/statics/css/toastify.min.css +0 -0
  67. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/statics/js/asyncz.js +0 -0
  68. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/statics/js/toastify.min.js +0 -0
  69. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/templates/404.html +0 -0
  70. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/templates/layout.html +0 -0
  71. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/templates/login.html +0 -0
  72. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/templates/logs/index.html +0 -0
  73. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/templates/logs/partials/table.html +0 -0
  74. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/templates/shared/loading.html +0 -0
  75. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/contrib/dashboard/templates/shared/messages.html +0 -0
  76. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/datastructures.py +0 -0
  77. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/events/__init__.py +0 -0
  78. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/events/base.py +0 -0
  79. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/events/constants.py +0 -0
  80. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/exceptions.py +0 -0
  81. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/executors/__init__.py +0 -0
  82. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/executors/debug.py +0 -0
  83. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/executors/process_pool.py +0 -0
  84. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/executors/types.py +0 -0
  85. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/file_locking.py +0 -0
  86. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/locks.py +0 -0
  87. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/monkay.py +0 -0
  88. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/protocols.py +0 -0
  89. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/py.typed +0 -0
  90. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/schedulers/__init__.py +0 -0
  91. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/schedulers/asyncio.py +0 -0
  92. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/schedulers/datastructures.py +0 -0
  93. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/schedulers/utils.py +0 -0
  94. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/state.py +0 -0
  95. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/stores/__init__.py +0 -0
  96. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/stores/base.py +0 -0
  97. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/stores/file.py +0 -0
  98. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/stores/memory.py +0 -0
  99. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/stores/mongo.py +0 -0
  100. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/stores/redis.py +0 -0
  101. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/stores/sqlalchemy.py +0 -0
  102. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/stores/types.py +0 -0
  103. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/tasks/base.py +0 -0
  104. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/triggers/__init__.py +0 -0
  105. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/triggers/base.py +0 -0
  106. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/triggers/combination.py +0 -0
  107. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/triggers/cron/__init__.py +0 -0
  108. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/triggers/cron/constants.py +0 -0
  109. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/triggers/cron/expressions.py +0 -0
  110. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/triggers/cron/fields.py +0 -0
  111. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/triggers/cron/trigger.py +0 -0
  112. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/triggers/cron/types.py +0 -0
  113. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/triggers/date.py +0 -0
  114. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/triggers/interval.py +0 -0
  115. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/triggers/shutdown.py +0 -0
  116. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/triggers/types.py +0 -0
  117. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/typing.py +0 -0
  118. {asyncz-0.14.2 → asyncz-0.15.0}/asyncz/utils.py +0 -0
@@ -3,8 +3,8 @@
3
3
  *.sqlite3
4
4
  .hypothesis/
5
5
  .idea/
6
- .mypy_cache/
7
6
  .pytest_cache/
7
+ .ruff_cache/
8
8
  .scannerwork/
9
9
  .tox/
10
10
  .venv/
@@ -26,3 +26,4 @@ target/
26
26
  .coverage
27
27
  .python-version
28
28
  coverage.*
29
+ docs/generated
asyncz-0.15.0/PKG-INFO ADDED
@@ -0,0 +1,218 @@
1
+ Metadata-Version: 2.4
2
+ Name: asyncz
3
+ Version: 0.15.0
4
+ Summary: The scheduler that nobody wants but every application needs.
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
+ Project-URL: Funding, https://github.com/sponsors/tarsil
9
+ Project-URL: Source, https://github.com/dymmond/asyncz
10
+ Author-email: Tiago Silva <tiago.arasilva@gmail.com>
11
+ License-File: LICENSE
12
+ Keywords: apscheduler,asgi,asyncz,cron,fastapi,framework,pydantic,scheduler,starlette
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Environment :: Web Environment
15
+ Classifier: Framework :: AnyIO
16
+ Classifier: Framework :: AsyncIO
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Intended Audience :: Information Technology
19
+ Classifier: Intended Audience :: System Administrators
20
+ Classifier: License :: OSI Approved :: BSD License
21
+ Classifier: Operating System :: OS Independent
22
+ Classifier: Programming Language :: Python
23
+ Classifier: Programming Language :: Python :: 3
24
+ Classifier: Programming Language :: Python :: 3 :: Only
25
+ Classifier: Programming Language :: Python :: 3.10
26
+ Classifier: Programming Language :: Python :: 3.11
27
+ Classifier: Programming Language :: Python :: 3.12
28
+ Classifier: Programming Language :: Python :: 3.13
29
+ Classifier: Programming Language :: Python :: 3.14
30
+ Classifier: Topic :: Internet
31
+ Classifier: Topic :: Internet :: WWW/HTTP
32
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
33
+ Classifier: Topic :: Software Development
34
+ Classifier: Topic :: Software Development :: Libraries
35
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
36
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
37
+ Classifier: Typing :: Typed
38
+ Requires-Python: >=3.10
39
+ Requires-Dist: cryptography
40
+ Requires-Dist: monkay>=0.5.0
41
+ Requires-Dist: pydantic<3.0.0,>=2.5.3
42
+ Requires-Dist: sayer>=0.7.4
43
+ Provides-Extra: dashboard
44
+ Requires-Dist: lilya[standard]>=0.22.15; extra == 'dashboard'
45
+ Provides-Extra: docs
46
+ Requires-Dist: griffe-typingdoc<1.0,>=0.2.2; extra == 'docs'
47
+ Requires-Dist: mkdocs-meta-descriptions-plugin>=2.3.0; extra == 'docs'
48
+ Requires-Dist: mkdocstrings-python>=2.0.3; extra == 'docs'
49
+ Requires-Dist: mkdocstrings>=0.28.3; extra == 'docs'
50
+ Requires-Dist: pyyaml<7.0.0,>=6.0; extra == 'docs'
51
+ Requires-Dist: sayer>=0.7.4; extra == 'docs'
52
+ Requires-Dist: typing-extensions>=3.10.0; extra == 'docs'
53
+ Requires-Dist: zensical<0.1.0,>=0.0.23; extra == 'docs'
54
+ Provides-Extra: localtime
55
+ Requires-Dist: tzlocal; extra == 'localtime'
56
+ Provides-Extra: testing
57
+ Requires-Dist: freezegun; extra == 'testing'
58
+ Requires-Dist: httpx; extra == 'testing'
59
+ Requires-Dist: ipdb; extra == 'testing'
60
+ Requires-Dist: lilya[standard]>=0.22.15; extra == 'testing'
61
+ Requires-Dist: pymongo<5.0.0,>=4.3.3; extra == 'testing'
62
+ Requires-Dist: pytest-asyncio<2.0.0,>=1.0; extra == 'testing'
63
+ Requires-Dist: pytest-cov<8.0.0,>=2.12.0; extra == 'testing'
64
+ Requires-Dist: pytest<10.0.0,>=7.1.3; extra == 'testing'
65
+ Requires-Dist: pytz>=2022.6; extra == 'testing'
66
+ Requires-Dist: ravyn>=0.3.7; extra == 'testing'
67
+ Requires-Dist: redis<8.0.0,>=4.4.0; extra == 'testing'
68
+ Requires-Dist: sqlalchemy; extra == 'testing'
69
+ Requires-Dist: starlette; extra == 'testing'
70
+ Requires-Dist: ty; extra == 'testing'
71
+ Requires-Dist: tzlocal; extra == 'testing'
72
+ Description-Content-Type: text/markdown
73
+
74
+ # Asyncz
75
+
76
+ <p align="center">
77
+ <a href="https://asyncz.tarsil.io"><img src="https://res.cloudinary.com/tarsild/image/upload/v1687363326/packages/asyncz/asyncz-new_wiyih8.png" alt='Asyncz'></a>
78
+ </p>
79
+
80
+ <p align="center">
81
+ <em>🚀 The scheduler that simply works. 🚀</em>
82
+ </p>
83
+
84
+ <p align="center">
85
+ <a href="https://github.com/dymmond/asyncz/actions/workflows/test-suite.yml/badge.svg?event=push&branch=main" target="_blank">
86
+ <img src="https://github.com/dymmond/asyncz/actions/workflows/test-suite.yml/badge.svg?event=push&branch=main" alt="Test Suite">
87
+ </a>
88
+
89
+ <a href="https://pypi.org/project/asyncz" target="_blank">
90
+ <img src="https://img.shields.io/pypi/v/asyncz?color=%2334D058&label=pypi%20package" alt="Package version">
91
+ </a>
92
+
93
+ <a href="https://pypi.org/project/asyncz" target="_blank">
94
+ <img src="https://img.shields.io/pypi/pyversions/asyncz.svg?color=%2334D058" alt="Supported Python versions">
95
+ </a>
96
+ </p>
97
+
98
+ ---
99
+
100
+ **Documentation**: [https://asyncz.dymmond.com](https://asyncz.dymmond.com) 📚
101
+
102
+ **Source Code**: [https://github.com/dymmond/asyncz](https://github.com/dymmond/asyncz)
103
+
104
+ ---
105
+
106
+ Asyncz is an async-first scheduler for Python applications and ASGI services. It wraps the core APScheduler model in a codebase that is focused on `asyncio`, explicit task objects, pluggable stores and executors, and framework-friendly lifecycle integration.
107
+
108
+ Documentation: [https://asyncz.dymmond.com](https://asyncz.dymmond.com)
109
+
110
+ ## Highlights
111
+
112
+ - `AsyncIOScheduler` and `NativeAsyncIOScheduler` for async runtimes.
113
+ - Built-in triggers for `date`, `interval`, `cron`, `and`, `or`, and `shutdown`.
114
+ - Built-in stores for `memory`, `file`, `mongodb`, `redis`, and `sqlalchemy`.
115
+ - Executors for in-event-loop work, thread pools, process pools, and direct debug execution.
116
+ - CLI commands for starting schedulers and managing persisted tasks.
117
+ - Task inspection snapshots and filtered task queries for dashboards, CLIs, and admin tooling.
118
+ - Optional dashboard UI for browsing tasks, filtering by schedule metadata, and viewing captured logs.
119
+ - Standard-library logging throughout the project.
120
+
121
+ ## Installation
122
+
123
+ ```bash
124
+ pip install asyncz
125
+ ```
126
+
127
+ Useful extras:
128
+
129
+ ```bash
130
+ pip install "asyncz[dashboard]"
131
+ pip install "asyncz[localtime]"
132
+ ```
133
+
134
+ ## Quick start
135
+
136
+ ```python
137
+ import logging
138
+
139
+ from asyncz.schedulers import AsyncIOScheduler
140
+
141
+ logging.basicConfig(level=logging.INFO)
142
+
143
+ scheduler = AsyncIOScheduler()
144
+
145
+
146
+ def cleanup() -> None:
147
+ logging.getLogger(__name__).info("cleanup finished")
148
+
149
+
150
+ scheduler.add_task(cleanup, "interval", minutes=5, id="cleanup-task")
151
+ scheduler.start()
152
+ ```
153
+
154
+ ## Core concepts
155
+
156
+ Asyncz is built around four main component types:
157
+
158
+ - [Schedulers](https://asyncz.dymmond.com/schedulers/)
159
+ - [Triggers](https://asyncz.dymmond.com/triggers/)
160
+ - [Stores](https://asyncz.dymmond.com/stores/)
161
+ - [Executors](https://asyncz.dymmond.com/executors/)
162
+
163
+ Tasks are the public unit of scheduling. A task combines a callable, a trigger, an executor alias, and the metadata needed to persist and reschedule it correctly.
164
+
165
+ ## Logging
166
+
167
+ Asyncz uses Python's built-in `logging` module. The default logger namespaces are:
168
+
169
+ - `asyncz.schedulers`
170
+ - `asyncz.executors.<alias>`
171
+ - `asyncz.stores.<alias>`
172
+
173
+ If you need custom logger creation, pass your own `loggers_class` to the scheduler. That class only needs to implement the same dictionary-like contract used by `ClassicLogging`.
174
+
175
+ ## ASGI integration
176
+
177
+ Asyncz can wrap an ASGI app directly:
178
+
179
+ ```python
180
+ from asyncz.schedulers import AsyncIOScheduler
181
+
182
+ scheduler = AsyncIOScheduler()
183
+ application = scheduler.asgi(application)
184
+ ```
185
+
186
+ Or you can wire startup and shutdown hooks manually:
187
+
188
+ ```python
189
+ from asyncz.schedulers import AsyncIOScheduler
190
+
191
+ scheduler = AsyncIOScheduler()
192
+
193
+ app = Lilya(
194
+ routes=[...],
195
+ on_startup=[scheduler.start],
196
+ on_shutdown=[scheduler.shutdown],
197
+ )
198
+ ```
199
+
200
+ The scheduler also supports synchronous and asynchronous context managers, which makes it easy to use inside lifespan handlers.
201
+
202
+ ## Persistent stores and encryption
203
+
204
+ The default store is in-memory. For durable scheduling, configure a file, MongoDB, Redis, or SQLAlchemy store.
205
+
206
+ Persistent stores support the `ASYNCZ_STORE_ENCRYPTION_KEY` environment variable. When it is set, task payloads are encrypted before they are written to the backing store.
207
+
208
+ ## CLI and dashboard
209
+
210
+ Asyncz ships with:
211
+
212
+ - a CLI for `start`, `add`, `list`, `run`, `pause`, `resume`, and `remove`
213
+ - a Lilya-based dashboard with task controls, task filters, and a log viewer
214
+
215
+ See the documentation for usage details:
216
+
217
+ - [CLI guide](https://asyncz.dymmond.com/cli/)
218
+ - [Dashboard guide](https://asyncz.dymmond.com/dashboard/dashboard/)
@@ -0,0 +1,145 @@
1
+ # Asyncz
2
+
3
+ <p align="center">
4
+ <a href="https://asyncz.tarsil.io"><img src="https://res.cloudinary.com/tarsild/image/upload/v1687363326/packages/asyncz/asyncz-new_wiyih8.png" alt='Asyncz'></a>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <em>🚀 The scheduler that simply works. 🚀</em>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://github.com/dymmond/asyncz/actions/workflows/test-suite.yml/badge.svg?event=push&branch=main" target="_blank">
13
+ <img src="https://github.com/dymmond/asyncz/actions/workflows/test-suite.yml/badge.svg?event=push&branch=main" alt="Test Suite">
14
+ </a>
15
+
16
+ <a href="https://pypi.org/project/asyncz" target="_blank">
17
+ <img src="https://img.shields.io/pypi/v/asyncz?color=%2334D058&label=pypi%20package" alt="Package version">
18
+ </a>
19
+
20
+ <a href="https://pypi.org/project/asyncz" target="_blank">
21
+ <img src="https://img.shields.io/pypi/pyversions/asyncz.svg?color=%2334D058" alt="Supported Python versions">
22
+ </a>
23
+ </p>
24
+
25
+ ---
26
+
27
+ **Documentation**: [https://asyncz.dymmond.com](https://asyncz.dymmond.com) 📚
28
+
29
+ **Source Code**: [https://github.com/dymmond/asyncz](https://github.com/dymmond/asyncz)
30
+
31
+ ---
32
+
33
+ Asyncz is an async-first scheduler for Python applications and ASGI services. It wraps the core APScheduler model in a codebase that is focused on `asyncio`, explicit task objects, pluggable stores and executors, and framework-friendly lifecycle integration.
34
+
35
+ Documentation: [https://asyncz.dymmond.com](https://asyncz.dymmond.com)
36
+
37
+ ## Highlights
38
+
39
+ - `AsyncIOScheduler` and `NativeAsyncIOScheduler` for async runtimes.
40
+ - Built-in triggers for `date`, `interval`, `cron`, `and`, `or`, and `shutdown`.
41
+ - Built-in stores for `memory`, `file`, `mongodb`, `redis`, and `sqlalchemy`.
42
+ - Executors for in-event-loop work, thread pools, process pools, and direct debug execution.
43
+ - CLI commands for starting schedulers and managing persisted tasks.
44
+ - Task inspection snapshots and filtered task queries for dashboards, CLIs, and admin tooling.
45
+ - Optional dashboard UI for browsing tasks, filtering by schedule metadata, and viewing captured logs.
46
+ - Standard-library logging throughout the project.
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ pip install asyncz
52
+ ```
53
+
54
+ Useful extras:
55
+
56
+ ```bash
57
+ pip install "asyncz[dashboard]"
58
+ pip install "asyncz[localtime]"
59
+ ```
60
+
61
+ ## Quick start
62
+
63
+ ```python
64
+ import logging
65
+
66
+ from asyncz.schedulers import AsyncIOScheduler
67
+
68
+ logging.basicConfig(level=logging.INFO)
69
+
70
+ scheduler = AsyncIOScheduler()
71
+
72
+
73
+ def cleanup() -> None:
74
+ logging.getLogger(__name__).info("cleanup finished")
75
+
76
+
77
+ scheduler.add_task(cleanup, "interval", minutes=5, id="cleanup-task")
78
+ scheduler.start()
79
+ ```
80
+
81
+ ## Core concepts
82
+
83
+ Asyncz is built around four main component types:
84
+
85
+ - [Schedulers](https://asyncz.dymmond.com/schedulers/)
86
+ - [Triggers](https://asyncz.dymmond.com/triggers/)
87
+ - [Stores](https://asyncz.dymmond.com/stores/)
88
+ - [Executors](https://asyncz.dymmond.com/executors/)
89
+
90
+ Tasks are the public unit of scheduling. A task combines a callable, a trigger, an executor alias, and the metadata needed to persist and reschedule it correctly.
91
+
92
+ ## Logging
93
+
94
+ Asyncz uses Python's built-in `logging` module. The default logger namespaces are:
95
+
96
+ - `asyncz.schedulers`
97
+ - `asyncz.executors.<alias>`
98
+ - `asyncz.stores.<alias>`
99
+
100
+ If you need custom logger creation, pass your own `loggers_class` to the scheduler. That class only needs to implement the same dictionary-like contract used by `ClassicLogging`.
101
+
102
+ ## ASGI integration
103
+
104
+ Asyncz can wrap an ASGI app directly:
105
+
106
+ ```python
107
+ from asyncz.schedulers import AsyncIOScheduler
108
+
109
+ scheduler = AsyncIOScheduler()
110
+ application = scheduler.asgi(application)
111
+ ```
112
+
113
+ Or you can wire startup and shutdown hooks manually:
114
+
115
+ ```python
116
+ from asyncz.schedulers import AsyncIOScheduler
117
+
118
+ scheduler = AsyncIOScheduler()
119
+
120
+ app = Lilya(
121
+ routes=[...],
122
+ on_startup=[scheduler.start],
123
+ on_shutdown=[scheduler.shutdown],
124
+ )
125
+ ```
126
+
127
+ The scheduler also supports synchronous and asynchronous context managers, which makes it easy to use inside lifespan handlers.
128
+
129
+ ## Persistent stores and encryption
130
+
131
+ The default store is in-memory. For durable scheduling, configure a file, MongoDB, Redis, or SQLAlchemy store.
132
+
133
+ Persistent stores support the `ASYNCZ_STORE_ENCRYPTION_KEY` environment variable. When it is set, task payloads are encrypted before they are written to the backing store.
134
+
135
+ ## CLI and dashboard
136
+
137
+ Asyncz ships with:
138
+
139
+ - a CLI for `start`, `add`, `list`, `run`, `pause`, `resume`, and `remove`
140
+ - a Lilya-based dashboard with task controls, task filters, and a log viewer
141
+
142
+ See the documentation for usage details:
143
+
144
+ - [CLI guide](https://asyncz.dymmond.com/cli/)
145
+ - [Dashboard guide](https://asyncz.dymmond.com/dashboard/dashboard/)
@@ -2,7 +2,7 @@ from typing import TYPE_CHECKING
2
2
 
3
3
  from .monkay import create_monkay
4
4
 
5
- __version__ = "0.14.2"
5
+ __version__ = "0.15.0"
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  from .conf import settings
@@ -8,12 +8,41 @@ from sayer import Option, command, info
8
8
 
9
9
  from asyncz.cli.bootstrap_loader import load_bootstrap_scheduler
10
10
  from asyncz.cli.utils import build_stores_map, ensure_loop, maybe_await
11
+ from asyncz.enums import TaskScheduleState
11
12
  from asyncz.schedulers import AsyncIOScheduler
12
13
 
13
14
 
14
15
  @command(name="list")
15
16
  def list_jobs(
16
17
  as_json: Annotated[bool, Option(False, "--json", help="Output as JSON")],
18
+ state: Annotated[
19
+ str | None,
20
+ Option(None, "--state", help="Filter by task state: pending, paused, or scheduled."),
21
+ ],
22
+ executor: Annotated[
23
+ str | None,
24
+ Option(None, "--executor", help="Filter by executor alias."),
25
+ ],
26
+ trigger: Annotated[
27
+ str | None,
28
+ Option(None, "--trigger", help="Filter by trigger alias or trigger class name."),
29
+ ],
30
+ q: Annotated[
31
+ str | None,
32
+ Option(None, "--query", help="Case-insensitive free-text task search."),
33
+ ],
34
+ sort_by: Annotated[
35
+ str,
36
+ Option(
37
+ "next_run_time",
38
+ "--sort-by",
39
+ help="Sort by id, name, next_run_time, schedule_state, executor, store, or trigger.",
40
+ ),
41
+ ],
42
+ descending: Annotated[
43
+ bool,
44
+ Option(False, "--desc", help="Reverse the selected sort order."),
45
+ ],
17
46
  bootstrap: Annotated[
18
47
  str | None,
19
48
  Option(
@@ -35,6 +64,7 @@ def list_jobs(
35
64
  asyncz list
36
65
  asyncz list --store durable=sqlite:///scheduler.db
37
66
  asyncz list --json --store default=memory
67
+ asyncz list --state paused --sort-by name
38
68
  asyncz list --bootstrap ravyn.contrib.asyncz:AsynczSpec
39
69
  """
40
70
  loop = ensure_loop()
@@ -60,29 +90,50 @@ def list_jobs(
60
90
  await maybe_await(scheduler.start())
61
91
 
62
92
  try:
63
- # 2) Fetch jobs
64
- jobs = await maybe_await(scheduler.get_tasks())
93
+ # 2) Fetch task snapshots
94
+ schedule_state = (
95
+ TaskScheduleState(state.strip().lower()) if state is not None else None
96
+ )
97
+ infos = await maybe_await(
98
+ scheduler.get_task_infos(
99
+ schedule_state=schedule_state,
100
+ executor=executor,
101
+ trigger=trigger,
102
+ q=q,
103
+ sort_by=sort_by,
104
+ descending=descending,
105
+ )
106
+ )
65
107
 
66
108
  # 3) Format payload
67
109
  payload: list[dict[str, Any]] = [
68
110
  {
69
- "id": j.id,
70
- "name": j.name,
71
- "trigger": type(j.trigger).__name__,
72
- "next_run_time": getattr(j, "next_run_time", None),
111
+ "id": item.id,
112
+ "name": item.name,
113
+ "trigger": item.trigger_name,
114
+ "trigger_alias": item.trigger_alias,
115
+ "trigger_description": item.trigger_description,
116
+ "next_run_time": item.next_run_time,
117
+ "state": item.schedule_state.value,
118
+ "store": item.store_alias,
119
+ "executor": item.executor,
120
+ "callable_name": item.callable_name,
121
+ "callable_reference": item.callable_reference,
73
122
  }
74
- for j in jobs
123
+ for item in infos
75
124
  ]
76
125
 
77
126
  # 4) Output
78
127
  if as_json:
79
- info(json.dumps(payload, default=str))
128
+ print(json.dumps(payload, default=str))
80
129
  return payload
81
130
  else:
82
131
  for r in payload:
83
132
  info(
84
133
  f"{r['id']:<32} {r['name'] or '-':<24} "
85
- f"{r['trigger']:<16} {r['next_run_time']}"
134
+ f"{r['state']:<10} {r['trigger'] or '-':<16} "
135
+ f"{(r['store'] or '-'): <12} {(r['executor'] or '-'): <12} "
136
+ f"{r['next_run_time']}"
86
137
  )
87
138
  return None
88
139
  finally:
@@ -9,8 +9,6 @@ from sayer import Argument, Option, command, info, success
9
9
  from asyncz.cli.bootstrap_loader import load_bootstrap_scheduler
10
10
  from asyncz.cli.utils import build_stores_map, ensure_loop, maybe_await
11
11
  from asyncz.schedulers import AsyncIOScheduler
12
- from asyncz.stores.base import BaseStore
13
- from asyncz.tasks import Task as AsynczTask
14
12
 
15
13
 
16
14
  @command
@@ -28,11 +26,8 @@ def pause(
28
26
  """
29
27
  Pause a job by ID.
30
28
 
31
- This command initializes a temporary scheduler, connects to the specified job store,
32
- and instructs the store to temporarily suspend the execution of the specified job ID.
33
-
34
- This implementation achieves the pause by **directly setting the task's next run time to None**
35
- in the persistent store, avoiding scheduler events that might interfere with CLI execution.
29
+ The CLI delegates to ``scheduler.pause_task(...)`` so command-line behavior
30
+ stays aligned with the scheduler's own pause semantics.
36
31
 
37
32
  Examples:
38
33
  asyncz pause <job_id>
@@ -59,19 +54,12 @@ def pause(
59
54
  scheduler = AsyncIOScheduler(**cfg)
60
55
  await maybe_await(scheduler.start())
61
56
 
62
- # 2) Locate the task and the store alias that holds it
63
- task: AsynczTask
64
- store_alias: str
65
- task, store_alias = scheduler.lookup_task(job_id, None) # type: ignore
66
-
67
- # 3) Mark paused: clear next_run_time and persist in the originating store
68
- task.update_task(next_run_time=None)
69
- store_obj: BaseStore = scheduler.lookup_store(store_alias) # type: ignore
70
- store_obj.update_task(task)
57
+ # 2) Delegate to the scheduler API.
58
+ await maybe_await(scheduler.pause_task(job_id))
71
59
 
72
60
  success(f"Paused job {job_id}")
73
61
 
74
- # 4) Shutdown only if we created a temporary scheduler
62
+ # 3) Shutdown only if we created a temporary scheduler
75
63
  if not bootstrap_mode:
76
64
  await maybe_await(scheduler.shutdown())
77
65
 
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import contextlib
5
- from datetime import datetime
6
5
  from typing import Annotated, Any
7
6
 
8
7
  from sayer import Argument, Option, command, info, success
@@ -10,8 +9,6 @@ from sayer import Argument, Option, command, info, success
10
9
  from asyncz.cli.bootstrap_loader import load_bootstrap_scheduler
11
10
  from asyncz.cli.utils import build_stores_map, ensure_loop, maybe_await
12
11
  from asyncz.schedulers import AsyncIOScheduler
13
- from asyncz.stores.base import BaseStore
14
- from asyncz.tasks import Task as AsynczTask
15
12
 
16
13
 
17
14
  @command
@@ -29,8 +26,8 @@ def resume(
29
26
  """
30
27
  Resume a paused job by ID.
31
28
 
32
- This command initializes a temporary scheduler instance, connects to the specified job store,
33
- and instructs the store to reactivate the execution schedule for the specified job ID.
29
+ The CLI delegates to ``scheduler.resume_task(...)`` so paused-job recovery
30
+ follows the same logic as programmatic callers and the dashboard.
34
31
 
35
32
  Examples:
36
33
  asyncz resume <job_id>
@@ -56,30 +53,14 @@ def resume(
56
53
  scheduler = AsyncIOScheduler(**cfg)
57
54
  await maybe_await(scheduler.start())
58
55
 
59
- # 2) Lookup the task and its backing store alias
60
- # Use None to let the scheduler find the correct store alias for the task.
61
- task: AsynczTask
62
- store_alias: str
63
- task, store_alias = scheduler.lookup_task(job_id, None) # type: ignore
64
-
65
- # 3) Compute the next run time from "now" (resume semantics)
66
- now: datetime = datetime.now(scheduler.timezone)
67
- next_run: datetime | None = task.trigger.get_next_trigger_time( # type: ignore[union-attr]
68
- scheduler.timezone, None, now
69
- )
70
-
71
- # 4) Update or delete in the appropriate store
72
- if next_run:
73
- task.update_task(next_run_time=next_run)
74
- store_obj: BaseStore = scheduler.lookup_store(store_alias) # type: ignore
75
- store_obj.update_task(task)
76
- success(f"Resumed job {job_id}")
77
- else:
78
- # No further runs: remove the task entirely
79
- scheduler.delete_task(job_id, store_alias)
56
+ # 2) Delegate to the scheduler API and mirror the resulting state in the message.
57
+ resumed_task = await maybe_await(scheduler.resume_task(job_id))
58
+ if resumed_task is None:
80
59
  success(f"Removed job {job_id} (schedule finished)")
60
+ else:
61
+ success(f"Resumed job {job_id}")
81
62
 
82
- # 5) Shutdown only if we created a temporary scheduler
63
+ # 3) Shutdown only if we created a temporary scheduler
83
64
  if not bootstrap_mode:
84
65
  await maybe_await(scheduler.shutdown())
85
66
 
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ from typing import Annotated, Any
6
+
7
+ from sayer import Argument, Option, command, info
8
+
9
+ from asyncz.cli.bootstrap_loader import load_bootstrap_scheduler
10
+ from asyncz.cli.utils import build_stores_map, ensure_loop, maybe_await
11
+ from asyncz.schedulers import AsyncIOScheduler
12
+
13
+
14
+ @command
15
+ def run(
16
+ job_id: Annotated[str, Argument(help="Job ID to run now")],
17
+ bootstrap: Annotated[
18
+ str | None,
19
+ Option(
20
+ None,
21
+ help="Dotted path to a class with get_scheduler(), e.g. 'ravyn.contrib.asyncz:AsynczSpec'",
22
+ ),
23
+ ],
24
+ store: Annotated[list[str], Option([], "--store", help="Store spec alias=value. Repeatable.")],
25
+ ) -> None:
26
+ """
27
+ Trigger a job to run immediately.
28
+
29
+ This command delegates to ``scheduler.run_task(...)`` so the CLI, dashboard,
30
+ and programmatic APIs all share the same "run now" semantics.
31
+
32
+ Examples:
33
+
34
+ asyncz run <job_id>
35
+ asyncz run <job_id> --store durable=sqlite:///scheduler.db
36
+ """
37
+ loop: asyncio.AbstractEventLoop = ensure_loop()
38
+
39
+ async def main() -> None:
40
+ """The core asynchronous logic for running the job immediately."""
41
+ bootstrap_mode = bool(bootstrap)
42
+ if bootstrap_mode:
43
+ if store:
44
+ info("Using --bootstrap; ignoring --store flag.")
45
+
46
+ scheduler: AsyncIOScheduler = load_bootstrap_scheduler(bootstrap) # type: ignore[arg-type]
47
+ # Try starting the scheduler if not already running
48
+ with contextlib.suppress(Exception):
49
+ await maybe_await(scheduler.start())
50
+ else:
51
+ # 1. Build configuration and initialize a temporary scheduler
52
+ cfg: dict[str, dict[str, Any]] = {"stores": build_stores_map(store)} if store else {}
53
+ scheduler = AsyncIOScheduler(**cfg)
54
+ await maybe_await(scheduler.start())
55
+
56
+ # 2. Delegate the "run now" flow to the scheduler itself.
57
+ await maybe_await(scheduler.run_task(job_id))
58
+ info(f"Triggered job {job_id}")
59
+
60
+ # 3. Shutdown the temporary scheduler
61
+ if not bootstrap_mode:
62
+ await maybe_await(scheduler.shutdown())
63
+
64
+ loop.run_until_complete(main())
@@ -75,4 +75,4 @@ def parse_trigger(*, cron: str | None, interval: str | None, at: str | None) ->
75
75
  when: dt.datetime = dt.datetime.fromisoformat(at) # type: ignore[arg-type]
76
76
  from asyncz.triggers.date import DateTrigger
77
77
 
78
- return DateTrigger(run_date=when)
78
+ return DateTrigger(run_at=when)