toms-fast 0.2.1__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.
Files changed (60) hide show
  1. toms_fast-0.2.1.dist-info/METADATA +467 -0
  2. toms_fast-0.2.1.dist-info/RECORD +60 -0
  3. toms_fast-0.2.1.dist-info/WHEEL +4 -0
  4. toms_fast-0.2.1.dist-info/entry_points.txt +2 -0
  5. tomskit/__init__.py +0 -0
  6. tomskit/celery/README.md +693 -0
  7. tomskit/celery/__init__.py +4 -0
  8. tomskit/celery/celery.py +306 -0
  9. tomskit/celery/config.py +377 -0
  10. tomskit/cli/__init__.py +207 -0
  11. tomskit/cli/__main__.py +8 -0
  12. tomskit/cli/scaffold.py +123 -0
  13. tomskit/cli/templates/__init__.py +42 -0
  14. tomskit/cli/templates/base.py +348 -0
  15. tomskit/cli/templates/celery.py +101 -0
  16. tomskit/cli/templates/extensions.py +213 -0
  17. tomskit/cli/templates/fastapi.py +400 -0
  18. tomskit/cli/templates/migrations.py +281 -0
  19. tomskit/cli/templates_config.py +122 -0
  20. tomskit/logger/README.md +466 -0
  21. tomskit/logger/__init__.py +4 -0
  22. tomskit/logger/config.py +106 -0
  23. tomskit/logger/logger.py +290 -0
  24. tomskit/py.typed +0 -0
  25. tomskit/redis/README.md +462 -0
  26. tomskit/redis/__init__.py +6 -0
  27. tomskit/redis/config.py +85 -0
  28. tomskit/redis/redis_pool.py +87 -0
  29. tomskit/redis/redis_sync.py +66 -0
  30. tomskit/server/__init__.py +47 -0
  31. tomskit/server/config.py +117 -0
  32. tomskit/server/exceptions.py +412 -0
  33. tomskit/server/middleware.py +371 -0
  34. tomskit/server/parser.py +312 -0
  35. tomskit/server/resource.py +464 -0
  36. tomskit/server/server.py +276 -0
  37. tomskit/server/type.py +263 -0
  38. tomskit/sqlalchemy/README.md +590 -0
  39. tomskit/sqlalchemy/__init__.py +20 -0
  40. tomskit/sqlalchemy/config.py +125 -0
  41. tomskit/sqlalchemy/database.py +125 -0
  42. tomskit/sqlalchemy/pagination.py +359 -0
  43. tomskit/sqlalchemy/property.py +19 -0
  44. tomskit/sqlalchemy/sqlalchemy.py +131 -0
  45. tomskit/sqlalchemy/types.py +32 -0
  46. tomskit/task/README.md +67 -0
  47. tomskit/task/__init__.py +4 -0
  48. tomskit/task/task_manager.py +124 -0
  49. tomskit/tools/README.md +63 -0
  50. tomskit/tools/__init__.py +18 -0
  51. tomskit/tools/config.py +70 -0
  52. tomskit/tools/warnings.py +37 -0
  53. tomskit/tools/woker.py +81 -0
  54. tomskit/utils/README.md +666 -0
  55. tomskit/utils/README_SERIALIZER.md +644 -0
  56. tomskit/utils/__init__.py +35 -0
  57. tomskit/utils/fields.py +434 -0
  58. tomskit/utils/marshal_utils.py +137 -0
  59. tomskit/utils/response_utils.py +13 -0
  60. tomskit/utils/serializers.py +447 -0
@@ -0,0 +1,125 @@
1
+ from typing import Any
2
+ from urllib.parse import quote_plus
3
+ from pydantic import Field, NonNegativeInt, PositiveInt, computed_field
4
+ from pydantic_settings import BaseSettings
5
+
6
+
7
+ class DatabaseConfig(BaseSettings):
8
+ """
9
+ Configuration settings for the database
10
+ """
11
+
12
+ DB_HOST: str = Field(
13
+ description="db host",
14
+ default="localhost",
15
+ )
16
+
17
+ DB_PORT: PositiveInt = Field(
18
+ description="db port",
19
+ default=5432,
20
+ )
21
+
22
+ DB_USERNAME: str = Field(
23
+ description="db username",
24
+ default="",
25
+ )
26
+
27
+ DB_PASSWORD: str = Field(
28
+ description="db password",
29
+ default="",
30
+ )
31
+
32
+ DB_DATABASE: str = Field(
33
+ description="db database",
34
+ default="tomskitdb",
35
+ )
36
+
37
+ DB_CHARSET: str = Field(
38
+ description="db charset",
39
+ default="",
40
+ )
41
+
42
+ DB_EXTRAS: str = Field(
43
+ description="db extras options. Example: keepalives_idle=60&keepalives=1",
44
+ default="",
45
+ )
46
+
47
+ SQLALCHEMY_DATABASE_URI_SCHEME: str = Field(
48
+ description="db uri scheme",
49
+ default="mysql+aiomysql",
50
+ )
51
+
52
+ SQLALCHEMY_DATABASE_SYNC_URI_SCHEME: str = Field(
53
+ description="db uri scheme",
54
+ default="mysql+pymysql",
55
+ )
56
+
57
+ @computed_field # type: ignore
58
+ @property
59
+ def SQLALCHEMY_DATABASE_URI(self) -> str:
60
+ db_extras = (
61
+ f"{self.DB_EXTRAS}&client_encoding={self.DB_CHARSET}" if self.DB_CHARSET else self.DB_EXTRAS
62
+ ).strip("&")
63
+ db_extras = f"?{db_extras}" if db_extras else ""
64
+ return (
65
+ f"{self.SQLALCHEMY_DATABASE_URI_SCHEME}://"
66
+ f"{quote_plus(self.DB_USERNAME)}:{quote_plus(self.DB_PASSWORD)}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_DATABASE}"
67
+ f"{db_extras}"
68
+ )
69
+
70
+ @computed_field # type: ignore
71
+ @property
72
+ def SQLALCHEMY_DATABASE_SYNC_URI(self) -> str:
73
+ db_extras = (
74
+ f"{self.DB_EXTRAS}&client_encoding={self.DB_CHARSET}" if self.DB_CHARSET else self.DB_EXTRAS
75
+ ).strip("&")
76
+ db_extras = f"?{db_extras}" if db_extras else ""
77
+ return (
78
+ f"{self.SQLALCHEMY_DATABASE_SYNC_URI_SCHEME}://"
79
+ f"{quote_plus(self.DB_USERNAME)}:{quote_plus(self.DB_PASSWORD)}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_DATABASE}"
80
+ f"{db_extras}"
81
+ )
82
+
83
+
84
+ SQLALCHEMY_POOL_SIZE: NonNegativeInt = Field(
85
+ description="pool size of SqlAlchemy",
86
+ default=300,
87
+ )
88
+
89
+ SQLALCHEMY_MAX_OVERFLOW: NonNegativeInt = Field(
90
+ description="max overflows for SqlAlchemy",
91
+ default=10,
92
+ )
93
+
94
+ SQLALCHEMY_POOL_RECYCLE: NonNegativeInt = Field(
95
+ description="SqlAlchemy pool recycle",
96
+ default=3600,
97
+ )
98
+
99
+ SQLALCHEMY_POOL_PRE_PING: bool = Field(
100
+ description="whether to enable pool pre-ping in SqlAlchemy",
101
+ default=False,
102
+ )
103
+
104
+ SQLALCHEMY_ECHO: bool = Field(
105
+ description="whether to enable SqlAlchemy echo",
106
+ default=False,
107
+ )
108
+
109
+ SQLALCHEMY_POOL_ECHO: bool = Field(
110
+ description="whether to enable pool echo in SqlAlchemy",
111
+ default=False,
112
+ )
113
+
114
+ @computed_field # type: ignore
115
+ @property
116
+ def SQLALCHEMY_ENGINE_OPTIONS(self) -> dict[str, Any]:
117
+ return {
118
+ "pool_size": self.SQLALCHEMY_POOL_SIZE,
119
+ "max_overflow": self.SQLALCHEMY_MAX_OVERFLOW,
120
+ "pool_recycle": self.SQLALCHEMY_POOL_RECYCLE,
121
+ "pool_pre_ping": self.SQLALCHEMY_POOL_PRE_PING,
122
+ "echo": self.SQLALCHEMY_ECHO,
123
+ "echo_pool": self.SQLALCHEMY_POOL_ECHO,
124
+ # "connect_args": {"options": "-c timezone=UTC"},
125
+ }
@@ -0,0 +1,125 @@
1
+ """
2
+ 数据库扩展模块
3
+ """
4
+
5
+ from contextvars import ContextVar
6
+ from typing import Any, Optional
7
+
8
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
9
+ from sqlalchemy.sql import Select
10
+
11
+ from tomskit.sqlalchemy import Pagination, SelectPagination, SQLAlchemy
12
+
13
+ class DatabaseSession(SQLAlchemy):
14
+ database_session_ctx: ContextVar[Optional[AsyncSession]] = ContextVar('database_session', default=None)
15
+
16
+ async def paginate(self,
17
+ select: Select[Any],
18
+ *,
19
+ page: int | None = None,
20
+ per_page: int | None = None,
21
+ max_per_page: int | None = None,
22
+ error_out: bool = True,
23
+ count: bool = True,
24
+ ) -> Pagination:
25
+ return await SelectPagination(
26
+ select=select,
27
+ session=self.session,
28
+ page=page,
29
+ per_page=per_page,
30
+ max_per_page=max_per_page,
31
+ error_out=error_out,
32
+ count=count,
33
+ ) # type: ignore
34
+
35
+ @property
36
+ def session(self):
37
+ return self.database_session_ctx.get()
38
+
39
+ def create_session(self)-> AsyncSession:
40
+ """
41
+ 创建一个新的会话并手动将其设置为ContextVar。
42
+ """
43
+ session = self._SessionLocal() # type: ignore
44
+ self.database_session_ctx.set(session)
45
+ return session
46
+
47
+ async def close_session(self, session):
48
+ """
49
+ Close the session and reset the context variable manually.
50
+ """
51
+ await session.aclose()
52
+ self.database_session_ctx.set(None)
53
+
54
+ def initialize_session_pool(self, db_url: str, engine_options: Optional[dict[str, Any]] = None):
55
+ """
56
+ Initialize the database with the given database URL.
57
+ Create the AsyncEngine and SessionLocal for database operations.
58
+ """
59
+ # Create the asynchronous engine
60
+ default_options = {
61
+ "pool_size": 10, # 连接池大小 (Connection pool size)
62
+ "max_overflow": 20, # 允许的额外连接数 (Extra connections allowed)
63
+ "pool_timeout": 30, # 获取连接的超时时间 (Timeout for acquiring a connection)
64
+ "pool_recycle": 1800, # 空闲后回收连接的时间 (Recycle connections after being idle)
65
+ "echo": False, # 调试时打印SQL查询 (Echo SQL queries for debugging)
66
+ "echo_pool": False # 调试时打印连接池信息 (Echo pool information for debugging)
67
+ }
68
+
69
+ engine_options = engine_options.copy() if engine_options else {}
70
+ for key, default_val in default_options.items():
71
+ if key not in engine_options:
72
+ engine_options[key] = default_val
73
+
74
+ self._engine = create_async_engine(db_url, **engine_options)
75
+
76
+ # Create the session factory for AsyncSession
77
+ self._SessionLocal = async_sessionmaker(
78
+ bind=self._engine,
79
+ class_=AsyncSession,
80
+ expire_on_commit=False
81
+ )
82
+
83
+ # Start of Selection
84
+ def get_session_pool_info(self) -> dict:
85
+ """
86
+ 获取当前数据库连接池的详细信息
87
+ pool_size: 连接池大小
88
+ pool_checkedin: 已检查入的连接数
89
+ pool_checkedout: 已检查出的连接数
90
+ pool_overflow: 溢出的连接数
91
+ """
92
+ if self._engine is None or self._engine.pool is None:
93
+ return {"error": "数据库引擎未初始化"}
94
+
95
+ return {
96
+ "pool_size": self._engine.pool.size(), # type: ignore
97
+ "pool_checkedin": self._engine.pool.checkedin(), # type: ignore
98
+ "pool_checkedout": self._engine.pool.checkedout(), # type: ignore
99
+ "pool_overflow": self._engine.pool.overflow(), # type: ignore
100
+ }
101
+
102
+
103
+ async def close_session_pool(self):
104
+ if self._engine is not None:
105
+ await self._engine.dispose()
106
+
107
+ def create_celery_session(self, config: dict[str, Any]):
108
+ self.initialize_session_pool(
109
+ config["SQLALCHEMY_DATABASE_URI"],
110
+ config["SQLALCHEMY_ENGINE_OPTIONS"]
111
+ )
112
+ session = self.create_session()
113
+ return session
114
+
115
+ async def close_celery_session(self, session):
116
+ """
117
+ 关闭会话并手动重置上下文变量。
118
+ """
119
+ await session.aclose()
120
+ self.database_session_ctx.set(None)
121
+ if self._engine is not None:
122
+ await self._engine.dispose()
123
+
124
+
125
+ db = DatabaseSession()
@@ -0,0 +1,359 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+ from math import ceil
5
+
6
+ import sqlalchemy as sa
7
+ import sqlalchemy.orm as sa_orm
8
+
9
+ from tomskit.server import raise_api_error
10
+
11
+
12
+ class Pagination:
13
+ """Apply an offset and limit to the query based on the current page and number of
14
+ items per page.
15
+
16
+ Don't create pagination objects manually. They are created by
17
+ :meth:`.SQLAlchemy.paginate` and :meth:`.Query.paginate`.
18
+
19
+ This is a base class, a subclass must implement :meth:`_query_items` and
20
+ :meth:`_query_count`. Those methods will use arguments passed as ``kwargs`` to
21
+ perform the queries.
22
+
23
+ :param page: The current page, used to calculate the offset. Defaults to the
24
+ ``page`` query arg during a request, or 1 otherwise.
25
+ :param per_page: The maximum number of items on a page, used to calculate the
26
+ offset and limit. Defaults to the ``per_page`` query arg during a request,
27
+ or 20 otherwise.
28
+ :param max_per_page: The maximum allowed value for ``per_page``, to limit a
29
+ user-provided value. Use ``None`` for no limit. Defaults to 100.
30
+ :param error_out: Abort with a ``404 Not Found`` error if no items are returned
31
+ and ``page`` is not 1, or if ``page`` or ``per_page`` is less than 1, or if
32
+ either are not ints.
33
+ :param count: Calculate the total number of values by issuing an extra count
34
+ query. For very complex queries this may be inaccurate or slow, so it can be
35
+ disabled and set manually if necessary.
36
+ :param kwargs: Information about the query to paginate. Different subclasses will
37
+ require different arguments.
38
+
39
+ .. versionchanged:: 3.0
40
+ Iterating over a pagination object iterates over its items.
41
+
42
+ .. versionchanged:: 3.0
43
+ Creating instances manually is not a public API.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ page: int | None = None,
49
+ per_page: int | None = None,
50
+ max_per_page: int | None = 100,
51
+ error_out: bool = True,
52
+ count: bool = True,
53
+ **kwargs: t.Any,
54
+ ) -> None:
55
+ self._query_args = kwargs
56
+ page, per_page = self._prepare_page_args(
57
+ page=page,
58
+ per_page=per_page,
59
+ max_per_page=max_per_page,
60
+ error_out=error_out,
61
+ )
62
+
63
+ self.page: int = page
64
+ """The current page."""
65
+
66
+ self.per_page: int = per_page
67
+ """The maximum number of items on a page."""
68
+
69
+ self.max_per_page: int | None = max_per_page
70
+ """The maximum allowed value for ``per_page``."""
71
+
72
+ self.error_out = error_out
73
+ self.count = count
74
+ # items = self._query_items()
75
+
76
+
77
+ # if not items and page != 1 and error_out:
78
+ # raise_api_error(404)
79
+
80
+ # self.items: list[t.Any] = items
81
+ """The items on the current page. Iterating over the pagination object is
82
+ equivalent to iterating over the items.
83
+ """
84
+
85
+ # if count:
86
+ # total = self._query_count()
87
+ # else:
88
+ # total = None
89
+
90
+ # self.total: int | None = total
91
+ """The total number of items across all pages."""
92
+
93
+ def __await__(self):
94
+ return self.initialize().__await__()
95
+
96
+ async def initialize(self):
97
+ items = await self._query_items()
98
+ if not items and self.page != 1 and self.error_out:
99
+ raise_api_error(404, message="Page not found")
100
+ self.items: list[t.Any] = items # type: ignore
101
+
102
+ if self.count:
103
+ total = await self._query_count()
104
+ else:
105
+ total = None
106
+
107
+ self.total: int | None = total # type: ignore
108
+
109
+ return self
110
+
111
+ @staticmethod
112
+ def _prepare_page_args(
113
+ *,
114
+ page: int | None = None,
115
+ per_page: int | None = None,
116
+ max_per_page: int | None = None,
117
+ error_out: bool = True,
118
+ ) -> tuple[int, int]:
119
+
120
+ if page is None:
121
+ page = 1
122
+
123
+ if per_page is None:
124
+ per_page = 20
125
+
126
+ if max_per_page is not None:
127
+ per_page = min(per_page, max_per_page)
128
+
129
+ if page < 1:
130
+ if error_out:
131
+ raise_api_error(404, message=f"Invalid page number: {page}")
132
+ else:
133
+ page = 1
134
+
135
+ if per_page < 1:
136
+ if error_out:
137
+ raise_api_error(404, message=f"Invalid items per page value: {per_page}")
138
+ else:
139
+ per_page = 20
140
+
141
+ return page, per_page
142
+
143
+ @property
144
+ def _query_offset(self) -> int:
145
+ """The index of the first item to query, passed to ``offset()``.
146
+
147
+ :meta private:
148
+
149
+ .. versionadded:: 3.0
150
+ """
151
+ return (self.page - 1) * self.per_page
152
+
153
+ async def _query_items(self) -> list[t.Any]:
154
+ """Execute the query to get the items on the current page.
155
+
156
+ Uses init arguments stored in :attr:`_query_args`.
157
+
158
+ :meta private:
159
+
160
+ .. versionadded:: 3.0
161
+ """
162
+ raise NotImplementedError
163
+
164
+ async def _query_count(self) -> int:
165
+ """Execute the query to get the total number of items.
166
+
167
+ Uses init arguments stored in :attr:`_query_args`.
168
+
169
+ :meta private:
170
+
171
+ .. versionadded:: 3.0
172
+ """
173
+ raise NotImplementedError
174
+
175
+ @property
176
+ def first(self) -> int:
177
+ """The number of the first item on the page, starting from 1, or 0 if there are
178
+ no items.
179
+
180
+ .. versionadded:: 3.0
181
+ """
182
+ if len(self.items) == 0:
183
+ return 0
184
+
185
+ return (self.page - 1) * self.per_page + 1
186
+
187
+ @property
188
+ def last(self) -> int:
189
+ """The number of the last item on the page, starting from 1, inclusive, or 0 if
190
+ there are no items.
191
+
192
+ .. versionadded:: 3.0
193
+ """
194
+ first = self.first
195
+ return max(first, first + len(self.items) - 1)
196
+
197
+ @property
198
+ def pages(self) -> int:
199
+ """The total number of pages."""
200
+ if self.total == 0 or self.total is None:
201
+ return 0
202
+
203
+ return ceil(self.total / self.per_page)
204
+
205
+ @property
206
+ def has_prev(self) -> bool:
207
+ """``True`` if this is not the first page."""
208
+ return self.page > 1
209
+
210
+ @property
211
+ def prev_num(self) -> int | None:
212
+ """The previous page number, or ``None`` if this is the first page."""
213
+ if not self.has_prev:
214
+ return None
215
+
216
+ return self.page - 1
217
+
218
+ async def prev(self, *, error_out: bool = False) -> Pagination:
219
+ """Query the :class:`Pagination` object for the previous page.
220
+
221
+ :param error_out: Abort with a ``404 Not Found`` error if no items are returned
222
+ and ``page`` is not 1, or if ``page`` or ``per_page`` is less than 1, or if
223
+ either are not ints.
224
+ """
225
+ p = await type(self)(
226
+ page=self.page - 1,
227
+ per_page=self.per_page,
228
+ error_out=error_out,
229
+ count=False,
230
+ **self._query_args,
231
+ )
232
+ p.total = self.total
233
+ return p
234
+
235
+ @property
236
+ def has_next(self) -> bool:
237
+ """``True`` if this is not the last page."""
238
+ return self.page < self.pages
239
+
240
+ @property
241
+ def next_num(self) -> int | None:
242
+ """The next page number, or ``None`` if this is the last page."""
243
+ if not self.has_next:
244
+ return None
245
+
246
+ return self.page + 1
247
+
248
+ async def next(self, *, error_out: bool = False) -> Pagination:
249
+ """Query the :class:`Pagination` object for the next page.
250
+
251
+ :param error_out: Abort with a ``404 Not Found`` error if no items are returned
252
+ and ``page`` is not 1, or if ``page`` or ``per_page`` is less than 1, or if
253
+ either are not ints.
254
+ """
255
+ p = await type(self)(
256
+ page=self.page + 1,
257
+ per_page=self.per_page,
258
+ max_per_page=self.max_per_page,
259
+ error_out=error_out,
260
+ count=False,
261
+ **self._query_args,
262
+ )
263
+ p.total = self.total
264
+ return p
265
+
266
+ def iter_pages(
267
+ self,
268
+ *,
269
+ left_edge: int = 2,
270
+ left_current: int = 2,
271
+ right_current: int = 4,
272
+ right_edge: int = 2,
273
+ ) -> t.Iterator[int | None]:
274
+ """Yield page numbers for a pagination widget. Skipped pages between the edges
275
+ and middle are represented by a ``None``.
276
+
277
+ For example, if there are 20 pages and the current page is 7, the following
278
+ values are yielded.
279
+
280
+ .. code-block:: python
281
+
282
+ 1, 2, None, 5, 6, 7, 8, 9, 10, 11, None, 19, 20
283
+
284
+ :param left_edge: How many pages to show from the first page.
285
+ :param left_current: How many pages to show left of the current page.
286
+ :param right_current: How many pages to show right of the current page.
287
+ :param right_edge: How many pages to show from the last page.
288
+
289
+ .. versionchanged:: 3.0
290
+ Improved efficiency of calculating what to yield.
291
+
292
+ .. versionchanged:: 3.0
293
+ ``right_current`` boundary is inclusive.
294
+
295
+ .. versionchanged:: 3.0
296
+ All parameters are keyword-only.
297
+ """
298
+ pages_end = self.pages + 1
299
+
300
+ if pages_end == 1:
301
+ return
302
+
303
+ left_end = min(1 + left_edge, pages_end)
304
+ yield from range(1, left_end)
305
+
306
+ if left_end == pages_end:
307
+ return
308
+
309
+ mid_start = max(left_end, self.page - left_current)
310
+ mid_end = min(self.page + right_current + 1, pages_end)
311
+
312
+ if mid_start - left_end > 0:
313
+ yield None
314
+
315
+ yield from range(mid_start, mid_end)
316
+
317
+ if mid_end == pages_end:
318
+ return
319
+
320
+ right_start = max(mid_end, pages_end - right_edge)
321
+
322
+ if right_start - mid_end > 0:
323
+ yield None
324
+
325
+ yield from range(right_start, pages_end)
326
+
327
+ def __iter__(self) -> t.Iterator[t.Any]:
328
+ yield from self.items
329
+
330
+
331
+ class SelectPagination(Pagination):
332
+ """Returned by :meth:`.SQLAlchemy.paginate`. Takes ``select`` and ``session``
333
+ arguments in addition to the :class:`Pagination` arguments.
334
+
335
+ .. versionadded:: 3.0
336
+ """
337
+
338
+ def __await__(self):
339
+ return self.initialize().__await__()
340
+
341
+ async def initialize(self):
342
+ await super().initialize()
343
+ return self
344
+
345
+ async def _query_items(self) -> list[t.Any]:
346
+ select = self._query_args["select"]
347
+ select = select.limit(self.per_page).offset(self._query_offset)
348
+ session = self._query_args["session"]
349
+ result = await session.execute(select)
350
+ return list(result.unique().scalars())
351
+
352
+ async def _query_count(self) -> int:
353
+ select = self._query_args["select"]
354
+ sub = select.options(sa_orm.lazyload("*")).order_by(None).subquery()
355
+ session = self._query_args["session"]
356
+ result = await session.execute(sa.select(sa.func.count()).select_from(sub))
357
+ out = result.scalar()
358
+ return out # type: ignore[no-any-return]
359
+
@@ -0,0 +1,19 @@
1
+ import functools
2
+
3
+ class cached_async_property:
4
+ def __init__(self, func):
5
+ self.func = func
6
+ self.attr_name = f"_cached_{func.__name__}"
7
+ functools.update_wrapper(self, func)
8
+
9
+ def __get__(self, instance, owner):
10
+ if instance is None:
11
+ return self
12
+
13
+ async def wrapper():
14
+ if not hasattr(instance, self.attr_name):
15
+ value = await self.func(instance)
16
+ setattr(instance, self.attr_name, value)
17
+ return getattr(instance, self.attr_name)
18
+
19
+ return wrapper()