async-timer 1.1.6__tar.gz → 1.2.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 (40) hide show
  1. async_timer-1.2.0/LICENSE +21 -0
  2. async_timer-1.2.0/PKG-INFO +265 -0
  3. async_timer-1.2.0/README.md +217 -0
  4. async_timer-1.2.0/pyproject.toml +137 -0
  5. async_timer-1.2.0/src/async_timer/__init__.py +14 -0
  6. async_timer-1.2.0/src/async_timer/decorators.py +72 -0
  7. async_timer-1.2.0/src/async_timer/group.py +88 -0
  8. async_timer-1.2.0/src/async_timer/pacemaker.py +223 -0
  9. async_timer-1.2.0/src/async_timer/py.typed +0 -0
  10. async_timer-1.2.0/src/async_timer/subscription.py +218 -0
  11. async_timer-1.1.6/src/async_timer/traget_caller.py → async_timer-1.2.0/src/async_timer/target_caller.py +30 -18
  12. async_timer-1.2.0/src/async_timer/timer.py +516 -0
  13. {async_timer-1.1.6 → async_timer-1.2.0}/src/mock_async_timer/__init__.py +1 -0
  14. async_timer-1.2.0/src/mock_async_timer/py.typed +0 -0
  15. async_timer-1.2.0/src/mock_async_timer/timer.py +57 -0
  16. async_timer-1.2.0/tests/async_timer/conftest.py +51 -0
  17. async_timer-1.2.0/tests/async_timer/test_audit_fixes.py +219 -0
  18. async_timer-1.2.0/tests/async_timer/test_decorators.py +158 -0
  19. async_timer-1.2.0/tests/async_timer/test_group.py +145 -0
  20. async_timer-1.2.0/tests/async_timer/test_pacemaker.py +77 -0
  21. async_timer-1.2.0/tests/async_timer/test_pacemaker_features.py +253 -0
  22. async_timer-1.2.0/tests/async_timer/test_simple.py +16 -0
  23. async_timer-1.2.0/tests/async_timer/test_subscription.py +606 -0
  24. async_timer-1.2.0/tests/async_timer/test_target_caller.py +63 -0
  25. async_timer-1.2.0/tests/async_timer/test_threadsafe.py +300 -0
  26. async_timer-1.2.0/tests/async_timer/test_timer.py +403 -0
  27. async_timer-1.2.0/tests/async_timer/test_timer_async_cancel.py +55 -0
  28. async_timer-1.2.0/tests/async_timer/test_timer_features.py +250 -0
  29. async_timer-1.2.0/tests/async_timer/test_timer_lifecycle.py +352 -0
  30. async_timer-1.2.0/tests/mock_async_timer/test_mock_timer_lifecycle.py +162 -0
  31. async_timer-1.2.0/tests/mock_async_timer/test_mock_timer_pacemaker.py +41 -0
  32. async_timer-1.2.0/tests/mock_async_timer/test_mock_timer_simple.py +75 -0
  33. async_timer-1.2.0/tests/typing_probe.py +96 -0
  34. async_timer-1.1.6/PKG-INFO +0 -114
  35. async_timer-1.1.6/README.md +0 -92
  36. async_timer-1.1.6/pyproject.toml +0 -86
  37. async_timer-1.1.6/src/async_timer/__init__.py +0 -2
  38. async_timer-1.1.6/src/async_timer/pacemaker.py +0 -71
  39. async_timer-1.1.6/src/async_timer/timer.py +0 -236
  40. async_timer-1.1.6/src/mock_async_timer/timer.py +0 -46
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Ilja Orlovs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,265 @@
1
+ Metadata-Version: 2.1
2
+ Name: async-timer
3
+ Version: 1.2.0
4
+ Summary: The missing Python async timer.
5
+ Keywords: async,asyncio,timer,scheduler,periodic,interval
6
+ Author-Email: Ilja Orlovs <vrghost@gmail.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2023 Ilja Orlovs
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
29
+ Classifier: Development Status :: 5 - Production/Stable
30
+ Classifier: Framework :: AsyncIO
31
+ Classifier: Intended Audience :: Developers
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Operating System :: OS Independent
34
+ Classifier: Programming Language :: Python :: 3 :: Only
35
+ Classifier: Programming Language :: Python :: 3.9
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: Topic :: Software Development :: Libraries
41
+ Classifier: Typing :: Typed
42
+ Project-URL: Homepage, https://github.com/IljaOrlovs/async-timer
43
+ Project-URL: Source, https://github.com/IljaOrlovs/async-timer
44
+ Project-URL: Issues, https://github.com/IljaOrlovs/async-timer/issues
45
+ Project-URL: Changelog, https://github.com/IljaOrlovs/async-timer/blob/main/CHANGELOG.md
46
+ Requires-Python: >=3.9
47
+ Description-Content-Type: text/markdown
48
+
49
+ # Async timer
50
+
51
+ The missing Python async timer.
52
+
53
+ [![Tests](docs/badges/tests.svg)](docs/badges/tests.svg)
54
+ [![Coverage](docs/badges/coverage.svg)](docs/badges/coverage.svg)
55
+ [![CI](https://github.com/IljaOrlovs/async-timer/actions/workflows/main.yml/badge.svg)](https://github.com/IljaOrlovs/async-timer/actions/workflows/main.yml)
56
+
57
+ Run something repeatedly on an interval in asyncio — useful for cache refresh, periodic polling, metrics emission, and similar background work.
58
+
59
+ ## Features
60
+
61
+ * **Zero runtime dependencies.**
62
+ * **Any callable shape.** Sync or async functions, generators, async generators, or callables returning any of those.
63
+ * **Two delivery models.** `join()` / `wait()` / `async for self` is single-shot fan-out (latest value, may drop intermediate ticks under slow consumers). `subscribe()` gives each consumer a buffered queue (every tick, optional `maxsize` for bounded drop-oldest).
64
+ * **Scheduling modes.** `fixed_delay` (default; next tick fires `delay` after the previous one finishes) or `fixed_rate` (anchored to wall clock; missed slots skipped + logged). Optional `initial_delay` and `jitter`.
65
+ * **Trigger on demand.** `await timer.trigger()` fires now and resumes the schedule.
66
+ * **Last-value cache.** `timer.last_result` / `timer.last_tick_at` — no blocking.
67
+ * **Cancel anytime.** Explicit `cancel()` or constructor `cancel_aws` (awaitables that stop the timer when they resolve). `await cancel()` waits for cleanup before returning; safe from inside the target/callbacks.
68
+ * **Restartable.** `start()` after `cancel()` works (raises if `cancel_aws` was used — those are single-shot).
69
+ * **Decorator.** `@async_timer.every(5)` wraps a function into a Timer; original on `.func`.
70
+ * **Groups.** `TimerGroup()` starts/cancels a set of timers together.
71
+ * **Named.** `name="db_refresh"` shows in `repr()` and scopes the logger.
72
+ * **Test-friendly.** `mock_async_timer.MockTimer` replaces real sleeps with an `AsyncMock`.
73
+
74
+ ## Requirements
75
+
76
+ Python 3.9+.
77
+
78
+ ## Installation
79
+
80
+ ```bash
81
+ pip install async-timer
82
+ ```
83
+
84
+ ## Examples
85
+
86
+ ### FastAPI lifespan
87
+
88
+ ```python
89
+ import contextlib
90
+ import time
91
+ import uvicorn
92
+ from fastapi import FastAPI
93
+ import async_timer
94
+
95
+ DB_CACHE = {"initialised": False}
96
+
97
+ async def refresh_db():
98
+ DB_CACHE.update(initialised=True, cur_value=time.time())
99
+
100
+ @contextlib.asynccontextmanager
101
+ async def lifespan(_app: FastAPI):
102
+ async with async_timer.Timer(delay=5, target=refresh_db) as timer:
103
+ await timer.wait(hit_count=1) # wait for first tick
104
+ yield
105
+
106
+ app = FastAPI(lifespan=lifespan)
107
+
108
+ @app.get("/")
109
+ async def root():
110
+ return {"db_cache": DB_CACHE}
111
+
112
+ if __name__ == "__main__":
113
+ uvicorn.run(app, host="0.0.0.0", port=8000)
114
+ ```
115
+
116
+ ### `join()`
117
+
118
+ ```python
119
+ import asyncio
120
+ import async_timer
121
+
122
+ async def main():
123
+ timer = async_timer.Timer(12, target=lambda: 42)
124
+ timer.start()
125
+ val = await timer.join() # 42, after the first tick
126
+ await timer.cancel()
127
+
128
+ asyncio.run(main())
129
+ ```
130
+
131
+ ### `async for`
132
+
133
+ ```python
134
+ import asyncio, time
135
+ import async_timer
136
+
137
+ async def main():
138
+ async with async_timer.Timer(14, target=time.time) as timer:
139
+ async for t in timer:
140
+ print(t) # current time every 14 seconds
141
+
142
+ asyncio.run(main())
143
+ ```
144
+
145
+ ### Decorator
146
+
147
+ ```python
148
+ import async_timer
149
+
150
+ @async_timer.every(5, mode="fixed_rate", name="db_refresh")
151
+ async def refresh_db():
152
+ ...
153
+
154
+ await refresh_db.func() # call the undecorated fn (tests)
155
+
156
+ async def main():
157
+ refresh_db.start()
158
+ await refresh_db.join()
159
+ await refresh_db.cancel()
160
+ ```
161
+
162
+ ### `TimerGroup`
163
+
164
+ ```python
165
+ import async_timer
166
+
167
+ async def lifespan():
168
+ async with async_timer.TimerGroup() as group:
169
+ group.add(async_timer.Timer(5, target=refresh_db))
170
+ group.add(async_timer.Timer(60, target=prune_cache))
171
+ yield # both running; both cancelled on exit
172
+ ```
173
+
174
+ ### Trigger now
175
+
176
+ ```python
177
+ async def force_refresh(timer):
178
+ return await timer.trigger()
179
+ ```
180
+
181
+ ### Latest value, no blocking
182
+
183
+ ```python
184
+ @async_timer.every(5)
185
+ async def refresh_db():
186
+ return await db.fetch()
187
+
188
+ def get_cached():
189
+ return refresh_db.last_result # None until the first tick
190
+ ```
191
+
192
+ ### Every-tick delivery via `subscribe()`
193
+
194
+ `join()` / `async for self` drop ticks under slow consumers (single-shot fan-out). Use `subscribe()` when you need every tick:
195
+
196
+ ```python
197
+ async with timer.subscribe() as feed:
198
+ async for value in feed:
199
+ await log_it(value) # never misses a tick from subscribe-time
200
+ await asyncio.sleep(3.0) # even though the consumer is slow
201
+ ```
202
+
203
+ Bounded queue (drop oldest + log when full):
204
+
205
+ ```python
206
+ async with timer.subscribe(maxsize=10, name="metrics-sink") as feed:
207
+ async for value in feed:
208
+ await slow_export(value)
209
+ ```
210
+
211
+ Multiple subscribers each get an independent copy:
212
+
213
+ ```python
214
+ async with timer.subscribe() as a, timer.subscribe() as b:
215
+ ...
216
+ ```
217
+
218
+ Consumer-side load shedding:
219
+
220
+ ```python
221
+ async with timer.subscribe() as feed:
222
+ async for value in feed:
223
+ if feed.qsize > 100:
224
+ feed.drop_oldest(feed.qsize - 1) # keep only the newest
225
+ log.warning("shed %d ticks", feed.dropped_count)
226
+ await slow_export(value)
227
+ ```
228
+
229
+ `drop_oldest()` never swallows end-of-stream / exception sentinels. Target exceptions re-raise from the subscriber's iteration.
230
+
231
+ ## Thread safety
232
+
233
+ A `Timer` runs in a single asyncio event loop. Most state-mutating
234
+ operations must be called from the loop's thread. The following are
235
+ explicitly safe to use from any thread:
236
+
237
+ **Read-only attributes** (atomic under CPython's GIL):
238
+
239
+ * `timer.last_result`, `timer.last_tick_at`, `timer.hit_count`
240
+ * `timer.is_running()`, `timer.delay`, `timer.name`
241
+ * `subscription.qsize`, `subscription.dropped_count`
242
+
243
+ **`set_delay(new_delay)`** is a single attribute write — safe from any
244
+ thread; takes effect on the next sleep.
245
+
246
+ **Cross-thread control methods** — marshal the operation back to the
247
+ timer's loop and block for completion:
248
+
249
+ ```python
250
+ # From a sync REST handler, signal handler, worker thread, etc.:
251
+ timer.cancel_threadsafe(timeout=5.0) # raises TimeoutError if exceeded
252
+ result = timer.trigger_threadsafe(timeout=5.0)
253
+ feed.close_threadsafe()
254
+ ```
255
+
256
+ These raise `RuntimeError` with a clear message if called from the
257
+ timer's own loop thread (use `await cancel()` / `await trigger()`
258
+ instead), or if the timer has not been started yet, or if the bound
259
+ event loop has been closed.
260
+
261
+ Anything else (`subscribe()`, awaiting `join()` / `wait()`, iterating
262
+ `async for` over the timer or a subscription, reading from a
263
+ subscription queue) must happen on the loop's thread. From other
264
+ threads, use `asyncio.run_coroutine_threadsafe(coro, loop)` to
265
+ dispatch.
@@ -0,0 +1,217 @@
1
+ # Async timer
2
+
3
+ The missing Python async timer.
4
+
5
+ [![Tests](docs/badges/tests.svg)](docs/badges/tests.svg)
6
+ [![Coverage](docs/badges/coverage.svg)](docs/badges/coverage.svg)
7
+ [![CI](https://github.com/IljaOrlovs/async-timer/actions/workflows/main.yml/badge.svg)](https://github.com/IljaOrlovs/async-timer/actions/workflows/main.yml)
8
+
9
+ Run something repeatedly on an interval in asyncio — useful for cache refresh, periodic polling, metrics emission, and similar background work.
10
+
11
+ ## Features
12
+
13
+ * **Zero runtime dependencies.**
14
+ * **Any callable shape.** Sync or async functions, generators, async generators, or callables returning any of those.
15
+ * **Two delivery models.** `join()` / `wait()` / `async for self` is single-shot fan-out (latest value, may drop intermediate ticks under slow consumers). `subscribe()` gives each consumer a buffered queue (every tick, optional `maxsize` for bounded drop-oldest).
16
+ * **Scheduling modes.** `fixed_delay` (default; next tick fires `delay` after the previous one finishes) or `fixed_rate` (anchored to wall clock; missed slots skipped + logged). Optional `initial_delay` and `jitter`.
17
+ * **Trigger on demand.** `await timer.trigger()` fires now and resumes the schedule.
18
+ * **Last-value cache.** `timer.last_result` / `timer.last_tick_at` — no blocking.
19
+ * **Cancel anytime.** Explicit `cancel()` or constructor `cancel_aws` (awaitables that stop the timer when they resolve). `await cancel()` waits for cleanup before returning; safe from inside the target/callbacks.
20
+ * **Restartable.** `start()` after `cancel()` works (raises if `cancel_aws` was used — those are single-shot).
21
+ * **Decorator.** `@async_timer.every(5)` wraps a function into a Timer; original on `.func`.
22
+ * **Groups.** `TimerGroup()` starts/cancels a set of timers together.
23
+ * **Named.** `name="db_refresh"` shows in `repr()` and scopes the logger.
24
+ * **Test-friendly.** `mock_async_timer.MockTimer` replaces real sleeps with an `AsyncMock`.
25
+
26
+ ## Requirements
27
+
28
+ Python 3.9+.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install async-timer
34
+ ```
35
+
36
+ ## Examples
37
+
38
+ ### FastAPI lifespan
39
+
40
+ ```python
41
+ import contextlib
42
+ import time
43
+ import uvicorn
44
+ from fastapi import FastAPI
45
+ import async_timer
46
+
47
+ DB_CACHE = {"initialised": False}
48
+
49
+ async def refresh_db():
50
+ DB_CACHE.update(initialised=True, cur_value=time.time())
51
+
52
+ @contextlib.asynccontextmanager
53
+ async def lifespan(_app: FastAPI):
54
+ async with async_timer.Timer(delay=5, target=refresh_db) as timer:
55
+ await timer.wait(hit_count=1) # wait for first tick
56
+ yield
57
+
58
+ app = FastAPI(lifespan=lifespan)
59
+
60
+ @app.get("/")
61
+ async def root():
62
+ return {"db_cache": DB_CACHE}
63
+
64
+ if __name__ == "__main__":
65
+ uvicorn.run(app, host="0.0.0.0", port=8000)
66
+ ```
67
+
68
+ ### `join()`
69
+
70
+ ```python
71
+ import asyncio
72
+ import async_timer
73
+
74
+ async def main():
75
+ timer = async_timer.Timer(12, target=lambda: 42)
76
+ timer.start()
77
+ val = await timer.join() # 42, after the first tick
78
+ await timer.cancel()
79
+
80
+ asyncio.run(main())
81
+ ```
82
+
83
+ ### `async for`
84
+
85
+ ```python
86
+ import asyncio, time
87
+ import async_timer
88
+
89
+ async def main():
90
+ async with async_timer.Timer(14, target=time.time) as timer:
91
+ async for t in timer:
92
+ print(t) # current time every 14 seconds
93
+
94
+ asyncio.run(main())
95
+ ```
96
+
97
+ ### Decorator
98
+
99
+ ```python
100
+ import async_timer
101
+
102
+ @async_timer.every(5, mode="fixed_rate", name="db_refresh")
103
+ async def refresh_db():
104
+ ...
105
+
106
+ await refresh_db.func() # call the undecorated fn (tests)
107
+
108
+ async def main():
109
+ refresh_db.start()
110
+ await refresh_db.join()
111
+ await refresh_db.cancel()
112
+ ```
113
+
114
+ ### `TimerGroup`
115
+
116
+ ```python
117
+ import async_timer
118
+
119
+ async def lifespan():
120
+ async with async_timer.TimerGroup() as group:
121
+ group.add(async_timer.Timer(5, target=refresh_db))
122
+ group.add(async_timer.Timer(60, target=prune_cache))
123
+ yield # both running; both cancelled on exit
124
+ ```
125
+
126
+ ### Trigger now
127
+
128
+ ```python
129
+ async def force_refresh(timer):
130
+ return await timer.trigger()
131
+ ```
132
+
133
+ ### Latest value, no blocking
134
+
135
+ ```python
136
+ @async_timer.every(5)
137
+ async def refresh_db():
138
+ return await db.fetch()
139
+
140
+ def get_cached():
141
+ return refresh_db.last_result # None until the first tick
142
+ ```
143
+
144
+ ### Every-tick delivery via `subscribe()`
145
+
146
+ `join()` / `async for self` drop ticks under slow consumers (single-shot fan-out). Use `subscribe()` when you need every tick:
147
+
148
+ ```python
149
+ async with timer.subscribe() as feed:
150
+ async for value in feed:
151
+ await log_it(value) # never misses a tick from subscribe-time
152
+ await asyncio.sleep(3.0) # even though the consumer is slow
153
+ ```
154
+
155
+ Bounded queue (drop oldest + log when full):
156
+
157
+ ```python
158
+ async with timer.subscribe(maxsize=10, name="metrics-sink") as feed:
159
+ async for value in feed:
160
+ await slow_export(value)
161
+ ```
162
+
163
+ Multiple subscribers each get an independent copy:
164
+
165
+ ```python
166
+ async with timer.subscribe() as a, timer.subscribe() as b:
167
+ ...
168
+ ```
169
+
170
+ Consumer-side load shedding:
171
+
172
+ ```python
173
+ async with timer.subscribe() as feed:
174
+ async for value in feed:
175
+ if feed.qsize > 100:
176
+ feed.drop_oldest(feed.qsize - 1) # keep only the newest
177
+ log.warning("shed %d ticks", feed.dropped_count)
178
+ await slow_export(value)
179
+ ```
180
+
181
+ `drop_oldest()` never swallows end-of-stream / exception sentinels. Target exceptions re-raise from the subscriber's iteration.
182
+
183
+ ## Thread safety
184
+
185
+ A `Timer` runs in a single asyncio event loop. Most state-mutating
186
+ operations must be called from the loop's thread. The following are
187
+ explicitly safe to use from any thread:
188
+
189
+ **Read-only attributes** (atomic under CPython's GIL):
190
+
191
+ * `timer.last_result`, `timer.last_tick_at`, `timer.hit_count`
192
+ * `timer.is_running()`, `timer.delay`, `timer.name`
193
+ * `subscription.qsize`, `subscription.dropped_count`
194
+
195
+ **`set_delay(new_delay)`** is a single attribute write — safe from any
196
+ thread; takes effect on the next sleep.
197
+
198
+ **Cross-thread control methods** — marshal the operation back to the
199
+ timer's loop and block for completion:
200
+
201
+ ```python
202
+ # From a sync REST handler, signal handler, worker thread, etc.:
203
+ timer.cancel_threadsafe(timeout=5.0) # raises TimeoutError if exceeded
204
+ result = timer.trigger_threadsafe(timeout=5.0)
205
+ feed.close_threadsafe()
206
+ ```
207
+
208
+ These raise `RuntimeError` with a clear message if called from the
209
+ timer's own loop thread (use `await cancel()` / `await trigger()`
210
+ instead), or if the timer has not been started yet, or if the bound
211
+ event loop has been closed.
212
+
213
+ Anything else (`subscribe()`, awaiting `join()` / `wait()`, iterating
214
+ `async for` over the timer or a subscription, reading from a
215
+ subscription queue) must happen on the loop's thread. From other
216
+ threads, use `asyncio.run_coroutine_threadsafe(coro, loop)` to
217
+ dispatch.
@@ -0,0 +1,137 @@
1
+ [project]
2
+ name = "async-timer"
3
+ dynamic = []
4
+ description = "The missing Python async timer."
5
+ authors = [
6
+ { name = "Ilja Orlovs", email = "vrghost@gmail.com" },
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.9"
10
+ keywords = [
11
+ "async",
12
+ "asyncio",
13
+ "timer",
14
+ "scheduler",
15
+ "periodic",
16
+ "interval",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 5 - Production/Stable",
20
+ "Framework :: AsyncIO",
21
+ "Intended Audience :: Developers",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: OS Independent",
24
+ "Programming Language :: Python :: 3 :: Only",
25
+ "Programming Language :: Python :: 3.9",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
30
+ "Topic :: Software Development :: Libraries",
31
+ "Typing :: Typed",
32
+ ]
33
+ dependencies = []
34
+ version = "1.2.0"
35
+
36
+ [project.license]
37
+ file = "LICENSE"
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/IljaOrlovs/async-timer"
41
+ Source = "https://github.com/IljaOrlovs/async-timer"
42
+ Issues = "https://github.com/IljaOrlovs/async-timer/issues"
43
+ Changelog = "https://github.com/IljaOrlovs/async-timer/blob/main/CHANGELOG.md"
44
+
45
+ [dependency-groups]
46
+ dev = [
47
+ "pytest>=8.3",
48
+ "pytest-asyncio>=0.24",
49
+ "pytest-local-badge>=1.0.3",
50
+ "pytest-cov>=5.0",
51
+ "pytest-timeout>=2.3",
52
+ "ruff>=0.8",
53
+ "asyncstdlib>=3.12",
54
+ "pyright>=1.1.409",
55
+ ]
56
+
57
+ [build-system]
58
+ requires = [
59
+ "pdm-backend",
60
+ ]
61
+ build-backend = "pdm.backend"
62
+
63
+ [tool.pdm]
64
+ distribution = true
65
+
66
+ [tool.pdm.version]
67
+ source = "scm"
68
+ tag_regex = "^v(?P<version>\\d+\\.\\d+\\.\\d+.*)$"
69
+ fallback_version = "0.0.0.dev0"
70
+
71
+ [tool.pdm.build]
72
+ package-dir = "src"
73
+ includes = [
74
+ "src/async_timer",
75
+ "src/mock_async_timer",
76
+ ]
77
+
78
+ [tool.ruff]
79
+ target-version = "py39"
80
+ line-length = 88
81
+
82
+ [tool.ruff.lint]
83
+ select = [
84
+ "A",
85
+ "B",
86
+ "C",
87
+ "E",
88
+ "F",
89
+ "I",
90
+ "W",
91
+ "N",
92
+ "C4",
93
+ "T20",
94
+ "PTH",
95
+ ]
96
+ ignore = [
97
+ "N802",
98
+ ]
99
+
100
+ [tool.ruff.lint.per-file-ignores]
101
+ "__init__.py" = [
102
+ "F401",
103
+ "E402",
104
+ ]
105
+
106
+ [tool.ruff.lint.isort]
107
+ order-by-type = true
108
+ known-first-party = [
109
+ "async_timer",
110
+ "mock_async_timer",
111
+ ]
112
+ forced-separate = [
113
+ "tests",
114
+ ]
115
+
116
+ [tool.ruff.format]
117
+ quote-style = "double"
118
+
119
+ [tool.pyright]
120
+ include = [
121
+ "src",
122
+ "tests",
123
+ ]
124
+ exclude = [
125
+ "**/__pycache__",
126
+ ".venv",
127
+ "docs",
128
+ ]
129
+ reportIncompatibleVariableOverride = false
130
+
131
+ [tool.pytest.ini_options]
132
+ addopts = "-v -l --color=yes --cov=async_timer --cov=mock_async_timer --cov-report term-missing --no-cov-on-fail --local-badge-output-dir docs/badges/"
133
+ testpaths = [
134
+ "tests",
135
+ ]
136
+ timeout = 3
137
+ asyncio_mode = "auto"
@@ -0,0 +1,14 @@
1
+ from importlib import metadata
2
+
3
+ from . import decorators, group, pacemaker, subscription, target_caller, timer
4
+ from .decorators import every
5
+ from .group import TimerGroup
6
+ from .subscription import Subscription
7
+ from .timer import Timer
8
+
9
+ try:
10
+ __version__ = metadata.version("async-timer")
11
+ except metadata.PackageNotFoundError: # pragma: no cover - editable w/o dist
12
+ __version__ = "0.0.0+unknown"
13
+
14
+ del metadata