- 新增图像生成接口,支持试用、积分和自定义API Key模式 - 实现生成图片结果异步上传至MinIO存储,带重试机制 - 优化积分预扣除和异常退还逻辑,保障用户积分准确 - 添加获取生成历史记录接口,支持时间范围和分页 - 提供本地字典配置接口,支持模型、比例、提示模板和尺寸 - 实现图片批量上传接口,支持S3兼容对象存储 feat(admin): 增加管理员角色管理与权限分配接口 - 实现角色列表查询、角色创建、更新及删除功能 - 增加权限列表查询接口 - 实现用户角色分配接口,便于统一管理用户权限 - 增加系统字典增删查改接口,支持分类过滤和排序 - 权限控制全面覆盖管理接口,保证安全访问 feat(auth): 完善用户登录注册及权限相关接口与页面 - 实现手机号验证码发送及校验功能,保障注册安全 - 支持手机号注册、登录及退出接口,集成日志记录 - 增加修改密码功能,验证原密码后更新 - 提供动态导航菜单接口,基于权限展示不同菜单 - 实现管理界面路由及日志、角色、字典管理页面访问权限控制 - 添加系统日志查询接口,支持关键词和等级筛选 feat(app): 初始化Flask应用并配置蓝图与数据库 - 创建应用程序工厂,加载配置,初始化数据库和Redis客户端 - 注册认证、API及管理员蓝图,整合路由 - 根路由渲染主页模板 - 应用上下文中自动创建数据库表,保证运行环境准备完毕 feat(database): 提供数据库创建与迁移支持脚本 - 新增数据库创建脚本,支持自动检测是否已存在 - 添加数据库表初始化脚本,支持创建和删除所有表 - 实现RBAC权限初始化,包含基础权限和角色创建 - 新增字段手动修复脚本,添加用户API Key和积分字段 - 强制迁移脚本支持清理连接和修复表结构,初始化默认数据及角色分配 feat(config): 新增系统配置参数 - 配置数据库、Redis、Session和MinIO相关参数 - 添加AI接口地址及试用Key配置 - 集成阿里云短信服务配置及开发模式相关参数 feat(extensions): 初始化数据库、Redis和MinIO客户端 - 创建全局SQLAlchemy数据库实例和Redis客户端 - 配置基于boto3的MinIO兼容S3客户端 chore(logs): 添加示例系统日志文件 - 记录用户请求、验证码发送成功与失败的日志信息
269 lines
7.0 KiB
Python
269 lines
7.0 KiB
Python
import asyncio
|
|
import logging
|
|
import socket
|
|
from abc import ABC, abstractmethod
|
|
from collections.abc import Sized
|
|
from http.cookies import BaseCookie, Morsel
|
|
from typing import (
|
|
TYPE_CHECKING,
|
|
Any,
|
|
Awaitable,
|
|
Callable,
|
|
Dict,
|
|
Generator,
|
|
Iterable,
|
|
List,
|
|
Optional,
|
|
Sequence,
|
|
Tuple,
|
|
TypedDict,
|
|
Union,
|
|
)
|
|
|
|
from multidict import CIMultiDict
|
|
from yarl import URL
|
|
|
|
from ._cookie_helpers import parse_set_cookie_headers
|
|
from .typedefs import LooseCookies
|
|
|
|
if TYPE_CHECKING:
|
|
from .web_app import Application
|
|
from .web_exceptions import HTTPException
|
|
from .web_request import BaseRequest, Request
|
|
from .web_response import StreamResponse
|
|
else:
|
|
BaseRequest = Request = Application = StreamResponse = None
|
|
HTTPException = None
|
|
|
|
|
|
class AbstractRouter(ABC):
|
|
def __init__(self) -> None:
|
|
self._frozen = False
|
|
|
|
def post_init(self, app: Application) -> None:
|
|
"""Post init stage.
|
|
|
|
Not an abstract method for sake of backward compatibility,
|
|
but if the router wants to be aware of the application
|
|
it can override this.
|
|
"""
|
|
|
|
@property
|
|
def frozen(self) -> bool:
|
|
return self._frozen
|
|
|
|
def freeze(self) -> None:
|
|
"""Freeze router."""
|
|
self._frozen = True
|
|
|
|
@abstractmethod
|
|
async def resolve(self, request: Request) -> "AbstractMatchInfo":
|
|
"""Return MATCH_INFO for given request"""
|
|
|
|
|
|
class AbstractMatchInfo(ABC):
|
|
|
|
__slots__ = ()
|
|
|
|
@property # pragma: no branch
|
|
@abstractmethod
|
|
def handler(self) -> Callable[[Request], Awaitable[StreamResponse]]:
|
|
"""Execute matched request handler"""
|
|
|
|
@property
|
|
@abstractmethod
|
|
def expect_handler(
|
|
self,
|
|
) -> Callable[[Request], Awaitable[Optional[StreamResponse]]]:
|
|
"""Expect handler for 100-continue processing"""
|
|
|
|
@property # pragma: no branch
|
|
@abstractmethod
|
|
def http_exception(self) -> Optional[HTTPException]:
|
|
"""HTTPException instance raised on router's resolving, or None"""
|
|
|
|
@abstractmethod # pragma: no branch
|
|
def get_info(self) -> Dict[str, Any]:
|
|
"""Return a dict with additional info useful for introspection"""
|
|
|
|
@property # pragma: no branch
|
|
@abstractmethod
|
|
def apps(self) -> Tuple[Application, ...]:
|
|
"""Stack of nested applications.
|
|
|
|
Top level application is left-most element.
|
|
|
|
"""
|
|
|
|
@abstractmethod
|
|
def add_app(self, app: Application) -> None:
|
|
"""Add application to the nested apps stack."""
|
|
|
|
@abstractmethod
|
|
def freeze(self) -> None:
|
|
"""Freeze the match info.
|
|
|
|
The method is called after route resolution.
|
|
|
|
After the call .add_app() is forbidden.
|
|
|
|
"""
|
|
|
|
|
|
class AbstractView(ABC):
|
|
"""Abstract class based view."""
|
|
|
|
def __init__(self, request: Request) -> None:
|
|
self._request = request
|
|
|
|
@property
|
|
def request(self) -> Request:
|
|
"""Request instance."""
|
|
return self._request
|
|
|
|
@abstractmethod
|
|
def __await__(self) -> Generator[None, None, StreamResponse]:
|
|
"""Execute the view handler."""
|
|
|
|
|
|
class ResolveResult(TypedDict):
|
|
"""Resolve result.
|
|
|
|
This is the result returned from an AbstractResolver's
|
|
resolve method.
|
|
|
|
:param hostname: The hostname that was provided.
|
|
:param host: The IP address that was resolved.
|
|
:param port: The port that was resolved.
|
|
:param family: The address family that was resolved.
|
|
:param proto: The protocol that was resolved.
|
|
:param flags: The flags that were resolved.
|
|
"""
|
|
|
|
hostname: str
|
|
host: str
|
|
port: int
|
|
family: int
|
|
proto: int
|
|
flags: int
|
|
|
|
|
|
class AbstractResolver(ABC):
|
|
"""Abstract DNS resolver."""
|
|
|
|
@abstractmethod
|
|
async def resolve(
|
|
self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET
|
|
) -> List[ResolveResult]:
|
|
"""Return IP address for given hostname"""
|
|
|
|
@abstractmethod
|
|
async def close(self) -> None:
|
|
"""Release resolver"""
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
IterableBase = Iterable[Morsel[str]]
|
|
else:
|
|
IterableBase = Iterable
|
|
|
|
|
|
ClearCookiePredicate = Callable[["Morsel[str]"], bool]
|
|
|
|
|
|
class AbstractCookieJar(Sized, IterableBase):
|
|
"""Abstract Cookie Jar."""
|
|
|
|
def __init__(self, *, loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
|
|
self._loop = loop or asyncio.get_running_loop()
|
|
|
|
@property
|
|
@abstractmethod
|
|
def quote_cookie(self) -> bool:
|
|
"""Return True if cookies should be quoted."""
|
|
|
|
@abstractmethod
|
|
def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None:
|
|
"""Clear all cookies if no predicate is passed."""
|
|
|
|
@abstractmethod
|
|
def clear_domain(self, domain: str) -> None:
|
|
"""Clear all cookies for domain and all subdomains."""
|
|
|
|
@abstractmethod
|
|
def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> None:
|
|
"""Update cookies."""
|
|
|
|
def update_cookies_from_headers(
|
|
self, headers: Sequence[str], response_url: URL
|
|
) -> None:
|
|
"""Update cookies from raw Set-Cookie headers."""
|
|
if headers and (cookies_to_update := parse_set_cookie_headers(headers)):
|
|
self.update_cookies(cookies_to_update, response_url)
|
|
|
|
@abstractmethod
|
|
def filter_cookies(self, request_url: URL) -> "BaseCookie[str]":
|
|
"""Return the jar's cookies filtered by their attributes."""
|
|
|
|
|
|
class AbstractStreamWriter(ABC):
|
|
"""Abstract stream writer."""
|
|
|
|
buffer_size: int = 0
|
|
output_size: int = 0
|
|
length: Optional[int] = 0
|
|
|
|
@abstractmethod
|
|
async def write(self, chunk: Union[bytes, bytearray, memoryview]) -> None:
|
|
"""Write chunk into stream."""
|
|
|
|
@abstractmethod
|
|
async def write_eof(self, chunk: bytes = b"") -> None:
|
|
"""Write last chunk."""
|
|
|
|
@abstractmethod
|
|
async def drain(self) -> None:
|
|
"""Flush the write buffer."""
|
|
|
|
@abstractmethod
|
|
def enable_compression(
|
|
self, encoding: str = "deflate", strategy: Optional[int] = None
|
|
) -> None:
|
|
"""Enable HTTP body compression"""
|
|
|
|
@abstractmethod
|
|
def enable_chunking(self) -> None:
|
|
"""Enable HTTP chunked mode"""
|
|
|
|
@abstractmethod
|
|
async def write_headers(
|
|
self, status_line: str, headers: "CIMultiDict[str]"
|
|
) -> None:
|
|
"""Write HTTP headers"""
|
|
|
|
def send_headers(self) -> None:
|
|
"""Force sending buffered headers if not already sent.
|
|
|
|
Required only if write_headers() buffers headers instead of sending immediately.
|
|
For backwards compatibility, this method does nothing by default.
|
|
"""
|
|
|
|
|
|
class AbstractAccessLogger(ABC):
|
|
"""Abstract writer to access log."""
|
|
|
|
__slots__ = ("logger", "log_format")
|
|
|
|
def __init__(self, logger: logging.Logger, log_format: str) -> None:
|
|
self.logger = logger
|
|
self.log_format = log_format
|
|
|
|
@abstractmethod
|
|
def log(self, request: BaseRequest, response: StreamResponse, time: float) -> None:
|
|
"""Emit log to logger."""
|
|
|
|
@property
|
|
def enabled(self) -> bool:
|
|
"""Check if logger is enabled."""
|
|
return True
|