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.
- toms_fast-0.2.1.dist-info/METADATA +467 -0
- toms_fast-0.2.1.dist-info/RECORD +60 -0
- toms_fast-0.2.1.dist-info/WHEEL +4 -0
- toms_fast-0.2.1.dist-info/entry_points.txt +2 -0
- tomskit/__init__.py +0 -0
- tomskit/celery/README.md +693 -0
- tomskit/celery/__init__.py +4 -0
- tomskit/celery/celery.py +306 -0
- tomskit/celery/config.py +377 -0
- tomskit/cli/__init__.py +207 -0
- tomskit/cli/__main__.py +8 -0
- tomskit/cli/scaffold.py +123 -0
- tomskit/cli/templates/__init__.py +42 -0
- tomskit/cli/templates/base.py +348 -0
- tomskit/cli/templates/celery.py +101 -0
- tomskit/cli/templates/extensions.py +213 -0
- tomskit/cli/templates/fastapi.py +400 -0
- tomskit/cli/templates/migrations.py +281 -0
- tomskit/cli/templates_config.py +122 -0
- tomskit/logger/README.md +466 -0
- tomskit/logger/__init__.py +4 -0
- tomskit/logger/config.py +106 -0
- tomskit/logger/logger.py +290 -0
- tomskit/py.typed +0 -0
- tomskit/redis/README.md +462 -0
- tomskit/redis/__init__.py +6 -0
- tomskit/redis/config.py +85 -0
- tomskit/redis/redis_pool.py +87 -0
- tomskit/redis/redis_sync.py +66 -0
- tomskit/server/__init__.py +47 -0
- tomskit/server/config.py +117 -0
- tomskit/server/exceptions.py +412 -0
- tomskit/server/middleware.py +371 -0
- tomskit/server/parser.py +312 -0
- tomskit/server/resource.py +464 -0
- tomskit/server/server.py +276 -0
- tomskit/server/type.py +263 -0
- tomskit/sqlalchemy/README.md +590 -0
- tomskit/sqlalchemy/__init__.py +20 -0
- tomskit/sqlalchemy/config.py +125 -0
- tomskit/sqlalchemy/database.py +125 -0
- tomskit/sqlalchemy/pagination.py +359 -0
- tomskit/sqlalchemy/property.py +19 -0
- tomskit/sqlalchemy/sqlalchemy.py +131 -0
- tomskit/sqlalchemy/types.py +32 -0
- tomskit/task/README.md +67 -0
- tomskit/task/__init__.py +4 -0
- tomskit/task/task_manager.py +124 -0
- tomskit/tools/README.md +63 -0
- tomskit/tools/__init__.py +18 -0
- tomskit/tools/config.py +70 -0
- tomskit/tools/warnings.py +37 -0
- tomskit/tools/woker.py +81 -0
- tomskit/utils/README.md +666 -0
- tomskit/utils/README_SERIALIZER.md +644 -0
- tomskit/utils/__init__.py +35 -0
- tomskit/utils/fields.py +434 -0
- tomskit/utils/marshal_utils.py +137 -0
- tomskit/utils/response_utils.py +13 -0
- 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()
|