atask 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.
atask-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rocky Haotian Du
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.
atask-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: atask
3
+ Version: 0.1.0
4
+ Summary: AsyncTask
5
+ Author: Rocky Haotian Du
6
+ Author-email: Rocky Haotian Du <rocky_d@yeah.net>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Python: >=3.12
19
+ Project-URL: Homepage, https://pypi.org/project/atask/
20
+ Project-URL: Repository, https://github.com/rocky-d/atask
21
+ Description-Content-Type: text/markdown
22
+
23
+ # atask
atask-0.1.0/README.md ADDED
@@ -0,0 +1 @@
1
+ # atask
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.8.17,<0.9.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "atask"
7
+ version = "0.1.0"
8
+ description = "AsyncTask"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Rocky Haotian Du", email = "rocky_d@yeah.net" }]
13
+ requires-python = ">=3.12"
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ ]
25
+ dependencies = []
26
+
27
+ [project.urls]
28
+ Homepage = "https://pypi.org/project/atask/"
29
+ Repository = "https://github.com/rocky-d/atask"
30
+
31
+ [dependency-groups]
32
+ dev = [
33
+ "ruff>=0.15.9",
34
+ "ty>=0.0.28",
35
+ "pytest>=8.0.0",
36
+ "pytest-asyncio>=1.0.0",
37
+ "pytest-cov>=6.0.0",
38
+ ]
39
+
40
+ [tool.pytest.ini_options]
41
+ asyncio_mode = "auto"
@@ -0,0 +1,343 @@
1
+ """Lightweight async task abstraction over `asyncio`.
2
+
3
+ - [Homepage](https://pypi.org/project/atask/)
4
+ - [Repository](https://github.com/rocky-d/atask)
5
+ """
6
+
7
+ import asyncio as aio
8
+ from contextvars import Context
9
+ from types import TracebackType
10
+ from typing import Any, Awaitable, Generator, Iterable, Self, Type, final
11
+
12
+ __all__ = [
13
+ "AsyncTask",
14
+ "AsyncTaskGroup",
15
+ ]
16
+
17
+
18
+ class AsyncTask[T](Awaitable[T]):
19
+ """Abstract base for an async task with lifecycle-managed resources.
20
+
21
+ Bridges `asyncio.Task` with class resource management.
22
+
23
+ Lifecycle:
24
+ `__init__` -> `start` -> `_run` -> (`join` | `cancel`) -> `stop`
25
+
26
+ Subclasses override `_run` to define task logic. Parameters are passed via
27
+ `__init__`, not `_run`. Override `start`, `cancel`, `stop` for resource
28
+ management (always call `super()`).
29
+
30
+ Supports `await`, `async with`, and manual control flow.
31
+
32
+ Examples:
33
+ Define a task:
34
+
35
+ class MyTask(AsyncTask[int]):
36
+ async def _run(self) -> int:
37
+ await asyncio.sleep(1.0)
38
+ return 22
39
+
40
+ Manual lifecycle:
41
+
42
+ task = MyTask()
43
+ await task.start()
44
+ await task.join()
45
+ task.result # 22
46
+ await task.stop()
47
+
48
+ Async context manager:
49
+
50
+ async with MyTask() as task:
51
+ result = await task # 22
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ *,
57
+ name: str | None = None,
58
+ context: Context | None = None,
59
+ ) -> None:
60
+ """Initializes the task in a finished, unstarted state.
61
+
62
+ The internal future starts done with `asyncio.InvalidStateError` so
63
+ `start` is the sole transition into running state.
64
+
65
+ Args:
66
+ name: Optional name forwarded to `asyncio.create_task`.
67
+ context: Optional `contextvars.Context` for the task.
68
+ """
69
+ self._name = name
70
+ self._context = context
71
+ self._fut = aio.Future()
72
+ self._fut.set_exception(aio.InvalidStateError())
73
+ self._fut.exception()
74
+ self._started = False
75
+
76
+ @final
77
+ def __await__(
78
+ self,
79
+ ) -> Generator[Any, None, T]:
80
+ """Awaits task completion and returns the result."""
81
+ yield from self.join().__await__()
82
+ return self.result
83
+
84
+ @final
85
+ async def __aenter__(
86
+ self,
87
+ ) -> Self:
88
+ """Starts the task for use as an async context manager."""
89
+ await self.start()
90
+ return self
91
+
92
+ @final
93
+ async def __aexit__(
94
+ self,
95
+ exc_type: Type[BaseException] | None,
96
+ exc_value: BaseException | None,
97
+ exc_traceback: TracebackType | None,
98
+ ) -> None:
99
+ """Stops the task upon exiting the async context."""
100
+ await self.stop(exc_type, exc_value, exc_traceback)
101
+
102
+ @final
103
+ @property
104
+ def name(
105
+ self,
106
+ ) -> str | None:
107
+ """The task name forwarded to `asyncio.create_task`."""
108
+ return self._name
109
+
110
+ @final
111
+ @property
112
+ def context(
113
+ self,
114
+ ) -> Context | None:
115
+ """The `contextvars.Context` for the task."""
116
+ return self._context
117
+
118
+ @final
119
+ @property
120
+ def started(
121
+ self,
122
+ ) -> bool:
123
+ """Whether resources have been acquired and not yet released."""
124
+ return self._started
125
+
126
+ @final
127
+ @property
128
+ def done(
129
+ self,
130
+ ) -> bool:
131
+ """Whether the underlying future has completed."""
132
+ return self._fut.done()
133
+
134
+ @final
135
+ @property
136
+ def cancelled(
137
+ self,
138
+ ) -> bool:
139
+ """Whether the task was cancelled."""
140
+ return self._fut.cancelled()
141
+
142
+ @final
143
+ @property
144
+ def running(
145
+ self,
146
+ ) -> bool:
147
+ """Whether the task is currently executing (`started` and not `done`)."""
148
+ return self.started and not self.done
149
+
150
+ @final
151
+ @property
152
+ def result(
153
+ self,
154
+ ) -> T:
155
+ """The task result. Raises if not done or if the task failed."""
156
+ return self._fut.result()
157
+
158
+ @final
159
+ @property
160
+ def exception(
161
+ self,
162
+ ) -> BaseException | None:
163
+ """The exception raised by the task, or `None` on success."""
164
+ return self._fut.exception()
165
+
166
+ async def _run(
167
+ self,
168
+ ) -> T:
169
+ """Defines the task's async logic.
170
+
171
+ Subclasses must override this. Accepts no arguments beyond `self`;
172
+ configure parameters via `__init__` instead.
173
+
174
+ Returns:
175
+ The task result of type `T`.
176
+
177
+ Raises:
178
+ NotImplementedError: If not overridden.
179
+ """
180
+ raise NotImplementedError()
181
+
182
+ async def start(
183
+ self,
184
+ ) -> None:
185
+ """Acquires resources and starts the task.
186
+
187
+ No-op if already started. Overrides should acquire resources before
188
+ calling `super().start()`.
189
+ """
190
+ if self.started:
191
+ return
192
+ self._started = True
193
+ self._fut = aio.create_task(self._run(), name=self._name, context=self._context)
194
+
195
+ @final
196
+ async def join(
197
+ self,
198
+ ) -> None:
199
+ """Waits for the task to complete. No-op if not started."""
200
+ if not self.started:
201
+ return
202
+ await self._fut
203
+
204
+ async def cancel(
205
+ self,
206
+ msg: Any | None = None,
207
+ ) -> None:
208
+ """Cancels the running task.
209
+
210
+ No-op if not started. Overrides should cancel managed resources
211
+ before calling `super().cancel()`.
212
+
213
+ Args:
214
+ msg: Optional cancellation message.
215
+ """
216
+ if not self.started:
217
+ return
218
+ self._fut.cancel(msg=msg)
219
+ try:
220
+ await self._fut
221
+ except aio.CancelledError:
222
+ pass
223
+
224
+ async def stop(
225
+ self,
226
+ exc_type: Type[BaseException] | None = None,
227
+ exc_value: BaseException | None = None,
228
+ exc_traceback: TracebackType | None = None,
229
+ ) -> None:
230
+ """Releases resources after the task has finished.
231
+
232
+ Requires the task to be started and done. Raises if the task is
233
+ still running. Overrides should release managed resources before
234
+ calling `super().stop()`.
235
+
236
+ Args:
237
+ exc_type: Exception type from the async context manager, if any.
238
+ exc_value: Exception value from the async context manager, if any.
239
+ exc_traceback: Traceback from the async context manager, if any.
240
+
241
+ Raises:
242
+ asyncio.InvalidStateError: If the task is still running.
243
+ """
244
+ if not self.started:
245
+ return
246
+ if not self.done:
247
+ raise aio.InvalidStateError() from exc_value
248
+ self._started = False
249
+
250
+
251
+ class AsyncTaskGroup[T](AsyncTask[list[T]]):
252
+ """An `AsyncTask` subclass that runs multiple tasks concurrently.
253
+
254
+ Delegates lifecycle operations to all contained tasks. Results are
255
+ collected as a list preserving insertion order.
256
+
257
+ Examples:
258
+ Run tasks concurrently:
259
+
260
+ group = AsyncTaskGroup([TaskA(), TaskB()])
261
+ async with group as g:
262
+ results = await g # [result_a, result_b]
263
+ """
264
+
265
+ def __init__(
266
+ self,
267
+ atsks: Iterable[AsyncTask[T]],
268
+ *,
269
+ name: str | None = None,
270
+ context: Context | None = None,
271
+ ) -> None:
272
+ """Initializes the group with the given tasks.
273
+
274
+ Args:
275
+ atsks: `AsyncTask` instances to run concurrently.
276
+ name: Optional group name forwarded to `asyncio.create_task`.
277
+ context: Optional `contextvars.Context` for the group task.
278
+ """
279
+ super().__init__(name=name, context=context)
280
+ self._atsks = list(atsks)
281
+
282
+ async def _run(
283
+ self,
284
+ ) -> list[T]:
285
+ """Joins all subtasks concurrently and collects their results.
286
+
287
+ Returns:
288
+ List of results in insertion order.
289
+ """
290
+ async with aio.TaskGroup() as tg:
291
+ for atsk in self._atsks:
292
+ tg.create_task(atsk.join())
293
+ return [atsk.result for atsk in self._atsks]
294
+
295
+ async def start(
296
+ self,
297
+ ) -> None:
298
+ """Starts all subtasks concurrently, then starts the group task."""
299
+ if self.started:
300
+ return
301
+ async with aio.TaskGroup() as tg:
302
+ for atsk in self._atsks:
303
+ tg.create_task(atsk.start())
304
+ await super().start()
305
+
306
+ async def cancel(
307
+ self,
308
+ msg: Any | None = None,
309
+ ) -> None:
310
+ """Cancels all subtasks concurrently, then cancels the group task.
311
+
312
+ Args:
313
+ msg: Optional cancellation message.
314
+ """
315
+ if not self.started:
316
+ return
317
+ async with aio.TaskGroup() as tg:
318
+ for atsk in self._atsks:
319
+ tg.create_task(atsk.cancel(msg))
320
+ await super().cancel(msg)
321
+
322
+ async def stop(
323
+ self,
324
+ exc_type: Type[BaseException] | None = None,
325
+ exc_value: BaseException | None = None,
326
+ exc_traceback: TracebackType | None = None,
327
+ ) -> None:
328
+ """Stops all subtasks concurrently, then stops the group task.
329
+
330
+ Args:
331
+ exc_type: Exception type from the async context manager, if any.
332
+ exc_value: Exception value from the async context manager, if any.
333
+ exc_traceback: Traceback from the async context manager, if any.
334
+
335
+ Raises:
336
+ asyncio.InvalidStateError: If any task is still running.
337
+ """
338
+ if not self.started:
339
+ return
340
+ async with aio.TaskGroup() as tg:
341
+ for atsk in self._atsks:
342
+ tg.create_task(atsk.stop(exc_type, exc_value, exc_traceback))
343
+ await super().stop(exc_type, exc_value, exc_traceback)
File without changes