- 新增图像生成接口,支持试用、积分和自定义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): 添加示例系统日志文件 - 记录用户请求、验证码发送成功与失败的日志信息
432 lines
15 KiB
Python
432 lines
15 KiB
Python
import os
|
|
from json import JSONDecodeError, loads
|
|
from typing import Dict, List, Optional, Tuple, Union
|
|
|
|
from redis.exceptions import DataError
|
|
from redis.utils import deprecated_function
|
|
|
|
from ._util import JsonType
|
|
from .decoders import decode_dict_keys
|
|
from .path import Path
|
|
|
|
|
|
class JSONCommands:
|
|
"""json commands."""
|
|
|
|
def arrappend(
|
|
self, name: str, path: Optional[str] = Path.root_path(), *args: JsonType
|
|
) -> List[Optional[int]]:
|
|
"""Append the objects ``args`` to the array under the
|
|
``path` in key ``name``.
|
|
|
|
For more information see `JSON.ARRAPPEND <https://redis.io/commands/json.arrappend>`_..
|
|
""" # noqa
|
|
pieces = [name, str(path)]
|
|
for o in args:
|
|
pieces.append(self._encode(o))
|
|
return self.execute_command("JSON.ARRAPPEND", *pieces)
|
|
|
|
def arrindex(
|
|
self,
|
|
name: str,
|
|
path: str,
|
|
scalar: int,
|
|
start: Optional[int] = None,
|
|
stop: Optional[int] = None,
|
|
) -> List[Optional[int]]:
|
|
"""
|
|
Return the index of ``scalar`` in the JSON array under ``path`` at key
|
|
``name``.
|
|
|
|
The search can be limited using the optional inclusive ``start``
|
|
and exclusive ``stop`` indices.
|
|
|
|
For more information see `JSON.ARRINDEX <https://redis.io/commands/json.arrindex>`_.
|
|
""" # noqa
|
|
pieces = [name, str(path), self._encode(scalar)]
|
|
if start is not None:
|
|
pieces.append(start)
|
|
if stop is not None:
|
|
pieces.append(stop)
|
|
|
|
return self.execute_command("JSON.ARRINDEX", *pieces, keys=[name])
|
|
|
|
def arrinsert(
|
|
self, name: str, path: str, index: int, *args: JsonType
|
|
) -> List[Optional[int]]:
|
|
"""Insert the objects ``args`` to the array at index ``index``
|
|
under the ``path` in key ``name``.
|
|
|
|
For more information see `JSON.ARRINSERT <https://redis.io/commands/json.arrinsert>`_.
|
|
""" # noqa
|
|
pieces = [name, str(path), index]
|
|
for o in args:
|
|
pieces.append(self._encode(o))
|
|
return self.execute_command("JSON.ARRINSERT", *pieces)
|
|
|
|
def arrlen(
|
|
self, name: str, path: Optional[str] = Path.root_path()
|
|
) -> List[Optional[int]]:
|
|
"""Return the length of the array JSON value under ``path``
|
|
at key``name``.
|
|
|
|
For more information see `JSON.ARRLEN <https://redis.io/commands/json.arrlen>`_.
|
|
""" # noqa
|
|
return self.execute_command("JSON.ARRLEN", name, str(path), keys=[name])
|
|
|
|
def arrpop(
|
|
self,
|
|
name: str,
|
|
path: Optional[str] = Path.root_path(),
|
|
index: Optional[int] = -1,
|
|
) -> List[Optional[str]]:
|
|
"""Pop the element at ``index`` in the array JSON value under
|
|
``path`` at key ``name``.
|
|
|
|
For more information see `JSON.ARRPOP <https://redis.io/commands/json.arrpop>`_.
|
|
""" # noqa
|
|
return self.execute_command("JSON.ARRPOP", name, str(path), index)
|
|
|
|
def arrtrim(
|
|
self, name: str, path: str, start: int, stop: int
|
|
) -> List[Optional[int]]:
|
|
"""Trim the array JSON value under ``path`` at key ``name`` to the
|
|
inclusive range given by ``start`` and ``stop``.
|
|
|
|
For more information see `JSON.ARRTRIM <https://redis.io/commands/json.arrtrim>`_.
|
|
""" # noqa
|
|
return self.execute_command("JSON.ARRTRIM", name, str(path), start, stop)
|
|
|
|
def type(self, name: str, path: Optional[str] = Path.root_path()) -> List[str]:
|
|
"""Get the type of the JSON value under ``path`` from key ``name``.
|
|
|
|
For more information see `JSON.TYPE <https://redis.io/commands/json.type>`_.
|
|
""" # noqa
|
|
return self.execute_command("JSON.TYPE", name, str(path), keys=[name])
|
|
|
|
def resp(self, name: str, path: Optional[str] = Path.root_path()) -> List:
|
|
"""Return the JSON value under ``path`` at key ``name``.
|
|
|
|
For more information see `JSON.RESP <https://redis.io/commands/json.resp>`_.
|
|
""" # noqa
|
|
return self.execute_command("JSON.RESP", name, str(path), keys=[name])
|
|
|
|
def objkeys(
|
|
self, name: str, path: Optional[str] = Path.root_path()
|
|
) -> List[Optional[List[str]]]:
|
|
"""Return the key names in the dictionary JSON value under ``path`` at
|
|
key ``name``.
|
|
|
|
For more information see `JSON.OBJKEYS <https://redis.io/commands/json.objkeys>`_.
|
|
""" # noqa
|
|
return self.execute_command("JSON.OBJKEYS", name, str(path), keys=[name])
|
|
|
|
def objlen(
|
|
self, name: str, path: Optional[str] = Path.root_path()
|
|
) -> List[Optional[int]]:
|
|
"""Return the length of the dictionary JSON value under ``path`` at key
|
|
``name``.
|
|
|
|
For more information see `JSON.OBJLEN <https://redis.io/commands/json.objlen>`_.
|
|
""" # noqa
|
|
return self.execute_command("JSON.OBJLEN", name, str(path), keys=[name])
|
|
|
|
def numincrby(self, name: str, path: str, number: int) -> str:
|
|
"""Increment the numeric (integer or floating point) JSON value under
|
|
``path`` at key ``name`` by the provided ``number``.
|
|
|
|
For more information see `JSON.NUMINCRBY <https://redis.io/commands/json.numincrby>`_.
|
|
""" # noqa
|
|
return self.execute_command(
|
|
"JSON.NUMINCRBY", name, str(path), self._encode(number)
|
|
)
|
|
|
|
@deprecated_function(version="4.0.0", reason="deprecated since redisjson 1.0.0")
|
|
def nummultby(self, name: str, path: str, number: int) -> str:
|
|
"""Multiply the numeric (integer or floating point) JSON value under
|
|
``path`` at key ``name`` with the provided ``number``.
|
|
|
|
For more information see `JSON.NUMMULTBY <https://redis.io/commands/json.nummultby>`_.
|
|
""" # noqa
|
|
return self.execute_command(
|
|
"JSON.NUMMULTBY", name, str(path), self._encode(number)
|
|
)
|
|
|
|
def clear(self, name: str, path: Optional[str] = Path.root_path()) -> int:
|
|
"""Empty arrays and objects (to have zero slots/keys without deleting the
|
|
array/object).
|
|
|
|
Return the count of cleared paths (ignoring non-array and non-objects
|
|
paths).
|
|
|
|
For more information see `JSON.CLEAR <https://redis.io/commands/json.clear>`_.
|
|
""" # noqa
|
|
return self.execute_command("JSON.CLEAR", name, str(path))
|
|
|
|
def delete(self, key: str, path: Optional[str] = Path.root_path()) -> int:
|
|
"""Delete the JSON value stored at key ``key`` under ``path``.
|
|
|
|
For more information see `JSON.DEL <https://redis.io/commands/json.del>`_.
|
|
"""
|
|
return self.execute_command("JSON.DEL", key, str(path))
|
|
|
|
# forget is an alias for delete
|
|
forget = delete
|
|
|
|
def get(
|
|
self, name: str, *args, no_escape: Optional[bool] = False
|
|
) -> Optional[List[JsonType]]:
|
|
"""
|
|
Get the object stored as a JSON value at key ``name``.
|
|
|
|
``args`` is zero or more paths, and defaults to root path
|
|
```no_escape`` is a boolean flag to add no_escape option to get
|
|
non-ascii characters
|
|
|
|
For more information see `JSON.GET <https://redis.io/commands/json.get>`_.
|
|
""" # noqa
|
|
pieces = [name]
|
|
if no_escape:
|
|
pieces.append("noescape")
|
|
|
|
if len(args) == 0:
|
|
pieces.append(Path.root_path())
|
|
|
|
else:
|
|
for p in args:
|
|
pieces.append(str(p))
|
|
|
|
# Handle case where key doesn't exist. The JSONDecoder would raise a
|
|
# TypeError exception since it can't decode None
|
|
try:
|
|
return self.execute_command("JSON.GET", *pieces, keys=[name])
|
|
except TypeError:
|
|
return None
|
|
|
|
def mget(self, keys: List[str], path: str) -> List[JsonType]:
|
|
"""
|
|
Get the objects stored as a JSON values under ``path``. ``keys``
|
|
is a list of one or more keys.
|
|
|
|
For more information see `JSON.MGET <https://redis.io/commands/json.mget>`_.
|
|
""" # noqa
|
|
pieces = []
|
|
pieces += keys
|
|
pieces.append(str(path))
|
|
return self.execute_command("JSON.MGET", *pieces, keys=keys)
|
|
|
|
def set(
|
|
self,
|
|
name: str,
|
|
path: str,
|
|
obj: JsonType,
|
|
nx: Optional[bool] = False,
|
|
xx: Optional[bool] = False,
|
|
decode_keys: Optional[bool] = False,
|
|
) -> Optional[str]:
|
|
"""
|
|
Set the JSON value at key ``name`` under the ``path`` to ``obj``.
|
|
|
|
``nx`` if set to True, set ``value`` only if it does not exist.
|
|
``xx`` if set to True, set ``value`` only if it exists.
|
|
``decode_keys`` If set to True, the keys of ``obj`` will be decoded
|
|
with utf-8.
|
|
|
|
For the purpose of using this within a pipeline, this command is also
|
|
aliased to JSON.SET.
|
|
|
|
For more information see `JSON.SET <https://redis.io/commands/json.set>`_.
|
|
"""
|
|
if decode_keys:
|
|
obj = decode_dict_keys(obj)
|
|
|
|
pieces = [name, str(path), self._encode(obj)]
|
|
|
|
# Handle existential modifiers
|
|
if nx and xx:
|
|
raise Exception(
|
|
"nx and xx are mutually exclusive: use one, the "
|
|
"other or neither - but not both"
|
|
)
|
|
elif nx:
|
|
pieces.append("NX")
|
|
elif xx:
|
|
pieces.append("XX")
|
|
return self.execute_command("JSON.SET", *pieces)
|
|
|
|
def mset(self, triplets: List[Tuple[str, str, JsonType]]) -> Optional[str]:
|
|
"""
|
|
Set the JSON value at key ``name`` under the ``path`` to ``obj``
|
|
for one or more keys.
|
|
|
|
``triplets`` is a list of one or more triplets of key, path, value.
|
|
|
|
For the purpose of using this within a pipeline, this command is also
|
|
aliased to JSON.MSET.
|
|
|
|
For more information see `JSON.MSET <https://redis.io/commands/json.mset>`_.
|
|
"""
|
|
pieces = []
|
|
for triplet in triplets:
|
|
pieces.extend([triplet[0], str(triplet[1]), self._encode(triplet[2])])
|
|
return self.execute_command("JSON.MSET", *pieces)
|
|
|
|
def merge(
|
|
self,
|
|
name: str,
|
|
path: str,
|
|
obj: JsonType,
|
|
decode_keys: Optional[bool] = False,
|
|
) -> Optional[str]:
|
|
"""
|
|
Merges a given JSON value into matching paths. Consequently, JSON values
|
|
at matching paths are updated, deleted, or expanded with new children
|
|
|
|
``decode_keys`` If set to True, the keys of ``obj`` will be decoded
|
|
with utf-8.
|
|
|
|
For more information see `JSON.MERGE <https://redis.io/commands/json.merge>`_.
|
|
"""
|
|
if decode_keys:
|
|
obj = decode_dict_keys(obj)
|
|
|
|
pieces = [name, str(path), self._encode(obj)]
|
|
|
|
return self.execute_command("JSON.MERGE", *pieces)
|
|
|
|
def set_file(
|
|
self,
|
|
name: str,
|
|
path: str,
|
|
file_name: str,
|
|
nx: Optional[bool] = False,
|
|
xx: Optional[bool] = False,
|
|
decode_keys: Optional[bool] = False,
|
|
) -> Optional[str]:
|
|
"""
|
|
Set the JSON value at key ``name`` under the ``path`` to the content
|
|
of the json file ``file_name``.
|
|
|
|
``nx`` if set to True, set ``value`` only if it does not exist.
|
|
``xx`` if set to True, set ``value`` only if it exists.
|
|
``decode_keys`` If set to True, the keys of ``obj`` will be decoded
|
|
with utf-8.
|
|
|
|
"""
|
|
|
|
with open(file_name) as fp:
|
|
file_content = loads(fp.read())
|
|
|
|
return self.set(name, path, file_content, nx=nx, xx=xx, decode_keys=decode_keys)
|
|
|
|
def set_path(
|
|
self,
|
|
json_path: str,
|
|
root_folder: str,
|
|
nx: Optional[bool] = False,
|
|
xx: Optional[bool] = False,
|
|
decode_keys: Optional[bool] = False,
|
|
) -> Dict[str, bool]:
|
|
"""
|
|
Iterate over ``root_folder`` and set each JSON file to a value
|
|
under ``json_path`` with the file name as the key.
|
|
|
|
``nx`` if set to True, set ``value`` only if it does not exist.
|
|
``xx`` if set to True, set ``value`` only if it exists.
|
|
``decode_keys`` If set to True, the keys of ``obj`` will be decoded
|
|
with utf-8.
|
|
|
|
"""
|
|
set_files_result = {}
|
|
for root, dirs, files in os.walk(root_folder):
|
|
for file in files:
|
|
file_path = os.path.join(root, file)
|
|
try:
|
|
file_name = file_path.rsplit(".")[0]
|
|
self.set_file(
|
|
file_name,
|
|
json_path,
|
|
file_path,
|
|
nx=nx,
|
|
xx=xx,
|
|
decode_keys=decode_keys,
|
|
)
|
|
set_files_result[file_path] = True
|
|
except JSONDecodeError:
|
|
set_files_result[file_path] = False
|
|
|
|
return set_files_result
|
|
|
|
def strlen(self, name: str, path: Optional[str] = None) -> List[Optional[int]]:
|
|
"""Return the length of the string JSON value under ``path`` at key
|
|
``name``.
|
|
|
|
For more information see `JSON.STRLEN <https://redis.io/commands/json.strlen>`_.
|
|
""" # noqa
|
|
pieces = [name]
|
|
if path is not None:
|
|
pieces.append(str(path))
|
|
return self.execute_command("JSON.STRLEN", *pieces, keys=[name])
|
|
|
|
def toggle(
|
|
self, name: str, path: Optional[str] = Path.root_path()
|
|
) -> Union[bool, List[Optional[int]]]:
|
|
"""Toggle boolean value under ``path`` at key ``name``.
|
|
returning the new value.
|
|
|
|
For more information see `JSON.TOGGLE <https://redis.io/commands/json.toggle>`_.
|
|
""" # noqa
|
|
return self.execute_command("JSON.TOGGLE", name, str(path))
|
|
|
|
def strappend(
|
|
self, name: str, value: str, path: Optional[str] = Path.root_path()
|
|
) -> Union[int, List[Optional[int]]]:
|
|
"""Append to the string JSON value. If two options are specified after
|
|
the key name, the path is determined to be the first. If a single
|
|
option is passed, then the root_path (i.e Path.root_path()) is used.
|
|
|
|
For more information see `JSON.STRAPPEND <https://redis.io/commands/json.strappend>`_.
|
|
""" # noqa
|
|
pieces = [name, str(path), self._encode(value)]
|
|
return self.execute_command("JSON.STRAPPEND", *pieces)
|
|
|
|
def debug(
|
|
self,
|
|
subcommand: str,
|
|
key: Optional[str] = None,
|
|
path: Optional[str] = Path.root_path(),
|
|
) -> Union[int, List[str]]:
|
|
"""Return the memory usage in bytes of a value under ``path`` from
|
|
key ``name``.
|
|
|
|
For more information see `JSON.DEBUG <https://redis.io/commands/json.debug>`_.
|
|
""" # noqa
|
|
valid_subcommands = ["MEMORY", "HELP"]
|
|
if subcommand not in valid_subcommands:
|
|
raise DataError("The only valid subcommands are ", str(valid_subcommands))
|
|
pieces = [subcommand]
|
|
if subcommand == "MEMORY":
|
|
if key is None:
|
|
raise DataError("No key specified")
|
|
pieces.append(key)
|
|
pieces.append(str(path))
|
|
return self.execute_command("JSON.DEBUG", *pieces)
|
|
|
|
@deprecated_function(
|
|
version="4.0.0", reason="redisjson-py supported this, call get directly."
|
|
)
|
|
def jsonget(self, *args, **kwargs):
|
|
return self.get(*args, **kwargs)
|
|
|
|
@deprecated_function(
|
|
version="4.0.0", reason="redisjson-py supported this, call get directly."
|
|
)
|
|
def jsonmget(self, *args, **kwargs):
|
|
return self.mget(*args, **kwargs)
|
|
|
|
@deprecated_function(
|
|
version="4.0.0", reason="redisjson-py supported this, call get directly."
|
|
)
|
|
def jsonset(self, *args, **kwargs):
|
|
return self.set(*args, **kwargs)
|