fastapi-rtk 1.0.5__py3-none-any.whl → 1.0.7__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/_version.py +1 -1
- fastapi_rtk/api/model_rest_api.py +12 -9
- fastapi_rtk/backends/sqla/db.py +24 -7
- fastapi_rtk/backends/sqla/interface.py +0 -54
- fastapi_rtk/backends/sqla/model.py +16 -1
- fastapi_rtk/bases/filter.py +1 -1
- fastapi_rtk/const.py +1 -1
- fastapi_rtk/utils/async_task_runner.py +115 -29
- fastapi_rtk/utils/self_dependencies.py +1 -1
- {fastapi_rtk-1.0.5.dist-info → fastapi_rtk-1.0.7.dist-info}/METADATA +1 -1
- {fastapi_rtk-1.0.5.dist-info → fastapi_rtk-1.0.7.dist-info}/RECORD +14 -14
- {fastapi_rtk-1.0.5.dist-info → fastapi_rtk-1.0.7.dist-info}/WHEEL +1 -1
- {fastapi_rtk-1.0.5.dist-info → fastapi_rtk-1.0.7.dist-info}/entry_points.txt +0 -0
- {fastapi_rtk-1.0.5.dist-info → fastapi_rtk-1.0.7.dist-info}/licenses/LICENSE +0 -0
fastapi_rtk/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.0.
|
|
1
|
+
__version__ = "1.0.7"
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import copy
|
|
3
2
|
import csv
|
|
4
3
|
import enum
|
|
5
4
|
import re
|
|
@@ -256,9 +255,7 @@ class ModelRestApi(BaseApi):
|
|
|
256
255
|
|
|
257
256
|
Example:
|
|
258
257
|
```python
|
|
259
|
-
opr_filters = [
|
|
260
|
-
[FilterEqualOnNameAndAge],
|
|
261
|
-
]
|
|
258
|
+
opr_filters = [FilterEqualOnNameAndAge]
|
|
262
259
|
```
|
|
263
260
|
"""
|
|
264
261
|
label_columns = lazy(lambda: dict[str, str]())
|
|
@@ -2591,12 +2588,18 @@ class ModelRestApi(BaseApi):
|
|
|
2591
2588
|
Returns:
|
|
2592
2589
|
dict: The generated JSONForms schema.
|
|
2593
2590
|
"""
|
|
2594
|
-
|
|
2591
|
+
cache_key = f"jsonforms_schema_{schema.__name__}"
|
|
2592
|
+
jsonforms_schema = self.cache.get(cache_key)
|
|
2593
|
+
if not jsonforms_schema:
|
|
2594
|
+
self.cache[cache_key] = jsonforms_schema = schema.model_json_schema()
|
|
2595
2595
|
|
|
2596
2596
|
# Remove unused vars
|
|
2597
2597
|
jsonforms_schema.pop("$defs", None)
|
|
2598
2598
|
|
|
2599
|
-
|
|
2599
|
+
result = jsonforms_schema.copy()
|
|
2600
|
+
result["properties"] = jsonforms_schema["properties"].copy()
|
|
2601
|
+
for key, value in result["properties"].items():
|
|
2602
|
+
value = value.copy()
|
|
2600
2603
|
label = self.label_columns.get(key)
|
|
2601
2604
|
if label:
|
|
2602
2605
|
value["title"] = label
|
|
@@ -2621,7 +2624,7 @@ class ModelRestApi(BaseApi):
|
|
|
2621
2624
|
]
|
|
2622
2625
|
value["contentMediaType"] = ", ".join(allowed_extensions)
|
|
2623
2626
|
if self.datamodel.is_files(key) or self.datamodel.is_images(key):
|
|
2624
|
-
current_value = copy
|
|
2627
|
+
current_value = value.copy()
|
|
2625
2628
|
value["type"] = "array"
|
|
2626
2629
|
value["items"] = current_value
|
|
2627
2630
|
elif self.datamodel.is_boolean(key):
|
|
@@ -2688,9 +2691,9 @@ class ModelRestApi(BaseApi):
|
|
|
2688
2691
|
if key in g.sensitive_data.get(self.datamodel.obj.__name__, []):
|
|
2689
2692
|
value["format"] = "password"
|
|
2690
2693
|
|
|
2691
|
-
|
|
2694
|
+
result["properties"][key] = value
|
|
2692
2695
|
|
|
2693
|
-
return
|
|
2696
|
+
return result
|
|
2694
2697
|
|
|
2695
2698
|
async def _export_data(
|
|
2696
2699
|
self,
|
fastapi_rtk/backends/sqla/db.py
CHANGED
|
@@ -34,6 +34,7 @@ LOAD_TYPE_MAPPING = {
|
|
|
34
34
|
|
|
35
35
|
class LoadColumn(typing.TypedDict):
|
|
36
36
|
statement: Select[tuple[T]] | _AbstractLoad
|
|
37
|
+
statement_type: typing.Literal["select", "joinedload", "selectinload"] | None
|
|
37
38
|
type: typing.Literal["defer", "some", "all"]
|
|
38
39
|
columns: list[str]
|
|
39
40
|
related_columns: collections.defaultdict[str, "LoadColumn"]
|
|
@@ -42,6 +43,7 @@ class LoadColumn(typing.TypedDict):
|
|
|
42
43
|
def create_load_column(statement: Select[tuple[T]] | _AbstractLoad | None = None):
|
|
43
44
|
return LoadColumn(
|
|
44
45
|
statement=statement,
|
|
46
|
+
statement_type=None,
|
|
45
47
|
type="defer",
|
|
46
48
|
columns=[],
|
|
47
49
|
related_columns=collections.defaultdict(create_load_column),
|
|
@@ -104,7 +106,7 @@ class SQLAQueryBuilder(AbstractQueryBuilder[Select[tuple[T]]]):
|
|
|
104
106
|
statement = statement.options(cache_option)
|
|
105
107
|
return statement
|
|
106
108
|
|
|
107
|
-
load_column = self._load_columns_recursively(statement, list_columns)
|
|
109
|
+
load_column = self._load_columns_recursively(statement, "select", list_columns)
|
|
108
110
|
logger.debug(f"Load Column:\n{prettify_dict(load_column)}")
|
|
109
111
|
return self._load_columns_from_dictionary(statement, load_column, list_columns)
|
|
110
112
|
|
|
@@ -365,27 +367,32 @@ class SQLAQueryBuilder(AbstractQueryBuilder[Select[tuple[T]]]):
|
|
|
365
367
|
return statement
|
|
366
368
|
|
|
367
369
|
def _load_columns_recursively(
|
|
368
|
-
self,
|
|
370
|
+
self,
|
|
371
|
+
statement: Select[tuple[T]] | _AbstractLoad,
|
|
372
|
+
statement_type: typing.Literal["select", "joinedload", "selectinload"],
|
|
373
|
+
columns: list[str],
|
|
369
374
|
):
|
|
370
375
|
"""
|
|
371
376
|
Load specified columns into the given SQLAlchemy statement. This returns a dictionary that can be used with the `load_columns_from_dictionary` method.
|
|
372
377
|
|
|
373
378
|
Args:
|
|
374
379
|
statement (Select[tuple[T]] | _AbstractLoad): The SQLAlchemy statement to which the columns will be loaded.
|
|
380
|
+
statement_type (typing.Literal["select", "joinedload", "selectinload"]): The type of statement to use for loading.
|
|
375
381
|
columns (list[str]): A list of column names to be loaded.
|
|
376
382
|
|
|
377
383
|
Returns:
|
|
378
384
|
dict: A dictionary that can be used with the `load_columns_from_dictionary` method.
|
|
379
385
|
"""
|
|
380
386
|
load_column = create_load_column(statement)
|
|
387
|
+
load_column["statement_type"] = statement_type
|
|
381
388
|
for col in columns:
|
|
382
389
|
sub_col = ""
|
|
383
390
|
if "." in col:
|
|
384
391
|
col, sub_col = col.split(".", 1)
|
|
385
392
|
|
|
386
|
-
# If it is not a relation, load only the column if it is in the
|
|
393
|
+
# If it is not a relation, load only the column if it is in the column list, else skip
|
|
387
394
|
if not self.datamodel.is_relation(col):
|
|
388
|
-
if col in self.datamodel.
|
|
395
|
+
if col in self.datamodel.get_column_list():
|
|
389
396
|
load_column["columns"].append(col)
|
|
390
397
|
load_column["type"] = "some"
|
|
391
398
|
continue
|
|
@@ -393,10 +400,18 @@ class SQLAQueryBuilder(AbstractQueryBuilder[Select[tuple[T]]]):
|
|
|
393
400
|
if self.datamodel.is_relation_one_to_one(
|
|
394
401
|
col
|
|
395
402
|
) or self.datamodel.is_relation_many_to_one(col):
|
|
403
|
+
load_column["related_columns"][col]["statement_type"] = (
|
|
404
|
+
load_column["related_columns"][col]["statement_type"]
|
|
405
|
+
or "joinedload"
|
|
406
|
+
)
|
|
396
407
|
load_column["related_columns"][col]["statement"] = load_column[
|
|
397
408
|
"related_columns"
|
|
398
409
|
][col]["statement"] or joinedload(self.datamodel.obj.load_options(col))
|
|
399
410
|
else:
|
|
411
|
+
load_column["related_columns"][col]["statement_type"] = (
|
|
412
|
+
load_column["related_columns"][col]["statement_type"]
|
|
413
|
+
or "selectinload"
|
|
414
|
+
)
|
|
400
415
|
load_column["related_columns"][col]["statement"] = load_column[
|
|
401
416
|
"related_columns"
|
|
402
417
|
][col]["statement"] or selectinload(
|
|
@@ -410,7 +425,9 @@ class SQLAQueryBuilder(AbstractQueryBuilder[Select[tuple[T]]]):
|
|
|
410
425
|
load_column["related_columns"][col] = deep_merge(
|
|
411
426
|
load_column["related_columns"][col],
|
|
412
427
|
interface.query._load_columns_recursively(
|
|
413
|
-
load_column["related_columns"][col]["statement"],
|
|
428
|
+
load_column["related_columns"][col]["statement"],
|
|
429
|
+
load_column["related_columns"][col]["statement_type"],
|
|
430
|
+
[sub_col],
|
|
414
431
|
),
|
|
415
432
|
rules={
|
|
416
433
|
"type": lambda x1, x2: LOAD_TYPE_MAPPING[x2]
|
|
@@ -427,8 +444,8 @@ class SQLAQueryBuilder(AbstractQueryBuilder[Select[tuple[T]]]):
|
|
|
427
444
|
load_column["related_columns"][col]["type"] = "all"
|
|
428
445
|
continue
|
|
429
446
|
|
|
430
|
-
# Skip if the sub column is not in the
|
|
431
|
-
if sub_col not in interface.
|
|
447
|
+
# Skip if the sub column is not in the column list or if it is a relation
|
|
448
|
+
if sub_col not in interface.get_column_list() or interface.is_relation(
|
|
432
449
|
sub_col
|
|
433
450
|
):
|
|
434
451
|
continue
|
|
@@ -7,9 +7,7 @@ from typing import Annotated, Literal, Type
|
|
|
7
7
|
|
|
8
8
|
import fastapi
|
|
9
9
|
import marshmallow_sqlalchemy
|
|
10
|
-
import pydantic
|
|
11
10
|
import sqlalchemy
|
|
12
|
-
import sqlalchemy.exc
|
|
13
11
|
from pydantic import (
|
|
14
12
|
AfterValidator,
|
|
15
13
|
BeforeValidator,
|
|
@@ -45,7 +43,6 @@ from ...utils import (
|
|
|
45
43
|
lazy_import,
|
|
46
44
|
lazy_self,
|
|
47
45
|
safe_call,
|
|
48
|
-
safe_call_sync,
|
|
49
46
|
smart_run,
|
|
50
47
|
)
|
|
51
48
|
from .db import SQLAQueryBuilder
|
|
@@ -682,57 +679,6 @@ class SQLAInterface(AbstractInterface[ModelType, Session | AsyncSession, Column]
|
|
|
682
679
|
|
|
683
680
|
return result
|
|
684
681
|
|
|
685
|
-
def _generate_schema_from_dict(self, schema_dict):
|
|
686
|
-
# Allow await for deferrable columns
|
|
687
|
-
deferrable_columns = [
|
|
688
|
-
key for key in schema_dict.keys() if key in self.list_properties
|
|
689
|
-
]
|
|
690
|
-
model_name = schema_dict.get("__name__", "UnknownModel")
|
|
691
|
-
if deferrable_columns:
|
|
692
|
-
|
|
693
|
-
async def fill_deferrable(model: Model, col: str):
|
|
694
|
-
try:
|
|
695
|
-
return getattr(model, col)
|
|
696
|
-
except sqlalchemy.exc.MissingGreenlet:
|
|
697
|
-
logger.warning(
|
|
698
|
-
f"'MissingGreenlet' error when accessing {model.__class__.__name__}.{col} to create pydantic model {model_name}, trying to await it instead."
|
|
699
|
-
)
|
|
700
|
-
try:
|
|
701
|
-
await getattr(model.awaitable_attrs, col)
|
|
702
|
-
except Exception as e:
|
|
703
|
-
return_value = (
|
|
704
|
-
[]
|
|
705
|
-
if self.is_relation_one_to_many(col)
|
|
706
|
-
or self.is_relation_many_to_many(col)
|
|
707
|
-
else None
|
|
708
|
-
)
|
|
709
|
-
logger.error(
|
|
710
|
-
f"Error when awaiting {model.__class__.__name__}.{col} to create pydantic model {model_name}, returning {return_value} instead: {e}"
|
|
711
|
-
)
|
|
712
|
-
return return_value
|
|
713
|
-
return getattr(model, col)
|
|
714
|
-
|
|
715
|
-
async def fill_deferables(data: Model):
|
|
716
|
-
async with AsyncTaskRunner():
|
|
717
|
-
for col in deferrable_columns:
|
|
718
|
-
AsyncTaskRunner.add_task(
|
|
719
|
-
lambda col=col: fill_deferrable(data, col)
|
|
720
|
-
)
|
|
721
|
-
|
|
722
|
-
def fill_deferrables_validator(data):
|
|
723
|
-
if isinstance(data, Model):
|
|
724
|
-
safe_call_sync(fill_deferables(data))
|
|
725
|
-
return data
|
|
726
|
-
|
|
727
|
-
decorated_func = pydantic.model_validator(mode="before")(
|
|
728
|
-
fill_deferrables_validator
|
|
729
|
-
)
|
|
730
|
-
schema_dict["__validators__"] = schema_dict.get("__validators__", {})
|
|
731
|
-
schema_dict["__validators__"][fill_deferrables_validator.__name__] = (
|
|
732
|
-
decorated_func
|
|
733
|
-
)
|
|
734
|
-
return super()._generate_schema_from_dict(schema_dict)
|
|
735
|
-
|
|
736
682
|
"""
|
|
737
683
|
--------------------------------------------------------------------------------------------------------
|
|
738
684
|
SQLA RELATED METHODS - ONLY IN SQLAInterface
|
|
@@ -13,7 +13,7 @@ from sqlalchemy.util.typing import Literal
|
|
|
13
13
|
from ...bases.model import BasicModel
|
|
14
14
|
from ...const import DEFAULT_METADATA_KEY
|
|
15
15
|
from ...exceptions import FastAPIReactToolkitException
|
|
16
|
-
from ...utils import smart_run_sync
|
|
16
|
+
from ...utils import AsyncTaskRunner, smart_run_sync
|
|
17
17
|
|
|
18
18
|
__all__ = ["Model", "metadata", "metadatas", "Base"]
|
|
19
19
|
|
|
@@ -125,6 +125,21 @@ class Model(sqlalchemy.ext.asyncio.AsyncAttrs, BasicModel, DeclarativeBase):
|
|
|
125
125
|
def get_pk_attrs(cls):
|
|
126
126
|
return [col.name for col in cls.__mapper__.primary_key]
|
|
127
127
|
|
|
128
|
+
async def load(self, *cols: list[str] | str):
|
|
129
|
+
"""
|
|
130
|
+
Asynchronously loads the specified columns of the model instance.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
*cols (list[str] | str): The columns to load. Can be a list of strings or individual string arguments.
|
|
134
|
+
"""
|
|
135
|
+
cols = [
|
|
136
|
+
item for col in cols for item in (col if isinstance(col, list) else [col])
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
async with AsyncTaskRunner() as runner:
|
|
140
|
+
for col in cols:
|
|
141
|
+
runner.add_task(getattr(self.awaitable_attrs, col))
|
|
142
|
+
|
|
128
143
|
|
|
129
144
|
class Table(SA_Table):
|
|
130
145
|
"""
|
fastapi_rtk/bases/filter.py
CHANGED
fastapi_rtk/const.py
CHANGED
|
@@ -76,7 +76,7 @@ DEFAULT_COOKIE_NAME = "dataTactics"
|
|
|
76
76
|
DEFAULT_BASEDIR = "app"
|
|
77
77
|
DEFAULT_STATIC_FOLDER = DEFAULT_BASEDIR + "/static"
|
|
78
78
|
DEFAULT_TEMPLATE_FOLDER = DEFAULT_BASEDIR + "/templates"
|
|
79
|
-
DEFAULT_PROFILER_FOLDER = DEFAULT_STATIC_FOLDER + "/
|
|
79
|
+
DEFAULT_PROFILER_FOLDER = DEFAULT_STATIC_FOLDER + "/profiler"
|
|
80
80
|
DEFAULT_LANG_FOLDER = DEFAULT_BASEDIR + "/lang"
|
|
81
81
|
DEFAULT_LANGUAGES = "en,de"
|
|
82
82
|
DEFAULT_TRANSLATIONS_KEY = "translations"
|
|
@@ -9,6 +9,8 @@ from .prettify_dict import prettify_dict
|
|
|
9
9
|
|
|
10
10
|
__all__ = ["AsyncTaskRunner"]
|
|
11
11
|
|
|
12
|
+
T = typing.TypeVar("T")
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
class CallerInfo(typing.TypedDict):
|
|
14
16
|
"""
|
|
@@ -36,26 +38,37 @@ class AsyncTaskException(AsyncTaskRunnerException):
|
|
|
36
38
|
self.caller = caller
|
|
37
39
|
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
except Exception as e:
|
|
53
|
-
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())
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
def __str__(self) -> str:
|
|
56
|
+
"""Direct access without await - returns value or raises error."""
|
|
57
|
+
return str(self._ensure_result())
|
|
56
58
|
|
|
59
|
+
# Make it behave like the actual value when accessed
|
|
60
|
+
def __getattr__(self, name):
|
|
61
|
+
return getattr(self._ensure_result(), name)
|
|
57
62
|
|
|
58
|
-
|
|
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]):
|
|
59
72
|
"""
|
|
60
73
|
Represents a task to be run asynchronously.
|
|
61
74
|
|
|
@@ -64,19 +77,51 @@ class AsyncTask:
|
|
|
64
77
|
|
|
65
78
|
def __init__(
|
|
66
79
|
self,
|
|
67
|
-
task: typing.Callable[
|
|
80
|
+
task: typing.Callable[..., typing.Awaitable[T]] | typing.Awaitable[T],
|
|
68
81
|
/,
|
|
69
82
|
caller: CallerInfo | None = None,
|
|
70
83
|
tags: list[str] | None = None,
|
|
71
84
|
):
|
|
72
|
-
self.task =
|
|
85
|
+
self.task = task
|
|
73
86
|
self.caller = caller
|
|
74
87
|
self.tags = tags or []
|
|
75
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
|
+
|
|
76
99
|
def __call__(self):
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
80
125
|
|
|
81
126
|
|
|
82
127
|
class AsyncTaskRunner:
|
|
@@ -144,19 +189,59 @@ class AsyncTaskRunner:
|
|
|
144
189
|
"""
|
|
145
190
|
self.run_tasks_even_if_exception = run_tasks_even_if_exception
|
|
146
191
|
|
|
192
|
+
@typing.overload
|
|
147
193
|
@classmethod
|
|
148
194
|
def add_task(
|
|
149
195
|
cls,
|
|
150
|
-
|
|
196
|
+
task: typing.Callable[..., typing.Awaitable[T]]
|
|
197
|
+
| typing.Awaitable[T]
|
|
198
|
+
| AsyncTask[T],
|
|
199
|
+
*,
|
|
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],
|
|
151
226
|
tags: list[str] | None = None,
|
|
152
227
|
instance: "AsyncTaskRunner | None" = None,
|
|
153
228
|
):
|
|
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.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
AsyncTask[T] | list[AsyncTask[T]]: The added task(s).
|
|
238
|
+
"""
|
|
154
239
|
task_list = cls._get_current_task_list("add_task", instance=instance)
|
|
155
240
|
# Get caller info
|
|
156
241
|
frame = inspect.currentframe()
|
|
157
242
|
caller = inspect.getouterframes(frame, 2)[1] if frame else None
|
|
158
243
|
|
|
159
|
-
async_tasks = [
|
|
244
|
+
async_tasks: list[AsyncTask[T]] = [
|
|
160
245
|
AsyncTask(
|
|
161
246
|
task,
|
|
162
247
|
caller=CallerInfo(
|
|
@@ -171,6 +256,7 @@ class AsyncTaskRunner:
|
|
|
171
256
|
for task in tasks
|
|
172
257
|
]
|
|
173
258
|
task_list.extend(async_tasks)
|
|
259
|
+
return async_tasks if len(async_tasks) > 1 else async_tasks[0]
|
|
174
260
|
|
|
175
261
|
@classmethod
|
|
176
262
|
def remove_tasks_by_tag(
|
|
@@ -215,16 +301,16 @@ class AsyncTaskRunner:
|
|
|
215
301
|
return
|
|
216
302
|
|
|
217
303
|
if tasks:
|
|
218
|
-
|
|
304
|
+
exceptions_with_index = list[tuple[AsyncTaskException, int]]()
|
|
219
305
|
futures = await asyncio.gather(
|
|
220
306
|
*(task() for task in tasks), return_exceptions=True
|
|
221
307
|
)
|
|
222
|
-
for future in futures:
|
|
308
|
+
for index, future in enumerate(futures):
|
|
223
309
|
if isinstance(future, AsyncTaskException):
|
|
224
310
|
# Handle exceptions from tasks
|
|
225
|
-
|
|
311
|
+
exceptions_with_index.append((future, index))
|
|
226
312
|
|
|
227
|
-
if
|
|
313
|
+
if exceptions_with_index:
|
|
228
314
|
raise AsyncTaskRunnerException(
|
|
229
315
|
f"\n{
|
|
230
316
|
prettify_dict(
|
|
@@ -238,7 +324,7 @@ class AsyncTaskRunner:
|
|
|
238
324
|
tb.format_exception(exc.original_exception)
|
|
239
325
|
),
|
|
240
326
|
}
|
|
241
|
-
for
|
|
327
|
+
for exc, index in exceptions_with_index
|
|
242
328
|
},
|
|
243
329
|
}
|
|
244
330
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-rtk
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.7
|
|
4
4
|
Summary: A package that provides a set of tools to build a FastAPI application with a Class-Based CRUD API.
|
|
5
5
|
Project-URL: Homepage, https://codeberg.org/datatactics/fastapi-rtk
|
|
6
6
|
Project-URL: Issues, https://codeberg.org/datatactics/fastapi-rtk/issues
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
fastapi_rtk/__init__.py,sha256=acLIihNMCZI3prFTq0cru1-k3kPjSZb2hhcqNrW7xJo,6203
|
|
2
|
-
fastapi_rtk/_version.py,sha256=
|
|
2
|
+
fastapi_rtk/_version.py,sha256=BW7SWRpHoxuOQZ67pS20yog2LWYl-nK7-BEFBNrHGgA,22
|
|
3
3
|
fastapi_rtk/apis.py,sha256=6X_Lhl98m7lKrDRybg2Oe24pLFLJ29eCOQSwCAvpKhY,172
|
|
4
4
|
fastapi_rtk/config.py,sha256=9PZF9E5i1gxmnsZEprZZKxVHSk0dFEklJSplX9NEqdo,14036
|
|
5
|
-
fastapi_rtk/const.py,sha256=
|
|
5
|
+
fastapi_rtk/const.py,sha256=sEj_cYeerj9pVwbCu0k5Sy1EYpdr1EHzUjqqbnporgc,4905
|
|
6
6
|
fastapi_rtk/db.py,sha256=nAPIibCCyaGm7Kw9updiVx5LWASkdfX01GxG5Gp12hM,24505
|
|
7
7
|
fastapi_rtk/decorators.py,sha256=HqAFSiO0l5_M0idWs0IcY24FdzbAcDQDQoifM_WgZAQ,14515
|
|
8
8
|
fastapi_rtk/dependencies.py,sha256=H31uMZRA0FdZZXmzIVS2j9gBAovJDYdb_5ZgmrKd9qc,6070
|
|
@@ -21,7 +21,7 @@ fastapi_rtk/types.py,sha256=-LPnTIbHvqJW81__gab3EWrhjNmznHhptz0BtXkEAHQ,3612
|
|
|
21
21
|
fastapi_rtk/version.py,sha256=D2cmQf2LNeHOiEfcNzVOOfcAmuLvPEmGEtZv5G54D0c,195
|
|
22
22
|
fastapi_rtk/api/__init__.py,sha256=MwFR7HHppnhbjZGg3sOdQ6nqy9uxnHHXvicpswNFMNA,245
|
|
23
23
|
fastapi_rtk/api/base_api.py,sha256=42I9v3b25lqxNAMDGEtajA5-btIDSyUWF0xMDgGkA8c,8078
|
|
24
|
-
fastapi_rtk/api/model_rest_api.py,sha256=
|
|
24
|
+
fastapi_rtk/api/model_rest_api.py,sha256=8ztCFUSYMAn74QcVKN9lNYDOSWixsXHCpLHtc9RAShA,105175
|
|
25
25
|
fastapi_rtk/auth/__init__.py,sha256=iX7O41NivBYDfdomEaqm4lUx9KD17wI4g3EFLF6kUTw,336
|
|
26
26
|
fastapi_rtk/auth/auth.py,sha256=MZmuueioiMbSHjd_F3frKEqCA3yjtanRWyKOy6CnOd0,20994
|
|
27
27
|
fastapi_rtk/auth/hashers/__init__.py,sha256=uBThFj2VPPSMSioxYTktNiM4-mVgtDAjTpKA3ZzWxxs,110
|
|
@@ -45,11 +45,11 @@ fastapi_rtk/backends/generic/model.py,sha256=olRvHD57CCKnZoyia7EcvecZfXjN-ku17gx
|
|
|
45
45
|
fastapi_rtk/backends/generic/session.py,sha256=J8OJSuGcFkZiOg7SJIbkKnMUlo_njQjRJ1vNl4k8Ve4,19053
|
|
46
46
|
fastapi_rtk/backends/sqla/__init__.py,sha256=p4TRI65-R6TM6G_-OAnwYV7Hk9WVkKsnhN2sFta1-cU,1795
|
|
47
47
|
fastapi_rtk/backends/sqla/column.py,sha256=KutGcyFr3ZeHFTL8c313I4CNxosGq54neLLB7N53GZs,240
|
|
48
|
-
fastapi_rtk/backends/sqla/db.py,sha256=
|
|
48
|
+
fastapi_rtk/backends/sqla/db.py,sha256=8VUU-S4W2WWpkROR178dj0ahCirP5CKP7fPyoz25Ui0,18135
|
|
49
49
|
fastapi_rtk/backends/sqla/exceptions.py,sha256=rRtvgXyDwGSbjvJIPJIOjOBYytvNv9VXkZOBy6p2J80,61
|
|
50
50
|
fastapi_rtk/backends/sqla/filters.py,sha256=H2eholy7VTfVwcBTAV0BWO6NXJYA-eDp_YvptLFKO3E,20927
|
|
51
|
-
fastapi_rtk/backends/sqla/interface.py,sha256=
|
|
52
|
-
fastapi_rtk/backends/sqla/model.py,sha256=
|
|
51
|
+
fastapi_rtk/backends/sqla/interface.py,sha256=9I2JiShqmyakfgdXE-DA5TW35xdvMs_-pDDccKnX5cA,27678
|
|
52
|
+
fastapi_rtk/backends/sqla/model.py,sha256=N0fxuAHbw_RCfFPRw-cjWQdBEanfogLRXOUEPqZ1Qac,7168
|
|
53
53
|
fastapi_rtk/backends/sqla/session.py,sha256=di52RhRWzUchcquWMU9KKd0F1N5te1GfAtcfCnekiwI,412
|
|
54
54
|
fastapi_rtk/backends/sqla/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
55
|
fastapi_rtk/backends/sqla/extensions/audit/__init__.py,sha256=CaVc9fV584PVazvGGMOKwX4xC3OvIae9kXBx6xQAj5g,131
|
|
@@ -61,7 +61,7 @@ fastapi_rtk/backends/sqla/extensions/geoalchemy2/geometry_converter.py,sha256=sc
|
|
|
61
61
|
fastapi_rtk/bases/__init__.py,sha256=v3tMVuX20UbvjI_mTWpDAePWAA38e3pjlYEiICgY4j8,440
|
|
62
62
|
fastapi_rtk/bases/db.py,sha256=D27BhF89J0OaLHjALDCa85eNf35lBaTz6VV7EDa4wuM,18711
|
|
63
63
|
fastapi_rtk/bases/file_manager.py,sha256=d1ZZaY-OgDxjSWQ49DNQNSDHKznzr-03wki_eR2iU2A,8230
|
|
64
|
-
fastapi_rtk/bases/filter.py,sha256=
|
|
64
|
+
fastapi_rtk/bases/filter.py,sha256=XmWTcLaIcBj9pKF1PMAKdwSnZNpdT8Df3uLeUIOGUDE,1840
|
|
65
65
|
fastapi_rtk/bases/interface.py,sha256=Cq9Duxa3w-tw342P424h88fc0_X1DoxCdTa3rAN-6jM,45380
|
|
66
66
|
fastapi_rtk/bases/model.py,sha256=nUZf0AVs0Mzqh2u_ALiRNYN1bfOU9PzYLvEFHDQ57Y0,1692
|
|
67
67
|
fastapi_rtk/bases/session.py,sha256=Q92cRW0nI5IYSsBXLkTJjK5ztMSBNRt-olMwxTIeel0,173
|
|
@@ -106,7 +106,7 @@ fastapi_rtk/security/sqla/apis.py,sha256=J3BCqSnaX-w9raTA5LHa7muW24kA09nAm9kN6b3
|
|
|
106
106
|
fastapi_rtk/security/sqla/models.py,sha256=dwIZHNU-6b1r0M2swlYyRtSgI1e1hnvav6d6jdmffM8,8096
|
|
107
107
|
fastapi_rtk/security/sqla/security_manager.py,sha256=eBXhDTjqQYySCKQXnD6wEeHBGw8oytwVvmKpi9btNYI,17371
|
|
108
108
|
fastapi_rtk/utils/__init__.py,sha256=0X4BwrVDl4S3mB7DLyHaZVednefMjRIjBIDT3yv_CHM,1875
|
|
109
|
-
fastapi_rtk/utils/async_task_runner.py,sha256=
|
|
109
|
+
fastapi_rtk/utils/async_task_runner.py,sha256=HzykQSdeAmNjZfVB5vUDVwrSCVFr8__67ACQk60pSsk,15545
|
|
110
110
|
fastapi_rtk/utils/class_factory.py,sha256=jlVw8yCh-tYsMnR5Hm8fgxtL0kvXwnhe6DPJA1sUh7k,598
|
|
111
111
|
fastapi_rtk/utils/csv_json_converter.py,sha256=7szrPiB7DrK5S-s2GaHVCmEP9_OXk9RFwbZmRtAKM5A,14036
|
|
112
112
|
fastapi_rtk/utils/deep_merge.py,sha256=PHtKJgXfCngOBGVliX9aVlEFcwCxr-GlzU-w6vjgAIs,2426
|
|
@@ -119,15 +119,15 @@ fastapi_rtk/utils/multiple_async_contexts.py,sha256=-juAliUeG4XI1J-p31KknsJvzvM5
|
|
|
119
119
|
fastapi_rtk/utils/prettify_dict.py,sha256=VKve-52fE9usIzflD3mNtvql_p5F2jq9HYBTJF_1MX0,662
|
|
120
120
|
fastapi_rtk/utils/pydantic.py,sha256=c4TGu6fP5qmsMJcEerhgq3R1iTpJn_1oA91d43GaVlo,2408
|
|
121
121
|
fastapi_rtk/utils/run_utils.py,sha256=2aieVAGp4e7vByzkwnmXk5c-crLl-Ia1rwdHl6LRQ34,6237
|
|
122
|
-
fastapi_rtk/utils/self_dependencies.py,sha256=
|
|
122
|
+
fastapi_rtk/utils/self_dependencies.py,sha256=SjX8BOC3UvuLxfElo1MMgugCX8ZEgsCgLy0g011a4UU,4102
|
|
123
123
|
fastapi_rtk/utils/smartdefaultdict.py,sha256=UdA_N1eQ3TooE9_ci0O_5QKUjGkQfo-c2BwEX5OkXfY,645
|
|
124
124
|
fastapi_rtk/utils/sqla.py,sha256=To4PhsO5orPJVqjdLh5C9y_xPgiy8-zhrJdSqhR_tsc,690
|
|
125
125
|
fastapi_rtk/utils/timezone.py,sha256=62S0pPWuDFFXxV1YTFCsc4uKiSP_Ba36Fv7S3gYjfhs,570
|
|
126
126
|
fastapi_rtk/utils/update_signature.py,sha256=PIzZgNpGEwvDNgQ3G51Zi-QhVV3mbxvUvSwDwf_-yYs,2209
|
|
127
127
|
fastapi_rtk/utils/use_default_when_none.py,sha256=H2HqhKy_8eYk3i1xijEjuaKak0oWkMIkrdz6T7DK9QU,469
|
|
128
128
|
fastapi_rtk/utils/werkzeug.py,sha256=1Gv-oyqSmhVGrmNbB9fDqpqJzPpANOzWf4zMMrhW9UA,3245
|
|
129
|
-
fastapi_rtk-1.0.
|
|
130
|
-
fastapi_rtk-1.0.
|
|
131
|
-
fastapi_rtk-1.0.
|
|
132
|
-
fastapi_rtk-1.0.
|
|
133
|
-
fastapi_rtk-1.0.
|
|
129
|
+
fastapi_rtk-1.0.7.dist-info/METADATA,sha256=yuKVrabzSpcffX7GiyKHtzOgZQLY4HHwFnaVp8hP-UQ,1270
|
|
130
|
+
fastapi_rtk-1.0.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
131
|
+
fastapi_rtk-1.0.7.dist-info/entry_points.txt,sha256=UuTkxSVIokSlVN28TMhoxzRRUaPxlVRSH3Gsx6yip60,53
|
|
132
|
+
fastapi_rtk-1.0.7.dist-info/licenses/LICENSE,sha256=NDrWi4Qwcxal3u1r2lBWGA6TVh3OeW7yMan098mQz98,1073
|
|
133
|
+
fastapi_rtk-1.0.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|