fastapi-rtk 1.0.4__py3-none-any.whl → 1.0.6__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 CHANGED
@@ -1 +1 @@
1
- __version__ = "1.0.4"
1
+ __version__ = "1.0.6"
@@ -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,19 @@ 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 load the specified columns for the current instance.
131
+
132
+ Args:
133
+ cols (list[str] | str): A list of column names to load or a single column name.
134
+ """
135
+ async with AsyncTaskRunner() as runner:
136
+ if isinstance(cols, str):
137
+ cols = [cols]
138
+ for col in cols:
139
+ runner.add_task(getattr(self.awaitable_attrs, col))
140
+
128
141
 
129
142
  class Table(SA_Table):
130
143
  """
@@ -2,12 +2,15 @@ import asyncio
2
2
  import contextvars
3
3
  import functools
4
4
  import inspect
5
+ import traceback as tb
5
6
  import typing
6
7
 
7
8
  from .prettify_dict import prettify_dict
8
9
 
9
10
  __all__ = ["AsyncTaskRunner"]
10
11
 
12
+ T = typing.TypeVar("T")
13
+
11
14
 
12
15
  class CallerInfo(typing.TypedDict):
13
16
  """
@@ -35,26 +38,37 @@ class AsyncTaskException(AsyncTaskRunnerException):
35
38
  self.caller = caller
36
39
 
37
40
 
38
- def wrap_in_async_task_exception(
39
- task: typing.Callable[[], typing.Coroutine | None] | typing.Coroutine,
40
- /,
41
- caller: CallerInfo | None = None,
42
- ):
43
- async def wrapper():
44
- try:
45
- tsk = task
46
- if callable(tsk):
47
- tsk = tsk()
48
- if tsk is None:
49
- return None
50
- return await tsk
51
- except Exception as e:
52
- 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())
53
54
 
54
- return wrapper
55
+ def __str__(self) -> str:
56
+ """Direct access without await - returns value or raises error."""
57
+ return str(self._ensure_result())
55
58
 
59
+ # Make it behave like the actual value when accessed
60
+ def __getattr__(self, name):
61
+ return getattr(self._ensure_result(), name)
56
62
 
57
- class AsyncTask:
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]):
58
72
  """
59
73
  Represents a task to be run asynchronously.
60
74
 
@@ -63,19 +77,51 @@ class AsyncTask:
63
77
 
64
78
  def __init__(
65
79
  self,
66
- task: typing.Callable[[], typing.Coroutine | None] | typing.Coroutine,
80
+ task: typing.Callable[..., typing.Awaitable[T]] | typing.Awaitable[T],
67
81
  /,
68
82
  caller: CallerInfo | None = None,
69
83
  tags: list[str] | None = None,
70
84
  ):
71
- self.task = wrap_in_async_task_exception(task, caller=caller)
85
+ self.task = task
72
86
  self.caller = caller
73
87
  self.tags = tags or []
74
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
+
75
99
  def __call__(self):
76
- if callable(self.task):
77
- return self.task()
78
- return self.task
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
79
125
 
80
126
 
81
127
  class AsyncTaskRunner:
@@ -143,19 +189,59 @@ class AsyncTaskRunner:
143
189
  """
144
190
  self.run_tasks_even_if_exception = run_tasks_even_if_exception
145
191
 
192
+ @typing.overload
146
193
  @classmethod
147
194
  def add_task(
148
195
  cls,
149
- *tasks: typing.Callable[[], typing.Coroutine | None] | typing.Coroutine,
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],
150
226
  tags: list[str] | None = None,
151
227
  instance: "AsyncTaskRunner | None" = None,
152
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
+ """
153
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(
@@ -170,6 +256,7 @@ class AsyncTaskRunner:
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
261
  @classmethod
175
262
  def remove_tasks_by_tag(
@@ -214,16 +301,16 @@ class AsyncTaskRunner:
214
301
  return
215
302
 
216
303
  if tasks:
217
- exceptions: list[AsyncTaskException] = []
304
+ exceptions_with_index = list[tuple[AsyncTaskException, int]]()
218
305
  futures = await asyncio.gather(
219
306
  *(task() for task in tasks), return_exceptions=True
220
307
  )
221
- for future in futures:
308
+ for index, future in enumerate(futures):
222
309
  if isinstance(future, AsyncTaskException):
223
310
  # Handle exceptions from tasks
224
- exceptions.append(future)
311
+ exceptions_with_index.append((future, index))
225
312
 
226
- if exceptions:
313
+ if exceptions_with_index:
227
314
  raise AsyncTaskRunnerException(
228
315
  f"\n{
229
316
  prettify_dict(
@@ -233,9 +320,11 @@ class AsyncTaskRunner:
233
320
  f'Task {index + 1}': {
234
321
  'message': str(exc),
235
322
  'caller': exc.caller,
236
- 'traceback': exc.original_exception.__traceback__,
323
+ 'traceback': ''.join(
324
+ tb.format_exception(exc.original_exception)
325
+ ),
237
326
  }
238
- for index, exc in enumerate(exceptions)
327
+ for exc, index in exceptions_with_index
239
328
  },
240
329
  }
241
330
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-rtk
3
- Version: 1.0.4
3
+ Version: 1.0.6
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,5 +1,5 @@
1
1
  fastapi_rtk/__init__.py,sha256=acLIihNMCZI3prFTq0cru1-k3kPjSZb2hhcqNrW7xJo,6203
2
- fastapi_rtk/_version.py,sha256=acuR_XSJzp4OrQ5T8-Ac5gYe48mUwObuwjRmisFmZ7k,22
2
+ fastapi_rtk/_version.py,sha256=mqMuQB3aqJVPrHHqJMLjqiMKUiJjozc7EPLcX5DpKHg,22
3
3
  fastapi_rtk/apis.py,sha256=6X_Lhl98m7lKrDRybg2Oe24pLFLJ29eCOQSwCAvpKhY,172
4
4
  fastapi_rtk/config.py,sha256=9PZF9E5i1gxmnsZEprZZKxVHSk0dFEklJSplX9NEqdo,14036
5
5
  fastapi_rtk/const.py,sha256=huvh4Zor77fgUkhU4-x6LgkOglSxeKXOlXdhnai_5CQ,4905
@@ -48,8 +48,8 @@ fastapi_rtk/backends/sqla/column.py,sha256=KutGcyFr3ZeHFTL8c313I4CNxosGq54neLLB7
48
48
  fastapi_rtk/backends/sqla/db.py,sha256=hnE1jQFZai96IaQ7jWqAsiEK0PXnGpMgvgZIGSlPBsM,17245
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=7JQ1hK49PDd2_E8KeEiivbWot3zP7ZiZifmVI7-eYZY,30058
52
- fastapi_rtk/backends/sqla/model.py,sha256=dkhnVvpKAqYAqtRYl9JsARCqEqmhc6wgGy9JHj4TNdQ,6612
51
+ fastapi_rtk/backends/sqla/interface.py,sha256=9I2JiShqmyakfgdXE-DA5TW35xdvMs_-pDDccKnX5cA,27678
52
+ fastapi_rtk/backends/sqla/model.py,sha256=GqWJyb4uq7AK58_zZw5jx4BQ6xWi5thwNqr8WGfnJ6o,7099
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
@@ -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=PNo0vdKQAUOZ3HqGzU7E232qV8V218hKB7q8_uwx2dM,12653
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
@@ -126,8 +126,8 @@ fastapi_rtk/utils/timezone.py,sha256=62S0pPWuDFFXxV1YTFCsc4uKiSP_Ba36Fv7S3gYjfhs
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.4.dist-info/METADATA,sha256=aikWHJ-PodEuoMt2b67pUt1bBo8LaVv4lMk6D9lBTbc,1270
130
- fastapi_rtk-1.0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
131
- fastapi_rtk-1.0.4.dist-info/entry_points.txt,sha256=UuTkxSVIokSlVN28TMhoxzRRUaPxlVRSH3Gsx6yip60,53
132
- fastapi_rtk-1.0.4.dist-info/licenses/LICENSE,sha256=NDrWi4Qwcxal3u1r2lBWGA6TVh3OeW7yMan098mQz98,1073
133
- fastapi_rtk-1.0.4.dist-info/RECORD,,
129
+ fastapi_rtk-1.0.6.dist-info/METADATA,sha256=M9gJt_FXPypoq_6Q-U2L6mE9m-9Y_MZpqcorsYAy9iI,1270
130
+ fastapi_rtk-1.0.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
131
+ fastapi_rtk-1.0.6.dist-info/entry_points.txt,sha256=UuTkxSVIokSlVN28TMhoxzRRUaPxlVRSH3Gsx6yip60,53
132
+ fastapi_rtk-1.0.6.dist-info/licenses/LICENSE,sha256=NDrWi4Qwcxal3u1r2lBWGA6TVh3OeW7yMan098mQz98,1073
133
+ fastapi_rtk-1.0.6.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any