fastapi-rtk 0.2.27__py3-none-any.whl → 1.0.13__py3-none-any.whl
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.
- fastapi_rtk/__init__.py +39 -35
- fastapi_rtk/_version.py +1 -0
- fastapi_rtk/api/model_rest_api.py +476 -221
- fastapi_rtk/auth/auth.py +0 -9
- fastapi_rtk/backends/generic/__init__.py +6 -0
- fastapi_rtk/backends/generic/column.py +21 -12
- fastapi_rtk/backends/generic/db.py +42 -7
- fastapi_rtk/backends/generic/filters.py +21 -16
- fastapi_rtk/backends/generic/interface.py +14 -8
- fastapi_rtk/backends/generic/model.py +19 -11
- fastapi_rtk/backends/sqla/__init__.py +1 -0
- fastapi_rtk/backends/sqla/db.py +77 -17
- fastapi_rtk/backends/sqla/extensions/audit/audit.py +401 -189
- fastapi_rtk/backends/sqla/extensions/geoalchemy2/filters.py +15 -12
- fastapi_rtk/backends/sqla/filters.py +50 -21
- fastapi_rtk/backends/sqla/interface.py +96 -34
- fastapi_rtk/backends/sqla/model.py +56 -39
- fastapi_rtk/bases/__init__.py +20 -0
- fastapi_rtk/bases/db.py +94 -7
- fastapi_rtk/bases/file_manager.py +47 -3
- fastapi_rtk/bases/filter.py +22 -0
- fastapi_rtk/bases/interface.py +49 -5
- fastapi_rtk/bases/model.py +3 -0
- fastapi_rtk/bases/session.py +2 -0
- fastapi_rtk/cli/cli.py +62 -9
- fastapi_rtk/cli/commands/__init__.py +23 -0
- fastapi_rtk/cli/{db.py → commands/db/__init__.py} +107 -50
- fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/env.py +2 -3
- fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/env.py +10 -9
- fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/script.py.mako +3 -1
- fastapi_rtk/cli/{export.py → commands/export.py} +12 -10
- fastapi_rtk/cli/{security.py → commands/security.py} +73 -7
- fastapi_rtk/cli/commands/translate.py +299 -0
- fastapi_rtk/cli/decorators.py +9 -4
- fastapi_rtk/cli/utils.py +46 -0
- fastapi_rtk/config.py +41 -1
- fastapi_rtk/const.py +29 -1
- fastapi_rtk/db.py +76 -40
- fastapi_rtk/decorators.py +1 -1
- fastapi_rtk/dependencies.py +134 -62
- fastapi_rtk/exceptions.py +51 -1
- fastapi_rtk/fastapi_react_toolkit.py +186 -171
- fastapi_rtk/file_managers/file_manager.py +8 -6
- fastapi_rtk/file_managers/s3_file_manager.py +69 -33
- fastapi_rtk/globals.py +22 -12
- fastapi_rtk/lang/__init__.py +3 -0
- fastapi_rtk/lang/babel/__init__.py +4 -0
- fastapi_rtk/lang/babel/cli.py +40 -0
- fastapi_rtk/lang/babel/config.py +17 -0
- fastapi_rtk/lang/babel.cfg +1 -0
- fastapi_rtk/lang/lazy_text.py +120 -0
- fastapi_rtk/lang/messages.pot +238 -0
- fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.mo +0 -0
- fastapi_rtk/lang/translations/de/LC_MESSAGES/messages.po +248 -0
- fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.mo +0 -0
- fastapi_rtk/lang/translations/en/LC_MESSAGES/messages.po +244 -0
- fastapi_rtk/manager.py +355 -37
- fastapi_rtk/mixins.py +12 -0
- fastapi_rtk/routers.py +208 -72
- fastapi_rtk/schemas.py +142 -39
- fastapi_rtk/security/sqla/apis.py +39 -13
- fastapi_rtk/security/sqla/models.py +8 -23
- fastapi_rtk/security/sqla/security_manager.py +369 -11
- fastapi_rtk/setting.py +446 -88
- fastapi_rtk/types.py +94 -27
- fastapi_rtk/utils/__init__.py +8 -0
- fastapi_rtk/utils/async_task_runner.py +286 -61
- fastapi_rtk/utils/csv_json_converter.py +243 -40
- fastapi_rtk/utils/hooks.py +34 -0
- fastapi_rtk/utils/merge_schema.py +3 -3
- fastapi_rtk/utils/multiple_async_contexts.py +21 -0
- fastapi_rtk/utils/pydantic.py +46 -1
- fastapi_rtk/utils/run_utils.py +31 -1
- fastapi_rtk/utils/self_dependencies.py +1 -1
- fastapi_rtk/utils/use_default_when_none.py +1 -1
- fastapi_rtk/version.py +6 -1
- fastapi_rtk-1.0.13.dist-info/METADATA +28 -0
- fastapi_rtk-1.0.13.dist-info/RECORD +133 -0
- {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/WHEEL +1 -2
- fastapi_rtk/backends/gremlinpython/__init__.py +0 -108
- fastapi_rtk/backends/gremlinpython/column.py +0 -208
- fastapi_rtk/backends/gremlinpython/db.py +0 -228
- fastapi_rtk/backends/gremlinpython/exceptions.py +0 -34
- fastapi_rtk/backends/gremlinpython/filters.py +0 -461
- fastapi_rtk/backends/gremlinpython/interface.py +0 -734
- fastapi_rtk/backends/gremlinpython/model.py +0 -364
- fastapi_rtk/backends/gremlinpython/session.py +0 -23
- fastapi_rtk/cli/commands.py +0 -295
- fastapi_rtk-0.2.27.dist-info/METADATA +0 -23
- fastapi_rtk-0.2.27.dist-info/RECORD +0 -126
- fastapi_rtk-0.2.27.dist-info/top_level.txt +0 -1
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/README +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/alembic.ini.mako +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi/script.py.mako +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/README +0 -0
- /fastapi_rtk/cli/{templates → commands/db/templates}/fastapi-multidb/alembic.ini.mako +0 -0
- {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/entry_points.txt +0 -0
- {fastapi_rtk-0.2.27.dist-info → fastapi_rtk-1.0.13.dist-info}/licenses/LICENSE +0 -0
fastapi_rtk/types.py
CHANGED
|
@@ -1,33 +1,20 @@
|
|
|
1
|
-
import typing
|
|
2
|
-
|
|
3
1
|
import sqlalchemy.dialects.postgresql as postgresql
|
|
4
2
|
from sqlalchemy import types as sa_types
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class FileColumn(sa_types.TypeDecorator):
|
|
10
|
-
"""
|
|
11
|
-
Extends SQLAlchemy to support and mostly identify a File Column
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
impl = sa_types.Text
|
|
15
|
-
cache_ok = True
|
|
16
|
-
|
|
4
|
+
from .exceptions import FastAPIReactToolkitException
|
|
5
|
+
from .globals import g
|
|
6
|
+
from .utils import lazy
|
|
17
7
|
|
|
18
|
-
|
|
19
|
-
""
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
sa_types.TypeDecorator.__init__(self, **kw)
|
|
29
|
-
self.thumbnail_size = thumbnail_size
|
|
30
|
-
self.size = size
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ListColumn",
|
|
10
|
+
"JSONBListColumn",
|
|
11
|
+
"FileColumn",
|
|
12
|
+
"ImageColumn",
|
|
13
|
+
"FileColumns",
|
|
14
|
+
"ImageColumns",
|
|
15
|
+
"JSONBFileColumns",
|
|
16
|
+
"JSONBImageColumns",
|
|
17
|
+
]
|
|
31
18
|
|
|
32
19
|
|
|
33
20
|
class ListColumn(sa_types.TypeDecorator):
|
|
@@ -66,4 +53,84 @@ class JSONBListColumn(ListColumn):
|
|
|
66
53
|
impl = postgresql.JSONB
|
|
67
54
|
|
|
68
55
|
|
|
69
|
-
|
|
56
|
+
class BaseFileColumn:
|
|
57
|
+
manager = lazy(lambda: g.file_manager)
|
|
58
|
+
origin = "g.file_manager"
|
|
59
|
+
|
|
60
|
+
def __init__(self, allowed_extensions: list[str] | None = None, *args, **kwargs):
|
|
61
|
+
super().__init__(*args, **kwargs)
|
|
62
|
+
if allowed_extensions is None:
|
|
63
|
+
allowed_extensions = self.manager.allowed_extensions
|
|
64
|
+
else:
|
|
65
|
+
# Check whether all extensions are in the allowed extensions from `g.file_manager`
|
|
66
|
+
if self.manager.allowed_extensions is not None and not all(
|
|
67
|
+
ext in self.manager.allowed_extensions for ext in allowed_extensions
|
|
68
|
+
):
|
|
69
|
+
raise FastAPIReactToolkitException(
|
|
70
|
+
f"Some extensions are not in the allowed extensions from {self.origin}",
|
|
71
|
+
)
|
|
72
|
+
self.allowed_extensions = allowed_extensions
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class FileColumn(BaseFileColumn, sa_types.TypeDecorator):
|
|
76
|
+
"""
|
|
77
|
+
Extends SQLAlchemy to support and mostly identify a File Column.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
impl = sa_types.Text
|
|
81
|
+
cache_ok = True
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ImageColumn(BaseFileColumn, sa_types.TypeDecorator):
|
|
85
|
+
"""
|
|
86
|
+
Extends SQLAlchemy to support and mostly identify an Image Column.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
impl = sa_types.Text
|
|
90
|
+
cache_ok = True
|
|
91
|
+
manager = lazy(lambda: g.image_manager)
|
|
92
|
+
origin = "g.image_manager"
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
allowed_extensions: list[str] | None = None,
|
|
97
|
+
thumbnail_size=(20, 20, True),
|
|
98
|
+
size=(100, 100, True),
|
|
99
|
+
*args,
|
|
100
|
+
**kwargs,
|
|
101
|
+
):
|
|
102
|
+
super().__init__(allowed_extensions, *args, **kwargs)
|
|
103
|
+
self.thumbnail_size = thumbnail_size
|
|
104
|
+
self.size = size
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class FileColumns(FileColumn, ListColumn):
|
|
108
|
+
"""
|
|
109
|
+
A column that represents a list of files.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
impl = ListColumn
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ImageColumns(ImageColumn, ListColumn):
|
|
116
|
+
"""
|
|
117
|
+
A column that represents a list of images.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
impl = ListColumn
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class JSONBFileColumns(FileColumn, JSONBListColumn):
|
|
124
|
+
"""
|
|
125
|
+
A column that represents a list of files stored as JSONB.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
impl = JSONBListColumn
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class JSONBImageColumns(ImageColumn, JSONBListColumn):
|
|
132
|
+
"""
|
|
133
|
+
A column that represents a list of images stored as JSONB.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
impl = JSONBListColumn
|
fastapi_rtk/utils/__init__.py
CHANGED
|
@@ -6,8 +6,10 @@ from .csv_json_converter import *
|
|
|
6
6
|
from .deep_merge import *
|
|
7
7
|
from .extender_mixin import *
|
|
8
8
|
from .flask_appbuilder_utils import *
|
|
9
|
+
from .hooks import *
|
|
9
10
|
from .lazy import *
|
|
10
11
|
from .merge_schema import *
|
|
12
|
+
from .multiple_async_contexts import *
|
|
11
13
|
from .prettify_dict import *
|
|
12
14
|
from .pydantic import *
|
|
13
15
|
from .run_utils import *
|
|
@@ -34,22 +36,28 @@ __all__ = [
|
|
|
34
36
|
# .flask_appbuilder_utils
|
|
35
37
|
"uuid_namegen",
|
|
36
38
|
"secure_filename",
|
|
39
|
+
# .hooks
|
|
40
|
+
"hooks",
|
|
37
41
|
# .lazy
|
|
38
42
|
"lazy",
|
|
39
43
|
"lazy_import",
|
|
40
44
|
"lazy_self",
|
|
41
45
|
# .merge_schema
|
|
42
46
|
"merge_schema",
|
|
47
|
+
# . multiple_async_contexts
|
|
48
|
+
"multiple_async_contexts",
|
|
43
49
|
# .prettify_dict
|
|
44
50
|
"prettify_dict",
|
|
45
51
|
# .pydantic
|
|
46
52
|
"generate_schema_from_typed_dict",
|
|
53
|
+
"get_pydantic_model_field",
|
|
47
54
|
# .run_utils
|
|
48
55
|
"smart_run",
|
|
49
56
|
"smart_run_sync",
|
|
50
57
|
"safe_call",
|
|
51
58
|
"safe_call_sync",
|
|
52
59
|
"run_coroutine_in_threadpool",
|
|
60
|
+
"run_function_in_threadpool",
|
|
53
61
|
"call_with_valid_kwargs",
|
|
54
62
|
# .self_dependencies
|
|
55
63
|
"SelfDepends",
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import contextvars
|
|
3
|
+
import functools
|
|
3
4
|
import inspect
|
|
5
|
+
import traceback as tb
|
|
4
6
|
import typing
|
|
5
7
|
|
|
6
8
|
from .prettify_dict import prettify_dict
|
|
7
9
|
|
|
8
10
|
__all__ = ["AsyncTaskRunner"]
|
|
9
11
|
|
|
12
|
+
T = typing.TypeVar("T")
|
|
13
|
+
|
|
10
14
|
|
|
11
15
|
class CallerInfo(typing.TypedDict):
|
|
12
16
|
"""
|
|
@@ -34,26 +38,37 @@ class AsyncTaskException(AsyncTaskRunnerException):
|
|
|
34
38
|
self.caller = caller
|
|
35
39
|
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
except Exception as e:
|
|
51
|
-
raise AsyncTaskException(str(e), original_exception=e, caller=caller) from e
|
|
41
|
+
class ResultAccessor(typing.Generic[T]):
|
|
42
|
+
"""Helper class that can be awaited OR accessed directly."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, task: "AsyncTask[T]"):
|
|
45
|
+
self._task = task
|
|
46
|
+
|
|
47
|
+
def __await__(self):
|
|
48
|
+
"""Allow awaiting: await task.result"""
|
|
49
|
+
return self._task.__await__()
|
|
50
|
+
|
|
51
|
+
def __repr__(self) -> str:
|
|
52
|
+
"""Direct access without await - returns value or raises error."""
|
|
53
|
+
return repr(self._ensure_result())
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
def __str__(self) -> str:
|
|
56
|
+
"""Direct access without await - returns value or raises error."""
|
|
57
|
+
return str(self._ensure_result())
|
|
54
58
|
|
|
59
|
+
# Make it behave like the actual value when accessed
|
|
60
|
+
def __getattr__(self, name):
|
|
61
|
+
return getattr(self._ensure_result(), name)
|
|
55
62
|
|
|
56
|
-
|
|
63
|
+
def _ensure_result(self):
|
|
64
|
+
if not hasattr(self._task, "_result"):
|
|
65
|
+
raise RuntimeError(
|
|
66
|
+
"Task has not been executed yet. Await the task to get the result."
|
|
67
|
+
)
|
|
68
|
+
return self._task._result
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class AsyncTask(typing.Generic[T]):
|
|
57
72
|
"""
|
|
58
73
|
Represents a task to be run asynchronously.
|
|
59
74
|
|
|
@@ -62,19 +77,51 @@ class AsyncTask:
|
|
|
62
77
|
|
|
63
78
|
def __init__(
|
|
64
79
|
self,
|
|
65
|
-
task: typing.Callable[
|
|
80
|
+
task: typing.Callable[..., typing.Awaitable[T]] | typing.Awaitable[T],
|
|
66
81
|
/,
|
|
67
82
|
caller: CallerInfo | None = None,
|
|
68
83
|
tags: list[str] | None = None,
|
|
69
84
|
):
|
|
70
|
-
self.task =
|
|
85
|
+
self.task = task
|
|
71
86
|
self.caller = caller
|
|
72
87
|
self.tags = tags or []
|
|
73
88
|
|
|
89
|
+
@property
|
|
90
|
+
def result(self):
|
|
91
|
+
"""
|
|
92
|
+
Provides access to the result of the task.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
ResultAccessor[T]: An accessor that can be awaited or accessed directly.
|
|
96
|
+
"""
|
|
97
|
+
return ResultAccessor(self)
|
|
98
|
+
|
|
74
99
|
def __call__(self):
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
100
|
+
return self._run()
|
|
101
|
+
|
|
102
|
+
def __await__(self):
|
|
103
|
+
return self._run().__await__()
|
|
104
|
+
|
|
105
|
+
async def _run(self):
|
|
106
|
+
"""
|
|
107
|
+
Runs the task and caches the result.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
T: The result of the task.
|
|
111
|
+
"""
|
|
112
|
+
if hasattr(self, "_result"):
|
|
113
|
+
return self._result
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
coro = self.task
|
|
117
|
+
if callable(coro):
|
|
118
|
+
coro = coro()
|
|
119
|
+
self._result = await coro
|
|
120
|
+
return self._result
|
|
121
|
+
except Exception as e:
|
|
122
|
+
raise AsyncTaskException(
|
|
123
|
+
str(e), original_exception=e, caller=self.caller
|
|
124
|
+
) from e
|
|
78
125
|
|
|
79
126
|
|
|
80
127
|
class AsyncTaskRunner:
|
|
@@ -129,6 +176,9 @@ class AsyncTaskRunner:
|
|
|
129
176
|
_task_stack: contextvars.ContextVar[typing.Optional[list[list[AsyncTask]]]] = (
|
|
130
177
|
contextvars.ContextVar("_task_stack", default=None)
|
|
131
178
|
)
|
|
179
|
+
_runner_stack: contextvars.ContextVar[typing.Optional[list["AsyncTaskRunner"]]] = (
|
|
180
|
+
contextvars.ContextVar("_runner_stack", default=None)
|
|
181
|
+
)
|
|
132
182
|
|
|
133
183
|
def __init__(self, run_tasks_even_if_exception=False):
|
|
134
184
|
"""
|
|
@@ -139,23 +189,59 @@ class AsyncTaskRunner:
|
|
|
139
189
|
"""
|
|
140
190
|
self.run_tasks_even_if_exception = run_tasks_even_if_exception
|
|
141
191
|
|
|
142
|
-
@
|
|
192
|
+
@typing.overload
|
|
193
|
+
@classmethod
|
|
143
194
|
def add_task(
|
|
144
|
-
|
|
195
|
+
cls,
|
|
196
|
+
task: typing.Callable[..., typing.Awaitable[T]]
|
|
197
|
+
| typing.Awaitable[T]
|
|
198
|
+
| AsyncTask[T],
|
|
199
|
+
*,
|
|
145
200
|
tags: list[str] | None = None,
|
|
201
|
+
instance: "AsyncTaskRunner | None" = None,
|
|
202
|
+
) -> AsyncTask[T]: ...
|
|
203
|
+
|
|
204
|
+
@typing.overload
|
|
205
|
+
@classmethod
|
|
206
|
+
def add_task(
|
|
207
|
+
cls,
|
|
208
|
+
task1: typing.Callable[..., typing.Awaitable[T]]
|
|
209
|
+
| typing.Awaitable[T]
|
|
210
|
+
| AsyncTask[T],
|
|
211
|
+
task2: typing.Callable[..., typing.Awaitable[T]]
|
|
212
|
+
| typing.Awaitable[T]
|
|
213
|
+
| AsyncTask[T],
|
|
214
|
+
*tasks: typing.Callable[..., typing.Awaitable[T]]
|
|
215
|
+
| typing.Awaitable[T]
|
|
216
|
+
| AsyncTask[T],
|
|
217
|
+
tags: list[str] | None = None,
|
|
218
|
+
instance: "AsyncTaskRunner | None" = None,
|
|
219
|
+
) -> list[AsyncTask[T]]: ...
|
|
220
|
+
@classmethod
|
|
221
|
+
def add_task(
|
|
222
|
+
cls,
|
|
223
|
+
*tasks: typing.Callable[..., typing.Awaitable[T]]
|
|
224
|
+
| typing.Awaitable[T]
|
|
225
|
+
| AsyncTask[T],
|
|
226
|
+
tags: list[str] | None = None,
|
|
227
|
+
instance: "AsyncTaskRunner | None" = None,
|
|
146
228
|
):
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
)
|
|
229
|
+
"""
|
|
230
|
+
Adds one or more tasks to the current context's task list. The tasks will be executed when exiting the context.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
tags (list[str] | None, optional): Tags to associate with the tasks. Defaults to None.
|
|
234
|
+
instance (AsyncTaskRunner | None, optional): The AsyncTaskRunner instance to add tasks to. Defaults to None.
|
|
153
235
|
|
|
236
|
+
Returns:
|
|
237
|
+
AsyncTask[T] | list[AsyncTask[T]]: The added task(s).
|
|
238
|
+
"""
|
|
239
|
+
task_list = cls._get_current_task_list("add_task", instance=instance)
|
|
154
240
|
# Get caller info
|
|
155
241
|
frame = inspect.currentframe()
|
|
156
242
|
caller = inspect.getouterframes(frame, 2)[1] if frame else None
|
|
157
243
|
|
|
158
|
-
async_tasks = [
|
|
244
|
+
async_tasks: list[AsyncTask[T]] = [
|
|
159
245
|
AsyncTask(
|
|
160
246
|
task,
|
|
161
247
|
caller=CallerInfo(
|
|
@@ -169,58 +255,62 @@ class AsyncTaskRunner:
|
|
|
169
255
|
else task
|
|
170
256
|
for task in tasks
|
|
171
257
|
]
|
|
172
|
-
|
|
258
|
+
task_list.extend(async_tasks)
|
|
259
|
+
return async_tasks if len(async_tasks) > 1 else async_tasks[0]
|
|
173
260
|
|
|
174
|
-
@
|
|
175
|
-
def remove_tasks_by_tag(
|
|
261
|
+
@classmethod
|
|
262
|
+
def remove_tasks_by_tag(
|
|
263
|
+
cls, tag: str, *, instance: "AsyncTaskRunner | None" = None
|
|
264
|
+
):
|
|
176
265
|
"""
|
|
177
266
|
Removes tasks with the specified tag from the current context's task list.
|
|
178
267
|
|
|
179
268
|
Args:
|
|
180
269
|
tag (str): The tag to filter tasks by.
|
|
181
270
|
"""
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
271
|
+
task_list = cls._get_current_task_list("remove_tasks_by_tag", instance=instance)
|
|
272
|
+
cls._update_current_task_list(
|
|
273
|
+
[task for task in task_list if tag not in task.tags], instance=instance
|
|
274
|
+
)
|
|
185
275
|
|
|
186
|
-
|
|
187
|
-
|
|
276
|
+
@classmethod
|
|
277
|
+
def get_current_runner(cls):
|
|
278
|
+
"""
|
|
279
|
+
Retrieves the current AsyncTaskRunner instance from the context stack.
|
|
188
280
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
281
|
+
Raises:
|
|
282
|
+
RuntimeError: If called outside of a context.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
AsyncTaskRunner: The current AsyncTaskRunner instance.
|
|
286
|
+
"""
|
|
287
|
+
stack = cls._get_current_runner_stack("get_current_runner")
|
|
288
|
+
return stack[-1]
|
|
196
289
|
|
|
197
|
-
|
|
290
|
+
async def __aenter__(self):
|
|
291
|
+
self._handle_enter_stack()
|
|
198
292
|
return self
|
|
199
293
|
|
|
200
294
|
async def __aexit__(self, exc_type, exc_value, traceback):
|
|
201
|
-
stack = self.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
tasks = stack.pop() # Pop the current context's task list
|
|
206
|
-
|
|
207
|
-
if self._token:
|
|
208
|
-
self._task_stack.reset(self._token)
|
|
295
|
+
stack = self._get_current_stack("__aexit__")
|
|
296
|
+
tasks = self._task_list
|
|
297
|
+
stack.pop() # Remove the current context's task list
|
|
298
|
+
self._handle_exit_stack()
|
|
209
299
|
|
|
210
300
|
if exc_type and not self.run_tasks_even_if_exception:
|
|
211
301
|
return
|
|
212
302
|
|
|
213
303
|
if tasks:
|
|
214
|
-
|
|
304
|
+
exceptions_with_index = list[tuple[AsyncTaskException, int]]()
|
|
215
305
|
futures = await asyncio.gather(
|
|
216
306
|
*(task() for task in tasks), return_exceptions=True
|
|
217
307
|
)
|
|
218
|
-
for future in futures:
|
|
308
|
+
for index, future in enumerate(futures):
|
|
219
309
|
if isinstance(future, AsyncTaskException):
|
|
220
310
|
# Handle exceptions from tasks
|
|
221
|
-
|
|
311
|
+
exceptions_with_index.append((future, index))
|
|
222
312
|
|
|
223
|
-
if
|
|
313
|
+
if exceptions_with_index:
|
|
224
314
|
raise AsyncTaskRunnerException(
|
|
225
315
|
f"\n{
|
|
226
316
|
prettify_dict(
|
|
@@ -230,11 +320,146 @@ class AsyncTaskRunner:
|
|
|
230
320
|
f'Task {index + 1}': {
|
|
231
321
|
'message': str(exc),
|
|
232
322
|
'caller': exc.caller,
|
|
233
|
-
'traceback':
|
|
323
|
+
'traceback': ''.join(
|
|
324
|
+
tb.format_exception(exc.original_exception)
|
|
325
|
+
),
|
|
234
326
|
}
|
|
235
|
-
for
|
|
327
|
+
for exc, index in exceptions_with_index
|
|
236
328
|
},
|
|
237
329
|
}
|
|
238
330
|
)
|
|
239
331
|
}"
|
|
240
332
|
)
|
|
333
|
+
|
|
334
|
+
@classmethod
|
|
335
|
+
def _get_current_task_list(
|
|
336
|
+
cls, func_name: str, *, instance: "AsyncTaskRunner | None" = None
|
|
337
|
+
):
|
|
338
|
+
"""
|
|
339
|
+
Retrieves the current task list from the context stack. If an instance is provided, it returns the task list from that instance.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
func_name (str): The name of the function requesting the task list.
|
|
343
|
+
instance (AsyncTaskRunner | None, optional): The instance to retrieve the task list from. Defaults to None.
|
|
344
|
+
|
|
345
|
+
Raises:
|
|
346
|
+
RuntimeError: If called outside of a context.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
list[AsyncTask]: The current task list.
|
|
350
|
+
"""
|
|
351
|
+
if instance:
|
|
352
|
+
return instance._task_list
|
|
353
|
+
return cls._get_current_stack(func_name)[-1]
|
|
354
|
+
|
|
355
|
+
@classmethod
|
|
356
|
+
def _update_current_task_list(
|
|
357
|
+
cls,
|
|
358
|
+
new_task_list: list[AsyncTask],
|
|
359
|
+
*,
|
|
360
|
+
instance: "AsyncTaskRunner | None" = None,
|
|
361
|
+
):
|
|
362
|
+
"""
|
|
363
|
+
Updates the current task list in the context stack. If an instance is provided, it updates the task list in that instance.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
new_task_list (list[AsyncTask]): The new task list to set.
|
|
367
|
+
instance (AsyncTaskRunner | None, optional): The instance to update the task list in. Defaults to None.
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
RuntimeError: If called outside of a context.
|
|
371
|
+
"""
|
|
372
|
+
task_list = cls._get_current_task_list(
|
|
373
|
+
"update_current_task_list", instance=instance
|
|
374
|
+
)
|
|
375
|
+
task_list.clear()
|
|
376
|
+
task_list.extend(new_task_list)
|
|
377
|
+
|
|
378
|
+
@classmethod
|
|
379
|
+
def _get_current_stack(cls, func_name: str):
|
|
380
|
+
"""
|
|
381
|
+
Retrieves the current task stack from the context variable.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
func_name (str): The name of the function requesting the task stack.
|
|
385
|
+
|
|
386
|
+
Raises:
|
|
387
|
+
RuntimeError: If called outside of a context.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
list[list[AsyncTask]]: The current task stack.
|
|
391
|
+
"""
|
|
392
|
+
stack = cls._task_stack.get()
|
|
393
|
+
if stack is None:
|
|
394
|
+
raise RuntimeError(
|
|
395
|
+
f"{cls.__name__}.{func_name}() called outside of context. "
|
|
396
|
+
f"Use `async with {cls.__name__}():` before calling this."
|
|
397
|
+
)
|
|
398
|
+
return stack
|
|
399
|
+
|
|
400
|
+
@classmethod
|
|
401
|
+
def _get_current_runner_stack(cls, func_name: str):
|
|
402
|
+
"""
|
|
403
|
+
Retrieves the current runner stack from the context variable.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
func_name (str): The name of the function requesting the runner stack.
|
|
407
|
+
|
|
408
|
+
Raises:
|
|
409
|
+
RuntimeError: If called outside of a context.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
list[AsyncTaskRunner]: The current runner stack.
|
|
413
|
+
"""
|
|
414
|
+
stack = cls._runner_stack.get()
|
|
415
|
+
if stack is None:
|
|
416
|
+
raise RuntimeError(
|
|
417
|
+
f"{cls.__name__}.{func_name}() called outside of context. "
|
|
418
|
+
f"Use `async with {cls.__name__}():` before calling this."
|
|
419
|
+
)
|
|
420
|
+
return stack
|
|
421
|
+
|
|
422
|
+
def _handle_enter_stack(self):
|
|
423
|
+
"""
|
|
424
|
+
Handles the entry into a new context by initializing or updating the task and runner stacks.
|
|
425
|
+
"""
|
|
426
|
+
self._handle_init_stack()
|
|
427
|
+
stack, runner_stack = (
|
|
428
|
+
self._get_current_stack(""),
|
|
429
|
+
self._get_current_runner_stack(""),
|
|
430
|
+
)
|
|
431
|
+
self._task_list = list[AsyncTask]()
|
|
432
|
+
stack.append(self._task_list) # Push a new task list for this context
|
|
433
|
+
runner_stack.append(self) # Push this instance to the runner stack
|
|
434
|
+
|
|
435
|
+
# Modify instance methods to bind self
|
|
436
|
+
self.add_task = functools.partial(self.__class__.add_task, instance=self)
|
|
437
|
+
self.remove_tasks_by_tag = functools.partial(
|
|
438
|
+
self.__class__.remove_tasks_by_tag, instance=self
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def _handle_exit_stack(self):
|
|
442
|
+
"""
|
|
443
|
+
Cleans up the context variables for task and runner stacks if they were initialized by this instance.
|
|
444
|
+
"""
|
|
445
|
+
if self._token:
|
|
446
|
+
self._task_stack.reset(self._token)
|
|
447
|
+
self._token = None
|
|
448
|
+
if self._runner_token:
|
|
449
|
+
self._runner_stack.reset(self._runner_token)
|
|
450
|
+
self._runner_token = None
|
|
451
|
+
|
|
452
|
+
def _handle_init_stack(self):
|
|
453
|
+
"""
|
|
454
|
+
Ensures that the context variables for task and runner stacks are initialized.
|
|
455
|
+
"""
|
|
456
|
+
try:
|
|
457
|
+
self._get_current_stack("")
|
|
458
|
+
self._token = None # Only reset when we set a new stack
|
|
459
|
+
except RuntimeError:
|
|
460
|
+
self._token = self._task_stack.set(list[list[AsyncTask]]())
|
|
461
|
+
try:
|
|
462
|
+
self._get_current_runner_stack("")
|
|
463
|
+
self._runner_token = None # Only reset when we set a new stack
|
|
464
|
+
except RuntimeError:
|
|
465
|
+
self._runner_token = self._runner_stack.set(list["AsyncTaskRunner"]())
|