- 新增图像生成接口,支持试用、积分和自定义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): 添加示例系统日志文件 - 记录用户请求、验证码发送成功与失败的日志信息
317 lines
11 KiB
Python
317 lines
11 KiB
Python
# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"). You
|
|
# may not use this file except in compliance with the License. A copy of
|
|
# the License is located at
|
|
#
|
|
# https://aws.amazon.com/apache2.0/
|
|
#
|
|
# or in the "license" file accompanying this file. This file is
|
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
|
# ANY KIND, either express or implied. See the License for the specific
|
|
# language governing permissions and limitations under the License.
|
|
|
|
import jmespath
|
|
from botocore import xform_name
|
|
|
|
from .params import get_data_member
|
|
|
|
|
|
def all_not_none(iterable):
|
|
"""
|
|
Return True if all elements of the iterable are not None (or if the
|
|
iterable is empty). This is like the built-in ``all``, except checks
|
|
against None, so 0 and False are allowable values.
|
|
"""
|
|
for element in iterable:
|
|
if element is None:
|
|
return False
|
|
return True
|
|
|
|
|
|
def build_identifiers(identifiers, parent, params=None, raw_response=None):
|
|
"""
|
|
Builds a mapping of identifier names to values based on the
|
|
identifier source location, type, and target. Identifier
|
|
values may be scalars or lists depending on the source type
|
|
and location.
|
|
|
|
:type identifiers: list
|
|
:param identifiers: List of :py:class:`~boto3.resources.model.Parameter`
|
|
definitions
|
|
:type parent: ServiceResource
|
|
:param parent: The resource instance to which this action is attached.
|
|
:type params: dict
|
|
:param params: Request parameters sent to the service.
|
|
:type raw_response: dict
|
|
:param raw_response: Low-level operation response.
|
|
:rtype: list
|
|
:return: An ordered list of ``(name, value)`` identifier tuples.
|
|
"""
|
|
results = []
|
|
|
|
for identifier in identifiers:
|
|
source = identifier.source
|
|
target = identifier.target
|
|
|
|
if source == 'response':
|
|
value = jmespath.search(identifier.path, raw_response)
|
|
elif source == 'requestParameter':
|
|
value = jmespath.search(identifier.path, params)
|
|
elif source == 'identifier':
|
|
value = getattr(parent, xform_name(identifier.name))
|
|
elif source == 'data':
|
|
# If this is a data member then it may incur a load
|
|
# action before returning the value.
|
|
value = get_data_member(parent, identifier.path)
|
|
elif source == 'input':
|
|
# This value is set by the user, so ignore it here
|
|
continue
|
|
else:
|
|
raise NotImplementedError(f'Unsupported source type: {source}')
|
|
|
|
results.append((xform_name(target), value))
|
|
|
|
return results
|
|
|
|
|
|
def build_empty_response(search_path, operation_name, service_model):
|
|
"""
|
|
Creates an appropriate empty response for the type that is expected,
|
|
based on the service model's shape type. For example, a value that
|
|
is normally a list would then return an empty list. A structure would
|
|
return an empty dict, and a number would return None.
|
|
|
|
:type search_path: string
|
|
:param search_path: JMESPath expression to search in the response
|
|
:type operation_name: string
|
|
:param operation_name: Name of the underlying service operation.
|
|
:type service_model: :ref:`botocore.model.ServiceModel`
|
|
:param service_model: The Botocore service model
|
|
:rtype: dict, list, or None
|
|
:return: An appropriate empty value
|
|
"""
|
|
response = None
|
|
|
|
operation_model = service_model.operation_model(operation_name)
|
|
shape = operation_model.output_shape
|
|
|
|
if search_path:
|
|
# Walk the search path and find the final shape. For example, given
|
|
# a path of ``foo.bar[0].baz``, we first find the shape for ``foo``,
|
|
# then the shape for ``bar`` (ignoring the indexing), and finally
|
|
# the shape for ``baz``.
|
|
for item in search_path.split('.'):
|
|
item = item.strip('[0123456789]$')
|
|
|
|
if shape.type_name == 'structure':
|
|
shape = shape.members[item]
|
|
elif shape.type_name == 'list':
|
|
shape = shape.member
|
|
else:
|
|
raise NotImplementedError(
|
|
f'Search path hits shape type {shape.type_name} from {item}'
|
|
)
|
|
|
|
# Anything not handled here is set to None
|
|
if shape.type_name == 'structure':
|
|
response = {}
|
|
elif shape.type_name == 'list':
|
|
response = []
|
|
elif shape.type_name == 'map':
|
|
response = {}
|
|
|
|
return response
|
|
|
|
|
|
class RawHandler:
|
|
"""
|
|
A raw action response handler. This passed through the response
|
|
dictionary, optionally after performing a JMESPath search if one
|
|
has been defined for the action.
|
|
|
|
:type search_path: string
|
|
:param search_path: JMESPath expression to search in the response
|
|
:rtype: dict
|
|
:return: Service response
|
|
"""
|
|
|
|
def __init__(self, search_path):
|
|
self.search_path = search_path
|
|
|
|
def __call__(self, parent, params, response):
|
|
"""
|
|
:type parent: ServiceResource
|
|
:param parent: The resource instance to which this action is attached.
|
|
:type params: dict
|
|
:param params: Request parameters sent to the service.
|
|
:type response: dict
|
|
:param response: Low-level operation response.
|
|
"""
|
|
# TODO: Remove the '$' check after JMESPath supports it
|
|
if self.search_path and self.search_path != '$':
|
|
response = jmespath.search(self.search_path, response)
|
|
|
|
return response
|
|
|
|
|
|
class ResourceHandler:
|
|
"""
|
|
Creates a new resource or list of new resources from the low-level
|
|
response based on the given response resource definition.
|
|
|
|
:type search_path: string
|
|
:param search_path: JMESPath expression to search in the response
|
|
|
|
:type factory: ResourceFactory
|
|
:param factory: The factory that created the resource class to which
|
|
this action is attached.
|
|
|
|
:type resource_model: :py:class:`~boto3.resources.model.ResponseResource`
|
|
:param resource_model: Response resource model.
|
|
|
|
:type service_context: :py:class:`~boto3.utils.ServiceContext`
|
|
:param service_context: Context about the AWS service
|
|
|
|
:type operation_name: string
|
|
:param operation_name: Name of the underlying service operation, if it
|
|
exists.
|
|
|
|
:rtype: ServiceResource or list
|
|
:return: New resource instance(s).
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
search_path,
|
|
factory,
|
|
resource_model,
|
|
service_context,
|
|
operation_name=None,
|
|
):
|
|
self.search_path = search_path
|
|
self.factory = factory
|
|
self.resource_model = resource_model
|
|
self.operation_name = operation_name
|
|
self.service_context = service_context
|
|
|
|
def __call__(self, parent, params, response):
|
|
"""
|
|
:type parent: ServiceResource
|
|
:param parent: The resource instance to which this action is attached.
|
|
:type params: dict
|
|
:param params: Request parameters sent to the service.
|
|
:type response: dict
|
|
:param response: Low-level operation response.
|
|
"""
|
|
resource_name = self.resource_model.type
|
|
json_definition = self.service_context.resource_json_definitions.get(
|
|
resource_name
|
|
)
|
|
|
|
# Load the new resource class that will result from this action.
|
|
resource_cls = self.factory.load_from_definition(
|
|
resource_name=resource_name,
|
|
single_resource_json_definition=json_definition,
|
|
service_context=self.service_context,
|
|
)
|
|
raw_response = response
|
|
search_response = None
|
|
|
|
# Anytime a path is defined, it means the response contains the
|
|
# resource's attributes, so resource_data gets set here. It
|
|
# eventually ends up in resource.meta.data, which is where
|
|
# the attribute properties look for data.
|
|
if self.search_path:
|
|
search_response = jmespath.search(self.search_path, raw_response)
|
|
|
|
# First, we parse all the identifiers, then create the individual
|
|
# response resources using them. Any identifiers that are lists
|
|
# will have one item consumed from the front of the list for each
|
|
# resource that is instantiated. Items which are not a list will
|
|
# be set as the same value on each new resource instance.
|
|
identifiers = dict(
|
|
build_identifiers(
|
|
self.resource_model.identifiers, parent, params, raw_response
|
|
)
|
|
)
|
|
|
|
# If any of the identifiers is a list, then the response is plural
|
|
plural = [v for v in identifiers.values() if isinstance(v, list)]
|
|
|
|
if plural:
|
|
response = []
|
|
|
|
# The number of items in an identifier that is a list will
|
|
# determine how many resource instances to create.
|
|
for i in range(len(plural[0])):
|
|
# Response item data is *only* available if a search path
|
|
# was given. This prevents accidentally loading unrelated
|
|
# data that may be in the response.
|
|
response_item = None
|
|
if search_response:
|
|
response_item = search_response[i]
|
|
response.append(
|
|
self.handle_response_item(
|
|
resource_cls, parent, identifiers, response_item
|
|
)
|
|
)
|
|
elif all_not_none(identifiers.values()):
|
|
# All identifiers must always exist, otherwise the resource
|
|
# cannot be instantiated.
|
|
response = self.handle_response_item(
|
|
resource_cls, parent, identifiers, search_response
|
|
)
|
|
else:
|
|
# The response should be empty, but that may mean an
|
|
# empty dict, list, or None based on whether we make
|
|
# a remote service call and what shape it is expected
|
|
# to return.
|
|
response = None
|
|
if self.operation_name is not None:
|
|
# A remote service call was made, so try and determine
|
|
# its shape.
|
|
response = build_empty_response(
|
|
self.search_path,
|
|
self.operation_name,
|
|
self.service_context.service_model,
|
|
)
|
|
|
|
return response
|
|
|
|
def handle_response_item(
|
|
self, resource_cls, parent, identifiers, resource_data
|
|
):
|
|
"""
|
|
Handles the creation of a single response item by setting
|
|
parameters and creating the appropriate resource instance.
|
|
|
|
:type resource_cls: ServiceResource subclass
|
|
:param resource_cls: The resource class to instantiate.
|
|
:type parent: ServiceResource
|
|
:param parent: The resource instance to which this action is attached.
|
|
:type identifiers: dict
|
|
:param identifiers: Map of identifier names to value or values.
|
|
:type resource_data: dict or None
|
|
:param resource_data: Data for resource attributes.
|
|
:rtype: ServiceResource
|
|
:return: New resource instance.
|
|
"""
|
|
kwargs = {
|
|
'client': parent.meta.client,
|
|
}
|
|
|
|
for name, value in identifiers.items():
|
|
# If value is a list, then consume the next item
|
|
if isinstance(value, list):
|
|
value = value.pop(0)
|
|
|
|
kwargs[name] = value
|
|
|
|
resource = resource_cls(**kwargs)
|
|
|
|
if resource_data is not None:
|
|
resource.meta.data = resource_data
|
|
|
|
return resource
|