feat(api): 实现图像生成及后台同步功能
- 新增图像生成接口,支持试用、积分和自定义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): 添加示例系统日志文件 - 记录用户请求、验证码发送成功与失败的日志信息
This commit is contained in:
commit
af7c11d7f9
BIN
__pycache__/app.cpython-312.pyc
Normal file
BIN
__pycache__/app.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/config.cpython-312.pyc
Normal file
BIN
__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/extensions.cpython-312.pyc
Normal file
BIN
__pycache__/extensions.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/init_rbac.cpython-312.pyc
Normal file
BIN
__pycache__/init_rbac.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/models.cpython-312.pyc
Normal file
BIN
__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
41
app.py
Normal file
41
app.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from flask import Flask, render_template
|
||||||
|
from config import Config
|
||||||
|
from extensions import db, redis_client
|
||||||
|
from blueprints.auth import auth_bp
|
||||||
|
from blueprints.api import api_bp
|
||||||
|
from blueprints.admin import admin_bp
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
# 导入模型(必须在 db.create_all() 之前导入)
|
||||||
|
import models
|
||||||
|
|
||||||
|
def create_app():
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_object(Config)
|
||||||
|
|
||||||
|
# 初始化扩展
|
||||||
|
db.init_app(app)
|
||||||
|
redis_client.init_app(app)
|
||||||
|
|
||||||
|
# 注册蓝图
|
||||||
|
app.register_blueprint(auth_bp)
|
||||||
|
app.register_blueprint(api_bp)
|
||||||
|
app.register_blueprint(admin_bp)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
# 自动创建数据库表
|
||||||
|
with app.app_context():
|
||||||
|
print("🔧 正在检查并创建数据库表...")
|
||||||
|
db.create_all()
|
||||||
|
print("✅ 数据库表已就绪")
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
BIN
blueprints/__pycache__/admin.cpython-312.pyc
Normal file
BIN
blueprints/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
blueprints/__pycache__/api.cpython-312.pyc
Normal file
BIN
blueprints/__pycache__/api.cpython-312.pyc
Normal file
Binary file not shown.
BIN
blueprints/__pycache__/auth.cpython-312.pyc
Normal file
BIN
blueprints/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
144
blueprints/admin.py
Normal file
144
blueprints/admin.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from extensions import db
|
||||||
|
from models import User, Role, Permission, SystemDict
|
||||||
|
from middlewares.auth import permission_required
|
||||||
|
|
||||||
|
admin_bp = Blueprint('admin', __name__, url_prefix='/api/admin')
|
||||||
|
|
||||||
|
# --- 角色管理 ---
|
||||||
|
@admin_bp.route('/roles', methods=['GET'])
|
||||||
|
@permission_required('manage_rbac')
|
||||||
|
def get_roles():
|
||||||
|
roles = Role.query.all()
|
||||||
|
return jsonify({
|
||||||
|
"roles": [{
|
||||||
|
"id": r.id,
|
||||||
|
"name": r.name,
|
||||||
|
"description": r.description,
|
||||||
|
"permissions": [p.name for p in r.permissions]
|
||||||
|
} for r in roles]
|
||||||
|
})
|
||||||
|
|
||||||
|
@admin_bp.route('/roles', methods=['POST'])
|
||||||
|
@permission_required('manage_rbac')
|
||||||
|
def save_role():
|
||||||
|
data = request.json
|
||||||
|
role_id = data.get('id')
|
||||||
|
|
||||||
|
if role_id:
|
||||||
|
role = Role.query.get(role_id)
|
||||||
|
if not role: return jsonify({"error": "角色不存在"}), 404
|
||||||
|
role.name = data['name']
|
||||||
|
role.description = data.get('description')
|
||||||
|
else:
|
||||||
|
role = Role(name=data['name'], description=data.get('description'))
|
||||||
|
db.session.add(role)
|
||||||
|
|
||||||
|
if 'permissions' in data:
|
||||||
|
perms = Permission.query.filter(Permission.name.in_(data['permissions'])).all()
|
||||||
|
role.permissions = perms
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({"message": "角色保存成功"})
|
||||||
|
|
||||||
|
@admin_bp.route('/roles/delete', methods=['POST'])
|
||||||
|
@permission_required('manage_rbac')
|
||||||
|
def delete_role():
|
||||||
|
data = request.json
|
||||||
|
role = Role.query.get(data.get('id'))
|
||||||
|
if role:
|
||||||
|
if role.name == '超级管理员':
|
||||||
|
return jsonify({"error": "不能删除超级管理员角色"}), 400
|
||||||
|
db.session.delete(role)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({"message": "角色删除成功"})
|
||||||
|
return jsonify({"error": "角色不存在"}), 404
|
||||||
|
|
||||||
|
# --- 权限管理 ---
|
||||||
|
@admin_bp.route('/permissions', methods=['GET'])
|
||||||
|
@permission_required('manage_rbac')
|
||||||
|
def get_permissions():
|
||||||
|
perms = Permission.query.all()
|
||||||
|
return jsonify({
|
||||||
|
"permissions": [{"name": p.name, "description": p.description} for p in perms]
|
||||||
|
})
|
||||||
|
|
||||||
|
# --- 用户角色分配 ---
|
||||||
|
@admin_bp.route('/users', methods=['GET'])
|
||||||
|
@permission_required('manage_users')
|
||||||
|
def get_users():
|
||||||
|
users = User.query.all()
|
||||||
|
return jsonify({
|
||||||
|
"users": [{
|
||||||
|
"id": u.id,
|
||||||
|
"phone": u.phone,
|
||||||
|
"role": u.role.name if u.role else "未分配"
|
||||||
|
} for u in users]
|
||||||
|
})
|
||||||
|
|
||||||
|
@admin_bp.route('/users/assign', methods=['POST'])
|
||||||
|
@permission_required('manage_users')
|
||||||
|
def assign_role():
|
||||||
|
data = request.json
|
||||||
|
user = User.query.get(data['user_id'])
|
||||||
|
role = Role.query.get(data['role_id'])
|
||||||
|
if user and role:
|
||||||
|
user.role = role
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({"message": "角色分配成功"})
|
||||||
|
return jsonify({"error": "用户或角色不存在"}), 404
|
||||||
|
|
||||||
|
# --- 字典管理 ---
|
||||||
|
@admin_bp.route('/dicts', methods=['GET'])
|
||||||
|
@permission_required('manage_dicts')
|
||||||
|
def get_dicts():
|
||||||
|
dict_type = request.args.get('type')
|
||||||
|
query = SystemDict.query
|
||||||
|
if dict_type:
|
||||||
|
query = query.filter_by(dict_type=dict_type)
|
||||||
|
dicts = query.order_by(SystemDict.dict_type, SystemDict.sort_order.desc()).all()
|
||||||
|
return jsonify({
|
||||||
|
"dicts": [{
|
||||||
|
"id": d.id,
|
||||||
|
"dict_type": d.dict_type,
|
||||||
|
"label": d.label,
|
||||||
|
"value": d.value,
|
||||||
|
"cost": d.cost,
|
||||||
|
"is_active": d.is_active,
|
||||||
|
"sort_order": d.sort_order
|
||||||
|
} for d in dicts]
|
||||||
|
})
|
||||||
|
|
||||||
|
@admin_bp.route('/dicts', methods=['POST'])
|
||||||
|
@permission_required('manage_dicts')
|
||||||
|
def save_dict():
|
||||||
|
data = request.json
|
||||||
|
dict_id = data.get('id')
|
||||||
|
|
||||||
|
if dict_id:
|
||||||
|
d = SystemDict.query.get(dict_id)
|
||||||
|
if not d: return jsonify({"error": "记录不存在"}), 404
|
||||||
|
else:
|
||||||
|
d = SystemDict()
|
||||||
|
db.session.add(d)
|
||||||
|
|
||||||
|
d.dict_type = data['dict_type']
|
||||||
|
d.label = data['label']
|
||||||
|
d.value = data['value']
|
||||||
|
d.cost = data.get('cost', 0)
|
||||||
|
d.is_active = data.get('is_active', True)
|
||||||
|
d.sort_order = data.get('sort_order', 0)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({"message": "保存成功"})
|
||||||
|
|
||||||
|
@admin_bp.route('/dicts/delete', methods=['POST'])
|
||||||
|
@permission_required('manage_dicts')
|
||||||
|
def delete_dict():
|
||||||
|
data = request.json
|
||||||
|
d = SystemDict.query.get(data.get('id'))
|
||||||
|
if d:
|
||||||
|
db.session.delete(d)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({"message": "删除成功"})
|
||||||
|
return jsonify({"error": "记录不存在"}), 404
|
||||||
260
blueprints/api.py
Normal file
260
blueprints/api.py
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import io
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from flask import Blueprint, request, jsonify, session, current_app
|
||||||
|
from urllib.parse import quote
|
||||||
|
from config import Config
|
||||||
|
from extensions import s3_client, redis_client, db
|
||||||
|
from models import GenerationRecord, User, SystemDict
|
||||||
|
from middlewares.auth import login_required
|
||||||
|
from services.logger import system_logger
|
||||||
|
|
||||||
|
api_bp = Blueprint('api', __name__)
|
||||||
|
|
||||||
|
def sync_images_background(app, record_id, raw_urls):
|
||||||
|
"""后台同步图片至 MinIO,带重试机制"""
|
||||||
|
with app.app_context():
|
||||||
|
minio_urls = []
|
||||||
|
for raw_url in raw_urls:
|
||||||
|
success = False
|
||||||
|
for attempt in range(3): # 3 次重试机制
|
||||||
|
try:
|
||||||
|
img_resp = requests.get(raw_url, timeout=30)
|
||||||
|
if img_resp.status_code == 200:
|
||||||
|
filename = f"gen-{uuid.uuid4().hex}.png"
|
||||||
|
s3_client.upload_fileobj(
|
||||||
|
io.BytesIO(img_resp.content),
|
||||||
|
Config.MINIO["bucket"],
|
||||||
|
filename,
|
||||||
|
ExtraArgs={"ContentType": "image/png"}
|
||||||
|
)
|
||||||
|
minio_urls.append(f"{Config.MINIO['public_url']}{quote(filename)}")
|
||||||
|
success = True
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ 第 {attempt+1} 次同步失败: {e}")
|
||||||
|
time.sleep(2 ** attempt) # 指数退避
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
# 如果最终失败,保留原始 URL 以便至少有内容可看
|
||||||
|
minio_urls.append(raw_url)
|
||||||
|
|
||||||
|
# 更新数据库记录为持久化 URL
|
||||||
|
try:
|
||||||
|
record = GenerationRecord.query.get(record_id)
|
||||||
|
if record:
|
||||||
|
record.image_urls = json.dumps(minio_urls)
|
||||||
|
db.session.commit()
|
||||||
|
print(f"✅ 记录 {record_id} 图片已完成持久化同步")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 更新记录失败: {e}")
|
||||||
|
|
||||||
|
@api_bp.route('/api/config')
|
||||||
|
def get_config():
|
||||||
|
"""从本地数据库字典获取配置"""
|
||||||
|
try:
|
||||||
|
dicts = SystemDict.query.filter_by(is_active=True).order_by(SystemDict.sort_order.desc()).all()
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"models": [],
|
||||||
|
"ratios": [],
|
||||||
|
"prompts": [],
|
||||||
|
"sizes": []
|
||||||
|
}
|
||||||
|
|
||||||
|
for d in dicts:
|
||||||
|
item = {"label": d.label, "value": d.value}
|
||||||
|
if d.dict_type == 'ai_model':
|
||||||
|
item["cost"] = d.cost
|
||||||
|
config["models"].append(item)
|
||||||
|
elif d.dict_type == 'aspect_ratio':
|
||||||
|
config["ratios"].append(item)
|
||||||
|
elif d.dict_type == 'prompt_tpl':
|
||||||
|
config["prompts"].append(item)
|
||||||
|
elif d.dict_type == 'ai_image_size':
|
||||||
|
config["sizes"].append(item)
|
||||||
|
|
||||||
|
return jsonify(config)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@api_bp.route('/api/upload', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def upload():
|
||||||
|
try:
|
||||||
|
files = request.files.getlist('images')
|
||||||
|
img_urls = []
|
||||||
|
for f in files:
|
||||||
|
ext = os.path.splitext(f.filename)[1]
|
||||||
|
filename = f"{uuid.uuid4().hex}{ext}"
|
||||||
|
s3_client.upload_fileobj(
|
||||||
|
f, Config.MINIO["bucket"], filename,
|
||||||
|
ExtraArgs={"ContentType": f.content_type}
|
||||||
|
)
|
||||||
|
img_urls.append(f"{Config.MINIO['public_url']}{quote(filename)}")
|
||||||
|
return jsonify({"urls": img_urls})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@api_bp.route('/api/generate', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def generate():
|
||||||
|
try:
|
||||||
|
user_id = session.get('user_id')
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
|
||||||
|
data = request.json if request.is_json else request.form
|
||||||
|
mode = data.get('mode', 'trial')
|
||||||
|
is_premium = data.get('is_premium', False)
|
||||||
|
input_key = data.get('apiKey')
|
||||||
|
|
||||||
|
target_api = Config.AI_API
|
||||||
|
api_key = None
|
||||||
|
use_trial = False
|
||||||
|
|
||||||
|
if mode == 'key':
|
||||||
|
# 自定义 Key 模式:优先使用本次输入的,否则使用数据库存的
|
||||||
|
api_key = input_key or user.api_key
|
||||||
|
if not api_key:
|
||||||
|
return jsonify({"error": "请先输入您的 API 密钥"}), 400
|
||||||
|
else:
|
||||||
|
# 积分/试用模式
|
||||||
|
if user.points > 0:
|
||||||
|
# 核心修复:优质模式使用专属 Key
|
||||||
|
api_key = Config.PREMIUM_KEY if is_premium else Config.TRIAL_KEY
|
||||||
|
target_api = Config.TRIAL_API
|
||||||
|
use_trial = True
|
||||||
|
else:
|
||||||
|
return jsonify({"error": "可用积分已耗尽,请充值或切换至自定义 Key 模式"}), 400
|
||||||
|
|
||||||
|
# 如果是 Key 模式且输入了新 Key,则自动更新到数据库保存
|
||||||
|
if mode == 'key' and input_key and input_key != user.api_key:
|
||||||
|
user.api_key = input_key
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# 获取模型及对应的消耗积分
|
||||||
|
model_value = data.get('model')
|
||||||
|
model_dict = SystemDict.query.filter_by(dict_type='ai_model', value=model_value).first()
|
||||||
|
cost = model_dict.cost if model_dict else 1
|
||||||
|
|
||||||
|
# 核心修复:优质模式积分消耗 X2
|
||||||
|
if use_trial and is_premium:
|
||||||
|
cost *= 2
|
||||||
|
|
||||||
|
# --- 积分预扣除逻辑 (点击即扣) ---
|
||||||
|
if use_trial:
|
||||||
|
if user.points < cost:
|
||||||
|
return jsonify({"error": f"可用积分不足,优质模式需要 {cost} 积分,您当前剩余 {user.points} 积分"}), 400
|
||||||
|
|
||||||
|
user.points -= cost
|
||||||
|
db.session.commit()
|
||||||
|
system_logger.info(f"积分预扣除 ({'优质' if is_premium else '普通'}试用)", phone=user.phone, cost=cost, remaining_points=user.points)
|
||||||
|
|
||||||
|
try:
|
||||||
|
prompt = data.get('prompt')
|
||||||
|
model = model_value
|
||||||
|
ratio = data.get('ratio')
|
||||||
|
size = data.get('size')
|
||||||
|
input_img_urls = data.get('image_urls', [])
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"prompt": prompt,
|
||||||
|
"model": model,
|
||||||
|
"response_format": "url",
|
||||||
|
"aspect_ratio": ratio,
|
||||||
|
"image": input_img_urls
|
||||||
|
}
|
||||||
|
if model == "nano-banana-2" and size:
|
||||||
|
payload["image_size"] = size
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||||
|
resp = requests.post(target_api, json=payload, headers=headers, timeout=300)
|
||||||
|
|
||||||
|
if resp.status_code != 200:
|
||||||
|
# API 报错,退还积分
|
||||||
|
if use_trial:
|
||||||
|
user.points += cost
|
||||||
|
db.session.commit()
|
||||||
|
system_logger.warning(f"API 报错,积分已退还", phone=user.phone, status_code=resp.status_code)
|
||||||
|
return jsonify({"error": resp.text}), resp.status_code
|
||||||
|
|
||||||
|
api_result = resp.json()
|
||||||
|
raw_urls = [item['url'] for item in api_result.get('data', [])]
|
||||||
|
|
||||||
|
# 立即写入数据库(先存原始 URL)
|
||||||
|
new_record = GenerationRecord(
|
||||||
|
user_id=session.get('user_id'),
|
||||||
|
prompt=prompt,
|
||||||
|
model=model,
|
||||||
|
image_urls=json.dumps(raw_urls)
|
||||||
|
)
|
||||||
|
db.session.add(new_record)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# 写入系统日志
|
||||||
|
system_logger.info(
|
||||||
|
f"用户生成图片成功",
|
||||||
|
phone=user.phone,
|
||||||
|
model=model,
|
||||||
|
record_id=new_record.id
|
||||||
|
)
|
||||||
|
|
||||||
|
# 启动后台线程同步图片,不阻塞前端返回
|
||||||
|
app = current_app._get_current_object()
|
||||||
|
threading.Thread(
|
||||||
|
target=sync_images_background,
|
||||||
|
args=(app, new_record.id, raw_urls)
|
||||||
|
).start()
|
||||||
|
|
||||||
|
# 立即返回原始 URL 给前端展示
|
||||||
|
return jsonify({
|
||||||
|
"data": [{"url": url} for url in raw_urls],
|
||||||
|
"message": "生成成功!作品正在后台同步至云存储。"
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 发生系统异常,退还积分
|
||||||
|
if use_trial:
|
||||||
|
user.points += cost
|
||||||
|
db.session.commit()
|
||||||
|
system_logger.error(f"生成异常,积分已退还", phone=user.phone, error=str(e))
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@api_bp.route('/api/history', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def get_history():
|
||||||
|
"""获取用户的历史生成记录 (支持分页,限 90 天内)"""
|
||||||
|
try:
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
user_id = session.get('user_id')
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
per_page = request.args.get('per_page', 10, type=int)
|
||||||
|
|
||||||
|
# 计算 90 天前的时间
|
||||||
|
ninety_days_ago = datetime.utcnow() - timedelta(days=90)
|
||||||
|
|
||||||
|
pagination = GenerationRecord.query.filter(
|
||||||
|
GenerationRecord.user_id == user_id,
|
||||||
|
GenerationRecord.created_at >= ninety_days_ago
|
||||||
|
).order_by(GenerationRecord.created_at.desc())\
|
||||||
|
.paginate(page=page, per_page=per_page, error_out=False)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"history": [{
|
||||||
|
"id": r.id,
|
||||||
|
"prompt": r.prompt,
|
||||||
|
"model": r.model,
|
||||||
|
"urls": json.loads(r.image_urls),
|
||||||
|
"time": r.created_at.strftime('%Y-%m-%d %H:%M')
|
||||||
|
} for r in pagination.items],
|
||||||
|
"has_next": pagination.has_next,
|
||||||
|
"total": pagination.total
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
199
blueprints/auth.py
Normal file
199
blueprints/auth.py
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
from flask import Blueprint, request, jsonify, session, render_template, redirect, url_for
|
||||||
|
from extensions import db
|
||||||
|
from models import User
|
||||||
|
from services.sms_service import SMSService
|
||||||
|
from services.logger import system_logger
|
||||||
|
from middlewares.auth import admin_required
|
||||||
|
|
||||||
|
auth_bp = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
@auth_bp.route('/login')
|
||||||
|
def login_page():
|
||||||
|
"""登录页面"""
|
||||||
|
if 'user_id' in session:
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
return render_template('login.html')
|
||||||
|
|
||||||
|
@auth_bp.route('/logs')
|
||||||
|
@admin_required
|
||||||
|
def logs_page():
|
||||||
|
"""日志查看页面"""
|
||||||
|
return render_template('logs.html')
|
||||||
|
|
||||||
|
@auth_bp.route('/rbac')
|
||||||
|
@admin_required
|
||||||
|
def rbac_page():
|
||||||
|
"""角色权限管理页面"""
|
||||||
|
return render_template('rbac.html')
|
||||||
|
|
||||||
|
@auth_bp.route('/dicts')
|
||||||
|
@admin_required
|
||||||
|
def dicts_page():
|
||||||
|
"""系统字典管理页面"""
|
||||||
|
return render_template('dicts.html')
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/send_code', methods=['POST'])
|
||||||
|
def send_code():
|
||||||
|
data = request.json
|
||||||
|
phone = data.get('phone')
|
||||||
|
if not phone:
|
||||||
|
return jsonify({"error": "请输入手机号"}), 400
|
||||||
|
|
||||||
|
system_logger.info(f"用户请求发送验证码", phone=phone)
|
||||||
|
success, msg = SMSService.send_code(phone)
|
||||||
|
if success:
|
||||||
|
system_logger.info(f"验证码发送成功", phone=phone)
|
||||||
|
return jsonify({"message": "验证码已发送"})
|
||||||
|
system_logger.warning(f"验证码发送失败: {msg}", phone=phone)
|
||||||
|
return jsonify({"error": f"发送失败: {msg}"}), 500
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/register', methods=['POST'])
|
||||||
|
def register():
|
||||||
|
data = request.json
|
||||||
|
phone = data.get('phone')
|
||||||
|
code = data.get('code')
|
||||||
|
password = data.get('password')
|
||||||
|
|
||||||
|
system_logger.info(f"用户注册请求", phone=phone)
|
||||||
|
|
||||||
|
if not SMSService.verify_code(phone, code):
|
||||||
|
system_logger.warning(f"注册失败: 验证码错误", phone=phone)
|
||||||
|
return jsonify({"error": "验证码错误或已过期"}), 400
|
||||||
|
|
||||||
|
if User.query.filter_by(phone=phone).first():
|
||||||
|
system_logger.warning(f"注册失败: 手机号已存在", phone=phone)
|
||||||
|
return jsonify({"error": "该手机号已注册"}), 400
|
||||||
|
|
||||||
|
# 获取默认角色:普通用户
|
||||||
|
from models import Role
|
||||||
|
user_role = Role.query.filter_by(name='普通用户').first()
|
||||||
|
|
||||||
|
user = User(phone=phone, role=user_role)
|
||||||
|
user.set_password(password)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
system_logger.info(f"用户注册成功", phone=phone, user_id=user.id)
|
||||||
|
return jsonify({"message": "注册成功"})
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/login', methods=['POST'])
|
||||||
|
def login():
|
||||||
|
data = request.json
|
||||||
|
phone = data.get('phone')
|
||||||
|
password = data.get('password')
|
||||||
|
|
||||||
|
system_logger.info(f"用户登录尝试", phone=phone)
|
||||||
|
|
||||||
|
user = User.query.filter_by(phone=phone).first()
|
||||||
|
if user and user.check_password(password):
|
||||||
|
session.permanent = True # 开启持久化会话 (受 Config.PERMANENT_SESSION_LIFETIME 控制)
|
||||||
|
session['user_id'] = user.id
|
||||||
|
system_logger.info(f"用户登录成功", phone=phone, user_id=user.id)
|
||||||
|
return jsonify({"message": "登录成功", "phone": phone})
|
||||||
|
|
||||||
|
system_logger.warning(f"登录失败: 手机号或密码错误", phone=phone)
|
||||||
|
return jsonify({"error": "手机号或密码错误"}), 401
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/logout', methods=['POST'])
|
||||||
|
def logout():
|
||||||
|
session.pop('user_id', None)
|
||||||
|
return jsonify({"message": "已退出登录"})
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/me', methods=['GET'])
|
||||||
|
def me():
|
||||||
|
user_id = session.get('user_id')
|
||||||
|
if not user_id:
|
||||||
|
return jsonify({"logged_in": False})
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
return jsonify({
|
||||||
|
"logged_in": True,
|
||||||
|
"phone": user.phone,
|
||||||
|
"api_key": user.api_key, # 返回已保存的 API Key
|
||||||
|
"points": user.points # 返回剩余试用积分
|
||||||
|
})
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/change_password', methods=['POST'])
|
||||||
|
def change_password():
|
||||||
|
user_id = session.get('user_id')
|
||||||
|
if not user_id:
|
||||||
|
return jsonify({"error": "请先登录"}), 401
|
||||||
|
|
||||||
|
data = request.json
|
||||||
|
old_password = data.get('old_password')
|
||||||
|
new_password = data.get('new_password')
|
||||||
|
|
||||||
|
if not old_password or not new_password:
|
||||||
|
return jsonify({"error": "请填写完整信息"}), 400
|
||||||
|
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
if not user.check_password(old_password):
|
||||||
|
return jsonify({"error": "原密码错误"}), 400
|
||||||
|
|
||||||
|
user.set_password(new_password)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
system_logger.info(f"用户修改密码成功", phone=user.phone, user_id=user.id)
|
||||||
|
return jsonify({"message": "密码修改成功"})
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/menu', methods=['GET'])
|
||||||
|
def get_menu():
|
||||||
|
"""获取动态导航菜单"""
|
||||||
|
user_id = session.get('user_id')
|
||||||
|
if not user_id:
|
||||||
|
return jsonify({"menu": []})
|
||||||
|
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
if not user:
|
||||||
|
return jsonify({"menu": []})
|
||||||
|
|
||||||
|
# 菜单定义库:名称, 图标, 链接, 所需权限
|
||||||
|
all_menus = [
|
||||||
|
{"name": "创作工作台", "icon": "wand-2", "url": "/", "perm": None},
|
||||||
|
{"name": "权限管理中心", "icon": "shield-check", "url": "/rbac", "perm": "manage_rbac"},
|
||||||
|
{"name": "系统字典管理", "icon": "book-open", "url": "/dicts", "perm": "manage_dicts"},
|
||||||
|
{"name": "系统日志审计", "icon": "terminal", "url": "/logs", "perm": "view_logs"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# 根据权限过滤
|
||||||
|
accessible_menu = []
|
||||||
|
for item in all_menus:
|
||||||
|
if item["perm"] is None or user.has_permission(item["perm"]):
|
||||||
|
accessible_menu.append(item)
|
||||||
|
|
||||||
|
return jsonify({"menu": accessible_menu})
|
||||||
|
|
||||||
|
@auth_bp.route('/api/auth/logs', methods=['GET'])
|
||||||
|
def get_logs():
|
||||||
|
"""获取系统日志(支持搜索和筛选)"""
|
||||||
|
user_id = session.get('user_id')
|
||||||
|
if not user_id:
|
||||||
|
return jsonify({"error": "请先登录"}), 401
|
||||||
|
|
||||||
|
from extensions import redis_client
|
||||||
|
import json
|
||||||
|
|
||||||
|
# 筛选参数
|
||||||
|
level_filter = request.args.get('level')
|
||||||
|
search_query = request.args.get('search', '').lower()
|
||||||
|
|
||||||
|
# 从 Redis 获取所有日志 (目前上限 2000 条)
|
||||||
|
logs = redis_client.lrange('system_logs', 0, -1)
|
||||||
|
log_list = []
|
||||||
|
|
||||||
|
for log in logs:
|
||||||
|
item = json.loads(log.decode('utf-8'))
|
||||||
|
|
||||||
|
# 级别过滤
|
||||||
|
if level_filter and item['level'] != level_filter:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 关键词搜索 (搜索内容、手机号或其它 Extra 字段)
|
||||||
|
if search_query:
|
||||||
|
message_match = search_query in item['message'].lower()
|
||||||
|
extra_match = any(search_query in str(v).lower() for v in item.get('extra', {}).values())
|
||||||
|
if not (message_match or extra_match):
|
||||||
|
continue
|
||||||
|
|
||||||
|
log_list.append(item)
|
||||||
|
|
||||||
|
return jsonify({"logs": log_list})
|
||||||
48
config.py
Normal file
48
config.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
# 基础配置
|
||||||
|
SECRET_KEY = os.getenv("SECRET_KEY", "vision-ai-secret-key")
|
||||||
|
|
||||||
|
# PostgreSQL 配置
|
||||||
|
SQLALCHEMY_DATABASE_URI = "postgresql://user_xREpkJ:password_DZz8DQ@331002.xyz:2022/ai_vision"
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
|
||||||
|
# Redis 配置
|
||||||
|
REDIS_URL = "redis://:redis_WWjNyb@331002.xyz:2020/0"
|
||||||
|
|
||||||
|
# Session 配置
|
||||||
|
PERMANENT_SESSION_LIFETIME = 604800 # 7 天 (单位:秒)
|
||||||
|
SESSION_COOKIE_SECURE = True # 开发环境设为 False,生产环境建议设为 True
|
||||||
|
SESSION_COOKIE_HTTPONLY = True # 防止 XSS 获取 Cookie
|
||||||
|
|
||||||
|
# MinIO 配置
|
||||||
|
MINIO = {
|
||||||
|
"endpoint": "http://331002.xyz:9000",
|
||||||
|
"access_key": "l0VlsxrkASbXN2YSQrJk",
|
||||||
|
"secret_key": "ZK8nXHieorl3fpbssUMGGfr8zZmbpXB5gAbma3z1",
|
||||||
|
"bucket": "images",
|
||||||
|
"public_url": "http://331002.xyz:9000/images/"
|
||||||
|
}
|
||||||
|
|
||||||
|
# AI API 配置
|
||||||
|
AI_API = "https://ai.t8star.cn/v1/images/generations"
|
||||||
|
|
||||||
|
# 试用模式配置
|
||||||
|
TRIAL_API = "https://ai.comfly.chat/v1/images/generations"
|
||||||
|
TRIAL_KEY = "sk-Rr8L5noW8Qga7K4jmey3yYZYL1a4SlhlNlo5iZrwqJRK1Pa1"
|
||||||
|
PREMIUM_KEY = "sk-168trRxnemem6nTpQn1rbmJ4SFKLwTMsZ0G6uk5OipP7FKAY"
|
||||||
|
|
||||||
|
DICT_URL = "https://nas.4x4g.com:10011/api/common/sys/dict"
|
||||||
|
PLATFORM = "lingmao"
|
||||||
|
|
||||||
|
# 阿里云短信配置
|
||||||
|
ALIBABA_CLOUD_ACCESS_KEY_ID = "LTAI5tAbHKxmPKVPYsABEdyq"
|
||||||
|
ALIBABA_CLOUD_ACCESS_KEY_SECRET = "v6URREddBqvGfwZrWH1DWoxs3w6RxZ"
|
||||||
|
SMS_SIGN_NAME = "速通互联验证码"
|
||||||
|
SMS_TEMPLATE_CODE = "100001"
|
||||||
|
SMS_NEED_PARAM = False # 该模板需要参数,如使用系统赠送模板请改为 False
|
||||||
|
|
||||||
|
# 开发模式配置
|
||||||
|
DEV_MODE = False # True=开发模式(固定验证码),False=生产模式(真实短信)
|
||||||
|
DEV_SMS_CODE = "888888" # 开发模式下的固定验证码
|
||||||
64
create_database.py
Normal file
64
create_database.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
数据库创建脚本
|
||||||
|
用于在 PostgreSQL 服务器上创建 ai_vision 数据库
|
||||||
|
"""
|
||||||
|
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
|
||||||
|
|
||||||
|
# 数据库连接信息
|
||||||
|
DB_HOST = "331002.xyz"
|
||||||
|
DB_PORT = 2022
|
||||||
|
DB_USER = "user_xREpkJ"
|
||||||
|
DB_PASSWORD = "password_DZz8DQ"
|
||||||
|
DB_NAME = "ai_vision"
|
||||||
|
|
||||||
|
def create_database():
|
||||||
|
"""创建数据库"""
|
||||||
|
try:
|
||||||
|
# 连接到默认的 postgres 数据库
|
||||||
|
print(f"🔗 正在连接到 PostgreSQL 服务器 {DB_HOST}:{DB_PORT}...")
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
host=DB_HOST,
|
||||||
|
port=DB_PORT,
|
||||||
|
user=DB_USER,
|
||||||
|
password=DB_PASSWORD,
|
||||||
|
database="postgres" # 先连接到默认数据库
|
||||||
|
)
|
||||||
|
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 检查数据库是否存在
|
||||||
|
cursor.execute(f"SELECT 1 FROM pg_database WHERE datname = '{DB_NAME}'")
|
||||||
|
exists = cursor.fetchone()
|
||||||
|
|
||||||
|
if exists:
|
||||||
|
print(f"✅ 数据库 {DB_NAME} 已经存在")
|
||||||
|
else:
|
||||||
|
# 创建数据库
|
||||||
|
print(f"🔧 正在创建数据库 {DB_NAME}...")
|
||||||
|
cursor.execute(f'CREATE DATABASE {DB_NAME}')
|
||||||
|
print(f"✅ 数据库 {DB_NAME} 创建成功!")
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print(f"\n📊 数据库信息:")
|
||||||
|
print(f" 主机: {DB_HOST}:{DB_PORT}")
|
||||||
|
print(f" 数据库名: {DB_NAME}")
|
||||||
|
print(f" 用户: {DB_USER}")
|
||||||
|
print(f"\n💡 下一步:运行 python init_db.py 创建数据表")
|
||||||
|
|
||||||
|
except psycopg2.Error as e:
|
||||||
|
print(f"❌ 数据库操作失败: {e}")
|
||||||
|
print(f"\n可能的原因:")
|
||||||
|
print(f" 1. 用户 {DB_USER} 没有创建数据库的权限")
|
||||||
|
print(f" 2. 网络连接问题")
|
||||||
|
print(f" 3. 数据库服务器配置限制")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 发生错误: {e}")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
create_database()
|
||||||
16
extensions.py
Normal file
16
extensions.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_redis import FlaskRedis
|
||||||
|
import boto3
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
|
redis_client = FlaskRedis()
|
||||||
|
|
||||||
|
# MinIO Client
|
||||||
|
s3_client = boto3.client(
|
||||||
|
"s3",
|
||||||
|
endpoint_url=Config.MINIO["endpoint"],
|
||||||
|
aws_access_key_id=Config.MINIO["access_key"],
|
||||||
|
aws_secret_access_key=Config.MINIO["secret_key"],
|
||||||
|
region_name="us-east-1"
|
||||||
|
)
|
||||||
25
fix_db_manual.py
Normal file
25
fix_db_manual.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import psycopg2
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
def migrate():
|
||||||
|
# 从 URI 解析连接参数
|
||||||
|
# postgresql://user:pass@host:port/dbname
|
||||||
|
uri = Config.SQLALCHEMY_DATABASE_URI
|
||||||
|
print(f"正在手动连接数据库进行迁移...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect(uri)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# 添加 api_key 字段
|
||||||
|
cur.execute("ALTER TABLE users ADD COLUMN IF NOT EXISTS api_key VARCHAR(255);")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
print("✅ 数据库字段 users.api_key 手动添加成功")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 迁移失败: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
migrate()
|
||||||
33
fix_db_manual_points.py
Normal file
33
fix_db_manual_points.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import psycopg2
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
def fix_db():
|
||||||
|
# 从 SQLALCHEMY_DATABASE_URI 提取连接信息
|
||||||
|
# 格式: postgresql://user:pass@host:port/db
|
||||||
|
uri = Config.SQLALCHEMY_DATABASE_URI
|
||||||
|
print(f"🔗 正在尝试连接数据库...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect(uri)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# 检查并添加 points 字段
|
||||||
|
cur.execute("""
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='points') THEN
|
||||||
|
ALTER TABLE users ADD COLUMN points INTEGER DEFAULT 2;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
""")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
print("✅ 数据库字段 points 处理完成 (默认值 2)")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 数据库修复失败: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fix_db()
|
||||||
81
force_init.py
Normal file
81
force_init.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
from app import app
|
||||||
|
from extensions import db
|
||||||
|
from models import User, Role, Permission
|
||||||
|
from sqlalchemy import text
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def force_migrate():
|
||||||
|
with app.app_context():
|
||||||
|
print("🛠️ 开始强制迁移...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. 尝试清除该表的其他活动连接 (仅限 PostgreSQL)
|
||||||
|
print("🧹 正在清理数据库死锁...")
|
||||||
|
db.session.execute(text("""
|
||||||
|
SELECT pg_terminate_backend(pid)
|
||||||
|
FROM pg_stat_activity
|
||||||
|
WHERE datname = current_database()
|
||||||
|
AND pid <> pg_backend_pid();
|
||||||
|
"""))
|
||||||
|
db.session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ 清理连接跳过 (可能是权限问题): {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 2. 创建所有新表
|
||||||
|
print("📦 正在同步表结构...")
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
# 3. 增加字段
|
||||||
|
print("📝 正在调整 users 表结构...")
|
||||||
|
db.session.execute(text('ALTER TABLE users ADD COLUMN IF NOT EXISTS role_id INTEGER REFERENCES roles(id)'))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# 4. 初始化角色和权限
|
||||||
|
print("🚀 正在初始化 RBAC 权限数据...")
|
||||||
|
perms = {
|
||||||
|
'view_logs': '查看系统日志',
|
||||||
|
'manage_rbac': '管理角色与权限',
|
||||||
|
'manage_users': '管理用户信息',
|
||||||
|
'manage_system': '系统最高权限'
|
||||||
|
}
|
||||||
|
perm_objs = {}
|
||||||
|
for code, desc in perms.items():
|
||||||
|
p = Permission.query.filter_by(name=code).first()
|
||||||
|
if not p:
|
||||||
|
p = Permission(name=code, description=desc)
|
||||||
|
db.session.add(p)
|
||||||
|
db.session.flush()
|
||||||
|
perm_objs[code] = p
|
||||||
|
|
||||||
|
# 创建管理员角色
|
||||||
|
admin_role = Role.query.filter_by(name='超级管理员').first()
|
||||||
|
if not admin_role:
|
||||||
|
admin_role = Role(name='超级管理员', description='系统最高权限持有者')
|
||||||
|
admin_role.permissions = list(perm_objs.values())
|
||||||
|
db.session.add(admin_role)
|
||||||
|
|
||||||
|
# 创建用户角色
|
||||||
|
user_role = Role.query.filter_by(name='普通用户').first()
|
||||||
|
if not user_role:
|
||||||
|
user_role = Role(name='普通用户', description='常规功能使用者')
|
||||||
|
db.session.add(user_role)
|
||||||
|
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
# 5. 修复旧数据:把所有现有用户设为超级管理员(方便你第一时间进入后台)
|
||||||
|
print("👤 正在升级现有用户为管理员...")
|
||||||
|
all_users = User.query.all()
|
||||||
|
for u in all_users:
|
||||||
|
u.role = admin_role
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print("✨ 迁移与初始化全部完成!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 运行出错: {e}")
|
||||||
|
db.session.rollback()
|
||||||
|
raise e
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
force_migrate()
|
||||||
53
init_db.py
Normal file
53
init_db.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
数据库初始化脚本
|
||||||
|
用于手动创建或重置数据库表
|
||||||
|
"""
|
||||||
|
|
||||||
|
from app import app
|
||||||
|
from extensions import db
|
||||||
|
import models
|
||||||
|
|
||||||
|
def init_database():
|
||||||
|
"""初始化数据库表"""
|
||||||
|
with app.app_context():
|
||||||
|
print("🔧 开始初始化数据库...")
|
||||||
|
|
||||||
|
# 创建所有表
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
# 检查表是否创建成功
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
inspector = inspect(db.engine)
|
||||||
|
tables = inspector.get_table_names()
|
||||||
|
|
||||||
|
print(f"\n✅ 数据库表已创建,共 {len(tables)} 张表:")
|
||||||
|
for table in tables:
|
||||||
|
print(f" - {table}")
|
||||||
|
|
||||||
|
print("\n📊 表结构详情:")
|
||||||
|
for table_name in tables:
|
||||||
|
columns = inspector.get_columns(table_name)
|
||||||
|
print(f"\n{table_name}:")
|
||||||
|
for col in columns:
|
||||||
|
print(f" {col['name']} ({col['type']})")
|
||||||
|
|
||||||
|
def drop_all_tables():
|
||||||
|
"""删除所有表(慎用)"""
|
||||||
|
with app.app_context():
|
||||||
|
print("⚠️ 警告:即将删除所有数据库表!")
|
||||||
|
confirm = input("确认删除?输入 yes 继续: ")
|
||||||
|
if confirm.lower() == 'yes':
|
||||||
|
db.drop_all()
|
||||||
|
print("✅ 所有表已删除")
|
||||||
|
else:
|
||||||
|
print("❌ 操作已取消")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == '--drop':
|
||||||
|
drop_all_tables()
|
||||||
|
else:
|
||||||
|
init_database()
|
||||||
54
init_dicts.py
Normal file
54
init_dicts.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import requests
|
||||||
|
from app import app
|
||||||
|
from extensions import db
|
||||||
|
from models import SystemDict
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
def fetch_and_init():
|
||||||
|
with app.app_context():
|
||||||
|
# 定义需要抓取的字典代码及对应的本地类型
|
||||||
|
target_mappings = {
|
||||||
|
"nano_model": "ai_model",
|
||||||
|
"aspect_ratio": "aspect_ratio",
|
||||||
|
"ai_prompt": "prompt_tpl",
|
||||||
|
"ai_image_size": "ai_image_size"
|
||||||
|
}
|
||||||
|
|
||||||
|
print("🚀 开始从远程接口获取字典数据...")
|
||||||
|
|
||||||
|
for remote_code, local_type in target_mappings.items():
|
||||||
|
try:
|
||||||
|
url = f"{Config.DICT_URL}?platform={Config.PLATFORM}&code={remote_code}"
|
||||||
|
response = requests.get(url, verify=False, timeout=15)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json().get("data", [])
|
||||||
|
print(f"📦 抓取到 {remote_code} ({len(data)} 条数据)")
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
label = item.get("label")
|
||||||
|
value = item.get("value")
|
||||||
|
|
||||||
|
# 检查本地是否已存在
|
||||||
|
exists = SystemDict.query.filter_by(dict_type=local_type, value=value).first()
|
||||||
|
if not exists:
|
||||||
|
new_dict = SystemDict(
|
||||||
|
dict_type=local_type,
|
||||||
|
label=label,
|
||||||
|
value=value,
|
||||||
|
cost=1 if local_type == 'ai_model' else 0, # 模型默认 1 积分,其余 0
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
db.session.add(new_dict)
|
||||||
|
else:
|
||||||
|
print(f"❌ 抓取 {remote_code} 失败: HTTP {response.status_code}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ 抓取 {remote_code} 发生异常: {e}")
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print("\n✅ 字典数据本地化初始化成功!")
|
||||||
|
print("💡 您现在可以直接在数据库 system_dicts 表中修改模型的 cost (积分消耗) 字段。")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fetch_and_init()
|
||||||
54
init_rbac.py
Normal file
54
init_rbac.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from app import app
|
||||||
|
from extensions import db
|
||||||
|
from models import User, Role, Permission
|
||||||
|
|
||||||
|
def init_rbac():
|
||||||
|
with app.app_context():
|
||||||
|
print("🚀 正在初始化 RBAC 系统...")
|
||||||
|
|
||||||
|
# 1. 创建基础权限
|
||||||
|
perms = {
|
||||||
|
'view_logs': '查看系统日志',
|
||||||
|
'manage_rbac': '管理角色与权限',
|
||||||
|
'manage_users': '管理用户信息',
|
||||||
|
'manage_dicts': '管理系统字典',
|
||||||
|
'manage_system': '系统最高权限'
|
||||||
|
}
|
||||||
|
perm_objs = {}
|
||||||
|
for code, desc in perms.items():
|
||||||
|
p = Permission.query.filter_by(name=code).first()
|
||||||
|
if not p:
|
||||||
|
p = Permission(name=code, description=desc)
|
||||||
|
db.session.add(p)
|
||||||
|
perm_objs[code] = p
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# 2. 创建基础角色
|
||||||
|
# 超级管理员角色
|
||||||
|
admin_role = Role.query.filter_by(name='超级管理员').first()
|
||||||
|
if not admin_role:
|
||||||
|
admin_role = Role(name='超级管理员', description='系统最高权限持有者')
|
||||||
|
admin_role.permissions = list(perm_objs.values())
|
||||||
|
db.session.add(admin_role)
|
||||||
|
|
||||||
|
# 普通用户角色
|
||||||
|
user_role = Role.query.filter_by(name='普通用户').first()
|
||||||
|
if not user_role:
|
||||||
|
user_role = Role(name='普通用户', description='常规功能使用者')
|
||||||
|
db.session.add(user_role)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# 3. 为现有用户分配超级管理员角色(作为测试)
|
||||||
|
# 请根据实际情况修改
|
||||||
|
first_user = User.query.first()
|
||||||
|
if first_user:
|
||||||
|
first_user.role = admin_role
|
||||||
|
db.session.commit()
|
||||||
|
print(f"✅ 已将用户 {first_user.phone} 设为超级管理员")
|
||||||
|
|
||||||
|
print("✨ RBAC 初始化完成")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
init_rbac()
|
||||||
57
logs/system.log
Normal file
57
logs/system.log
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
[2026-01-11 17:43:34] INFO - 用户请求发送验证码
|
||||||
|
[2026-01-11 17:43:36] WARNING - 验证码发送失败: Error: InvalidAccessKeyId.NotFound code: 404, Specified access key is not found. request id: 912B43E3-5393-53A4-92B7-669BA1DF61A3 Response: {'RequestId': '912B43E3-5393-53A4-92B7-669BA1DF61A3', 'Message': 'Specified access key is not found.', 'Recommend': 'https://api.aliyun.com/troubleshoot?q=InvalidAccessKeyId.NotFound&product=Dysmsapi&requestId=912B43E3-5393-53A4-92B7-669BA1DF61A3', 'HostId': 'dysmsapi.aliyuncs.com', 'Code': 'InvalidAccessKeyId.NotFound', 'statusCode': 404}
|
||||||
|
[2026-01-11 17:50:36] INFO - 用户请求发送验证码
|
||||||
|
[2026-01-11 17:50:37] WARNING - 验证码发送失败: Error: InvalidAccessKeyId.NotFound code: 404, Specified access key is not found. request id: 762395A5-97D5-569F-9A50-8D2AD36009C1 Response: {'RequestId': '762395A5-97D5-569F-9A50-8D2AD36009C1', 'Message': 'Specified access key is not found.', 'Recommend': 'https://api.aliyun.com/troubleshoot?q=InvalidAccessKeyId.NotFound&product=Dypnsapi&requestId=762395A5-97D5-569F-9A50-8D2AD36009C1', 'HostId': 'dypnsapi.aliyuncs.com', 'Code': 'InvalidAccessKeyId.NotFound', 'statusCode': 404}
|
||||||
|
[2026-01-11 17:52:03] INFO - 用户注册请求
|
||||||
|
[2026-01-11 17:52:03] WARNING - 注册失败: 验证码错误
|
||||||
|
[2026-01-11 17:58:53] INFO - 用户请求发送验证码
|
||||||
|
[2026-01-11 17:58:53] INFO - 验证码发送成功
|
||||||
|
[2026-01-11 17:59:51] INFO - 用户请求发送验证码
|
||||||
|
[2026-01-11 17:59:51] WARNING - 验证码发送失败: 请2秒后再试
|
||||||
|
[2026-01-11 17:59:56] INFO - 用户请求发送验证码
|
||||||
|
[2026-01-11 17:59:57] WARNING - 验证码发送失败: Error: MissingTemplateParam code: 400, TemplateParam is mandatory for this action. request id: E6CEAF97-4525-55C1-9C4F-DF7A4D008AF4 Response: {'RequestId': 'E6CEAF97-4525-55C1-9C4F-DF7A4D008AF4', 'Message': 'TemplateParam is mandatory for this action.', 'Recommend': 'https://api.aliyun.com/troubleshoot?q=MissingTemplateParam&product=Dypnsapi&requestId=E6CEAF97-4525-55C1-9C4F-DF7A4D008AF4', 'HostId': 'dypnsapi.aliyuncs.com', 'Code': 'MissingTemplateParam', 'statusCode': 400}
|
||||||
|
[2026-01-11 18:02:00] INFO - 用户请求发送验证码
|
||||||
|
[2026-01-11 18:02:01] WARNING - 验证码发送失败: Error: MissingTemplateParam code: 400, TemplateParam is mandatory for this action. request id: BDEBE5C4-D5C5-5FAD-AFFB-AE94F42FF7F8 Response: {'RequestId': 'BDEBE5C4-D5C5-5FAD-AFFB-AE94F42FF7F8', 'Message': 'TemplateParam is mandatory for this action.', 'Recommend': 'https://api.aliyun.com/troubleshoot?q=MissingTemplateParam&product=Dypnsapi&requestId=BDEBE5C4-D5C5-5FAD-AFFB-AE94F42FF7F8', 'HostId': 'dypnsapi.aliyuncs.com', 'Code': 'MissingTemplateParam', 'statusCode': 400}
|
||||||
|
[2026-01-11 18:04:11] INFO - 用户请求发送验证码
|
||||||
|
[2026-01-11 18:04:12] WARNING - 验证码发送失败: 请检查模板内容与模板参数是否匹配
|
||||||
|
[2026-01-11 18:05:41] INFO - 用户请求发送验证码
|
||||||
|
[2026-01-11 18:05:43] WARNING - 验证码发送失败: 非法参数
|
||||||
|
[2026-01-11 18:06:41] INFO - 用户请求发送验证码
|
||||||
|
[2026-01-11 18:06:42] WARNING - 验证码发送失败: check frequency failed
|
||||||
|
[2026-01-11 18:07:53] INFO - 用户请求发送验证码
|
||||||
|
[2026-01-11 18:07:53] INFO - 验证码发送成功
|
||||||
|
[2026-01-11 18:12:40] INFO - 用户请求发送验证码
|
||||||
|
[2026-01-11 18:12:42] WARNING - 验证码发送失败: 非法参数
|
||||||
|
[2026-01-11 18:13:42] INFO - 用户请求发送验证码
|
||||||
|
[2026-01-11 18:13:44] INFO - 验证码发送成功
|
||||||
|
[2026-01-11 18:14:18] INFO - 用户注册请求
|
||||||
|
[2026-01-11 18:14:19] WARNING - 注册失败: 验证码错误
|
||||||
|
[2026-01-11 18:16:05] INFO - 用户注册请求
|
||||||
|
[2026-01-11 18:16:07] WARNING - 注册失败: 验证码错误
|
||||||
|
[2026-01-11 18:17:34] INFO - 用户注册请求
|
||||||
|
[2026-01-11 18:17:35] WARNING - 注册失败: 验证码错误
|
||||||
|
[2026-01-11 18:20:57] INFO - 用户请求发送验证码
|
||||||
|
[2026-01-11 18:20:59] INFO - 验证码发送成功
|
||||||
|
[2026-01-11 18:21:11] INFO - 用户注册请求
|
||||||
|
[2026-01-11 18:21:12] INFO - 用户注册成功
|
||||||
|
[2026-01-11 18:21:14] INFO - 用户登录尝试
|
||||||
|
[2026-01-11 18:21:14] INFO - 用户登录成功
|
||||||
|
[2026-01-11 18:33:40] INFO - 用户登录尝试
|
||||||
|
[2026-01-11 18:33:40] WARNING - 登录失败: 手机号或密码错误
|
||||||
|
[2026-01-11 18:33:47] INFO - 用户登录尝试
|
||||||
|
[2026-01-11 18:33:47] INFO - 用户登录成功
|
||||||
|
[2026-01-11 18:34:19] INFO - 用户登录尝试
|
||||||
|
[2026-01-11 18:34:19] INFO - 用户登录成功
|
||||||
|
[2026-01-11 19:05:37] INFO - 用户登录尝试
|
||||||
|
[2026-01-11 19:05:37] INFO - 用户登录成功
|
||||||
|
[2026-01-11 19:14:10] INFO - 用户登录尝试
|
||||||
|
[2026-01-11 19:14:10] INFO - 用户登录成功
|
||||||
|
[2026-01-11 21:51:06] INFO - 用户登录尝试
|
||||||
|
[2026-01-11 21:51:06] INFO - 用户登录成功
|
||||||
|
[2026-01-11 21:59:14] INFO - 试用模式生成
|
||||||
|
[2026-01-11 21:59:14] INFO - 用户发起图片生成
|
||||||
|
[2026-01-11 22:09:52] INFO - 积分预扣除 (试用模式)
|
||||||
|
[2026-01-11 22:10:10] INFO - 用户生成图片成功
|
||||||
|
[2026-01-11 23:41:00] INFO - 用户登录尝试
|
||||||
|
[2026-01-11 23:41:23] INFO - 用户登录尝试
|
||||||
|
[2026-01-11 23:41:23] INFO - 用户登录成功
|
||||||
BIN
middlewares/__pycache__/auth.cpython-312.pyc
Normal file
BIN
middlewares/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
41
middlewares/auth.py
Normal file
41
middlewares/auth.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from functools import wraps
|
||||||
|
from flask import session, jsonify, redirect, url_for, request
|
||||||
|
from models import User
|
||||||
|
|
||||||
|
def login_required(f):
|
||||||
|
"""登录验证装饰器"""
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
user_id = session.get('user_id')
|
||||||
|
if not user_id:
|
||||||
|
if request.path.startswith('/api/'):
|
||||||
|
return jsonify({"error": "请先登录", "code": 401}), 401
|
||||||
|
# 记录当前路径以便登录后跳转
|
||||||
|
return redirect(url_for('auth.login_page', next=request.path))
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
def permission_required(perm_name):
|
||||||
|
"""动态权限验证装饰器"""
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
user_id = session.get('user_id')
|
||||||
|
if not user_id:
|
||||||
|
if request.path.startswith('/api/'):
|
||||||
|
return jsonify({"error": "请先登录", "code": 401}), 401
|
||||||
|
return redirect(url_for('auth.login_page', next=request.path))
|
||||||
|
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
if not user or not user.has_permission(perm_name):
|
||||||
|
if request.path.startswith('/api/'):
|
||||||
|
return jsonify({"error": f"需要权限: {perm_name}", "code": 403}), 403
|
||||||
|
# 如果没有权限,重定向到首页并提示
|
||||||
|
return redirect(url_for('index', error="权限不足"))
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def admin_required(f):
|
||||||
|
"""兼容旧版的管理员验证(内部调用 manage_system 权限)"""
|
||||||
|
return permission_required('manage_system')(f)
|
||||||
13
migrate_api_key.py
Normal file
13
migrate_api_key.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from app import app
|
||||||
|
from extensions import db
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
try:
|
||||||
|
# 尝试添加 api_key 字段到 users 表
|
||||||
|
db.session.execute(text('ALTER TABLE users ADD COLUMN IF NOT EXISTS api_key VARCHAR(255)'))
|
||||||
|
db.session.commit()
|
||||||
|
print("✅ 数据库字段 users.api_key 同步成功")
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"❌ 同步失败: {e}")
|
||||||
23
migrate_db.py
Normal file
23
migrate_db.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from app import app
|
||||||
|
from extensions import db
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
def migrate():
|
||||||
|
with app.app_context():
|
||||||
|
print("🔧 正在为 users 表增加 role 字段...")
|
||||||
|
try:
|
||||||
|
# 使用原生 SQL 增加字段
|
||||||
|
db.session.execute(text('ALTER TABLE users ADD COLUMN IF NOT EXISTS role VARCHAR(20) DEFAULT \'user\''))
|
||||||
|
db.session.commit()
|
||||||
|
print("✅ 字段增加成功")
|
||||||
|
|
||||||
|
# 设置管理员(可选,方便您测试)
|
||||||
|
# db.session.execute(text("UPDATE users SET role = 'admin' WHERE phone = '您的手机号'"))
|
||||||
|
# db.session.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 迁移失败: {e}")
|
||||||
|
db.session.rollback()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
migrate()
|
||||||
29
migrate_rbac.py
Normal file
29
migrate_rbac.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from app import app
|
||||||
|
from extensions import db
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
def migrate():
|
||||||
|
with app.app_context():
|
||||||
|
print("🔧 正在平滑迁移至 RBAC 体系...")
|
||||||
|
try:
|
||||||
|
# 1. 创建新表
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
# 2. 修改 users 表结构
|
||||||
|
# 增加 role_id
|
||||||
|
db.session.execute(text('ALTER TABLE users ADD COLUMN IF NOT EXISTS role_id INTEGER REFERENCES roles(id)'))
|
||||||
|
|
||||||
|
# 3. 尝试迁移旧数据:如果旧的 role 字段存在且值为 'admin',则关联超级管理员角色
|
||||||
|
# 我们先执行初始化脚本创建角色
|
||||||
|
from init_rbac import init_rbac
|
||||||
|
init_rbac()
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print("✅ 数据库结构迁移成功")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 迁移失败: {e}")
|
||||||
|
db.session.rollback()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
migrate()
|
||||||
84
models.py
Normal file
84
models.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
from extensions import db
|
||||||
|
from datetime import datetime
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
|
||||||
|
# 角色与权限的多对多关联表
|
||||||
|
role_permissions = db.Table('role_permissions',
|
||||||
|
db.Column('role_id', db.Integer, db.ForeignKey('roles.id'), primary_key=True),
|
||||||
|
db.Column('permission_id', db.Integer, db.ForeignKey('permissions.id'), primary_key=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
class Permission(db.Model):
|
||||||
|
__tablename__ = 'permissions'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String(50), unique=True, nullable=False) # 如: 'view_logs', 'manage_users'
|
||||||
|
description = db.Column(db.String(100))
|
||||||
|
|
||||||
|
class Role(db.Model):
|
||||||
|
__tablename__ = 'roles'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String(50), unique=True, nullable=False) # 如: '超级管理员', '普通用户'
|
||||||
|
description = db.Column(db.String(100))
|
||||||
|
# 角色拥有的权限
|
||||||
|
permissions = db.relationship('Permission', secondary=role_permissions, backref=db.backref('roles', lazy='dynamic'))
|
||||||
|
|
||||||
|
class User(db.Model):
|
||||||
|
__tablename__ = 'users'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
phone = db.Column(db.String(20), unique=True, nullable=False)
|
||||||
|
password_hash = db.Column(db.String(255), nullable=False)
|
||||||
|
api_key = db.Column(db.String(255)) # 存储用户的 API Key
|
||||||
|
points = db.Column(db.Integer, default=2) # 账户积分,默认赠送2次试用
|
||||||
|
# 关联角色 ID
|
||||||
|
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
# 关系映射
|
||||||
|
role = db.relationship('Role', backref=db.backref('users', lazy='dynamic'))
|
||||||
|
|
||||||
|
def has_permission(self, perm_name):
|
||||||
|
"""动态检查用户是否拥有某项权限"""
|
||||||
|
if not self.role:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 获取用户拥有的所有权限名称
|
||||||
|
perms = [p.name for p in self.role.permissions]
|
||||||
|
|
||||||
|
# 核心修复:如果是超级管理员(拥有 manage_system),则豁免所有具体权限检查
|
||||||
|
if 'manage_system' in perms:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return perm_name in perms
|
||||||
|
|
||||||
|
def set_password(self, password):
|
||||||
|
self.password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
|
class SystemDict(db.Model):
|
||||||
|
"""通用字典管理系统"""
|
||||||
|
__tablename__ = 'system_dicts'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
dict_type = db.Column(db.String(50), nullable=False) # 如: 'ai_model', 'aspect_ratio', 'prompt_tpl'
|
||||||
|
label = db.Column(db.String(100), nullable=False) # 显示名称
|
||||||
|
value = db.Column(db.Text, nullable=False) # 存储值或提示词内容
|
||||||
|
cost = db.Column(db.Integer, default=0) # 消耗积分 (仅针对 ai_model 有效)
|
||||||
|
is_active = db.Column(db.Boolean, default=True)
|
||||||
|
sort_order = db.Column(db.Integer, default=0) # 排序权重
|
||||||
|
|
||||||
|
class GenerationRecord(db.Model):
|
||||||
|
"""AI 生成记录"""
|
||||||
|
__tablename__ = 'generation_records'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
|
prompt = db.Column(db.Text)
|
||||||
|
model = db.Column(db.String(100))
|
||||||
|
# 存储生成的图片 URL 列表 (JSON 字符串)
|
||||||
|
image_urls = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
user = db.relationship('User', backref=db.backref('records', lazy='dynamic', order_by='GenerationRecord.created_at.desc()'))
|
||||||
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
BIN
services/__pycache__/logger.cpython-312.pyc
Normal file
BIN
services/__pycache__/logger.cpython-312.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/sms_service.cpython-312.pyc
Normal file
BIN
services/__pycache__/sms_service.cpython-312.pyc
Normal file
Binary file not shown.
73
services/logger.py
Normal file
73
services/logger.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
from extensions import redis_client
|
||||||
|
import json
|
||||||
|
|
||||||
|
# 创建日志目录
|
||||||
|
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs')
|
||||||
|
os.makedirs(LOG_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
class SystemLogger:
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = logging.getLogger('vision_ai')
|
||||||
|
self.logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# 文件处理器 (自动轮转,最大10MB,保留5个备份)
|
||||||
|
file_handler = RotatingFileHandler(
|
||||||
|
os.path.join(LOG_DIR, 'system.log'),
|
||||||
|
maxBytes=10*1024*1024,
|
||||||
|
backupCount=5,
|
||||||
|
encoding='utf-8'
|
||||||
|
)
|
||||||
|
file_handler.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# 控制台处理器
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# 格式化
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
'[%(asctime)s] %(levelname)s - %(message)s',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
|
)
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
self.logger.addHandler(file_handler)
|
||||||
|
self.logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
def _push_to_redis(self, level, message, extra=None):
|
||||||
|
"""同时推送到 Redis 用于前端实时查看"""
|
||||||
|
try:
|
||||||
|
log_entry = {
|
||||||
|
"time": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
"level": level,
|
||||||
|
"message": message,
|
||||||
|
"extra": extra or {}
|
||||||
|
}
|
||||||
|
# 保留最近 2000 条日志
|
||||||
|
redis_client.lpush('system_logs', json.dumps(log_entry, ensure_ascii=False))
|
||||||
|
redis_client.ltrim('system_logs', 0, 1999)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def info(self, message, **kwargs):
|
||||||
|
self.logger.info(message)
|
||||||
|
self._push_to_redis('INFO', message, kwargs)
|
||||||
|
|
||||||
|
def warning(self, message, **kwargs):
|
||||||
|
self.logger.warning(message)
|
||||||
|
self._push_to_redis('WARNING', message, kwargs)
|
||||||
|
|
||||||
|
def error(self, message, **kwargs):
|
||||||
|
self.logger.error(message)
|
||||||
|
self._push_to_redis('ERROR', message, kwargs)
|
||||||
|
|
||||||
|
def debug(self, message, **kwargs):
|
||||||
|
self.logger.debug(message)
|
||||||
|
self._push_to_redis('DEBUG', message, kwargs)
|
||||||
|
|
||||||
|
# 全局日志实例
|
||||||
|
system_logger = SystemLogger()
|
||||||
131
services/sms_service.py
Normal file
131
services/sms_service.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import random
|
||||||
|
import json
|
||||||
|
from extensions import redis_client
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
# 阿里云号码认证服务 SDK
|
||||||
|
try:
|
||||||
|
from alibabacloud_dypnsapi20170525.client import Client as Dypnsapi20170525Client
|
||||||
|
from alibabacloud_tea_openapi import models as open_api_models
|
||||||
|
from alibabacloud_dypnsapi20170525 import models as dypnsapi_20170525_models
|
||||||
|
from alibabacloud_tea_util import models as util_models
|
||||||
|
ALIYUN_SDK_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
ALIYUN_SDK_AVAILABLE = False
|
||||||
|
print("⚠️ 阿里云号码认证服务 SDK 未安装,请运行: pip install alibabacloud_dypnsapi20170525")
|
||||||
|
|
||||||
|
class SMSService:
|
||||||
|
@staticmethod
|
||||||
|
def _create_client():
|
||||||
|
"""创建号码认证服务客户端"""
|
||||||
|
config = open_api_models.Config(
|
||||||
|
access_key_id=Config.ALIBABA_CLOUD_ACCESS_KEY_ID,
|
||||||
|
access_key_secret=Config.ALIBABA_CLOUD_ACCESS_KEY_SECRET
|
||||||
|
)
|
||||||
|
config.endpoint = 'dypnsapi.aliyuncs.com'
|
||||||
|
return Dypnsapi20170525Client(config)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def send_code(phone):
|
||||||
|
# 检查是否在冷却期内
|
||||||
|
cooldown_key = f"sms_cooldown:{phone}"
|
||||||
|
if redis_client.exists(cooldown_key):
|
||||||
|
ttl = redis_client.ttl(cooldown_key)
|
||||||
|
return False, f"请{ttl}秒后再试"
|
||||||
|
|
||||||
|
# 开发模式:使用固定验证码
|
||||||
|
if Config.DEV_MODE:
|
||||||
|
code = Config.DEV_SMS_CODE
|
||||||
|
redis_client.set(f"sms_code:{phone}", code, ex=1800)
|
||||||
|
redis_client.set(cooldown_key, "1", ex=60)
|
||||||
|
print(f"🟡 [开发模式] 验证码: {code} (手机: {phone})")
|
||||||
|
return True, "验证码已发送(开发模式)"
|
||||||
|
|
||||||
|
# 生产模式:真实发送
|
||||||
|
# 调用阿里云号码认证服务(自动生成验证码)
|
||||||
|
success, biz_id = SMSService._call_aliyun(phone, None)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
# 发送成功后存储 BizId 用于校验
|
||||||
|
redis_client.set(f"sms_biz_id:{phone}", biz_id, ex=1800)
|
||||||
|
redis_client.set(cooldown_key, "1", ex=60)
|
||||||
|
print(f"✅ 短信已发送至 {phone}, BizId: {biz_id}")
|
||||||
|
return True, "发送成功"
|
||||||
|
else:
|
||||||
|
print(f"❌ 短信发送失败: {biz_id}") # biz_id 此时是错误信息
|
||||||
|
return False, biz_id
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def verify_code(phone, code):
|
||||||
|
"""校验验证码 - 统一使用本地 Redis 校验"""
|
||||||
|
# 从 Redis 获取验证码
|
||||||
|
cached_code = redis_client.get(f"sms_code:{phone}")
|
||||||
|
|
||||||
|
if not cached_code:
|
||||||
|
print(f"⚠️ 未找到验证码,手机号: {phone}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 比对验证码
|
||||||
|
stored_code = cached_code.decode('utf-8') if isinstance(cached_code, bytes) else str(cached_code)
|
||||||
|
input_code = str(code)
|
||||||
|
|
||||||
|
print(f"🔍 校验验证码: 输入={input_code}, 存储={stored_code}")
|
||||||
|
|
||||||
|
if stored_code == input_code:
|
||||||
|
# 校验成功,删除验证码(一次性使用)
|
||||||
|
redis_client.delete(f"sms_code:{phone}")
|
||||||
|
redis_client.delete(f"sms_biz_id:{phone}") # 也删除 BizId
|
||||||
|
print(f"✅ 验证码校验成功")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"❌ 验证码不匹配")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _call_aliyun(phone, code):
|
||||||
|
"""调用阿里云号码认证服务发送验证码"""
|
||||||
|
if not ALIYUN_SDK_AVAILABLE:
|
||||||
|
return False, "SDK未安装"
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = SMSService._create_client()
|
||||||
|
|
||||||
|
# 使用 SendSmsVerifyCode 接口
|
||||||
|
request = dypnsapi_20170525_models.SendSmsVerifyCodeRequest(
|
||||||
|
phone_number=phone,
|
||||||
|
sign_name=Config.SMS_SIGN_NAME,
|
||||||
|
template_code=Config.SMS_TEMPLATE_CODE,
|
||||||
|
# 模板需要 code 和 min 两个参数
|
||||||
|
template_param='{"code":"##code##","min":"30"}',
|
||||||
|
code_type=1, # 1=纯数字
|
||||||
|
code_length=6, # 6位验证码
|
||||||
|
interval=60, # 60秒重发间隔
|
||||||
|
valid_time=1800, # 30分钟有效期(秒)
|
||||||
|
return_verify_code=True # 返回验证码用于本地校验
|
||||||
|
)
|
||||||
|
|
||||||
|
runtime = util_models.RuntimeOptions()
|
||||||
|
resp = client.send_sms_verify_code_with_options(request, runtime)
|
||||||
|
|
||||||
|
if resp.body.code == "OK":
|
||||||
|
# 获取阿里云生成的验证码
|
||||||
|
verify_code = resp.body.model.verify_code if hasattr(resp.body.model, 'verify_code') else None
|
||||||
|
biz_id = resp.body.model.biz_id
|
||||||
|
|
||||||
|
if verify_code:
|
||||||
|
# 将验证码存储到 Redis 用于本地校验
|
||||||
|
redis_client.set(f"sms_code:{phone}", verify_code, ex=1800)
|
||||||
|
print(f"✅ 短信已发送至 {phone}, 验证码: {verify_code}, BizId: {biz_id}")
|
||||||
|
else:
|
||||||
|
# 如果没有返回验证码,只存储 BizId
|
||||||
|
redis_client.set(f"sms_biz_id:{phone}", biz_id, ex=1800)
|
||||||
|
print(f"✅ 短信已发送至 {phone}, BizId: {biz_id}")
|
||||||
|
|
||||||
|
return True, biz_id
|
||||||
|
else:
|
||||||
|
return False, resp.body.message
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
error_detail = traceback.format_exc()
|
||||||
|
print(f"❌ 详细错误信息:\n{error_detail}")
|
||||||
|
return False, str(e)
|
||||||
130
static/css/style.css
Normal file
130
static/css/style.css
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
:root {
|
||||||
|
--primary: #6366f1;
|
||||||
|
--primary-soft: rgba(99, 102, 241, 0.08);
|
||||||
|
--surface: #ffffff;
|
||||||
|
--background: #fcfdfe;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Plus Jakarta Sans', 'Noto Sans SC', sans-serif;
|
||||||
|
background-color: var(--background);
|
||||||
|
color: #1e293b;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 布局装饰 */
|
||||||
|
.bg-mesh {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: -1;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.03) 0px, transparent 50%),
|
||||||
|
radial-gradient(at 100% 100%, rgba(139, 92, 246, 0.03) 0px, transparent 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 侧边栏玻璃拟态 */
|
||||||
|
.glass-sidebar {
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
backdrop-filter: blur(20px) saturate(180%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(15, 23, 42, 0.6);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
background: white;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
padding: 2.5rem;
|
||||||
|
border-radius: 2.5rem;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 10px; }
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-frame {
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 3rem;
|
||||||
|
box-shadow: 0 20px 50px -10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 现代化通知系统 */
|
||||||
|
.toast-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 2rem;
|
||||||
|
right: 2rem;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
background: white;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 10px 40px -10px rgba(0,0,0,0.2);
|
||||||
|
border-left: 4px solid #6366f1;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 400px;
|
||||||
|
pointer-events: auto;
|
||||||
|
animation: slideInRight 0.3s ease-out;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success { border-left-color: #10b981; }
|
||||||
|
.toast.error { border-left-color: #ef4444; }
|
||||||
|
.toast.warning { border-left-color: #f59e0b; }
|
||||||
|
.toast.info { border-left-color: #3b82f6; }
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(400px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOutRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(400px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
74
static/js/auth.js
Normal file
74
static/js/auth.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
let isRegisterMode = false;
|
||||||
|
|
||||||
|
document.getElementById('authSwitchBtn').onclick = () => {
|
||||||
|
isRegisterMode = !isRegisterMode;
|
||||||
|
document.getElementById('authTitle').innerText = isRegisterMode ? "加入视界 AI" : "欢迎回来";
|
||||||
|
document.getElementById('authSub').innerText = isRegisterMode ? "注册并开启创作" : "请登录以开启 AI 创作之旅";
|
||||||
|
document.getElementById('authSubmitBtn').querySelector('span').innerText = isRegisterMode ? "立即注册" : "立即登录";
|
||||||
|
document.getElementById('authSwitchBtn').innerText = isRegisterMode ? "已有账号?返回登录" : "没有账号?立即注册";
|
||||||
|
document.getElementById('smsGroup').classList.toggle('hidden', !isRegisterMode);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('sendSmsBtn').onclick = async () => {
|
||||||
|
const phone = document.getElementById('authPhone').value;
|
||||||
|
const btn = document.getElementById('sendSmsBtn');
|
||||||
|
|
||||||
|
if(!phone) return showToast('请输入手机号', 'warning');
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
const originalText = btn.innerText;
|
||||||
|
|
||||||
|
const r = await fetch('/api/auth/send_code', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ phone })
|
||||||
|
});
|
||||||
|
const d = await r.json();
|
||||||
|
|
||||||
|
if(d.error) {
|
||||||
|
showToast(d.error, 'error');
|
||||||
|
btn.disabled = false;
|
||||||
|
} else {
|
||||||
|
showToast(d.message, 'success');
|
||||||
|
let countdown = 60;
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
btn.innerText = `${countdown}秒后重试`;
|
||||||
|
countdown--;
|
||||||
|
if(countdown < 0) {
|
||||||
|
clearInterval(timer);
|
||||||
|
btn.innerText = originalText;
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('authSubmitBtn').onclick = async () => {
|
||||||
|
const phone = document.getElementById('authPhone').value;
|
||||||
|
const password = document.getElementById('authPass').value;
|
||||||
|
const code = document.getElementById('authCode').value;
|
||||||
|
|
||||||
|
const url = isRegisterMode ? '/api/auth/register' : '/api/auth/login';
|
||||||
|
const body = isRegisterMode ? { phone, password, code } : { phone, password };
|
||||||
|
|
||||||
|
const r = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
const d = await r.json();
|
||||||
|
if(d.error) {
|
||||||
|
showToast(d.error, 'error');
|
||||||
|
} else {
|
||||||
|
showToast(d.message, 'success');
|
||||||
|
if(isRegisterMode) {
|
||||||
|
isRegisterMode = true;
|
||||||
|
document.getElementById('authSwitchBtn').click();
|
||||||
|
} else {
|
||||||
|
// 获取来源页面路径
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const nextUrl = urlParams.get('next') || '/';
|
||||||
|
window.location.href = nextUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
472
static/js/main.js
Normal file
472
static/js/main.js
Normal file
@ -0,0 +1,472 @@
|
|||||||
|
lucide.createIcons();
|
||||||
|
|
||||||
|
async function checkAuth() {
|
||||||
|
const r = await fetch('/api/auth/me');
|
||||||
|
const d = await r.json();
|
||||||
|
const profile = document.getElementById('userProfile');
|
||||||
|
const entry = document.getElementById('loginEntryBtn');
|
||||||
|
const loginHint = document.getElementById('loginHint');
|
||||||
|
const submitBtn = document.getElementById('submitBtn');
|
||||||
|
|
||||||
|
if(d.logged_in) {
|
||||||
|
if(profile) profile.classList.remove('hidden');
|
||||||
|
if(entry) entry.classList.add('hidden');
|
||||||
|
if(loginHint) loginHint.classList.add('hidden');
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||||
|
const phoneDisp = document.getElementById('userPhoneDisplay');
|
||||||
|
if(phoneDisp) phoneDisp.innerText = d.phone;
|
||||||
|
|
||||||
|
// 处理积分显示
|
||||||
|
const pointsBadge = document.getElementById('pointsBadge');
|
||||||
|
const pointsDisplay = document.getElementById('pointsDisplay');
|
||||||
|
if(pointsBadge && pointsDisplay) {
|
||||||
|
pointsBadge.classList.remove('hidden');
|
||||||
|
pointsDisplay.innerText = d.points;
|
||||||
|
}
|
||||||
|
const headerPoints = document.getElementById('headerPoints');
|
||||||
|
if(headerPoints) headerPoints.innerText = d.points;
|
||||||
|
|
||||||
|
// 如果用户已经有绑定的 Key,且当前没手动输入,则默认切到 Key 模式
|
||||||
|
if(d.api_key) {
|
||||||
|
switchMode('key');
|
||||||
|
const keyInput = document.getElementById('apiKey');
|
||||||
|
if(keyInput && !keyInput.value) keyInput.value = d.api_key;
|
||||||
|
} else {
|
||||||
|
switchMode('trial');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(profile) profile.classList.add('hidden');
|
||||||
|
if(entry) entry.classList.remove('hidden');
|
||||||
|
if(loginHint) loginHint.classList.remove('hidden');
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||||
|
}
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除 redundant logout 监听,因为 base.html 已处理全局登出
|
||||||
|
|
||||||
|
// 历史记录分页状态
|
||||||
|
let currentHistoryPage = 1;
|
||||||
|
let hasMoreHistory = true;
|
||||||
|
let isHistoryLoading = false;
|
||||||
|
|
||||||
|
// 存储当前生成的所有图片 URL
|
||||||
|
let currentGeneratedUrls = [];
|
||||||
|
let currentMode = 'trial'; // 'trial' 或 'key'
|
||||||
|
|
||||||
|
function switchMode(mode) {
|
||||||
|
currentMode = mode;
|
||||||
|
const trialBtn = document.getElementById('modeTrialBtn');
|
||||||
|
const keyBtn = document.getElementById('modeKeyBtn');
|
||||||
|
const keyInputGroup = document.getElementById('keyInputGroup');
|
||||||
|
const premiumToggle = document.getElementById('premiumToggle');
|
||||||
|
|
||||||
|
if(mode === 'trial') {
|
||||||
|
trialBtn.classList.add('border-indigo-500', 'bg-indigo-50', 'text-indigo-600', 'border-2');
|
||||||
|
trialBtn.classList.remove('border-slate-200', 'text-slate-400');
|
||||||
|
keyBtn.classList.remove('border-indigo-500', 'bg-indigo-50', 'text-indigo-600', 'border-2');
|
||||||
|
keyBtn.classList.add('border-slate-200', 'text-slate-400');
|
||||||
|
keyInputGroup.classList.add('hidden');
|
||||||
|
if(premiumToggle) premiumToggle.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
keyBtn.classList.add('border-indigo-500', 'bg-indigo-50', 'text-indigo-600', 'border-2');
|
||||||
|
keyBtn.classList.remove('border-slate-200', 'text-slate-400');
|
||||||
|
trialBtn.classList.remove('border-indigo-500', 'bg-indigo-50', 'text-indigo-600', 'border-2');
|
||||||
|
trialBtn.classList.add('border-slate-200', 'text-slate-400');
|
||||||
|
keyInputGroup.classList.remove('hidden');
|
||||||
|
if(premiumToggle) premiumToggle.classList.add('hidden');
|
||||||
|
}
|
||||||
|
updateCostPreview(); // 切换模式时同步计费预览
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadImage(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const blobUrl = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = blobUrl;
|
||||||
|
// 从 URL 提取文件名
|
||||||
|
const filename = url.split('/').pop().split('?')[0] || 'ai-vision-image.png';
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
window.URL.revokeObjectURL(blobUrl);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('下载失败:', e);
|
||||||
|
showToast('下载失败,请尝试右键保存', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadHistory(isLoadMore = false) {
|
||||||
|
if (isHistoryLoading || (!hasMoreHistory && isLoadMore)) return;
|
||||||
|
|
||||||
|
isHistoryLoading = true;
|
||||||
|
if (!isLoadMore) {
|
||||||
|
currentHistoryPage = 1;
|
||||||
|
document.getElementById('historyList').innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const footer = document.getElementById('historyFooter');
|
||||||
|
footer.classList.remove('hidden');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/history?page=${currentHistoryPage}&per_page=10`);
|
||||||
|
const d = await r.json();
|
||||||
|
|
||||||
|
const list = document.getElementById('historyList');
|
||||||
|
|
||||||
|
if (d.history && d.history.length > 0) {
|
||||||
|
const html = d.history.map(item => `
|
||||||
|
<div class="bg-white border border-slate-100 rounded-2xl p-4 space-y-3 hover:border-indigo-100 transition-all shadow-sm group">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-[10px] font-black text-slate-400 bg-slate-50 px-2 py-0.5 rounded-md uppercase tracking-widest">${item.time}</span>
|
||||||
|
<span class="text-[10px] font-bold text-indigo-500">${item.model}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-[11px] text-slate-600 line-clamp-2 leading-relaxed">${item.prompt}</p>
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
${item.urls.map(url => `
|
||||||
|
<div class="aspect-square rounded-lg overflow-hidden border border-slate-100 cursor-pointer transition-transform hover:scale-105" onclick="window.open('${url}')">
|
||||||
|
<img src="${url}" class="w-full h-full object-cover" loading="lazy">
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
if (isLoadMore) {
|
||||||
|
list.insertAdjacentHTML('beforeend', html);
|
||||||
|
} else {
|
||||||
|
list.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMoreHistory = d.has_next;
|
||||||
|
currentHistoryPage++;
|
||||||
|
} else if (!isLoadMore) {
|
||||||
|
list.innerHTML = `<div class="flex flex-col items-center justify-center h-64 text-slate-300">
|
||||||
|
<i data-lucide="inbox" class="w-12 h-12 mb-4"></i>
|
||||||
|
<span class="text-xs font-bold">暂无生成记录</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
lucide.createIcons();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载历史失败:', e);
|
||||||
|
if (!isLoadMore) {
|
||||||
|
document.getElementById('historyList').innerHTML = `<div class="text-center text-rose-400 text-xs font-bold py-10">加载失败: ${e.message}</div>`;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isHistoryLoading = false;
|
||||||
|
footer.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
checkAuth();
|
||||||
|
|
||||||
|
// 模式切换监听
|
||||||
|
const modeTrialBtn = document.getElementById('modeTrialBtn');
|
||||||
|
const modeKeyBtn = document.getElementById('modeKeyBtn');
|
||||||
|
const isPremiumCheckbox = document.getElementById('isPremium');
|
||||||
|
if(modeTrialBtn) modeTrialBtn.onclick = () => switchMode('trial');
|
||||||
|
if(modeKeyBtn) modeKeyBtn.onclick = () => switchMode('key');
|
||||||
|
if(isPremiumCheckbox) isPremiumCheckbox.onchange = () => updateCostPreview();
|
||||||
|
|
||||||
|
// 历史记录控制
|
||||||
|
const historyDrawer = document.getElementById('historyDrawer');
|
||||||
|
const showHistoryBtn = document.getElementById('showHistoryBtn');
|
||||||
|
const closeHistoryBtn = document.getElementById('closeHistoryBtn');
|
||||||
|
const historyList = document.getElementById('historyList');
|
||||||
|
|
||||||
|
if(showHistoryBtn) {
|
||||||
|
showHistoryBtn.onclick = () => {
|
||||||
|
historyDrawer.classList.remove('translate-x-full');
|
||||||
|
loadHistory(false);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if(closeHistoryBtn) {
|
||||||
|
closeHistoryBtn.onclick = () => {
|
||||||
|
historyDrawer.classList.add('translate-x-full');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 瀑布流滚动加载
|
||||||
|
if (historyList) {
|
||||||
|
historyList.onscroll = () => {
|
||||||
|
const threshold = 100;
|
||||||
|
if (historyList.scrollTop + historyList.clientHeight >= historyList.scrollHeight - threshold) {
|
||||||
|
loadHistory(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全部下载按钮逻辑
|
||||||
|
const downloadAllBtn = document.getElementById('downloadAllBtn');
|
||||||
|
if(downloadAllBtn) {
|
||||||
|
downloadAllBtn.onclick = async () => {
|
||||||
|
if(currentGeneratedUrls.length === 0) return;
|
||||||
|
showToast(`正在准备下载 ${currentGeneratedUrls.length} 张作品...`, 'info');
|
||||||
|
for(const url of currentGeneratedUrls) {
|
||||||
|
await downloadImage(url);
|
||||||
|
// 稍微延迟一下,防止浏览器拦截
|
||||||
|
await new Promise(r => setTimeout(r, 300));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新生成按钮逻辑
|
||||||
|
const regenBtn = document.getElementById('regenBtn');
|
||||||
|
if(regenBtn) {
|
||||||
|
regenBtn.onclick = () => {
|
||||||
|
const submitBtn = document.getElementById('submitBtn');
|
||||||
|
if(submitBtn) submitBtn.click();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有来自 URL 的错误提示
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
if(urlParams.has('error')) {
|
||||||
|
showToast(urlParams.get('error'), 'error');
|
||||||
|
// 清理 URL 参数以防刷新时重复提示
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/config');
|
||||||
|
const d = await r.json();
|
||||||
|
fillSelect('modelSelect', d.models);
|
||||||
|
fillSelect('ratioSelect', d.ratios);
|
||||||
|
fillSelect('sizeSelect', d.sizes);
|
||||||
|
fillSelect('promptTpl', [{label:'✨ 自定义创作', value:'manual'}, ...d.prompts]);
|
||||||
|
updateCostPreview(); // 初始化时显示默认模型的积分
|
||||||
|
} catch(e) { console.error(e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillSelect(id, list) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if(!el) return;
|
||||||
|
// 如果是模型选择,增加积分显示
|
||||||
|
if(id === 'modelSelect') {
|
||||||
|
el.innerHTML = list.map(i => `<option value="${i.value}" data-cost="${i.cost || 0}">${i.label} (${i.cost || 0}积分)</option>`).join('');
|
||||||
|
} else {
|
||||||
|
el.innerHTML = list.map(i => `<option value="${i.value}">${i.label}</option>`).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新计费预览显示
|
||||||
|
function updateCostPreview() {
|
||||||
|
const modelSelect = document.getElementById('modelSelect');
|
||||||
|
const costPreview = document.getElementById('costPreview');
|
||||||
|
const isPremium = document.getElementById('isPremium')?.checked || false;
|
||||||
|
const selectedOption = modelSelect.options[modelSelect.selectedIndex];
|
||||||
|
|
||||||
|
if (currentMode === 'trial' && selectedOption) {
|
||||||
|
let cost = parseInt(selectedOption.getAttribute('data-cost') || 0);
|
||||||
|
if(isPremium) cost *= 2; // 优质模式 2 倍积分
|
||||||
|
costPreview.innerText = `本次生成将消耗 ${cost} 积分`;
|
||||||
|
costPreview.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
costPreview.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('fileInput').onchange = (e) => {
|
||||||
|
const prev = document.getElementById('imagePreview');
|
||||||
|
prev.innerHTML = '';
|
||||||
|
Array.from(e.target.files).forEach(file => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (ev) => {
|
||||||
|
const d = document.createElement('div');
|
||||||
|
d.className = 'w-20 h-20 rounded-2xl overflow-hidden flex-shrink-0 border-2 border-white shadow-md';
|
||||||
|
d.innerHTML = `<img src="${ev.target.result}" class="w-full h-full object-cover">`;
|
||||||
|
prev.appendChild(d);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('modelSelect').onchange = (e) => {
|
||||||
|
document.getElementById('sizeGroup').classList.toggle('hidden', e.target.value !== 'nano-banana-2');
|
||||||
|
updateCostPreview(); // 切换模型时更新计费预览
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('promptTpl').onchange = (e) => {
|
||||||
|
const area = document.getElementById('manualPrompt');
|
||||||
|
if(e.target.value !== 'manual') {
|
||||||
|
area.value = e.target.value;
|
||||||
|
area.readOnly = true;
|
||||||
|
} else {
|
||||||
|
area.value = '';
|
||||||
|
area.readOnly = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('submitBtn').onclick = async () => {
|
||||||
|
const btn = document.getElementById('submitBtn');
|
||||||
|
const files = document.getElementById('fileInput').files;
|
||||||
|
const apiKey = document.getElementById('apiKey').value;
|
||||||
|
const num = parseInt(document.getElementById('numSelect').value);
|
||||||
|
|
||||||
|
// 检查登录状态并获取积分
|
||||||
|
const authCheck = await fetch('/api/auth/me');
|
||||||
|
const authData = await authCheck.json();
|
||||||
|
if(!authData.logged_in) {
|
||||||
|
showToast('请先登录后再生成作品', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据模式验证
|
||||||
|
if(currentMode === 'key') {
|
||||||
|
if(!apiKey) return showToast('请输入您的 API 密钥', 'warning');
|
||||||
|
} else {
|
||||||
|
if(authData.points <= 0) return showToast('可用积分已耗尽,请充值或切换至自定义 Key 模式', 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(files.length === 0) return showToast('请上传照片', 'warning');
|
||||||
|
|
||||||
|
// UI 锁定
|
||||||
|
btn.disabled = true;
|
||||||
|
const btnText = btn.querySelector('span');
|
||||||
|
btnText.innerText = "正在同步参考图...";
|
||||||
|
|
||||||
|
document.getElementById('statusInfo').classList.remove('hidden');
|
||||||
|
document.getElementById('placeholder').classList.add('hidden');
|
||||||
|
document.getElementById('finalWrapper').classList.remove('hidden');
|
||||||
|
const grid = document.getElementById('imageGrid');
|
||||||
|
grid.innerHTML = ''; // 清空
|
||||||
|
currentGeneratedUrls = []; // 重置当前生成列表
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 先上传参考图获取持久化 URL
|
||||||
|
const uploadData = new FormData();
|
||||||
|
for(let f of files) uploadData.append('images', f);
|
||||||
|
const upR = await fetch('/api/upload', { method: 'POST', body: uploadData });
|
||||||
|
const upRes = await upR.json();
|
||||||
|
if(upRes.error) throw new Error(upRes.error);
|
||||||
|
const image_urls = upRes.urls;
|
||||||
|
|
||||||
|
// 2. 并行启动多个生成任务
|
||||||
|
btnText.innerText = `AI 构思中 (0/${num})...`;
|
||||||
|
let finishedCount = 0;
|
||||||
|
|
||||||
|
const startTask = async (index) => {
|
||||||
|
const slot = document.createElement('div');
|
||||||
|
slot.className = 'image-frame relative bg-white/50 animate-pulse min-h-[300px] flex items-center justify-center rounded-[2.5rem] border border-slate-100 shadow-sm';
|
||||||
|
slot.innerHTML = `<div class="text-slate-400 text-xs font-bold italic">正在创作第 ${index + 1} 张...</div>`;
|
||||||
|
grid.appendChild(slot);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/generate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
mode: currentMode,
|
||||||
|
is_premium: document.getElementById('isPremium')?.checked || false,
|
||||||
|
apiKey: currentMode === 'key' ? apiKey : '',
|
||||||
|
prompt: document.getElementById('manualPrompt').value,
|
||||||
|
model: document.getElementById('modelSelect').value,
|
||||||
|
ratio: document.getElementById('ratioSelect').value,
|
||||||
|
size: document.getElementById('sizeSelect').value,
|
||||||
|
image_urls
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const res = await r.json();
|
||||||
|
if(res.error) throw new Error(res.error);
|
||||||
|
|
||||||
|
if(res.message) showToast(res.message, 'success');
|
||||||
|
|
||||||
|
const imgUrl = res.data[0].url;
|
||||||
|
currentGeneratedUrls.push(imgUrl);
|
||||||
|
|
||||||
|
slot.className = 'image-frame group relative animate-in zoom-in-95 duration-700';
|
||||||
|
slot.innerHTML = `
|
||||||
|
<img src="${imgUrl}" class="w-full h-auto rounded-[2.5rem] object-contain shadow-xl">
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-black/20 rounded-[2.5rem]">
|
||||||
|
<button onclick="downloadImage('${imgUrl}')" class="bg-white/90 p-4 rounded-full text-indigo-600 shadow-xl hover:scale-110 transition-transform">
|
||||||
|
<i data-lucide="download-cloud" class="w-6 h-6"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
lucide.createIcons();
|
||||||
|
} catch (e) {
|
||||||
|
slot.className = 'image-frame relative bg-red-50/50 flex items-center justify-center rounded-[2.5rem] border border-red-100 p-6';
|
||||||
|
if(e.message.includes('401') || e.message.includes('请先登录')) {
|
||||||
|
slot.innerHTML = `<div class="text-red-400 text-[10px] font-bold text-center">登录已过期,请重新登录</div>`;
|
||||||
|
} else {
|
||||||
|
slot.innerHTML = `<div class="text-red-400 text-[10px] font-bold text-center">渲染异常: ${e.message}</div>`;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
finishedCount++;
|
||||||
|
btnText.innerText = `AI 构思中 (${finishedCount}/${num})...`;
|
||||||
|
// 每次生成任务结束后,刷新一次积分显示
|
||||||
|
if(currentMode === 'trial') checkAuth();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tasks = Array.from({ length: num }, (_, i) => startTask(i));
|
||||||
|
await Promise.all(tasks);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
showToast('创作引擎中断: ' + e.message, 'error');
|
||||||
|
document.getElementById('placeholder').classList.remove('hidden');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btnText.innerText = "立即生成作品";
|
||||||
|
document.getElementById('statusInfo').classList.add('hidden');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
// 修改密码弹窗控制
|
||||||
|
function openPwdModal() {
|
||||||
|
const modal = document.getElementById('pwdModal');
|
||||||
|
if(!modal) return;
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
setTimeout(() => {
|
||||||
|
modal.classList.add('opacity-100');
|
||||||
|
modal.querySelector('div').classList.remove('scale-95');
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePwdModal() {
|
||||||
|
const modal = document.getElementById('pwdModal');
|
||||||
|
if(!modal) return;
|
||||||
|
modal.classList.remove('opacity-100');
|
||||||
|
modal.querySelector('div').classList.add('scale-95');
|
||||||
|
setTimeout(() => {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
document.getElementById('pwdForm').reset();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if(e.target.closest('#openPwdModalBtn')) {
|
||||||
|
openPwdModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('pwdForm')?.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const old_password = document.getElementById('oldPwd').value;
|
||||||
|
const new_password = document.getElementById('newPwd').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/auth/change_password', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({old_password, new_password})
|
||||||
|
});
|
||||||
|
const d = await r.json();
|
||||||
|
if(r.ok) {
|
||||||
|
showToast('密码修改成功,请记牢新密码', 'success');
|
||||||
|
closePwdModal();
|
||||||
|
} else {
|
||||||
|
showToast(d.error || '修改失败', 'error');
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
showToast('网络连接失败', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
10
sync_history_db.py
Normal file
10
sync_history_db.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from app import app
|
||||||
|
from extensions import db
|
||||||
|
from models import GenerationRecord
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
try:
|
||||||
|
db.create_all()
|
||||||
|
print("✅ 数据库表同步成功 (包括 GenerationRecord)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 同步失败: {e}")
|
||||||
98
templates/base.html
Normal file
98
templates/base.html
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}AI 视界{% endblock %}</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body class="text-slate-700 antialiased bg-slate-50 overflow-hidden">
|
||||||
|
<div class="bg-mesh"></div>
|
||||||
|
<div id="toastContainer" class="toast-container"></div>
|
||||||
|
|
||||||
|
<div class="flex h-screen w-screen overflow-hidden">
|
||||||
|
<!-- 全局系统菜单栏 (默认隐藏,仅在 initGlobalNav 成功后显示) -->
|
||||||
|
<nav id="globalNav" class="hidden w-20 flex-shrink-0 bg-slate-900 flex flex-col items-center py-8 z-40 shadow-2xl transition-all duration-500">
|
||||||
|
<div class="w-12 h-12 btn-primary logo-box rounded-2xl flex items-center justify-center mb-12 rotate-3">
|
||||||
|
<i data-lucide="scan-eye" class="w-7 h-7"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="dynamicMenuList" class="flex-1 w-full px-2 space-y-4"></div>
|
||||||
|
|
||||||
|
<div id="globalUserProfile" class="flex flex-col items-center gap-4 mb-4">
|
||||||
|
<div class="w-10 h-10 bg-slate-800 rounded-xl flex items-center justify-center text-slate-400 border border-slate-700">
|
||||||
|
<i data-lucide="user" class="w-5 h-5"></i>
|
||||||
|
</div>
|
||||||
|
<button id="globalLogoutBtn" class="text-slate-500 hover:text-rose-400 transition-colors">
|
||||||
|
<i data-lucide="log-out" class="w-5 h-5"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- 主内容区域 (全屏) -->
|
||||||
|
<main class="flex-1 flex overflow-hidden relative w-full h-full">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
lucide.createIcons();
|
||||||
|
|
||||||
|
// 自动加载菜单
|
||||||
|
async function initGlobalNav() {
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/auth/menu');
|
||||||
|
const d = await r.json();
|
||||||
|
if(d.menu && d.menu.length > 0) {
|
||||||
|
document.getElementById('globalNav').classList.remove('hidden');
|
||||||
|
const list = document.getElementById('dynamicMenuList');
|
||||||
|
list.innerHTML = d.menu.map(item => {
|
||||||
|
const isActive = window.location.pathname === item.url;
|
||||||
|
return `
|
||||||
|
<div class="relative group flex justify-center">
|
||||||
|
<a href="${item.url}"
|
||||||
|
class="w-12 h-12 flex items-center justify-center rounded-2xl transition-all duration-300 ${
|
||||||
|
isActive
|
||||||
|
? 'bg-indigo-600 text-white shadow-lg shadow-indigo-500/40'
|
||||||
|
: 'text-slate-500 hover:bg-slate-800 hover:text-white'
|
||||||
|
}">
|
||||||
|
<i data-lucide="${item.icon}" class="w-5 h-5"></i>
|
||||||
|
</a>
|
||||||
|
<div class="absolute left-full ml-4 px-3 py-2 bg-slate-800 text-white text-[10px] font-black rounded-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all whitespace-nowrap z-50 shadow-xl">
|
||||||
|
${item.name}
|
||||||
|
<div class="absolute right-full top-1/2 -translate-y-1/2 border-8 border-transparent border-r-slate-800"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
} catch(e) { console.error('菜单加载失败', e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('globalLogoutBtn').onclick = async () => {
|
||||||
|
await fetch('/api/auth/logout', { method: 'POST' });
|
||||||
|
window.location.href = '/login';
|
||||||
|
};
|
||||||
|
|
||||||
|
initGlobalNav();
|
||||||
|
|
||||||
|
function showToast(message, type = 'info') {
|
||||||
|
const container = document.getElementById('toastContainer');
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `toast ${type}`;
|
||||||
|
const colors = { success: 'text-emerald-500', error: 'text-rose-500', warning: 'text-amber-500', info: 'text-indigo-500' };
|
||||||
|
const icons = { success: 'check-circle', error: 'x-circle', warning: 'alert-triangle', info: 'info' };
|
||||||
|
toast.innerHTML = `<i data-lucide="${icons[type]}" class="w-5 h-5 ${colors[type]} flex-shrink-0"></i><span class="text-sm font-medium text-slate-700">${message}</span>`;
|
||||||
|
container.appendChild(toast);
|
||||||
|
lucide.createIcons();
|
||||||
|
setTimeout(() => { toast.style.animation = 'slideOutRight 0.3s ease-in'; setTimeout(() => toast.remove(), 300); }, 3000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
237
templates/dicts.html
Normal file
237
templates/dicts.html
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}系统字典管理 - AI 视界{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="min-h-screen p-8 lg:p-12">
|
||||||
|
<div class="max-w-6xl mx-auto space-y-8">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-12 h-12 bg-indigo-600 text-white rounded-2xl flex items-center justify-center shadow-lg">
|
||||||
|
<i data-lucide="book-open" class="w-7 h-7"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-black text-slate-900 tracking-tight">系统字典管理</h1>
|
||||||
|
<p class="text-slate-400 text-sm">统一维护模型、比例、提示词等系统参数</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onclick="openModal()" class="btn-primary px-6 py-3 rounded-xl text-sm font-bold shadow-lg flex items-center gap-2">
|
||||||
|
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||||
|
新增字典项
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选栏 -->
|
||||||
|
<div class="flex gap-4 bg-white p-4 rounded-3xl shadow-sm border border-slate-100">
|
||||||
|
<button onclick="loadDicts('')" class="dict-filter-btn px-4 py-2 rounded-xl text-xs font-bold transition-all bg-slate-100 text-slate-600 active-filter">全部</button>
|
||||||
|
<button onclick="loadDicts('ai_model')" class="dict-filter-btn px-4 py-2 rounded-xl text-xs font-bold transition-all hover:bg-slate-50 text-slate-400">AI 模型</button>
|
||||||
|
<button onclick="loadDicts('aspect_ratio')" class="dict-filter-btn px-4 py-2 rounded-xl text-xs font-bold transition-all hover:bg-slate-50 text-slate-400">画面比例</button>
|
||||||
|
<button onclick="loadDicts('ai_image_size')" class="dict-filter-btn px-4 py-2 rounded-xl text-xs font-bold transition-all hover:bg-slate-50 text-slate-400">输出尺寸</button>
|
||||||
|
<button onclick="loadDicts('prompt_tpl')" class="dict-filter-btn px-4 py-2 rounded-xl text-xs font-bold transition-all hover:bg-slate-50 text-slate-400">提示词模板</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格 -->
|
||||||
|
<div class="bg-white rounded-[2.5rem] shadow-xl border border-slate-100 overflow-hidden">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-slate-50 border-b border-slate-100">
|
||||||
|
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">类型</th>
|
||||||
|
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">显示名称</th>
|
||||||
|
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">存储值/内容</th>
|
||||||
|
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">积分消耗</th>
|
||||||
|
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">状态</th>
|
||||||
|
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="dictTableBody" class="text-sm font-medium">
|
||||||
|
<!-- 动态加载 -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑弹窗 -->
|
||||||
|
<div id="dictModal" class="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-50 flex items-center justify-center hidden opacity-0 transition-opacity duration-300">
|
||||||
|
<div class="bg-white w-full max-w-lg rounded-[2.5rem] shadow-2xl p-10 space-y-8 transform scale-95 transition-transform duration-300">
|
||||||
|
<h2 id="modalTitle" class="text-2xl font-black text-slate-900">新增字典项</h2>
|
||||||
|
<form id="dictForm" class="space-y-5">
|
||||||
|
<input type="hidden" id="dictId">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">字典类型</label>
|
||||||
|
<select id="dictType" class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold">
|
||||||
|
<option value="ai_model">AI 模型 (ai_model)</option>
|
||||||
|
<option value="aspect_ratio">画面比例 (aspect_ratio)</option>
|
||||||
|
<option value="ai_image_size">输出尺寸 (ai_image_size)</option>
|
||||||
|
<option value="prompt_tpl">提示词模板 (prompt_tpl)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">显示名称 (Label)</label>
|
||||||
|
<input type="text" id="dictLabel" required class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold">
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">存储值/内容 (Value)</label>
|
||||||
|
<textarea id="dictValue" required rows="3" class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold resize-none"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">积分消耗</label>
|
||||||
|
<input type="number" id="dictCost" value="0" class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold">
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">排序权重</label>
|
||||||
|
<input type="number" id="dictOrder" value="0" class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 pt-2">
|
||||||
|
<input type="checkbox" id="dictActive" checked class="w-4 h-4 rounded border-slate-200 text-indigo-600 focus:ring-indigo-500">
|
||||||
|
<label for="dictActive" class="text-sm font-bold text-slate-600">立即启用</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4 pt-4">
|
||||||
|
<button type="button" onclick="closeModal()" class="flex-1 px-8 py-4 rounded-2xl border border-slate-100 font-bold text-slate-400 hover:bg-slate-50 transition-all">取消</button>
|
||||||
|
<button type="submit" class="flex-1 btn-primary px-8 py-4 rounded-2xl font-bold shadow-lg shadow-indigo-100">保存配置</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
let currentType = '';
|
||||||
|
|
||||||
|
async function loadDicts(type = '') {
|
||||||
|
currentType = type;
|
||||||
|
// 更新按钮样式
|
||||||
|
document.querySelectorAll('.dict-filter-btn').forEach(btn => {
|
||||||
|
const btnType = btn.getAttribute('onclick').match(/'(.*)'/)[1];
|
||||||
|
if(btnType === type) {
|
||||||
|
btn.classList.add('bg-slate-100', 'text-slate-600');
|
||||||
|
btn.classList.remove('hover:bg-slate-50', 'text-slate-400');
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('bg-slate-100', 'text-slate-600');
|
||||||
|
btn.classList.add('hover:bg-slate-50', 'text-slate-400');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/admin/dicts?type=${type}`);
|
||||||
|
const d = await r.json();
|
||||||
|
const body = document.getElementById('dictTableBody');
|
||||||
|
|
||||||
|
body.innerHTML = d.dicts.map(item => `
|
||||||
|
<tr class="border-b border-slate-50 hover:bg-slate-50/50 transition-colors">
|
||||||
|
<td class="px-8 py-5">
|
||||||
|
<span class="px-2 py-0.5 rounded text-[10px] font-black uppercase ${
|
||||||
|
item.dict_type === 'ai_model' ? 'bg-indigo-50 text-indigo-600' :
|
||||||
|
item.dict_type === 'aspect_ratio' ? 'bg-emerald-50 text-emerald-600' :
|
||||||
|
item.dict_type === 'ai_image_size' ? 'bg-rose-50 text-rose-600' : 'bg-amber-50 text-amber-600'
|
||||||
|
}">${item.dict_type}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-8 py-5 text-slate-700 font-bold">${item.label}</td>
|
||||||
|
<td class="px-8 py-5 text-slate-400 text-xs truncate max-w-xs" title="${item.value}">${item.value}</td>
|
||||||
|
<td class="px-8 py-5">
|
||||||
|
<span class="text-amber-600 font-black">${item.cost}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-8 py-5">
|
||||||
|
<span class="${item.is_active ? 'text-emerald-500' : 'text-slate-300'}">
|
||||||
|
<i data-lucide="${item.is_active ? 'check-circle-2' : 'circle'}" class="w-4 h-4"></i>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-8 py-5">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button onclick='editDict(${JSON.stringify(item)})' class="text-indigo-400 hover:text-indigo-600 transition-colors">
|
||||||
|
<i data-lucide="edit-3" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
<button onclick="deleteDict(${item.id})" class="text-rose-300 hover:text-rose-500 transition-colors">
|
||||||
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
lucide.createIcons();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal(data = null) {
|
||||||
|
const modal = document.getElementById('dictModal');
|
||||||
|
const form = document.getElementById('dictForm');
|
||||||
|
document.getElementById('modalTitle').innerText = data ? '编辑字典项' : '新增字典项';
|
||||||
|
document.getElementById('dictId').value = data ? data.id : '';
|
||||||
|
document.getElementById('dictType').value = data ? data.dict_type : 'ai_model';
|
||||||
|
document.getElementById('dictLabel').value = data ? data.label : '';
|
||||||
|
document.getElementById('dictValue').value = data ? data.value : '';
|
||||||
|
document.getElementById('dictCost').value = data ? data.cost : 0;
|
||||||
|
document.getElementById('dictOrder').value = data ? data.sort_order : 0;
|
||||||
|
document.getElementById('dictActive').checked = data ? data.is_active : true;
|
||||||
|
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
setTimeout(() => {
|
||||||
|
modal.classList.remove('opacity-0');
|
||||||
|
modal.querySelector('div').classList.remove('scale-95');
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
const modal = document.getElementById('dictModal');
|
||||||
|
modal.classList.add('opacity-0');
|
||||||
|
modal.querySelector('div').classList.add('scale-95');
|
||||||
|
setTimeout(() => modal.classList.add('hidden'), 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
function editDict(item) {
|
||||||
|
openModal(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDict(id) {
|
||||||
|
if(!confirm('确定要删除此字典项吗?')) return;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/admin/dicts/delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id })
|
||||||
|
});
|
||||||
|
const d = await r.json();
|
||||||
|
if(d.message) {
|
||||||
|
showToast(d.message, 'success');
|
||||||
|
loadDicts(currentType);
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('dictForm').onsubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const payload = {
|
||||||
|
id: document.getElementById('dictId').value,
|
||||||
|
dict_type: document.getElementById('dictType').value,
|
||||||
|
label: document.getElementById('dictLabel').value,
|
||||||
|
value: document.getElementById('dictValue').value,
|
||||||
|
cost: parseInt(document.getElementById('dictCost').value),
|
||||||
|
sort_order: parseInt(document.getElementById('dictOrder').value),
|
||||||
|
is_active: document.getElementById('dictActive').checked
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/admin/dicts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const d = await r.json();
|
||||||
|
if(d.message) {
|
||||||
|
showToast(d.message, 'success');
|
||||||
|
closeModal();
|
||||||
|
loadDicts(currentType);
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
};
|
||||||
|
|
||||||
|
loadDicts();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
226
templates/index.html
Normal file
226
templates/index.html
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}工作台 - AI 视界{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- 中间:AI 功能设定区 -->
|
||||||
|
<aside class="w-80 lg:w-[340px] flex-shrink-0 glass-sidebar flex flex-col z-30 border-r border-slate-200/60 shadow-xl bg-white/50 backdrop-blur-xl">
|
||||||
|
<div class="p-6 pb-2">
|
||||||
|
<h2 class="text-xl font-black text-slate-900 tracking-tight">创作工作台</h2>
|
||||||
|
<p class="text-[10px] text-slate-400 font-bold uppercase tracking-widest mt-1">智能试戴引擎配置</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto px-6 py-4 space-y-6 custom-scrollbar">
|
||||||
|
<section class="space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="w-6 h-6 rounded-full bg-indigo-600 text-[10px] text-white flex items-center justify-center font-bold">01</span>
|
||||||
|
<h3 class="text-sm font-bold text-slate-800">授权验证</h3>
|
||||||
|
</div>
|
||||||
|
<!-- 积分显示 -->
|
||||||
|
<div id="pointsBadge" class="hidden px-2 py-0.5 bg-amber-50 text-amber-600 border border-amber-100 rounded-lg text-[10px] font-black uppercase">
|
||||||
|
可用积分: <span id="pointsDisplay">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button id="modeTrialBtn" class="flex-1 py-2 rounded-xl text-[10px] font-bold border-2 border-indigo-500 bg-indigo-50 text-indigo-600 transition-all">积分/试用模式</button>
|
||||||
|
<button id="modeKeyBtn" class="flex-1 py-2 rounded-xl text-[10px] font-bold border border-slate-200 text-slate-400 hover:bg-slate-50 transition-all">自定义 Key</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="premiumToggle" class="flex items-center justify-between bg-amber-50/50 border border-amber-100/50 p-3 rounded-2xl animate-in fade-in duration-500">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-7 h-7 bg-amber-100 text-amber-600 rounded-lg flex items-center justify-center">
|
||||||
|
<i data-lucide="sparkles" class="w-4 h-4"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-[10px] font-black text-amber-700 uppercase tracking-tight">优质渲染模式</div>
|
||||||
|
<div class="text-[8px] text-amber-500 font-bold">使用专属通道 · 积分消耗 X2</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input type="checkbox" id="isPremium" class="sr-only peer">
|
||||||
|
<div class="w-9 h-5 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-amber-500"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="keyInputGroup" class="hidden relative group animate-in slide-in-from-top-2 duration-300">
|
||||||
|
<input id="apiKey" type="password" placeholder="输入您的 OpenAI API 密钥" class="w-full bg-white border border-slate-200 rounded-2xl p-3.5 pl-11 text-xs outline-none focus:border-indigo-500 transition-all shadow-sm">
|
||||||
|
<i data-lucide="key-round" class="w-4 h-4 absolute left-4 top-1/2 -translate-y-1/2 text-indigo-400"></i>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="space-y-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="w-6 h-6 rounded-full bg-indigo-600 text-[10px] text-white flex items-center justify-center font-bold">02</span>
|
||||||
|
<h3 class="text-sm font-bold text-slate-800">渲染设定</h3>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest ml-1 flex items-center gap-1.5"><i data-lucide="cpu" class="w-3 h-3"></i>计算模型</label>
|
||||||
|
<select id="modelSelect" class="w-full bg-white border border-slate-200 rounded-xl p-3 text-xs font-bold outline-none focus:border-indigo-500 transition-all"></select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest ml-1 flex items-center gap-1.5"><i data-lucide="layout" class="w-3 h-3"></i>画面比例</label>
|
||||||
|
<select id="ratioSelect" class="w-full bg-white border border-slate-200 rounded-xl p-3 text-xs font-bold outline-none focus:border-indigo-500 transition-all"></select>
|
||||||
|
</div>
|
||||||
|
<div id="sizeGroup" class="hidden space-y-1.5">
|
||||||
|
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest ml-1 flex items-center gap-1.5"><i data-lucide="maximize" class="w-3 h-3"></i>输出尺寸</label>
|
||||||
|
<select id="sizeSelect" class="w-full bg-white border border-slate-200 rounded-xl p-3 text-xs font-bold outline-none focus:border-indigo-500 transition-all"></select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest ml-1 flex items-center gap-1.5"><i data-lucide="copy" class="w-3 h-3"></i>生成数量</label>
|
||||||
|
<select id="numSelect" class="w-full bg-white border border-slate-200 rounded-xl p-3 text-xs font-bold outline-none focus:border-indigo-500 transition-all">
|
||||||
|
<option value="1">1 张</option><option value="2">2 张</option><option value="3">3 张</option><option value="4">4 张</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-2xl border border-slate-100 overflow-hidden bg-white shadow-sm">
|
||||||
|
<select id="promptTpl" class="w-full bg-slate-50 border-b border-slate-100 p-3 text-[10px] font-bold text-indigo-600 outline-none cursor-pointer">
|
||||||
|
<option value="manual">✨ 自定义创作</option>
|
||||||
|
</select>
|
||||||
|
<textarea id="manualPrompt" rows="2" class="w-full p-3 text-xs outline-none resize-none leading-relaxed" placeholder="描述您的需求..."></textarea>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="space-y-4 pb-2">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="w-6 h-6 rounded-full bg-indigo-600 text-[10px] text-white flex items-center justify-center font-bold">03</span>
|
||||||
|
<h3 class="text-sm font-bold text-slate-800">参考底图</h3>
|
||||||
|
</div>
|
||||||
|
<div class="relative group">
|
||||||
|
<div class="border-2 border-dashed border-slate-100 rounded-3xl p-6 text-center bg-white/30 hover:border-indigo-200 transition-all cursor-pointer">
|
||||||
|
<i data-lucide="image-plus" class="w-6 h-6 mx-auto mb-2 text-slate-300"></i>
|
||||||
|
<p class="text-[10px] text-slate-400 font-bold">点击选择肖像照片</p>
|
||||||
|
</div>
|
||||||
|
<input id="fileInput" type="file" multiple class="absolute inset-0 opacity-0 cursor-pointer">
|
||||||
|
</div>
|
||||||
|
<div id="imagePreview" class="flex gap-3 overflow-x-auto py-1"></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6 bg-white/95 border-t border-slate-100">
|
||||||
|
<button id="submitBtn" class="w-full btn-primary py-4 rounded-2xl shadow-lg hover:scale-[1.02] active:scale-[0.98] gap-2 flex items-center justify-center">
|
||||||
|
<i data-lucide="wand-2" class="w-5 h-5"></i>
|
||||||
|
<span class="text-base font-bold tracking-widest">立即生成作品</span>
|
||||||
|
</button>
|
||||||
|
<p id="costPreview" class="text-center text-[10px] text-amber-600 font-bold mt-2 hidden"></p>
|
||||||
|
<p id="loginHint" class="text-center text-[10px] text-slate-400 mt-2 hidden">
|
||||||
|
<i data-lucide="lock" class="w-2.5 h-2.5 inline"></i> 请先登录以使用生成功能
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 右侧:主展示 -->
|
||||||
|
<main class="flex-1 relative flex flex-col bg-slate-50 overflow-hidden">
|
||||||
|
<div class="h-24 flex items-center justify-between px-12 relative z-10">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-2.5 h-2.5 bg-indigo-500 rounded-full animate-ping"></div>
|
||||||
|
<span class="text-[10px] font-black text-slate-500 uppercase tracking-widest text-xs">引擎就绪</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-6">
|
||||||
|
<!-- 历史记录触发按钮 -->
|
||||||
|
<button id="showHistoryBtn" class="flex items-center gap-2 bg-white/80 backdrop-blur-md px-4 py-2 rounded-2xl border border-white shadow-sm cursor-pointer hover:bg-white transition-all text-xs font-bold text-slate-600">
|
||||||
|
<i data-lucide="history" class="w-4 h-4 text-indigo-500"></i>
|
||||||
|
历史记录
|
||||||
|
</button>
|
||||||
|
<div id="userProfile" class="hidden flex items-center gap-3 bg-white/80 backdrop-blur-md px-4 py-2 rounded-2xl border border-white shadow-sm hover:bg-white transition-all">
|
||||||
|
<div class="w-8 h-8 bg-indigo-100 rounded-xl flex items-center justify-center text-indigo-600">
|
||||||
|
<i data-lucide="user" class="w-4 h-4"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span id="userPhoneDisplay" class="text-xs font-bold text-slate-600">--</span>
|
||||||
|
<span class="text-[9px] font-black text-amber-600 uppercase">余额: <span id="headerPoints">0</span> 积分</span>
|
||||||
|
</div>
|
||||||
|
<button id="openPwdModalBtn" title="修改密码" class="ml-1 p-1.5 text-slate-400 hover:text-indigo-600 transition-colors">
|
||||||
|
<i data-lucide="key-round" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<a id="loginEntryBtn" href="/login" class="bg-indigo-600 text-white px-6 py-2.5 rounded-2xl text-xs font-bold shadow-lg shadow-indigo-200 hover:bg-indigo-700 transition-all">
|
||||||
|
立即登录
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 flex items-center justify-center p-8 relative">
|
||||||
|
<div id="statusInfo" class="absolute top-8 left-1/2 -translate-x-1/2 hidden z-20">
|
||||||
|
<div class="bg-white/90 backdrop-blur-xl border border-indigo-100 px-6 py-3 rounded-2xl shadow-xl flex items-center gap-3">
|
||||||
|
<div class="w-2 h-2 bg-indigo-500 rounded-full animate-bounce"></div>
|
||||||
|
<span class="text-xs font-black text-slate-600 uppercase tracking-widest">AI 正在处理您的请求...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="resultCanvas" class="w-full h-full flex items-center justify-center">
|
||||||
|
<div id="placeholder" class="text-center max-w-lg">
|
||||||
|
<div class="w-48 h-48 bg-white rounded-[4.5rem] shadow-2xl flex items-center justify-center rotate-6 mx-auto mb-12 border border-slate-100">
|
||||||
|
<i data-lucide="glasses" class="w-20 h-20 text-indigo-500"></i>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-4xl font-black text-slate-900 mb-8">视界进化 · 艺术呈现</h2>
|
||||||
|
<p class="text-slate-500 text-lg font-medium">在左侧完成设定,开启 AI 试戴体验</p>
|
||||||
|
</div>
|
||||||
|
<div id="finalWrapper" class="hidden w-full h-full flex flex-col items-center justify-center py-6">
|
||||||
|
<div id="imageGrid" class="grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-4xl p-4"></div>
|
||||||
|
<div class="mt-14 flex items-center gap-8">
|
||||||
|
<!-- 全部下载按钮 -->
|
||||||
|
<button id="downloadAllBtn" class="bg-indigo-600 text-white px-8 py-4 rounded-[1.8rem] text-sm font-bold shadow-xl shadow-indigo-200 hover:bg-indigo-700 hover:scale-105 active:scale-95 transition-all flex items-center gap-3">
|
||||||
|
<i data-lucide="download" class="w-5 h-5"></i>
|
||||||
|
<span>全部下载</span>
|
||||||
|
</button>
|
||||||
|
<!-- 重新生成按钮 -->
|
||||||
|
<button id="regenBtn" class="w-16 h-16 bg-white border border-slate-100 rounded-[1.8rem] flex items-center justify-center text-slate-400 shadow-xl hover:text-indigo-600 transition-all hover:scale-110 active:scale-95 group">
|
||||||
|
<i data-lucide="refresh-cw" class="w-6 h-6 group-hover:rotate-180 transition-transform duration-500"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 历史记录滑出抽屉 (子页面) -->
|
||||||
|
<div id="historyDrawer" class="absolute inset-y-0 right-0 w-96 bg-white/95 backdrop-blur-2xl border-l border-slate-100 shadow-2xl z-50 translate-x-full transition-transform duration-500 flex flex-col">
|
||||||
|
<div class="p-8 border-b border-slate-100 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-black text-slate-900">创作历史</h3>
|
||||||
|
<p class="text-[10px] text-slate-400 font-bold uppercase tracking-widest mt-1">展示 90 天内的生成记录</p>
|
||||||
|
</div>
|
||||||
|
<button id="closeHistoryBtn" class="w-10 h-10 rounded-xl hover:bg-slate-100 flex items-center justify-center text-slate-400 transition-colors">
|
||||||
|
<i data-lucide="x" class="w-5 h-5"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="historyList" class="flex-1 overflow-y-auto p-6 space-y-4 custom-scrollbar">
|
||||||
|
<!-- 历史记录项 -->
|
||||||
|
</div>
|
||||||
|
<!-- 加载更多触发器 -->
|
||||||
|
<div id="historyFooter" class="p-4 border-t border-slate-50 flex justify-center hidden">
|
||||||
|
<div class="flex items-center gap-2 text-[10px] font-bold text-slate-400 animate-pulse">
|
||||||
|
<i data-lucide="loader-2" class="w-3 h-3 animate-spin"></i>
|
||||||
|
正在加载更多...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 修改密码弹窗 -->
|
||||||
|
<div id="pwdModal" class="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-50 flex items-center justify-center hidden opacity-0 transition-opacity duration-300">
|
||||||
|
<div class="bg-white w-full max-w-sm rounded-[2.5rem] shadow-2xl p-10 space-y-8 transform scale-95 transition-transform duration-300">
|
||||||
|
<h2 class="text-2xl font-black text-slate-900">修改登录密码</h2>
|
||||||
|
<form id="pwdForm" class="space-y-5">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">原密码</label>
|
||||||
|
<input type="password" id="oldPwd" required class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold">
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">新密码</label>
|
||||||
|
<input type="password" id="newPwd" required class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold">
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4 pt-4">
|
||||||
|
<button type="button" onclick="closePwdModal()" class="flex-1 px-8 py-4 rounded-2xl border border-slate-100 font-bold text-slate-400 hover:bg-slate-50 transition-all">取消</button>
|
||||||
|
<button type="submit" class="flex-1 btn-primary px-8 py-4 rounded-2xl font-bold shadow-lg shadow-indigo-100">确认修改</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
55
templates/login.html
Normal file
55
templates/login.html
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}登录 - AI 视界{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="w-full h-full flex items-center justify-center p-6 bg-slate-50/50 backdrop-blur-sm">
|
||||||
|
<div class="auth-card w-full max-w-md space-y-8 bg-white p-10 rounded-[2.5rem] shadow-2xl border border-slate-100 relative z-10">
|
||||||
|
<div class="text-center space-y-4">
|
||||||
|
<div class="w-16 h-16 btn-primary mx-auto rounded-2xl flex items-center justify-center shadow-lg rotate-3">
|
||||||
|
<i data-lucide="scan-eye" class="w-10 h-10"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 id="authTitle" class="text-3xl font-black text-slate-900 tracking-tight">欢迎回来</h2>
|
||||||
|
<p id="authSub" class="text-slate-400 text-sm mt-2">请登录以开启 AI 创作之旅</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="relative group">
|
||||||
|
<input id="authPhone" type="text" placeholder="手机号" class="w-full bg-slate-50 border border-slate-200 rounded-2xl p-4 pl-12 text-sm outline-none focus:border-indigo-500 transition-all">
|
||||||
|
<i data-lucide="phone" class="w-5 h-5 absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-indigo-500 transition-colors"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="smsGroup" class="hidden relative group">
|
||||||
|
<input id="authCode" type="text" placeholder="验证码" class="w-full bg-slate-50 border border-slate-200 rounded-2xl p-4 pl-12 text-sm outline-none focus:border-indigo-500 transition-all">
|
||||||
|
<i data-lucide="shield-check" class="w-5 h-5 absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-indigo-500 transition-colors"></i>
|
||||||
|
<button id="sendSmsBtn" class="absolute right-4 top-1/2 -translate-y-1/2 text-indigo-600 text-xs font-bold hover:text-indigo-700">发送验证码</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative group">
|
||||||
|
<input id="authPass" type="password" placeholder="密码" class="w-full bg-slate-50 border border-slate-200 rounded-2xl p-4 pl-12 text-sm outline-none focus:border-indigo-500 transition-all">
|
||||||
|
<i data-lucide="lock" class="w-5 h-5 absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-indigo-500 transition-colors"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="authSubmitBtn" class="w-full btn-primary py-5 rounded-2xl shadow-xl shadow-indigo-100 hover:scale-[1.02] active:scale-[0.98] transition-all">
|
||||||
|
<span class="text-lg font-bold">立即登录</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="text-center pt-2">
|
||||||
|
<button id="authSwitchBtn" class="text-sm text-slate-400 hover:text-indigo-600 font-bold transition-colors">没有账号?立即注册</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="/" class="text-xs text-slate-300 hover:text-slate-500 flex items-center justify-center gap-1">
|
||||||
|
<i data-lucide="arrow-left" class="w-3 h-3"></i> 返回首页
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
114
templates/logs.html
Normal file
114
templates/logs.html
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}系统日志 - AI 视界{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="min-h-screen p-8 lg:p-12">
|
||||||
|
<div class="max-w-6xl mx-auto space-y-8">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-12 h-12 bg-slate-900 text-white rounded-2xl flex items-center justify-center shadow-lg">
|
||||||
|
<i data-lucide="terminal" class="w-7 h-7"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-black text-slate-900 tracking-tight">系统审计日志</h1>
|
||||||
|
<p class="text-slate-400 text-sm">实时监控系统运行状态与安全事件</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<!-- 筛选工具栏 -->
|
||||||
|
<div class="flex items-center bg-white border border-slate-200 rounded-xl px-4 py-2 shadow-sm">
|
||||||
|
<i data-lucide="search" class="w-4 h-4 text-slate-400 mr-2"></i>
|
||||||
|
<input type="text" id="logSearch" placeholder="搜索消息或手机号..."
|
||||||
|
class="outline-none text-sm w-48 text-slate-600" oninput="loadLogs()">
|
||||||
|
</div>
|
||||||
|
<select id="logLevel" class="bg-white border border-slate-200 rounded-xl px-4 py-2 text-sm text-slate-600 outline-none shadow-sm" onchange="loadLogs()">
|
||||||
|
<option value="">全部级别</option>
|
||||||
|
<option value="INFO">INFO</option>
|
||||||
|
<option value="WARNING">WARNING</option>
|
||||||
|
<option value="ERROR">ERROR</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button onclick="loadLogs()" class="bg-white border border-slate-200 p-3 rounded-xl hover:bg-slate-50 transition-all shadow-sm">
|
||||||
|
<i data-lucide="refresh-cw" class="w-5 h-5 text-slate-600"></i>
|
||||||
|
</button>
|
||||||
|
<a href="/" class="btn-primary px-6 py-3 rounded-xl text-sm font-bold shadow-lg">返回工作台</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-[2.5rem] shadow-xl border border-slate-100 overflow-hidden">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-slate-50 border-b border-slate-100">
|
||||||
|
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">时间</th>
|
||||||
|
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">级别</th>
|
||||||
|
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">事件消息</th>
|
||||||
|
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">关键参数</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="logTableBody" class="text-sm font-medium">
|
||||||
|
<!-- 动态加载 -->
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="px-8 py-20 text-center text-slate-400 italic">正在连接日志服务器...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
async function loadLogs() {
|
||||||
|
const search = document.getElementById('logSearch')?.value || '';
|
||||||
|
const level = document.getElementById('logLevel')?.value || '';
|
||||||
|
const url = `/api/auth/logs?search=${encodeURIComponent(search)}&level=${level}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch(url);
|
||||||
|
const d = await r.json();
|
||||||
|
const body = document.getElementById('logTableBody');
|
||||||
|
|
||||||
|
if (d.error) {
|
||||||
|
body.innerHTML = `<tr><td colspan="4" class="px-8 py-20 text-center text-rose-500 font-bold">${d.error}</td></tr>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.logs.length === 0) {
|
||||||
|
body.innerHTML = `<tr><td colspan="4" class="px-8 py-20 text-center text-slate-400 italic">没有找到符合条件的日志</td></tr>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.innerHTML = d.logs.map(log => `
|
||||||
|
<tr class="border-b border-slate-50 hover:bg-slate-50/50 transition-colors">
|
||||||
|
<td class="px-8 py-5 text-slate-400 font-mono text-xs">${log.time}</td>
|
||||||
|
<td class="px-8 py-5">
|
||||||
|
<span class="px-2.5 py-1 rounded-lg text-[10px] font-black uppercase ${
|
||||||
|
log.level === 'INFO' ? 'bg-indigo-50 text-indigo-600' :
|
||||||
|
log.level === 'WARNING' ? 'bg-amber-50 text-amber-600' : 'bg-rose-50 text-rose-600'
|
||||||
|
}">${log.level}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-8 py-5 text-slate-700 font-bold">${log.message}</td>
|
||||||
|
<td class="px-8 py-5 text-slate-400 font-mono text-[10px]">
|
||||||
|
${Object.entries(log.extra).map(([k, v]) => `<span class="inline-block bg-slate-100 rounded px-1.5 py-0.5 mr-1 mb-1">${k}: ${v}</span>`).join('')}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化加载
|
||||||
|
loadLogs();
|
||||||
|
|
||||||
|
// 自动刷新逻辑:如果搜索框为空,则每5秒刷新一次
|
||||||
|
setInterval(() => {
|
||||||
|
const search = document.getElementById('logSearch')?.value || '';
|
||||||
|
if (!search) loadLogs();
|
||||||
|
}, 5000);
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
260
templates/rbac.html
Normal file
260
templates/rbac.html
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}权限管理 - AI 视界{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="min-h-screen p-8 lg:p-12">
|
||||||
|
<div class="max-w-6xl mx-auto space-y-12">
|
||||||
|
<!-- 头部 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-12 h-12 bg-indigo-600 text-white rounded-2xl flex items-center justify-center shadow-lg shadow-indigo-200">
|
||||||
|
<i data-lucide="shield-check" class="w-7 h-7"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-black text-slate-900 tracking-tight">动态 RBAC 权限中心</h1>
|
||||||
|
<p class="text-slate-400 text-sm">动态配置系统角色与权限资产</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="/" class="btn-primary px-6 py-3 rounded-xl text-sm font-bold shadow-lg">返回工作台</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
|
<!-- 左侧:编辑/创建角色 -->
|
||||||
|
<div class="lg:col-span-1 space-y-6">
|
||||||
|
<div class="bg-white p-8 rounded-[2.5rem] shadow-xl border border-slate-100">
|
||||||
|
<h3 id="formTitle" class="text-lg font-black text-slate-900 mb-6 flex items-center gap-2">
|
||||||
|
<i data-lucide="plus-circle" class="w-5 h-5 text-indigo-500"></i>创建新角色
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<input type="hidden" id="editRoleId">
|
||||||
|
<input id="newRoleName" type="text" placeholder="角色名称 (如: 审核员)" class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 text-sm font-bold outline-none focus:border-indigo-500">
|
||||||
|
<textarea id="newRoleDesc" placeholder="角色描述" class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 text-sm font-medium outline-none focus:border-indigo-500 h-24 resize-none"></textarea>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">分配权限</label>
|
||||||
|
<div id="permissionCheckboxes" class="grid grid-cols-1 gap-2 bg-slate-50 p-4 rounded-2xl">
|
||||||
|
<!-- 动态加载 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button id="cancelEditBtn" class="hidden flex-1 px-4 py-4 rounded-2xl border border-slate-100 text-slate-400 font-bold hover:bg-slate-50 transition-all">取消</button>
|
||||||
|
<button id="saveRoleBtn" class="flex-[2] btn-primary py-4 rounded-2xl shadow-lg font-bold">保存角色</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:现有角色 & 用户管理 -->
|
||||||
|
<div class="lg:col-span-2 space-y-8">
|
||||||
|
<!-- 角色列表 -->
|
||||||
|
<div class="bg-white rounded-[2.5rem] shadow-xl border border-slate-100 overflow-hidden">
|
||||||
|
<div class="p-8 border-b border-slate-50">
|
||||||
|
<h3 class="text-lg font-black text-slate-900 flex items-center gap-2">
|
||||||
|
<i data-lucide="users" class="w-5 h-5 text-indigo-500"></i>角色资产列表
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-slate-50">
|
||||||
|
<th class="px-8 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">角色名称</th>
|
||||||
|
<th class="px-8 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">描述</th>
|
||||||
|
<th class="px-8 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">权限集</th>
|
||||||
|
<th class="px-8 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="roleTableBody">
|
||||||
|
<!-- 动态加载 -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户列表 -->
|
||||||
|
<div class="bg-white rounded-[2.5rem] shadow-xl border border-slate-100 overflow-hidden">
|
||||||
|
<div class="p-8 border-b border-slate-50">
|
||||||
|
<h3 class="text-lg font-black text-slate-900 flex items-center gap-2">
|
||||||
|
<i data-lucide="user-cog" class="w-5 h-5 text-indigo-500"></i>用户角色分配
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<table class="w-full text-left">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-slate-50">
|
||||||
|
<th class="px-8 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">用户手机</th>
|
||||||
|
<th class="px-8 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">当前角色</th>
|
||||||
|
<th class="px-8 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="userTableBody">
|
||||||
|
<!-- 动态加载 -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
let allRoles = [];
|
||||||
|
|
||||||
|
async function loadPermissions() {
|
||||||
|
const r = await fetch('/api/admin/permissions');
|
||||||
|
const d = await r.json();
|
||||||
|
const container = document.getElementById('permissionCheckboxes');
|
||||||
|
container.innerHTML = d.permissions.map(p => `
|
||||||
|
<label class="flex items-center gap-3 cursor-pointer group">
|
||||||
|
<input type="checkbox" value="${p.name}" class="w-4 h-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
|
||||||
|
<span class="text-xs font-bold text-slate-600 group-hover:text-slate-900">${p.description}</span>
|
||||||
|
</label>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRoles() {
|
||||||
|
const r = await fetch('/api/admin/roles');
|
||||||
|
const d = await r.json();
|
||||||
|
allRoles = d.roles;
|
||||||
|
const container = document.getElementById('roleTableBody');
|
||||||
|
container.innerHTML = d.roles.map(role => `
|
||||||
|
<tr class="border-b border-slate-50 hover:bg-slate-50/50 transition-colors">
|
||||||
|
<td class="px-8 py-5">
|
||||||
|
<div class="font-black text-slate-900">${role.name}</div>
|
||||||
|
<div class="text-[10px] text-slate-400 font-bold uppercase tracking-tighter">ID: ${role.id}</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-8 py-5 text-xs text-slate-500 max-w-[150px] truncate">${role.description || '-'}</td>
|
||||||
|
<td class="px-8 py-5">
|
||||||
|
<div class="flex flex-wrap gap-1 max-w-[240px]">
|
||||||
|
${role.permissions.map(p => `<span class="px-1.5 py-0.5 bg-indigo-50 text-indigo-500 text-[9px] font-black rounded uppercase border border-indigo-100/50">${p}</span>`).join('')}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-8 py-5">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button onclick='editRole(${JSON.stringify(role)})' class="p-2 text-indigo-400 hover:bg-indigo-50 rounded-xl transition-all">
|
||||||
|
<i data-lucide="edit-2" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
${role.name !== '超级管理员' ? `
|
||||||
|
<button onclick="deleteRole(${role.id})" class="p-2 text-rose-300 hover:bg-rose-50 rounded-xl transition-all">
|
||||||
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function editRole(role) {
|
||||||
|
document.getElementById('formTitle').innerHTML = `<i data-lucide="edit-3" class="w-5 h-5 text-indigo-500"></i>编辑角色`;
|
||||||
|
document.getElementById('editRoleId').value = role.id;
|
||||||
|
document.getElementById('newRoleName').value = role.name;
|
||||||
|
document.getElementById('newRoleDesc').value = role.description || '';
|
||||||
|
document.getElementById('cancelEditBtn').classList.remove('hidden');
|
||||||
|
|
||||||
|
// 重置并勾选权限
|
||||||
|
document.querySelectorAll('#permissionCheckboxes input').forEach(cb => {
|
||||||
|
cb.checked = role.permissions.includes(cb.value);
|
||||||
|
});
|
||||||
|
lucide.createIcons();
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('cancelEditBtn').onclick = resetForm;
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
document.getElementById('formTitle').innerHTML = `<i data-lucide="plus-circle" class="w-5 h-5 text-indigo-500"></i>创建新角色`;
|
||||||
|
document.getElementById('editRoleId').value = '';
|
||||||
|
document.getElementById('newRoleName').value = '';
|
||||||
|
document.getElementById('newRoleDesc').value = '';
|
||||||
|
document.getElementById('cancelEditBtn').classList.add('hidden');
|
||||||
|
document.querySelectorAll('#permissionCheckboxes input').forEach(cb => cb.checked = false);
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteRole(id) {
|
||||||
|
if(!confirm('确定要删除此角色吗?所有关联该角色的用户将失去权限。')) return;
|
||||||
|
const r = await fetch('/api/admin/roles/delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ id })
|
||||||
|
});
|
||||||
|
const d = await r.json();
|
||||||
|
if(d.message) {
|
||||||
|
showToast(d.message, 'success');
|
||||||
|
loadRoles();
|
||||||
|
loadUsers();
|
||||||
|
} else {
|
||||||
|
showToast(d.error, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUsers() {
|
||||||
|
const r = await fetch('/api/admin/users');
|
||||||
|
const d = await r.json();
|
||||||
|
const body = document.getElementById('userTableBody');
|
||||||
|
body.innerHTML = d.users.map(user => `
|
||||||
|
<tr class="border-b border-slate-50">
|
||||||
|
<td class="px-8 py-4 text-sm font-bold text-slate-700">${user.phone}</td>
|
||||||
|
<td class="px-8 py-4">
|
||||||
|
<span class="px-3 py-1 bg-indigo-50 text-indigo-600 rounded-full text-xs font-black">${user.role}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-8 py-4">
|
||||||
|
<select onchange="assignRole(${user.id}, this.value)" class="bg-white border border-slate-200 rounded-xl px-3 py-1.5 text-xs font-bold outline-none">
|
||||||
|
<option value="">更改角色...</option>
|
||||||
|
${allRoles.map(role => `<option value="${role.id}">${role.name}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assignRole(userId, roleId) {
|
||||||
|
if(!roleId) return;
|
||||||
|
const r = await fetch('/api/admin/users/assign', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ user_id: userId, role_id: roleId })
|
||||||
|
});
|
||||||
|
const d = await r.json();
|
||||||
|
if(d.message) {
|
||||||
|
showToast(d.message, 'success');
|
||||||
|
loadUsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('saveRoleBtn').onclick = async () => {
|
||||||
|
const id = document.getElementById('editRoleId').value;
|
||||||
|
const name = document.getElementById('newRoleName').value;
|
||||||
|
const description = document.getElementById('newRoleDesc').value;
|
||||||
|
const permissions = Array.from(document.querySelectorAll('#permissionCheckboxes input:checked')).map(i => i.value);
|
||||||
|
|
||||||
|
if(!name) return showToast('请输入角色名称', 'warning');
|
||||||
|
|
||||||
|
const r = await fetch('/api/admin/roles', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ id, name, description, permissions })
|
||||||
|
});
|
||||||
|
const d = await r.json();
|
||||||
|
if(d.message) {
|
||||||
|
showToast(d.message, 'success');
|
||||||
|
loadRoles();
|
||||||
|
loadUsers();
|
||||||
|
resetForm();
|
||||||
|
} else {
|
||||||
|
showToast(d.error, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
await loadPermissions();
|
||||||
|
await loadRoles();
|
||||||
|
await loadUsers();
|
||||||
|
}
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
164
venv/Include/site/python3.12/greenlet/greenlet.h
Normal file
164
venv/Include/site/python3.12/greenlet/greenlet.h
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */
|
||||||
|
|
||||||
|
/* Greenlet object interface */
|
||||||
|
|
||||||
|
#ifndef Py_GREENLETOBJECT_H
|
||||||
|
#define Py_GREENLETOBJECT_H
|
||||||
|
|
||||||
|
|
||||||
|
#include <Python.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* This is deprecated and undocumented. It does not change. */
|
||||||
|
#define GREENLET_VERSION "1.0.0"
|
||||||
|
|
||||||
|
#ifndef GREENLET_MODULE
|
||||||
|
#define implementation_ptr_t void*
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct _greenlet {
|
||||||
|
PyObject_HEAD
|
||||||
|
PyObject* weakreflist;
|
||||||
|
PyObject* dict;
|
||||||
|
implementation_ptr_t pimpl;
|
||||||
|
} PyGreenlet;
|
||||||
|
|
||||||
|
#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type))
|
||||||
|
|
||||||
|
|
||||||
|
/* C API functions */
|
||||||
|
|
||||||
|
/* Total number of symbols that are exported */
|
||||||
|
#define PyGreenlet_API_pointers 12
|
||||||
|
|
||||||
|
#define PyGreenlet_Type_NUM 0
|
||||||
|
#define PyExc_GreenletError_NUM 1
|
||||||
|
#define PyExc_GreenletExit_NUM 2
|
||||||
|
|
||||||
|
#define PyGreenlet_New_NUM 3
|
||||||
|
#define PyGreenlet_GetCurrent_NUM 4
|
||||||
|
#define PyGreenlet_Throw_NUM 5
|
||||||
|
#define PyGreenlet_Switch_NUM 6
|
||||||
|
#define PyGreenlet_SetParent_NUM 7
|
||||||
|
|
||||||
|
#define PyGreenlet_MAIN_NUM 8
|
||||||
|
#define PyGreenlet_STARTED_NUM 9
|
||||||
|
#define PyGreenlet_ACTIVE_NUM 10
|
||||||
|
#define PyGreenlet_GET_PARENT_NUM 11
|
||||||
|
|
||||||
|
#ifndef GREENLET_MODULE
|
||||||
|
/* This section is used by modules that uses the greenlet C API */
|
||||||
|
static void** _PyGreenlet_API = NULL;
|
||||||
|
|
||||||
|
# define PyGreenlet_Type \
|
||||||
|
(*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM])
|
||||||
|
|
||||||
|
# define PyExc_GreenletError \
|
||||||
|
((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM])
|
||||||
|
|
||||||
|
# define PyExc_GreenletExit \
|
||||||
|
((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PyGreenlet_New(PyObject *args)
|
||||||
|
*
|
||||||
|
* greenlet.greenlet(run, parent=None)
|
||||||
|
*/
|
||||||
|
# define PyGreenlet_New \
|
||||||
|
(*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \
|
||||||
|
_PyGreenlet_API[PyGreenlet_New_NUM])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PyGreenlet_GetCurrent(void)
|
||||||
|
*
|
||||||
|
* greenlet.getcurrent()
|
||||||
|
*/
|
||||||
|
# define PyGreenlet_GetCurrent \
|
||||||
|
(*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PyGreenlet_Throw(
|
||||||
|
* PyGreenlet *greenlet,
|
||||||
|
* PyObject *typ,
|
||||||
|
* PyObject *val,
|
||||||
|
* PyObject *tb)
|
||||||
|
*
|
||||||
|
* g.throw(...)
|
||||||
|
*/
|
||||||
|
# define PyGreenlet_Throw \
|
||||||
|
(*(PyObject * (*)(PyGreenlet * self, \
|
||||||
|
PyObject * typ, \
|
||||||
|
PyObject * val, \
|
||||||
|
PyObject * tb)) \
|
||||||
|
_PyGreenlet_API[PyGreenlet_Throw_NUM])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args)
|
||||||
|
*
|
||||||
|
* g.switch(*args, **kwargs)
|
||||||
|
*/
|
||||||
|
# define PyGreenlet_Switch \
|
||||||
|
(*(PyObject * \
|
||||||
|
(*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \
|
||||||
|
_PyGreenlet_API[PyGreenlet_Switch_NUM])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent)
|
||||||
|
*
|
||||||
|
* g.parent = new_parent
|
||||||
|
*/
|
||||||
|
# define PyGreenlet_SetParent \
|
||||||
|
(*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \
|
||||||
|
_PyGreenlet_API[PyGreenlet_SetParent_NUM])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PyGreenlet_GetParent(PyObject* greenlet)
|
||||||
|
*
|
||||||
|
* return greenlet.parent;
|
||||||
|
*
|
||||||
|
* This could return NULL even if there is no exception active.
|
||||||
|
* If it does not return NULL, you are responsible for decrementing the
|
||||||
|
* reference count.
|
||||||
|
*/
|
||||||
|
# define PyGreenlet_GetParent \
|
||||||
|
(*(PyGreenlet* (*)(PyGreenlet*)) \
|
||||||
|
_PyGreenlet_API[PyGreenlet_GET_PARENT_NUM])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* deprecated, undocumented alias.
|
||||||
|
*/
|
||||||
|
# define PyGreenlet_GET_PARENT PyGreenlet_GetParent
|
||||||
|
|
||||||
|
# define PyGreenlet_MAIN \
|
||||||
|
(*(int (*)(PyGreenlet*)) \
|
||||||
|
_PyGreenlet_API[PyGreenlet_MAIN_NUM])
|
||||||
|
|
||||||
|
# define PyGreenlet_STARTED \
|
||||||
|
(*(int (*)(PyGreenlet*)) \
|
||||||
|
_PyGreenlet_API[PyGreenlet_STARTED_NUM])
|
||||||
|
|
||||||
|
# define PyGreenlet_ACTIVE \
|
||||||
|
(*(int (*)(PyGreenlet*)) \
|
||||||
|
_PyGreenlet_API[PyGreenlet_ACTIVE_NUM])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Macro that imports greenlet and initializes C API */
|
||||||
|
/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we
|
||||||
|
keep the older definition to be sure older code that might have a copy of
|
||||||
|
the header still works. */
|
||||||
|
# define PyGreenlet_Import() \
|
||||||
|
{ \
|
||||||
|
_PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* GREENLET_MODULE */
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif /* !Py_GREENLETOBJECT_H */
|
||||||
1
venv/Lib/site-packages/Tea/__init__.py
Normal file
1
venv/Lib/site-packages/Tea/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
__version__ = "0.4.3"
|
||||||
BIN
venv/Lib/site-packages/Tea/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
venv/Lib/site-packages/Tea/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
venv/Lib/site-packages/Tea/__pycache__/core.cpython-312.pyc
Normal file
BIN
venv/Lib/site-packages/Tea/__pycache__/core.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
venv/Lib/site-packages/Tea/__pycache__/model.cpython-312.pyc
Normal file
BIN
venv/Lib/site-packages/Tea/__pycache__/model.cpython-312.pyc
Normal file
Binary file not shown.
BIN
venv/Lib/site-packages/Tea/__pycache__/request.cpython-312.pyc
Normal file
BIN
venv/Lib/site-packages/Tea/__pycache__/request.cpython-312.pyc
Normal file
Binary file not shown.
BIN
venv/Lib/site-packages/Tea/__pycache__/response.cpython-312.pyc
Normal file
BIN
venv/Lib/site-packages/Tea/__pycache__/response.cpython-312.pyc
Normal file
Binary file not shown.
BIN
venv/Lib/site-packages/Tea/__pycache__/stream.cpython-312.pyc
Normal file
BIN
venv/Lib/site-packages/Tea/__pycache__/stream.cpython-312.pyc
Normal file
Binary file not shown.
381
venv/Lib/site-packages/Tea/core.py
Normal file
381
venv/Lib/site-packages/Tea/core.py
Normal file
@ -0,0 +1,381 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import ssl
|
||||||
|
import time
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from urllib.parse import urlencode, urlparse
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import certifi
|
||||||
|
from requests import Session, PreparedRequest, adapters, status_codes
|
||||||
|
|
||||||
|
from Tea.exceptions import RequiredArgumentException, RetryError
|
||||||
|
from Tea.model import TeaModel
|
||||||
|
from Tea.request import TeaRequest
|
||||||
|
from Tea.response import TeaResponse
|
||||||
|
from Tea.stream import BaseStream
|
||||||
|
|
||||||
|
DEFAULT_CONNECT_TIMEOUT = 5000
|
||||||
|
DEFAULT_READ_TIMEOUT = 10000
|
||||||
|
DEFAULT_POOL_SIZE = 10
|
||||||
|
|
||||||
|
logger = logging.getLogger('alibabacloud-tea')
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
logger.addHandler(ch)
|
||||||
|
|
||||||
|
|
||||||
|
class TLSVersion(Enum):
|
||||||
|
TLSv1 = 'TLSv1'
|
||||||
|
TLSv1_1 = 'TLSv1.1'
|
||||||
|
TLSv1_2 = 'TLSv1.2'
|
||||||
|
TLSv1_3 = 'TLSv1.3'
|
||||||
|
|
||||||
|
|
||||||
|
class _TLSAdapter(adapters.HTTPAdapter):
|
||||||
|
"""A HTTPAdapter that uses an arbitrary TLS version."""
|
||||||
|
|
||||||
|
def __init__(self, ssl_context=None, **kwargs):
|
||||||
|
self.ssl_context = ssl_context
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def init_poolmanager(self, *args, **kwargs):
|
||||||
|
"""Override the init_poolmanager method to set the SSL."""
|
||||||
|
kwargs['ssl_context'] = self.ssl_context
|
||||||
|
super().init_poolmanager(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class TeaCore:
|
||||||
|
_sessions = {}
|
||||||
|
http_adapter = adapters.HTTPAdapter(pool_connections=DEFAULT_POOL_SIZE, pool_maxsize=DEFAULT_POOL_SIZE * 4)
|
||||||
|
https_adapter = adapters.HTTPAdapter(pool_connections=DEFAULT_POOL_SIZE, pool_maxsize=DEFAULT_POOL_SIZE * 4)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_tls_minimum_version(sls_context, tls_min_version):
|
||||||
|
context = sls_context
|
||||||
|
if tls_min_version is not None:
|
||||||
|
if tls_min_version == 'TLSv1':
|
||||||
|
context.minimum_version = ssl.TLSVersion.TLSv1
|
||||||
|
elif tls_min_version == 'TLSv1.1':
|
||||||
|
context.minimum_version = ssl.TLSVersion.TLSv1_1
|
||||||
|
elif tls_min_version == 'TLSv1.2':
|
||||||
|
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||||
|
elif tls_min_version == 'TLSv1.3':
|
||||||
|
context.minimum_version = ssl.TLSVersion.TLSv1_3
|
||||||
|
return context
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_adapter(prefix, tls_min_version: str = None):
|
||||||
|
ca_cert = certifi.where()
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
if ca_cert and prefix.upper() == 'HTTPS':
|
||||||
|
context = TeaCore._set_tls_minimum_version(context, tls_min_version)
|
||||||
|
context.load_verify_locations(ca_cert)
|
||||||
|
adapter = _TLSAdapter(ssl_context=context, pool_connections=DEFAULT_POOL_SIZE,
|
||||||
|
pool_maxsize=DEFAULT_POOL_SIZE * 4)
|
||||||
|
return adapter
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_session(session_key: str, protocol: str, tls_min_version: str = None, verify: bool = True):
|
||||||
|
if session_key not in TeaCore._sessions:
|
||||||
|
session = Session()
|
||||||
|
adapter = TeaCore.get_adapter(protocol, tls_min_version)
|
||||||
|
if protocol.upper() == 'HTTPS':
|
||||||
|
if verify:
|
||||||
|
session.mount('https://', adapter)
|
||||||
|
else:
|
||||||
|
session.mount('https://', TeaCore.https_adapter)
|
||||||
|
else:
|
||||||
|
session.mount('http://', adapter)
|
||||||
|
TeaCore._sessions[session_key] = session
|
||||||
|
return TeaCore._sessions[session_key]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _prepare_http_debug(request, symbol):
|
||||||
|
base = ''
|
||||||
|
for key, value in request.headers.items():
|
||||||
|
base += f'\n{symbol} {key} : {value}'
|
||||||
|
return base
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _do_http_debug(request, response):
|
||||||
|
# logger the request
|
||||||
|
url = urlparse(request.url)
|
||||||
|
request_base = f'\n> {request.method.upper()} {url.path + url.query} HTTP/1.1'
|
||||||
|
logger.debug(request_base + TeaCore._prepare_http_debug(request, '>'))
|
||||||
|
|
||||||
|
# logger the response
|
||||||
|
response_base = f'\n< HTTP/1.1 {response.status_code}' \
|
||||||
|
f' {status_codes._codes.get(response.status_code)[0].upper()}'
|
||||||
|
logger.debug(response_base + TeaCore._prepare_http_debug(response, '<'))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def compose_url(request):
|
||||||
|
host = request.headers.get('host')
|
||||||
|
if not host:
|
||||||
|
raise RequiredArgumentException('endpoint')
|
||||||
|
else:
|
||||||
|
host = host.rstrip('/')
|
||||||
|
protocol = f'{request.protocol.lower()}://'
|
||||||
|
pathname = request.pathname
|
||||||
|
|
||||||
|
if host.startswith(('http://', 'https://')):
|
||||||
|
protocol = ''
|
||||||
|
|
||||||
|
if request.port == 80:
|
||||||
|
port = ''
|
||||||
|
else:
|
||||||
|
port = f':{request.port}'
|
||||||
|
|
||||||
|
url = protocol + host + port + pathname
|
||||||
|
|
||||||
|
if request.query:
|
||||||
|
if "?" in url:
|
||||||
|
if not url.endswith("&"):
|
||||||
|
url += "&"
|
||||||
|
else:
|
||||||
|
url += "?"
|
||||||
|
|
||||||
|
encode_query = {}
|
||||||
|
for key in request.query:
|
||||||
|
value = request.query[key]
|
||||||
|
if value is not None:
|
||||||
|
encode_query[key] = str(value)
|
||||||
|
url += urlencode(encode_query)
|
||||||
|
return url.rstrip("?&")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def async_do_action(
|
||||||
|
request: TeaRequest,
|
||||||
|
runtime_option=None
|
||||||
|
) -> TeaResponse:
|
||||||
|
runtime_option = runtime_option or {}
|
||||||
|
|
||||||
|
url = TeaCore.compose_url(request)
|
||||||
|
verify = not runtime_option.get('ignoreSSL', False)
|
||||||
|
tls_min_version = runtime_option.get('tlsMinVersion')
|
||||||
|
if isinstance(tls_min_version, Enum):
|
||||||
|
tls_min_version = tls_min_version.value
|
||||||
|
|
||||||
|
timeout = runtime_option.get('timeout')
|
||||||
|
connect_timeout = runtime_option.get('connectTimeout') or timeout or DEFAULT_CONNECT_TIMEOUT
|
||||||
|
read_timeout = runtime_option.get('readTimeout') or timeout or DEFAULT_READ_TIMEOUT
|
||||||
|
|
||||||
|
connect_timeout, read_timeout = (int(connect_timeout) / 1000, int(read_timeout) / 1000)
|
||||||
|
|
||||||
|
proxy = None
|
||||||
|
if request.protocol.upper() == 'HTTP':
|
||||||
|
proxy = runtime_option.get('httpProxy')
|
||||||
|
if not proxy:
|
||||||
|
proxy = os.environ.get('HTTP_PROXY') or os.environ.get('http_proxy')
|
||||||
|
elif request.protocol.upper() == 'HTTPS':
|
||||||
|
proxy = runtime_option.get('httpsProxy')
|
||||||
|
if not proxy:
|
||||||
|
proxy = os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy')
|
||||||
|
|
||||||
|
connector = None
|
||||||
|
ca_cert = certifi.where()
|
||||||
|
if ca_cert and request.protocol.upper() == 'HTTPS':
|
||||||
|
ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
||||||
|
ssl_context = TeaCore._set_tls_minimum_version(ssl_context, tls_min_version)
|
||||||
|
ssl_context.load_verify_locations(ca_cert)
|
||||||
|
connector = aiohttp.TCPConnector(
|
||||||
|
ssl=ssl_context,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
verify = False
|
||||||
|
|
||||||
|
timeout = aiohttp.ClientTimeout(
|
||||||
|
sock_read=read_timeout,
|
||||||
|
sock_connect=connect_timeout
|
||||||
|
)
|
||||||
|
async with aiohttp.ClientSession(
|
||||||
|
connector=connector
|
||||||
|
) as s:
|
||||||
|
body = b''
|
||||||
|
if isinstance(request.body, BaseStream):
|
||||||
|
for content in request.body:
|
||||||
|
body += content
|
||||||
|
elif isinstance(request.body, str):
|
||||||
|
body = request.body.encode('utf-8')
|
||||||
|
else:
|
||||||
|
body = request.body
|
||||||
|
try:
|
||||||
|
async with s.request(request.method, url,
|
||||||
|
data=body,
|
||||||
|
headers=request.headers,
|
||||||
|
ssl=verify,
|
||||||
|
proxy=proxy,
|
||||||
|
timeout=timeout) as response:
|
||||||
|
tea_resp = TeaResponse()
|
||||||
|
tea_resp.body = await response.read()
|
||||||
|
tea_resp.headers = {k.lower(): v for k, v in response.headers.items()}
|
||||||
|
tea_resp.status_code = response.status
|
||||||
|
tea_resp.status_message = response.reason
|
||||||
|
tea_resp.response = response
|
||||||
|
except IOError as e:
|
||||||
|
raise RetryError(str(e))
|
||||||
|
return tea_resp
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def do_action(
|
||||||
|
request: TeaRequest,
|
||||||
|
runtime_option=None
|
||||||
|
) -> TeaResponse:
|
||||||
|
url = TeaCore.compose_url(request)
|
||||||
|
|
||||||
|
runtime_option = runtime_option or {}
|
||||||
|
|
||||||
|
verify = not runtime_option.get('ignoreSSL', False)
|
||||||
|
tls_min_version = runtime_option.get('tlsMinVersion')
|
||||||
|
if isinstance(tls_min_version, Enum):
|
||||||
|
tls_min_version = tls_min_version.value
|
||||||
|
|
||||||
|
if verify:
|
||||||
|
verify = runtime_option.get('ca', True) if runtime_option.get('ca', True) is not None else True
|
||||||
|
cert = runtime_option.get('cert', None)
|
||||||
|
|
||||||
|
timeout = runtime_option.get('timeout')
|
||||||
|
connect_timeout = runtime_option.get('connectTimeout') or timeout or DEFAULT_CONNECT_TIMEOUT
|
||||||
|
read_timeout = runtime_option.get('readTimeout') or timeout or DEFAULT_READ_TIMEOUT
|
||||||
|
|
||||||
|
timeout = (int(connect_timeout) / 1000, int(read_timeout) / 1000)
|
||||||
|
|
||||||
|
if isinstance(request.body, str):
|
||||||
|
request.body = request.body.encode('utf-8')
|
||||||
|
|
||||||
|
p = PreparedRequest()
|
||||||
|
p.prepare(
|
||||||
|
method=request.method.upper(),
|
||||||
|
url=url,
|
||||||
|
data=request.body,
|
||||||
|
headers=request.headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
proxies = {}
|
||||||
|
http_proxy = runtime_option.get('httpProxy')
|
||||||
|
https_proxy = runtime_option.get('httpsProxy')
|
||||||
|
no_proxy = runtime_option.get('noProxy')
|
||||||
|
|
||||||
|
if not http_proxy:
|
||||||
|
http_proxy = os.environ.get('HTTP_PROXY') or os.environ.get('http_proxy')
|
||||||
|
if not https_proxy:
|
||||||
|
https_proxy = os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy')
|
||||||
|
|
||||||
|
if http_proxy:
|
||||||
|
proxies['http'] = http_proxy
|
||||||
|
if https_proxy:
|
||||||
|
proxies['https'] = https_proxy
|
||||||
|
if no_proxy:
|
||||||
|
proxies['no_proxy'] = no_proxy
|
||||||
|
|
||||||
|
host = request.headers.get('host')
|
||||||
|
host = host.rstrip('/')
|
||||||
|
|
||||||
|
session_key = f'{request.protocol.lower()}://{host}:{request.port}'
|
||||||
|
session = TeaCore._get_session(session_key=session_key, protocol=request.protocol,
|
||||||
|
tls_min_version=tls_min_version, verify=verify)
|
||||||
|
try:
|
||||||
|
resp = session.send(
|
||||||
|
p,
|
||||||
|
proxies=proxies,
|
||||||
|
timeout=timeout,
|
||||||
|
verify=verify,
|
||||||
|
cert=cert,
|
||||||
|
)
|
||||||
|
except IOError as e:
|
||||||
|
raise RetryError(str(e))
|
||||||
|
|
||||||
|
debug = runtime_option.get('debug') or os.getenv('DEBUG')
|
||||||
|
if debug and debug.lower() == 'sdk':
|
||||||
|
TeaCore._do_http_debug(p, resp)
|
||||||
|
|
||||||
|
response = TeaResponse()
|
||||||
|
response.status_message = resp.reason
|
||||||
|
response.status_code = resp.status_code
|
||||||
|
response.headers = {k.lower(): v for k, v in resp.headers.items()}
|
||||||
|
response.body = resp.content
|
||||||
|
response.response = resp
|
||||||
|
return response
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_response_body(resp) -> str:
|
||||||
|
return resp.content.decode("utf-8")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def allow_retry(dic, retry_times, now=None) -> bool:
|
||||||
|
if retry_times == 0:
|
||||||
|
return True
|
||||||
|
if dic is None or not dic.__contains__("maxAttempts") or \
|
||||||
|
dic.get('retryable') is not True and retry_times >= 1:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
retry = 0 if dic.get("maxAttempts") is None else int(
|
||||||
|
dic.get("maxAttempts"))
|
||||||
|
return retry >= retry_times
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_backoff_time(dic, retry_times) -> int:
|
||||||
|
default_back_off_time = 0
|
||||||
|
if dic is None or not dic.get("policy") or dic.get("policy") == "no":
|
||||||
|
return default_back_off_time
|
||||||
|
|
||||||
|
back_off_time = dic.get('period', default_back_off_time)
|
||||||
|
if not isinstance(back_off_time, int) and \
|
||||||
|
not (isinstance(back_off_time, str) and back_off_time.isdigit()):
|
||||||
|
return default_back_off_time
|
||||||
|
|
||||||
|
back_off_time = int(back_off_time)
|
||||||
|
if back_off_time < 0:
|
||||||
|
return retry_times
|
||||||
|
|
||||||
|
return back_off_time
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def sleep_async(t):
|
||||||
|
await asyncio.sleep(t)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sleep(t):
|
||||||
|
time.sleep(t)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_retryable(ex) -> bool:
|
||||||
|
return isinstance(ex, RetryError)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def bytes_readable(body):
|
||||||
|
return body
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def merge(*dic_list) -> dict:
|
||||||
|
dic_result = {}
|
||||||
|
for item in dic_list:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
dic_result.update(item)
|
||||||
|
elif isinstance(item, TeaModel):
|
||||||
|
dic_result.update(item.to_map())
|
||||||
|
return dic_result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_map(model: Optional[TeaModel]) -> Dict[str, Any]:
|
||||||
|
if isinstance(model, TeaModel):
|
||||||
|
return model.to_map()
|
||||||
|
else:
|
||||||
|
return dict()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_map(
|
||||||
|
model: TeaModel,
|
||||||
|
dic: Dict[str, Any]
|
||||||
|
) -> TeaModel:
|
||||||
|
if isinstance(model, TeaModel):
|
||||||
|
try:
|
||||||
|
return model.from_map(dic)
|
||||||
|
except Exception:
|
||||||
|
model._map = dic
|
||||||
|
return model
|
||||||
|
else:
|
||||||
|
return model
|
||||||
30
venv/Lib/site-packages/Tea/decorators.py
Normal file
30
venv/Lib/site-packages/Tea/decorators.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import warnings
|
||||||
|
import functools
|
||||||
|
|
||||||
|
|
||||||
|
def deprecated(reason):
|
||||||
|
"""This is a decorator which can be used to mark functions as deprecated.
|
||||||
|
It will result in a warning being emitted when the function is used.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reason (str): Explanation of why the function is deprecated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
original_func = func.__func__ if isinstance(func, staticmethod) or isinstance(func, classmethod) else func
|
||||||
|
|
||||||
|
@functools.wraps(original_func)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
warnings.warn(f"Call to deprecated function {original_func.__name__}. {reason}",
|
||||||
|
category=DeprecationWarning,
|
||||||
|
stacklevel=2)
|
||||||
|
return original_func(*args, **kwargs)
|
||||||
|
|
||||||
|
if isinstance(func, staticmethod):
|
||||||
|
return staticmethod(decorated_function)
|
||||||
|
elif isinstance(func, classmethod):
|
||||||
|
return classmethod(decorated_function)
|
||||||
|
else:
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
return decorator
|
||||||
56
venv/Lib/site-packages/Tea/exceptions.py
Normal file
56
venv/Lib/site-packages/Tea/exceptions.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
from .request import TeaRequest
|
||||||
|
|
||||||
|
|
||||||
|
class TeaException(Exception):
|
||||||
|
def __init__(self, dic):
|
||||||
|
self.code = dic.get("code")
|
||||||
|
self.message = dic.get("message")
|
||||||
|
self.data = dic.get("data")
|
||||||
|
self.description = dic.get("description")
|
||||||
|
self.accessDeniedDetail = dic.get("accessDeniedDetail")
|
||||||
|
if isinstance(dic.get("data"), dict) and dic.get("data").get("statusCode") is not None:
|
||||||
|
self.statusCode = dic.get("data").get("statusCode")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'Error: {self.code} {self.message} Response: {self.data}'
|
||||||
|
|
||||||
|
|
||||||
|
class ValidateException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RequiredArgumentException(ValidateException):
|
||||||
|
def __init__(self, arg):
|
||||||
|
self.arg = arg
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'"{self.arg}" is required.'
|
||||||
|
|
||||||
|
|
||||||
|
class RetryError(Exception):
|
||||||
|
def __init__(self, message):
|
||||||
|
self.message = message
|
||||||
|
self.data = None
|
||||||
|
|
||||||
|
|
||||||
|
class UnretryableException(TeaException):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
request: TeaRequest,
|
||||||
|
ex: Exception
|
||||||
|
):
|
||||||
|
self.last_request = request
|
||||||
|
self.inner_exception = ex
|
||||||
|
if isinstance(ex, TeaException):
|
||||||
|
super().__init__({
|
||||||
|
'code': ex.code,
|
||||||
|
'message': ex.message,
|
||||||
|
'data': ex.data
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
super().__init__({
|
||||||
|
'message': repr(ex),
|
||||||
|
})
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.inner_exception)
|
||||||
53
venv/Lib/site-packages/Tea/model.py
Normal file
53
venv/Lib/site-packages/Tea/model.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import re
|
||||||
|
from .exceptions import RequiredArgumentException, ValidateException
|
||||||
|
|
||||||
|
|
||||||
|
class TeaModel:
|
||||||
|
_map = None
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def to_map(self):
|
||||||
|
return self._map
|
||||||
|
|
||||||
|
def from_map(self, map=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_required(prop, prop_name):
|
||||||
|
if prop is None:
|
||||||
|
raise RequiredArgumentException(prop_name)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_max_length(prop, prop_name, max_length):
|
||||||
|
if len(prop) > max_length:
|
||||||
|
raise ValidateException(f'{prop_name} is exceed max-length: {max_length}')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_min_length(prop, prop_name, min_length):
|
||||||
|
if len(prop) < min_length:
|
||||||
|
raise ValidateException(f'{prop_name} is less than min-length: {min_length}')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_pattern(prop, prop_name, pattern):
|
||||||
|
match_obj = re.search(pattern, str(prop), re.M | re.I)
|
||||||
|
if not match_obj:
|
||||||
|
raise ValidateException(f'{prop_name} is not match: {pattern}')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_maximum(num, prop_name, maximum):
|
||||||
|
if num > maximum:
|
||||||
|
raise ValidateException(f'{prop_name} is greater than the maximum: {maximum}')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_minimum(num, prop_name, minimum):
|
||||||
|
if num < minimum:
|
||||||
|
raise ValidateException(f'{prop_name} is less than the minimum: {minimum}')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
s = self.to_map()
|
||||||
|
if s:
|
||||||
|
return str(s)
|
||||||
|
else:
|
||||||
|
return object.__str__(self)
|
||||||
29
venv/Lib/site-packages/Tea/request.py
Normal file
29
venv/Lib/site-packages/Tea/request.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
class TeaRequest:
|
||||||
|
_PROPERTY_DEFAULT_MAP = {
|
||||||
|
'query': {},
|
||||||
|
'protocol': 'http',
|
||||||
|
'port': 80,
|
||||||
|
'method': 'GET',
|
||||||
|
'headers': {},
|
||||||
|
'pathname': "",
|
||||||
|
'body': None,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.query = {}
|
||||||
|
self.protocol = "http"
|
||||||
|
self.port = 80
|
||||||
|
self.method = "GET"
|
||||||
|
self.headers = {}
|
||||||
|
self.pathname = ""
|
||||||
|
self.body = None
|
||||||
|
|
||||||
|
def __setattr__(self, key, value):
|
||||||
|
if key in self._PROPERTY_DEFAULT_MAP:
|
||||||
|
if not value:
|
||||||
|
if isinstance(self._PROPERTY_DEFAULT_MAP[key], (list, dict)):
|
||||||
|
self.__dict__[key] = self._PROPERTY_DEFAULT_MAP[key].copy()
|
||||||
|
else:
|
||||||
|
self.__dict__[key] = self._PROPERTY_DEFAULT_MAP[key]
|
||||||
|
return
|
||||||
|
self.__dict__[key] = value
|
||||||
8
venv/Lib/site-packages/Tea/response.py
Normal file
8
venv/Lib/site-packages/Tea/response.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
class TeaResponse:
|
||||||
|
# status
|
||||||
|
status_code = None
|
||||||
|
# reason
|
||||||
|
status_message = None
|
||||||
|
headers = None
|
||||||
|
response = None
|
||||||
|
body = None
|
||||||
38
venv/Lib/site-packages/Tea/stream.py
Normal file
38
venv/Lib/site-packages/Tea/stream.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
class BaseStream:
|
||||||
|
def __init__(self, size=1024):
|
||||||
|
self.size = size
|
||||||
|
|
||||||
|
def read(self, size=1024):
|
||||||
|
raise NotImplementedError('read method must be overridden')
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
raise NotImplementedError('__len__ method must be overridden')
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
raise NotImplementedError('__next__ method must be overridden')
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class _ReadableMc(type):
|
||||||
|
def __instancecheck__(self, instance):
|
||||||
|
if hasattr(instance, 'read') and hasattr(instance, '__iter__'):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class READABLE(metaclass=_ReadableMc):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class _WriteableMc(type):
|
||||||
|
def __instancecheck__(self, instance):
|
||||||
|
if hasattr(instance, 'write'):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class WRITABLE(metaclass=_WriteableMc):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
STREAM_CLASS = (READABLE, WRITABLE)
|
||||||
BIN
venv/Lib/site-packages/__pycache__/six.cpython-312.pyc
Normal file
BIN
venv/Lib/site-packages/__pycache__/six.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
venv/Lib/site-packages/_cffi_backend.cp312-win_amd64.pyd
Normal file
BIN
venv/Lib/site-packages/_cffi_backend.cp312-win_amd64.pyd
Normal file
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
318
venv/Lib/site-packages/aiofiles-24.1.0.dist-info/METADATA
Normal file
318
venv/Lib/site-packages/aiofiles-24.1.0.dist-info/METADATA
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
Metadata-Version: 2.3
|
||||||
|
Name: aiofiles
|
||||||
|
Version: 24.1.0
|
||||||
|
Summary: File support for asyncio.
|
||||||
|
Project-URL: Changelog, https://github.com/Tinche/aiofiles#history
|
||||||
|
Project-URL: Bug Tracker, https://github.com/Tinche/aiofiles/issues
|
||||||
|
Project-URL: repository, https://github.com/Tinche/aiofiles
|
||||||
|
Author-email: Tin Tvrtkovic <tinchester@gmail.com>
|
||||||
|
License: Apache-2.0
|
||||||
|
License-File: LICENSE
|
||||||
|
License-File: NOTICE
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Framework :: AsyncIO
|
||||||
|
Classifier: License :: OSI Approved :: Apache Software License
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Programming Language :: Python :: 3.8
|
||||||
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||||
|
Requires-Python: >=3.8
|
||||||
|
Description-Content-Type: text/markdown
|
||||||
|
|
||||||
|
# aiofiles: file support for asyncio
|
||||||
|
|
||||||
|
[](https://pypi.python.org/pypi/aiofiles)
|
||||||
|
[](https://github.com/Tinche/aiofiles/actions)
|
||||||
|
[](https://github.com/Tinche/aiofiles/actions/workflows/main.yml)
|
||||||
|
[](https://github.com/Tinche/aiofiles)
|
||||||
|
[](https://github.com/psf/black)
|
||||||
|
|
||||||
|
**aiofiles** is an Apache2 licensed library, written in Python, for handling local
|
||||||
|
disk files in asyncio applications.
|
||||||
|
|
||||||
|
Ordinary local file IO is blocking, and cannot easily and portably be made
|
||||||
|
asynchronous. This means doing file IO may interfere with asyncio applications,
|
||||||
|
which shouldn't block the executing thread. aiofiles helps with this by
|
||||||
|
introducing asynchronous versions of files that support delegating operations to
|
||||||
|
a separate thread pool.
|
||||||
|
|
||||||
|
```python
|
||||||
|
async with aiofiles.open('filename', mode='r') as f:
|
||||||
|
contents = await f.read()
|
||||||
|
print(contents)
|
||||||
|
'My file contents'
|
||||||
|
```
|
||||||
|
|
||||||
|
Asynchronous iteration is also supported.
|
||||||
|
|
||||||
|
```python
|
||||||
|
async with aiofiles.open('filename') as f:
|
||||||
|
async for line in f:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Asynchronous interface to tempfile module.
|
||||||
|
|
||||||
|
```python
|
||||||
|
async with aiofiles.tempfile.TemporaryFile('wb') as f:
|
||||||
|
await f.write(b'Hello, World!')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- a file API very similar to Python's standard, blocking API
|
||||||
|
- support for buffered and unbuffered binary files, and buffered text files
|
||||||
|
- support for `async`/`await` ([PEP 492](https://peps.python.org/pep-0492/)) constructs
|
||||||
|
- async interface to tempfile module
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To install aiofiles, simply:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ pip install aiofiles
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Files are opened using the `aiofiles.open()` coroutine, which in addition to
|
||||||
|
mirroring the builtin `open` accepts optional `loop` and `executor`
|
||||||
|
arguments. If `loop` is absent, the default loop will be used, as per the
|
||||||
|
set asyncio policy. If `executor` is not specified, the default event loop
|
||||||
|
executor will be used.
|
||||||
|
|
||||||
|
In case of success, an asynchronous file object is returned with an
|
||||||
|
API identical to an ordinary file, except the following methods are coroutines
|
||||||
|
and delegate to an executor:
|
||||||
|
|
||||||
|
- `close`
|
||||||
|
- `flush`
|
||||||
|
- `isatty`
|
||||||
|
- `read`
|
||||||
|
- `readall`
|
||||||
|
- `read1`
|
||||||
|
- `readinto`
|
||||||
|
- `readline`
|
||||||
|
- `readlines`
|
||||||
|
- `seek`
|
||||||
|
- `seekable`
|
||||||
|
- `tell`
|
||||||
|
- `truncate`
|
||||||
|
- `writable`
|
||||||
|
- `write`
|
||||||
|
- `writelines`
|
||||||
|
|
||||||
|
In case of failure, one of the usual exceptions will be raised.
|
||||||
|
|
||||||
|
`aiofiles.stdin`, `aiofiles.stdout`, `aiofiles.stderr`,
|
||||||
|
`aiofiles.stdin_bytes`, `aiofiles.stdout_bytes`, and
|
||||||
|
`aiofiles.stderr_bytes` provide async access to `sys.stdin`,
|
||||||
|
`sys.stdout`, `sys.stderr`, and their corresponding `.buffer` properties.
|
||||||
|
|
||||||
|
The `aiofiles.os` module contains executor-enabled coroutine versions of
|
||||||
|
several useful `os` functions that deal with files:
|
||||||
|
|
||||||
|
- `stat`
|
||||||
|
- `statvfs`
|
||||||
|
- `sendfile`
|
||||||
|
- `rename`
|
||||||
|
- `renames`
|
||||||
|
- `replace`
|
||||||
|
- `remove`
|
||||||
|
- `unlink`
|
||||||
|
- `mkdir`
|
||||||
|
- `makedirs`
|
||||||
|
- `rmdir`
|
||||||
|
- `removedirs`
|
||||||
|
- `link`
|
||||||
|
- `symlink`
|
||||||
|
- `readlink`
|
||||||
|
- `listdir`
|
||||||
|
- `scandir`
|
||||||
|
- `access`
|
||||||
|
- `getcwd`
|
||||||
|
- `path.abspath`
|
||||||
|
- `path.exists`
|
||||||
|
- `path.isfile`
|
||||||
|
- `path.isdir`
|
||||||
|
- `path.islink`
|
||||||
|
- `path.ismount`
|
||||||
|
- `path.getsize`
|
||||||
|
- `path.getatime`
|
||||||
|
- `path.getctime`
|
||||||
|
- `path.samefile`
|
||||||
|
- `path.sameopenfile`
|
||||||
|
|
||||||
|
### Tempfile
|
||||||
|
|
||||||
|
**aiofiles.tempfile** implements the following interfaces:
|
||||||
|
|
||||||
|
- TemporaryFile
|
||||||
|
- NamedTemporaryFile
|
||||||
|
- SpooledTemporaryFile
|
||||||
|
- TemporaryDirectory
|
||||||
|
|
||||||
|
Results return wrapped with a context manager allowing use with async with and async for.
|
||||||
|
|
||||||
|
```python
|
||||||
|
async with aiofiles.tempfile.NamedTemporaryFile('wb+') as f:
|
||||||
|
await f.write(b'Line1\n Line2')
|
||||||
|
await f.seek(0)
|
||||||
|
async for line in f:
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
async with aiofiles.tempfile.TemporaryDirectory() as d:
|
||||||
|
filename = os.path.join(d, "file.ext")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Writing tests for aiofiles
|
||||||
|
|
||||||
|
Real file IO can be mocked by patching `aiofiles.threadpool.sync_open`
|
||||||
|
as desired. The return type also needs to be registered with the
|
||||||
|
`aiofiles.threadpool.wrap` dispatcher:
|
||||||
|
|
||||||
|
```python
|
||||||
|
aiofiles.threadpool.wrap.register(mock.MagicMock)(
|
||||||
|
lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase(*args, **kwargs)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_stuff():
|
||||||
|
write_data = 'data'
|
||||||
|
read_file_chunks = [
|
||||||
|
b'file chunks 1',
|
||||||
|
b'file chunks 2',
|
||||||
|
b'file chunks 3',
|
||||||
|
b'',
|
||||||
|
]
|
||||||
|
file_chunks_iter = iter(read_file_chunks)
|
||||||
|
|
||||||
|
mock_file_stream = mock.MagicMock(
|
||||||
|
read=lambda *args, **kwargs: next(file_chunks_iter)
|
||||||
|
)
|
||||||
|
|
||||||
|
with mock.patch('aiofiles.threadpool.sync_open', return_value=mock_file_stream) as mock_open:
|
||||||
|
async with aiofiles.open('filename', 'w') as f:
|
||||||
|
await f.write(write_data)
|
||||||
|
assert f.read() == b'file chunks 1'
|
||||||
|
|
||||||
|
mock_file_stream.write.assert_called_once_with(write_data)
|
||||||
|
```
|
||||||
|
|
||||||
|
### History
|
||||||
|
|
||||||
|
#### 24.1.0 (2024-06-24)
|
||||||
|
|
||||||
|
- Import `os.link` conditionally to fix importing on android.
|
||||||
|
[#175](https://github.com/Tinche/aiofiles/issues/175)
|
||||||
|
- Remove spurious items from `aiofiles.os.__all__` when running on Windows.
|
||||||
|
- Switch to more modern async idioms: Remove types.coroutine and make AiofilesContextManager an awaitable instead a coroutine.
|
||||||
|
- Add `aiofiles.os.path.abspath` and `aiofiles.os.getcwd`.
|
||||||
|
[#174](https://github.com/Tinche/aiofiles/issues/181)
|
||||||
|
- _aiofiles_ is now tested on Python 3.13 too.
|
||||||
|
[#184](https://github.com/Tinche/aiofiles/pull/184)
|
||||||
|
- Dropped Python 3.7 support. If you require it, use version 23.2.1.
|
||||||
|
|
||||||
|
#### 23.2.1 (2023-08-09)
|
||||||
|
|
||||||
|
- Import `os.statvfs` conditionally to fix importing on non-UNIX systems.
|
||||||
|
[#171](https://github.com/Tinche/aiofiles/issues/171) [#172](https://github.com/Tinche/aiofiles/pull/172)
|
||||||
|
- aiofiles is now also tested on Windows.
|
||||||
|
|
||||||
|
#### 23.2.0 (2023-08-09)
|
||||||
|
|
||||||
|
- aiofiles is now tested on Python 3.12 too.
|
||||||
|
[#166](https://github.com/Tinche/aiofiles/issues/166) [#168](https://github.com/Tinche/aiofiles/pull/168)
|
||||||
|
- On Python 3.12, `aiofiles.tempfile.NamedTemporaryFile` now accepts a `delete_on_close` argument, just like the stdlib version.
|
||||||
|
- On Python 3.12, `aiofiles.tempfile.NamedTemporaryFile` no longer exposes a `delete` attribute, just like the stdlib version.
|
||||||
|
- Added `aiofiles.os.statvfs` and `aiofiles.os.path.ismount`.
|
||||||
|
[#162](https://github.com/Tinche/aiofiles/pull/162)
|
||||||
|
- Use [PDM](https://pdm.fming.dev/latest/) instead of Poetry.
|
||||||
|
[#169](https://github.com/Tinche/aiofiles/pull/169)
|
||||||
|
|
||||||
|
#### 23.1.0 (2023-02-09)
|
||||||
|
|
||||||
|
- Added `aiofiles.os.access`.
|
||||||
|
[#146](https://github.com/Tinche/aiofiles/pull/146)
|
||||||
|
- Removed `aiofiles.tempfile.temptypes.AsyncSpooledTemporaryFile.softspace`.
|
||||||
|
[#151](https://github.com/Tinche/aiofiles/pull/151)
|
||||||
|
- Added `aiofiles.stdin`, `aiofiles.stdin_bytes`, and other stdio streams.
|
||||||
|
[#154](https://github.com/Tinche/aiofiles/pull/154)
|
||||||
|
- Transition to `asyncio.get_running_loop` (vs `asyncio.get_event_loop`) internally.
|
||||||
|
|
||||||
|
#### 22.1.0 (2022-09-04)
|
||||||
|
|
||||||
|
- Added `aiofiles.os.path.islink`.
|
||||||
|
[#126](https://github.com/Tinche/aiofiles/pull/126)
|
||||||
|
- Added `aiofiles.os.readlink`.
|
||||||
|
[#125](https://github.com/Tinche/aiofiles/pull/125)
|
||||||
|
- Added `aiofiles.os.symlink`.
|
||||||
|
[#124](https://github.com/Tinche/aiofiles/pull/124)
|
||||||
|
- Added `aiofiles.os.unlink`.
|
||||||
|
[#123](https://github.com/Tinche/aiofiles/pull/123)
|
||||||
|
- Added `aiofiles.os.link`.
|
||||||
|
[#121](https://github.com/Tinche/aiofiles/pull/121)
|
||||||
|
- Added `aiofiles.os.renames`.
|
||||||
|
[#120](https://github.com/Tinche/aiofiles/pull/120)
|
||||||
|
- Added `aiofiles.os.{listdir, scandir}`.
|
||||||
|
[#143](https://github.com/Tinche/aiofiles/pull/143)
|
||||||
|
- Switched to CalVer.
|
||||||
|
- Dropped Python 3.6 support. If you require it, use version 0.8.0.
|
||||||
|
- aiofiles is now tested on Python 3.11.
|
||||||
|
|
||||||
|
#### 0.8.0 (2021-11-27)
|
||||||
|
|
||||||
|
- aiofiles is now tested on Python 3.10.
|
||||||
|
- Added `aiofiles.os.replace`.
|
||||||
|
[#107](https://github.com/Tinche/aiofiles/pull/107)
|
||||||
|
- Added `aiofiles.os.{makedirs, removedirs}`.
|
||||||
|
- Added `aiofiles.os.path.{exists, isfile, isdir, getsize, getatime, getctime, samefile, sameopenfile}`.
|
||||||
|
[#63](https://github.com/Tinche/aiofiles/pull/63)
|
||||||
|
- Added `suffix`, `prefix`, `dir` args to `aiofiles.tempfile.TemporaryDirectory`.
|
||||||
|
[#116](https://github.com/Tinche/aiofiles/pull/116)
|
||||||
|
|
||||||
|
#### 0.7.0 (2021-05-17)
|
||||||
|
|
||||||
|
- Added the `aiofiles.tempfile` module for async temporary files.
|
||||||
|
[#56](https://github.com/Tinche/aiofiles/pull/56)
|
||||||
|
- Switched to Poetry and GitHub actions.
|
||||||
|
- Dropped 3.5 support.
|
||||||
|
|
||||||
|
#### 0.6.0 (2020-10-27)
|
||||||
|
|
||||||
|
- `aiofiles` is now tested on ppc64le.
|
||||||
|
- Added `name` and `mode` properties to async file objects.
|
||||||
|
[#82](https://github.com/Tinche/aiofiles/pull/82)
|
||||||
|
- Fixed a DeprecationWarning internally.
|
||||||
|
[#75](https://github.com/Tinche/aiofiles/pull/75)
|
||||||
|
- Python 3.9 support and tests.
|
||||||
|
|
||||||
|
#### 0.5.0 (2020-04-12)
|
||||||
|
|
||||||
|
- Python 3.8 support. Code base modernization (using `async/await` instead of `asyncio.coroutine`/`yield from`).
|
||||||
|
- Added `aiofiles.os.remove`, `aiofiles.os.rename`, `aiofiles.os.mkdir`, `aiofiles.os.rmdir`.
|
||||||
|
[#62](https://github.com/Tinche/aiofiles/pull/62)
|
||||||
|
|
||||||
|
#### 0.4.0 (2018-08-11)
|
||||||
|
|
||||||
|
- Python 3.7 support.
|
||||||
|
- Removed Python 3.3/3.4 support. If you use these versions, stick to aiofiles 0.3.x.
|
||||||
|
|
||||||
|
#### 0.3.2 (2017-09-23)
|
||||||
|
|
||||||
|
- The LICENSE is now included in the sdist.
|
||||||
|
[#31](https://github.com/Tinche/aiofiles/pull/31)
|
||||||
|
|
||||||
|
#### 0.3.1 (2017-03-10)
|
||||||
|
|
||||||
|
- Introduced a changelog.
|
||||||
|
- `aiofiles.os.sendfile` will now work if the standard `os` module contains a `sendfile` function.
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
Contributions are very welcome. Tests can be run with `tox`, please ensure
|
||||||
|
the coverage at least stays the same before you submit a pull request.
|
||||||
26
venv/Lib/site-packages/aiofiles-24.1.0.dist-info/RECORD
Normal file
26
venv/Lib/site-packages/aiofiles-24.1.0.dist-info/RECORD
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
aiofiles-24.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
aiofiles-24.1.0.dist-info/METADATA,sha256=CvUJx21XclgI1Lp5Bt_4AyJesRYg0xCSx4exJZVmaSA,10708
|
||||||
|
aiofiles-24.1.0.dist-info/RECORD,,
|
||||||
|
aiofiles-24.1.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
||||||
|
aiofiles-24.1.0.dist-info/licenses/LICENSE,sha256=y16Ofl9KOYjhBjwULGDcLfdWBfTEZRXnduOspt-XbhQ,11325
|
||||||
|
aiofiles-24.1.0.dist-info/licenses/NOTICE,sha256=EExY0dRQvWR0wJ2LZLwBgnM6YKw9jCU-M0zegpRSD_E,55
|
||||||
|
aiofiles/__init__.py,sha256=1iAMJQyJtX3LGIS0AoFTJeO1aJ_RK2jpBSBhg0VoIrE,344
|
||||||
|
aiofiles/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
aiofiles/__pycache__/base.cpython-312.pyc,,
|
||||||
|
aiofiles/__pycache__/os.cpython-312.pyc,,
|
||||||
|
aiofiles/__pycache__/ospath.cpython-312.pyc,,
|
||||||
|
aiofiles/base.py,sha256=zo0FgkCqZ5aosjvxqIvDf2t-RFg1Lc6X8P6rZ56p6fQ,1784
|
||||||
|
aiofiles/os.py,sha256=0DrsG-eH4h7xRzglv9pIWsQuzqe7ZhVYw5FQS18fIys,1153
|
||||||
|
aiofiles/ospath.py,sha256=WaYelz_k6ykAFRLStr4bqYIfCVQ-5GGzIqIizykbY2Q,794
|
||||||
|
aiofiles/tempfile/__init__.py,sha256=hFSNTOjOUv371Ozdfy6FIxeln46Nm3xOVh4ZR3Q94V0,10244
|
||||||
|
aiofiles/tempfile/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
aiofiles/tempfile/__pycache__/temptypes.cpython-312.pyc,,
|
||||||
|
aiofiles/tempfile/temptypes.py,sha256=ddEvNjMLVlr7WUILCe6ypTqw77yREeIonTk16Uw_NVs,2093
|
||||||
|
aiofiles/threadpool/__init__.py,sha256=kt0hwwx3bLiYtnA1SORhW8mJ6z4W9Xr7MbY80UIJJrI,3133
|
||||||
|
aiofiles/threadpool/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
aiofiles/threadpool/__pycache__/binary.cpython-312.pyc,,
|
||||||
|
aiofiles/threadpool/__pycache__/text.cpython-312.pyc,,
|
||||||
|
aiofiles/threadpool/__pycache__/utils.cpython-312.pyc,,
|
||||||
|
aiofiles/threadpool/binary.py,sha256=hp-km9VCRu0MLz_wAEUfbCz7OL7xtn9iGAawabpnp5U,2315
|
||||||
|
aiofiles/threadpool/text.py,sha256=fNmpw2PEkj0BZSldipJXAgZqVGLxALcfOMiuDQ54Eas,1223
|
||||||
|
aiofiles/threadpool/utils.py,sha256=B59dSZwO_WZs2dFFycKeA91iD2Xq2nNw1EFF8YMBI5k,1868
|
||||||
4
venv/Lib/site-packages/aiofiles-24.1.0.dist-info/WHEEL
Normal file
4
venv/Lib/site-packages/aiofiles-24.1.0.dist-info/WHEEL
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: hatchling 1.25.0
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
@ -0,0 +1,202 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright {yyyy} {name of copyright owner}
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License 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.
|
||||||
|
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
Asyncio support for files
|
||||||
|
Copyright 2016 Tin Tvrtkovic
|
||||||
22
venv/Lib/site-packages/aiofiles/__init__.py
Normal file
22
venv/Lib/site-packages/aiofiles/__init__.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
"""Utilities for asyncio-friendly file handling."""
|
||||||
|
from .threadpool import (
|
||||||
|
open,
|
||||||
|
stdin,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
stdin_bytes,
|
||||||
|
stdout_bytes,
|
||||||
|
stderr_bytes,
|
||||||
|
)
|
||||||
|
from . import tempfile
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"open",
|
||||||
|
"tempfile",
|
||||||
|
"stdin",
|
||||||
|
"stdout",
|
||||||
|
"stderr",
|
||||||
|
"stdin_bytes",
|
||||||
|
"stdout_bytes",
|
||||||
|
"stderr_bytes",
|
||||||
|
]
|
||||||
Binary file not shown.
BIN
venv/Lib/site-packages/aiofiles/__pycache__/base.cpython-312.pyc
Normal file
BIN
venv/Lib/site-packages/aiofiles/__pycache__/base.cpython-312.pyc
Normal file
Binary file not shown.
BIN
venv/Lib/site-packages/aiofiles/__pycache__/os.cpython-312.pyc
Normal file
BIN
venv/Lib/site-packages/aiofiles/__pycache__/os.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
69
venv/Lib/site-packages/aiofiles/base.py
Normal file
69
venv/Lib/site-packages/aiofiles/base.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
"""Various base classes."""
|
||||||
|
from collections.abc import Awaitable
|
||||||
|
from contextlib import AbstractAsyncContextManager
|
||||||
|
from asyncio import get_running_loop
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncBase:
|
||||||
|
def __init__(self, file, loop, executor):
|
||||||
|
self._file = file
|
||||||
|
self._executor = executor
|
||||||
|
self._ref_loop = loop
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _loop(self):
|
||||||
|
return self._ref_loop or get_running_loop()
|
||||||
|
|
||||||
|
def __aiter__(self):
|
||||||
|
"""We are our own iterator."""
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return super().__repr__() + " wrapping " + repr(self._file)
|
||||||
|
|
||||||
|
async def __anext__(self):
|
||||||
|
"""Simulate normal file iteration."""
|
||||||
|
line = await self.readline()
|
||||||
|
if line:
|
||||||
|
return line
|
||||||
|
else:
|
||||||
|
raise StopAsyncIteration
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncIndirectBase(AsyncBase):
|
||||||
|
def __init__(self, name, loop, executor, indirect):
|
||||||
|
self._indirect = indirect
|
||||||
|
self._name = name
|
||||||
|
super().__init__(None, loop, executor)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _file(self):
|
||||||
|
return self._indirect()
|
||||||
|
|
||||||
|
@_file.setter
|
||||||
|
def _file(self, v):
|
||||||
|
pass # discard writes
|
||||||
|
|
||||||
|
|
||||||
|
class AiofilesContextManager(Awaitable, AbstractAsyncContextManager):
|
||||||
|
"""An adjusted async context manager for aiofiles."""
|
||||||
|
|
||||||
|
__slots__ = ("_coro", "_obj")
|
||||||
|
|
||||||
|
def __init__(self, coro):
|
||||||
|
self._coro = coro
|
||||||
|
self._obj = None
|
||||||
|
|
||||||
|
def __await__(self):
|
||||||
|
if self._obj is None:
|
||||||
|
self._obj = yield from self._coro.__await__()
|
||||||
|
return self._obj
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return await self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
await get_running_loop().run_in_executor(
|
||||||
|
None, self._obj._file.__exit__, exc_type, exc_val, exc_tb
|
||||||
|
)
|
||||||
|
self._obj = None
|
||||||
58
venv/Lib/site-packages/aiofiles/os.py
Normal file
58
venv/Lib/site-packages/aiofiles/os.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
"""Async executor versions of file functions from the os module."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from . import ospath as path
|
||||||
|
from .ospath import wrap
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"path",
|
||||||
|
"stat",
|
||||||
|
"rename",
|
||||||
|
"renames",
|
||||||
|
"replace",
|
||||||
|
"remove",
|
||||||
|
"unlink",
|
||||||
|
"mkdir",
|
||||||
|
"makedirs",
|
||||||
|
"rmdir",
|
||||||
|
"removedirs",
|
||||||
|
"symlink",
|
||||||
|
"readlink",
|
||||||
|
"listdir",
|
||||||
|
"scandir",
|
||||||
|
"access",
|
||||||
|
"wrap",
|
||||||
|
"getcwd",
|
||||||
|
]
|
||||||
|
if hasattr(os, "link"):
|
||||||
|
__all__ += ["link"]
|
||||||
|
if hasattr(os, "sendfile"):
|
||||||
|
__all__ += ["sendfile"]
|
||||||
|
if hasattr(os, "statvfs"):
|
||||||
|
__all__ += ["statvfs"]
|
||||||
|
|
||||||
|
|
||||||
|
stat = wrap(os.stat)
|
||||||
|
rename = wrap(os.rename)
|
||||||
|
renames = wrap(os.renames)
|
||||||
|
replace = wrap(os.replace)
|
||||||
|
remove = wrap(os.remove)
|
||||||
|
unlink = wrap(os.unlink)
|
||||||
|
mkdir = wrap(os.mkdir)
|
||||||
|
makedirs = wrap(os.makedirs)
|
||||||
|
rmdir = wrap(os.rmdir)
|
||||||
|
removedirs = wrap(os.removedirs)
|
||||||
|
symlink = wrap(os.symlink)
|
||||||
|
readlink = wrap(os.readlink)
|
||||||
|
listdir = wrap(os.listdir)
|
||||||
|
scandir = wrap(os.scandir)
|
||||||
|
access = wrap(os.access)
|
||||||
|
getcwd = wrap(os.getcwd)
|
||||||
|
|
||||||
|
if hasattr(os, "link"):
|
||||||
|
link = wrap(os.link)
|
||||||
|
if hasattr(os, "sendfile"):
|
||||||
|
sendfile = wrap(os.sendfile)
|
||||||
|
if hasattr(os, "statvfs"):
|
||||||
|
statvfs = wrap(os.statvfs)
|
||||||
30
venv/Lib/site-packages/aiofiles/ospath.py
Normal file
30
venv/Lib/site-packages/aiofiles/ospath.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"""Async executor versions of file functions from the os.path module."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from functools import partial, wraps
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
|
||||||
|
def wrap(func):
|
||||||
|
@wraps(func)
|
||||||
|
async def run(*args, loop=None, executor=None, **kwargs):
|
||||||
|
if loop is None:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
pfunc = partial(func, *args, **kwargs)
|
||||||
|
return await loop.run_in_executor(executor, pfunc)
|
||||||
|
|
||||||
|
return run
|
||||||
|
|
||||||
|
|
||||||
|
exists = wrap(path.exists)
|
||||||
|
isfile = wrap(path.isfile)
|
||||||
|
isdir = wrap(path.isdir)
|
||||||
|
islink = wrap(path.islink)
|
||||||
|
ismount = wrap(path.ismount)
|
||||||
|
getsize = wrap(path.getsize)
|
||||||
|
getmtime = wrap(path.getmtime)
|
||||||
|
getatime = wrap(path.getatime)
|
||||||
|
getctime = wrap(path.getctime)
|
||||||
|
samefile = wrap(path.samefile)
|
||||||
|
sameopenfile = wrap(path.sameopenfile)
|
||||||
|
abspath = wrap(path.abspath)
|
||||||
357
venv/Lib/site-packages/aiofiles/tempfile/__init__.py
Normal file
357
venv/Lib/site-packages/aiofiles/tempfile/__init__.py
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
import asyncio
|
||||||
|
from functools import partial, singledispatch
|
||||||
|
from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOBase
|
||||||
|
from tempfile import NamedTemporaryFile as syncNamedTemporaryFile
|
||||||
|
from tempfile import SpooledTemporaryFile as syncSpooledTemporaryFile
|
||||||
|
from tempfile import TemporaryDirectory as syncTemporaryDirectory
|
||||||
|
from tempfile import TemporaryFile as syncTemporaryFile
|
||||||
|
from tempfile import _TemporaryFileWrapper as syncTemporaryFileWrapper
|
||||||
|
|
||||||
|
from ..base import AiofilesContextManager
|
||||||
|
from ..threadpool.binary import AsyncBufferedIOBase, AsyncBufferedReader, AsyncFileIO
|
||||||
|
from ..threadpool.text import AsyncTextIOWrapper
|
||||||
|
from .temptypes import AsyncSpooledTemporaryFile, AsyncTemporaryDirectory
|
||||||
|
import sys
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"NamedTemporaryFile",
|
||||||
|
"TemporaryFile",
|
||||||
|
"SpooledTemporaryFile",
|
||||||
|
"TemporaryDirectory",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# Public methods for async open and return of temp file/directory
|
||||||
|
# objects with async interface
|
||||||
|
# ================================================================
|
||||||
|
if sys.version_info >= (3, 12):
|
||||||
|
|
||||||
|
def NamedTemporaryFile(
|
||||||
|
mode="w+b",
|
||||||
|
buffering=-1,
|
||||||
|
encoding=None,
|
||||||
|
newline=None,
|
||||||
|
suffix=None,
|
||||||
|
prefix=None,
|
||||||
|
dir=None,
|
||||||
|
delete=True,
|
||||||
|
delete_on_close=True,
|
||||||
|
loop=None,
|
||||||
|
executor=None,
|
||||||
|
):
|
||||||
|
"""Async open a named temporary file"""
|
||||||
|
return AiofilesContextManager(
|
||||||
|
_temporary_file(
|
||||||
|
named=True,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
newline=newline,
|
||||||
|
suffix=suffix,
|
||||||
|
prefix=prefix,
|
||||||
|
dir=dir,
|
||||||
|
delete=delete,
|
||||||
|
delete_on_close=delete_on_close,
|
||||||
|
loop=loop,
|
||||||
|
executor=executor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def NamedTemporaryFile(
|
||||||
|
mode="w+b",
|
||||||
|
buffering=-1,
|
||||||
|
encoding=None,
|
||||||
|
newline=None,
|
||||||
|
suffix=None,
|
||||||
|
prefix=None,
|
||||||
|
dir=None,
|
||||||
|
delete=True,
|
||||||
|
loop=None,
|
||||||
|
executor=None,
|
||||||
|
):
|
||||||
|
"""Async open a named temporary file"""
|
||||||
|
return AiofilesContextManager(
|
||||||
|
_temporary_file(
|
||||||
|
named=True,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
newline=newline,
|
||||||
|
suffix=suffix,
|
||||||
|
prefix=prefix,
|
||||||
|
dir=dir,
|
||||||
|
delete=delete,
|
||||||
|
loop=loop,
|
||||||
|
executor=executor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def TemporaryFile(
|
||||||
|
mode="w+b",
|
||||||
|
buffering=-1,
|
||||||
|
encoding=None,
|
||||||
|
newline=None,
|
||||||
|
suffix=None,
|
||||||
|
prefix=None,
|
||||||
|
dir=None,
|
||||||
|
loop=None,
|
||||||
|
executor=None,
|
||||||
|
):
|
||||||
|
"""Async open an unnamed temporary file"""
|
||||||
|
return AiofilesContextManager(
|
||||||
|
_temporary_file(
|
||||||
|
named=False,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
newline=newline,
|
||||||
|
suffix=suffix,
|
||||||
|
prefix=prefix,
|
||||||
|
dir=dir,
|
||||||
|
loop=loop,
|
||||||
|
executor=executor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def SpooledTemporaryFile(
|
||||||
|
max_size=0,
|
||||||
|
mode="w+b",
|
||||||
|
buffering=-1,
|
||||||
|
encoding=None,
|
||||||
|
newline=None,
|
||||||
|
suffix=None,
|
||||||
|
prefix=None,
|
||||||
|
dir=None,
|
||||||
|
loop=None,
|
||||||
|
executor=None,
|
||||||
|
):
|
||||||
|
"""Async open a spooled temporary file"""
|
||||||
|
return AiofilesContextManager(
|
||||||
|
_spooled_temporary_file(
|
||||||
|
max_size=max_size,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
newline=newline,
|
||||||
|
suffix=suffix,
|
||||||
|
prefix=prefix,
|
||||||
|
dir=dir,
|
||||||
|
loop=loop,
|
||||||
|
executor=executor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def TemporaryDirectory(suffix=None, prefix=None, dir=None, loop=None, executor=None):
|
||||||
|
"""Async open a temporary directory"""
|
||||||
|
return AiofilesContextManagerTempDir(
|
||||||
|
_temporary_directory(
|
||||||
|
suffix=suffix, prefix=prefix, dir=dir, loop=loop, executor=executor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# Internal coroutines to open new temp files/directories
|
||||||
|
# =========================================================
|
||||||
|
if sys.version_info >= (3, 12):
|
||||||
|
|
||||||
|
async def _temporary_file(
|
||||||
|
named=True,
|
||||||
|
mode="w+b",
|
||||||
|
buffering=-1,
|
||||||
|
encoding=None,
|
||||||
|
newline=None,
|
||||||
|
suffix=None,
|
||||||
|
prefix=None,
|
||||||
|
dir=None,
|
||||||
|
delete=True,
|
||||||
|
delete_on_close=True,
|
||||||
|
loop=None,
|
||||||
|
executor=None,
|
||||||
|
max_size=0,
|
||||||
|
):
|
||||||
|
"""Async method to open a temporary file with async interface"""
|
||||||
|
if loop is None:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
if named:
|
||||||
|
cb = partial(
|
||||||
|
syncNamedTemporaryFile,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
newline=newline,
|
||||||
|
suffix=suffix,
|
||||||
|
prefix=prefix,
|
||||||
|
dir=dir,
|
||||||
|
delete=delete,
|
||||||
|
delete_on_close=delete_on_close,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cb = partial(
|
||||||
|
syncTemporaryFile,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
newline=newline,
|
||||||
|
suffix=suffix,
|
||||||
|
prefix=prefix,
|
||||||
|
dir=dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
f = await loop.run_in_executor(executor, cb)
|
||||||
|
|
||||||
|
# Wrap based on type of underlying IO object
|
||||||
|
if type(f) is syncTemporaryFileWrapper:
|
||||||
|
# _TemporaryFileWrapper was used (named files)
|
||||||
|
result = wrap(f.file, f, loop=loop, executor=executor)
|
||||||
|
result._closer = f._closer
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
# IO object was returned directly without wrapper
|
||||||
|
return wrap(f, f, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
async def _temporary_file(
|
||||||
|
named=True,
|
||||||
|
mode="w+b",
|
||||||
|
buffering=-1,
|
||||||
|
encoding=None,
|
||||||
|
newline=None,
|
||||||
|
suffix=None,
|
||||||
|
prefix=None,
|
||||||
|
dir=None,
|
||||||
|
delete=True,
|
||||||
|
loop=None,
|
||||||
|
executor=None,
|
||||||
|
max_size=0,
|
||||||
|
):
|
||||||
|
"""Async method to open a temporary file with async interface"""
|
||||||
|
if loop is None:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
if named:
|
||||||
|
cb = partial(
|
||||||
|
syncNamedTemporaryFile,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
newline=newline,
|
||||||
|
suffix=suffix,
|
||||||
|
prefix=prefix,
|
||||||
|
dir=dir,
|
||||||
|
delete=delete,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cb = partial(
|
||||||
|
syncTemporaryFile,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
newline=newline,
|
||||||
|
suffix=suffix,
|
||||||
|
prefix=prefix,
|
||||||
|
dir=dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
f = await loop.run_in_executor(executor, cb)
|
||||||
|
|
||||||
|
# Wrap based on type of underlying IO object
|
||||||
|
if type(f) is syncTemporaryFileWrapper:
|
||||||
|
# _TemporaryFileWrapper was used (named files)
|
||||||
|
result = wrap(f.file, f, loop=loop, executor=executor)
|
||||||
|
# add delete property
|
||||||
|
result.delete = f.delete
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
# IO object was returned directly without wrapper
|
||||||
|
return wrap(f, f, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
async def _spooled_temporary_file(
|
||||||
|
max_size=0,
|
||||||
|
mode="w+b",
|
||||||
|
buffering=-1,
|
||||||
|
encoding=None,
|
||||||
|
newline=None,
|
||||||
|
suffix=None,
|
||||||
|
prefix=None,
|
||||||
|
dir=None,
|
||||||
|
loop=None,
|
||||||
|
executor=None,
|
||||||
|
):
|
||||||
|
"""Open a spooled temporary file with async interface"""
|
||||||
|
if loop is None:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
cb = partial(
|
||||||
|
syncSpooledTemporaryFile,
|
||||||
|
max_size=max_size,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
newline=newline,
|
||||||
|
suffix=suffix,
|
||||||
|
prefix=prefix,
|
||||||
|
dir=dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
f = await loop.run_in_executor(executor, cb)
|
||||||
|
|
||||||
|
# Single interface provided by SpooledTemporaryFile for all modes
|
||||||
|
return AsyncSpooledTemporaryFile(f, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
async def _temporary_directory(
|
||||||
|
suffix=None, prefix=None, dir=None, loop=None, executor=None
|
||||||
|
):
|
||||||
|
"""Async method to open a temporary directory with async interface"""
|
||||||
|
if loop is None:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
cb = partial(syncTemporaryDirectory, suffix, prefix, dir)
|
||||||
|
f = await loop.run_in_executor(executor, cb)
|
||||||
|
|
||||||
|
return AsyncTemporaryDirectory(f, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
class AiofilesContextManagerTempDir(AiofilesContextManager):
|
||||||
|
"""With returns the directory location, not the object (matching sync lib)"""
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
self._obj = await self._coro
|
||||||
|
return self._obj.name
|
||||||
|
|
||||||
|
|
||||||
|
@singledispatch
|
||||||
|
def wrap(base_io_obj, file, *, loop=None, executor=None):
|
||||||
|
"""Wrap the object with interface based on type of underlying IO"""
|
||||||
|
raise TypeError("Unsupported IO type: {}".format(base_io_obj))
|
||||||
|
|
||||||
|
|
||||||
|
@wrap.register(TextIOBase)
|
||||||
|
def _(base_io_obj, file, *, loop=None, executor=None):
|
||||||
|
return AsyncTextIOWrapper(file, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
@wrap.register(BufferedWriter)
|
||||||
|
def _(base_io_obj, file, *, loop=None, executor=None):
|
||||||
|
return AsyncBufferedIOBase(file, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
@wrap.register(BufferedReader)
|
||||||
|
@wrap.register(BufferedRandom)
|
||||||
|
def _(base_io_obj, file, *, loop=None, executor=None):
|
||||||
|
return AsyncBufferedReader(file, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
@wrap.register(FileIO)
|
||||||
|
def _(base_io_obj, file, *, loop=None, executor=None):
|
||||||
|
return AsyncFileIO(file, loop=loop, executor=executor)
|
||||||
Binary file not shown.
Binary file not shown.
69
venv/Lib/site-packages/aiofiles/tempfile/temptypes.py
Normal file
69
venv/Lib/site-packages/aiofiles/tempfile/temptypes.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
"""Async wrappers for spooled temp files and temp directory objects"""
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from ..base import AsyncBase
|
||||||
|
from ..threadpool.utils import (
|
||||||
|
cond_delegate_to_executor,
|
||||||
|
delegate_to_executor,
|
||||||
|
proxy_property_directly,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@delegate_to_executor("fileno", "rollover")
|
||||||
|
@cond_delegate_to_executor(
|
||||||
|
"close",
|
||||||
|
"flush",
|
||||||
|
"isatty",
|
||||||
|
"read",
|
||||||
|
"readline",
|
||||||
|
"readlines",
|
||||||
|
"seek",
|
||||||
|
"tell",
|
||||||
|
"truncate",
|
||||||
|
)
|
||||||
|
@proxy_property_directly("closed", "encoding", "mode", "name", "newlines")
|
||||||
|
class AsyncSpooledTemporaryFile(AsyncBase):
|
||||||
|
"""Async wrapper for SpooledTemporaryFile class"""
|
||||||
|
|
||||||
|
async def _check(self):
|
||||||
|
if self._file._rolled:
|
||||||
|
return
|
||||||
|
max_size = self._file._max_size
|
||||||
|
if max_size and self._file.tell() > max_size:
|
||||||
|
await self.rollover()
|
||||||
|
|
||||||
|
async def write(self, s):
|
||||||
|
"""Implementation to anticipate rollover"""
|
||||||
|
if self._file._rolled:
|
||||||
|
cb = partial(self._file.write, s)
|
||||||
|
return await self._loop.run_in_executor(self._executor, cb)
|
||||||
|
else:
|
||||||
|
file = self._file._file # reference underlying base IO object
|
||||||
|
rv = file.write(s)
|
||||||
|
await self._check()
|
||||||
|
return rv
|
||||||
|
|
||||||
|
async def writelines(self, iterable):
|
||||||
|
"""Implementation to anticipate rollover"""
|
||||||
|
if self._file._rolled:
|
||||||
|
cb = partial(self._file.writelines, iterable)
|
||||||
|
return await self._loop.run_in_executor(self._executor, cb)
|
||||||
|
else:
|
||||||
|
file = self._file._file # reference underlying base IO object
|
||||||
|
rv = file.writelines(iterable)
|
||||||
|
await self._check()
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
@delegate_to_executor("cleanup")
|
||||||
|
@proxy_property_directly("name")
|
||||||
|
class AsyncTemporaryDirectory:
|
||||||
|
"""Async wrapper for TemporaryDirectory class"""
|
||||||
|
|
||||||
|
def __init__(self, file, loop, executor):
|
||||||
|
self._file = file
|
||||||
|
self._loop = loop
|
||||||
|
self._executor = executor
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
await self.cleanup()
|
||||||
139
venv/Lib/site-packages/aiofiles/threadpool/__init__.py
Normal file
139
venv/Lib/site-packages/aiofiles/threadpool/__init__.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
"""Handle files using a thread pool executor."""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from functools import partial, singledispatch
|
||||||
|
from io import (
|
||||||
|
BufferedIOBase,
|
||||||
|
BufferedRandom,
|
||||||
|
BufferedReader,
|
||||||
|
BufferedWriter,
|
||||||
|
FileIO,
|
||||||
|
TextIOBase,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..base import AiofilesContextManager
|
||||||
|
from .binary import (
|
||||||
|
AsyncBufferedIOBase,
|
||||||
|
AsyncBufferedReader,
|
||||||
|
AsyncFileIO,
|
||||||
|
AsyncIndirectBufferedIOBase,
|
||||||
|
)
|
||||||
|
from .text import AsyncTextIndirectIOWrapper, AsyncTextIOWrapper
|
||||||
|
|
||||||
|
sync_open = open
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"open",
|
||||||
|
"stdin",
|
||||||
|
"stdout",
|
||||||
|
"stderr",
|
||||||
|
"stdin_bytes",
|
||||||
|
"stdout_bytes",
|
||||||
|
"stderr_bytes",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def open(
|
||||||
|
file,
|
||||||
|
mode="r",
|
||||||
|
buffering=-1,
|
||||||
|
encoding=None,
|
||||||
|
errors=None,
|
||||||
|
newline=None,
|
||||||
|
closefd=True,
|
||||||
|
opener=None,
|
||||||
|
*,
|
||||||
|
loop=None,
|
||||||
|
executor=None,
|
||||||
|
):
|
||||||
|
return AiofilesContextManager(
|
||||||
|
_open(
|
||||||
|
file,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
errors=errors,
|
||||||
|
newline=newline,
|
||||||
|
closefd=closefd,
|
||||||
|
opener=opener,
|
||||||
|
loop=loop,
|
||||||
|
executor=executor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _open(
|
||||||
|
file,
|
||||||
|
mode="r",
|
||||||
|
buffering=-1,
|
||||||
|
encoding=None,
|
||||||
|
errors=None,
|
||||||
|
newline=None,
|
||||||
|
closefd=True,
|
||||||
|
opener=None,
|
||||||
|
*,
|
||||||
|
loop=None,
|
||||||
|
executor=None,
|
||||||
|
):
|
||||||
|
"""Open an asyncio file."""
|
||||||
|
if loop is None:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
cb = partial(
|
||||||
|
sync_open,
|
||||||
|
file,
|
||||||
|
mode=mode,
|
||||||
|
buffering=buffering,
|
||||||
|
encoding=encoding,
|
||||||
|
errors=errors,
|
||||||
|
newline=newline,
|
||||||
|
closefd=closefd,
|
||||||
|
opener=opener,
|
||||||
|
)
|
||||||
|
f = await loop.run_in_executor(executor, cb)
|
||||||
|
|
||||||
|
return wrap(f, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
@singledispatch
|
||||||
|
def wrap(file, *, loop=None, executor=None):
|
||||||
|
raise TypeError("Unsupported io type: {}.".format(file))
|
||||||
|
|
||||||
|
|
||||||
|
@wrap.register(TextIOBase)
|
||||||
|
def _(file, *, loop=None, executor=None):
|
||||||
|
return AsyncTextIOWrapper(file, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
@wrap.register(BufferedWriter)
|
||||||
|
@wrap.register(BufferedIOBase)
|
||||||
|
def _(file, *, loop=None, executor=None):
|
||||||
|
return AsyncBufferedIOBase(file, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
@wrap.register(BufferedReader)
|
||||||
|
@wrap.register(BufferedRandom)
|
||||||
|
def _(file, *, loop=None, executor=None):
|
||||||
|
return AsyncBufferedReader(file, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
@wrap.register(FileIO)
|
||||||
|
def _(file, *, loop=None, executor=None):
|
||||||
|
return AsyncFileIO(file, loop=loop, executor=executor)
|
||||||
|
|
||||||
|
|
||||||
|
stdin = AsyncTextIndirectIOWrapper("sys.stdin", None, None, indirect=lambda: sys.stdin)
|
||||||
|
stdout = AsyncTextIndirectIOWrapper(
|
||||||
|
"sys.stdout", None, None, indirect=lambda: sys.stdout
|
||||||
|
)
|
||||||
|
stderr = AsyncTextIndirectIOWrapper(
|
||||||
|
"sys.stderr", None, None, indirect=lambda: sys.stderr
|
||||||
|
)
|
||||||
|
stdin_bytes = AsyncIndirectBufferedIOBase(
|
||||||
|
"sys.stdin.buffer", None, None, indirect=lambda: sys.stdin.buffer
|
||||||
|
)
|
||||||
|
stdout_bytes = AsyncIndirectBufferedIOBase(
|
||||||
|
"sys.stdout.buffer", None, None, indirect=lambda: sys.stdout.buffer
|
||||||
|
)
|
||||||
|
stderr_bytes = AsyncIndirectBufferedIOBase(
|
||||||
|
"sys.stderr.buffer", None, None, indirect=lambda: sys.stderr.buffer
|
||||||
|
)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
104
venv/Lib/site-packages/aiofiles/threadpool/binary.py
Normal file
104
venv/Lib/site-packages/aiofiles/threadpool/binary.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
from ..base import AsyncBase, AsyncIndirectBase
|
||||||
|
from .utils import delegate_to_executor, proxy_method_directly, proxy_property_directly
|
||||||
|
|
||||||
|
|
||||||
|
@delegate_to_executor(
|
||||||
|
"close",
|
||||||
|
"flush",
|
||||||
|
"isatty",
|
||||||
|
"read",
|
||||||
|
"read1",
|
||||||
|
"readinto",
|
||||||
|
"readline",
|
||||||
|
"readlines",
|
||||||
|
"seek",
|
||||||
|
"seekable",
|
||||||
|
"tell",
|
||||||
|
"truncate",
|
||||||
|
"writable",
|
||||||
|
"write",
|
||||||
|
"writelines",
|
||||||
|
)
|
||||||
|
@proxy_method_directly("detach", "fileno", "readable")
|
||||||
|
@proxy_property_directly("closed", "raw", "name", "mode")
|
||||||
|
class AsyncBufferedIOBase(AsyncBase):
|
||||||
|
"""The asyncio executor version of io.BufferedWriter and BufferedIOBase."""
|
||||||
|
|
||||||
|
|
||||||
|
@delegate_to_executor("peek")
|
||||||
|
class AsyncBufferedReader(AsyncBufferedIOBase):
|
||||||
|
"""The asyncio executor version of io.BufferedReader and Random."""
|
||||||
|
|
||||||
|
|
||||||
|
@delegate_to_executor(
|
||||||
|
"close",
|
||||||
|
"flush",
|
||||||
|
"isatty",
|
||||||
|
"read",
|
||||||
|
"readall",
|
||||||
|
"readinto",
|
||||||
|
"readline",
|
||||||
|
"readlines",
|
||||||
|
"seek",
|
||||||
|
"seekable",
|
||||||
|
"tell",
|
||||||
|
"truncate",
|
||||||
|
"writable",
|
||||||
|
"write",
|
||||||
|
"writelines",
|
||||||
|
)
|
||||||
|
@proxy_method_directly("fileno", "readable")
|
||||||
|
@proxy_property_directly("closed", "name", "mode")
|
||||||
|
class AsyncFileIO(AsyncBase):
|
||||||
|
"""The asyncio executor version of io.FileIO."""
|
||||||
|
|
||||||
|
|
||||||
|
@delegate_to_executor(
|
||||||
|
"close",
|
||||||
|
"flush",
|
||||||
|
"isatty",
|
||||||
|
"read",
|
||||||
|
"read1",
|
||||||
|
"readinto",
|
||||||
|
"readline",
|
||||||
|
"readlines",
|
||||||
|
"seek",
|
||||||
|
"seekable",
|
||||||
|
"tell",
|
||||||
|
"truncate",
|
||||||
|
"writable",
|
||||||
|
"write",
|
||||||
|
"writelines",
|
||||||
|
)
|
||||||
|
@proxy_method_directly("detach", "fileno", "readable")
|
||||||
|
@proxy_property_directly("closed", "raw", "name", "mode")
|
||||||
|
class AsyncIndirectBufferedIOBase(AsyncIndirectBase):
|
||||||
|
"""The indirect asyncio executor version of io.BufferedWriter and BufferedIOBase."""
|
||||||
|
|
||||||
|
|
||||||
|
@delegate_to_executor("peek")
|
||||||
|
class AsyncIndirectBufferedReader(AsyncIndirectBufferedIOBase):
|
||||||
|
"""The indirect asyncio executor version of io.BufferedReader and Random."""
|
||||||
|
|
||||||
|
|
||||||
|
@delegate_to_executor(
|
||||||
|
"close",
|
||||||
|
"flush",
|
||||||
|
"isatty",
|
||||||
|
"read",
|
||||||
|
"readall",
|
||||||
|
"readinto",
|
||||||
|
"readline",
|
||||||
|
"readlines",
|
||||||
|
"seek",
|
||||||
|
"seekable",
|
||||||
|
"tell",
|
||||||
|
"truncate",
|
||||||
|
"writable",
|
||||||
|
"write",
|
||||||
|
"writelines",
|
||||||
|
)
|
||||||
|
@proxy_method_directly("fileno", "readable")
|
||||||
|
@proxy_property_directly("closed", "name", "mode")
|
||||||
|
class AsyncIndirectFileIO(AsyncIndirectBase):
|
||||||
|
"""The indirect asyncio executor version of io.FileIO."""
|
||||||
64
venv/Lib/site-packages/aiofiles/threadpool/text.py
Normal file
64
venv/Lib/site-packages/aiofiles/threadpool/text.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
from ..base import AsyncBase, AsyncIndirectBase
|
||||||
|
from .utils import delegate_to_executor, proxy_method_directly, proxy_property_directly
|
||||||
|
|
||||||
|
|
||||||
|
@delegate_to_executor(
|
||||||
|
"close",
|
||||||
|
"flush",
|
||||||
|
"isatty",
|
||||||
|
"read",
|
||||||
|
"readable",
|
||||||
|
"readline",
|
||||||
|
"readlines",
|
||||||
|
"seek",
|
||||||
|
"seekable",
|
||||||
|
"tell",
|
||||||
|
"truncate",
|
||||||
|
"write",
|
||||||
|
"writable",
|
||||||
|
"writelines",
|
||||||
|
)
|
||||||
|
@proxy_method_directly("detach", "fileno", "readable")
|
||||||
|
@proxy_property_directly(
|
||||||
|
"buffer",
|
||||||
|
"closed",
|
||||||
|
"encoding",
|
||||||
|
"errors",
|
||||||
|
"line_buffering",
|
||||||
|
"newlines",
|
||||||
|
"name",
|
||||||
|
"mode",
|
||||||
|
)
|
||||||
|
class AsyncTextIOWrapper(AsyncBase):
|
||||||
|
"""The asyncio executor version of io.TextIOWrapper."""
|
||||||
|
|
||||||
|
|
||||||
|
@delegate_to_executor(
|
||||||
|
"close",
|
||||||
|
"flush",
|
||||||
|
"isatty",
|
||||||
|
"read",
|
||||||
|
"readable",
|
||||||
|
"readline",
|
||||||
|
"readlines",
|
||||||
|
"seek",
|
||||||
|
"seekable",
|
||||||
|
"tell",
|
||||||
|
"truncate",
|
||||||
|
"write",
|
||||||
|
"writable",
|
||||||
|
"writelines",
|
||||||
|
)
|
||||||
|
@proxy_method_directly("detach", "fileno", "readable")
|
||||||
|
@proxy_property_directly(
|
||||||
|
"buffer",
|
||||||
|
"closed",
|
||||||
|
"encoding",
|
||||||
|
"errors",
|
||||||
|
"line_buffering",
|
||||||
|
"newlines",
|
||||||
|
"name",
|
||||||
|
"mode",
|
||||||
|
)
|
||||||
|
class AsyncTextIndirectIOWrapper(AsyncIndirectBase):
|
||||||
|
"""The indirect asyncio executor version of io.TextIOWrapper."""
|
||||||
72
venv/Lib/site-packages/aiofiles/threadpool/utils.py
Normal file
72
venv/Lib/site-packages/aiofiles/threadpool/utils.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import functools
|
||||||
|
|
||||||
|
|
||||||
|
def delegate_to_executor(*attrs):
|
||||||
|
def cls_builder(cls):
|
||||||
|
for attr_name in attrs:
|
||||||
|
setattr(cls, attr_name, _make_delegate_method(attr_name))
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return cls_builder
|
||||||
|
|
||||||
|
|
||||||
|
def proxy_method_directly(*attrs):
|
||||||
|
def cls_builder(cls):
|
||||||
|
for attr_name in attrs:
|
||||||
|
setattr(cls, attr_name, _make_proxy_method(attr_name))
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return cls_builder
|
||||||
|
|
||||||
|
|
||||||
|
def proxy_property_directly(*attrs):
|
||||||
|
def cls_builder(cls):
|
||||||
|
for attr_name in attrs:
|
||||||
|
setattr(cls, attr_name, _make_proxy_property(attr_name))
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return cls_builder
|
||||||
|
|
||||||
|
|
||||||
|
def cond_delegate_to_executor(*attrs):
|
||||||
|
def cls_builder(cls):
|
||||||
|
for attr_name in attrs:
|
||||||
|
setattr(cls, attr_name, _make_cond_delegate_method(attr_name))
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return cls_builder
|
||||||
|
|
||||||
|
|
||||||
|
def _make_delegate_method(attr_name):
|
||||||
|
async def method(self, *args, **kwargs):
|
||||||
|
cb = functools.partial(getattr(self._file, attr_name), *args, **kwargs)
|
||||||
|
return await self._loop.run_in_executor(self._executor, cb)
|
||||||
|
|
||||||
|
return method
|
||||||
|
|
||||||
|
|
||||||
|
def _make_proxy_method(attr_name):
|
||||||
|
def method(self, *args, **kwargs):
|
||||||
|
return getattr(self._file, attr_name)(*args, **kwargs)
|
||||||
|
|
||||||
|
return method
|
||||||
|
|
||||||
|
|
||||||
|
def _make_proxy_property(attr_name):
|
||||||
|
def proxy_property(self):
|
||||||
|
return getattr(self._file, attr_name)
|
||||||
|
|
||||||
|
return property(proxy_property)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_cond_delegate_method(attr_name):
|
||||||
|
"""For spooled temp files, delegate only if rolled to file object"""
|
||||||
|
|
||||||
|
async def method(self, *args, **kwargs):
|
||||||
|
if self._file._rolled:
|
||||||
|
cb = functools.partial(getattr(self._file, attr_name), *args, **kwargs)
|
||||||
|
return await self._loop.run_in_executor(self._executor, cb)
|
||||||
|
else:
|
||||||
|
return getattr(self._file, attr_name)(*args, **kwargs)
|
||||||
|
|
||||||
|
return method
|
||||||
@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
279
venv/Lib/site-packages/aiohappyeyeballs-2.6.1.dist-info/LICENSE
Normal file
279
venv/Lib/site-packages/aiohappyeyeballs-2.6.1.dist-info/LICENSE
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
A. HISTORY OF THE SOFTWARE
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Python was created in the early 1990s by Guido van Rossum at Stichting
|
||||||
|
Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
|
||||||
|
as a successor of a language called ABC. Guido remains Python's
|
||||||
|
principal author, although it includes many contributions from others.
|
||||||
|
|
||||||
|
In 1995, Guido continued his work on Python at the Corporation for
|
||||||
|
National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
|
||||||
|
in Reston, Virginia where he released several versions of the
|
||||||
|
software.
|
||||||
|
|
||||||
|
In May 2000, Guido and the Python core development team moved to
|
||||||
|
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
|
||||||
|
year, the PythonLabs team moved to Digital Creations, which became
|
||||||
|
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
|
||||||
|
https://www.python.org/psf/) was formed, a non-profit organization
|
||||||
|
created specifically to own Python-related Intellectual Property.
|
||||||
|
Zope Corporation was a sponsoring member of the PSF.
|
||||||
|
|
||||||
|
All Python releases are Open Source (see https://opensource.org for
|
||||||
|
the Open Source Definition). Historically, most, but not all, Python
|
||||||
|
releases have also been GPL-compatible; the table below summarizes
|
||||||
|
the various releases.
|
||||||
|
|
||||||
|
Release Derived Year Owner GPL-
|
||||||
|
from compatible? (1)
|
||||||
|
|
||||||
|
0.9.0 thru 1.2 1991-1995 CWI yes
|
||||||
|
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
|
||||||
|
1.6 1.5.2 2000 CNRI no
|
||||||
|
2.0 1.6 2000 BeOpen.com no
|
||||||
|
1.6.1 1.6 2001 CNRI yes (2)
|
||||||
|
2.1 2.0+1.6.1 2001 PSF no
|
||||||
|
2.0.1 2.0+1.6.1 2001 PSF yes
|
||||||
|
2.1.1 2.1+2.0.1 2001 PSF yes
|
||||||
|
2.1.2 2.1.1 2002 PSF yes
|
||||||
|
2.1.3 2.1.2 2002 PSF yes
|
||||||
|
2.2 and above 2.1.1 2001-now PSF yes
|
||||||
|
|
||||||
|
Footnotes:
|
||||||
|
|
||||||
|
(1) GPL-compatible doesn't mean that we're distributing Python under
|
||||||
|
the GPL. All Python licenses, unlike the GPL, let you distribute
|
||||||
|
a modified version without making your changes open source. The
|
||||||
|
GPL-compatible licenses make it possible to combine Python with
|
||||||
|
other software that is released under the GPL; the others don't.
|
||||||
|
|
||||||
|
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
|
||||||
|
because its license has a choice of law clause. According to
|
||||||
|
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
|
||||||
|
is "not incompatible" with the GPL.
|
||||||
|
|
||||||
|
Thanks to the many outside volunteers who have worked under Guido's
|
||||||
|
direction to make these releases possible.
|
||||||
|
|
||||||
|
|
||||||
|
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
|
||||||
|
===============================================================
|
||||||
|
|
||||||
|
Python software and documentation are licensed under the
|
||||||
|
Python Software Foundation License Version 2.
|
||||||
|
|
||||||
|
Starting with Python 3.8.6, examples, recipes, and other code in
|
||||||
|
the documentation are dual licensed under the PSF License Version 2
|
||||||
|
and the Zero-Clause BSD license.
|
||||||
|
|
||||||
|
Some software incorporated into Python is under different licenses.
|
||||||
|
The licenses are listed with code falling under that license.
|
||||||
|
|
||||||
|
|
||||||
|
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
1. This LICENSE AGREEMENT is between the Python Software Foundation
|
||||||
|
("PSF"), and the Individual or Organization ("Licensee") accessing and
|
||||||
|
otherwise using this software ("Python") in source or binary form and
|
||||||
|
its associated documentation.
|
||||||
|
|
||||||
|
2. Subject to the terms and conditions of this License Agreement, PSF hereby
|
||||||
|
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
|
||||||
|
analyze, test, perform and/or display publicly, prepare derivative works,
|
||||||
|
distribute, and otherwise use Python alone or in any derivative version,
|
||||||
|
provided, however, that PSF's License Agreement and PSF's notice of copyright,
|
||||||
|
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
|
||||||
|
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation;
|
||||||
|
All Rights Reserved" are retained in Python alone or in any derivative version
|
||||||
|
prepared by Licensee.
|
||||||
|
|
||||||
|
3. In the event Licensee prepares a derivative work that is based on
|
||||||
|
or incorporates Python or any part thereof, and wants to make
|
||||||
|
the derivative work available to others as provided herein, then
|
||||||
|
Licensee hereby agrees to include in any such work a brief summary of
|
||||||
|
the changes made to Python.
|
||||||
|
|
||||||
|
4. PSF is making Python available to Licensee on an "AS IS"
|
||||||
|
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
|
||||||
|
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||||
|
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
|
||||||
|
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||||
|
|
||||||
|
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||||
|
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||||
|
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
|
||||||
|
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||||
|
|
||||||
|
6. This License Agreement will automatically terminate upon a material
|
||||||
|
breach of its terms and conditions.
|
||||||
|
|
||||||
|
7. Nothing in this License Agreement shall be deemed to create any
|
||||||
|
relationship of agency, partnership, or joint venture between PSF and
|
||||||
|
Licensee. This License Agreement does not grant permission to use PSF
|
||||||
|
trademarks or trade name in a trademark sense to endorse or promote
|
||||||
|
products or services of Licensee, or any third party.
|
||||||
|
|
||||||
|
8. By copying, installing or otherwise using Python, Licensee
|
||||||
|
agrees to be bound by the terms and conditions of this License
|
||||||
|
Agreement.
|
||||||
|
|
||||||
|
|
||||||
|
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
|
||||||
|
|
||||||
|
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
|
||||||
|
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
|
||||||
|
Individual or Organization ("Licensee") accessing and otherwise using
|
||||||
|
this software in source or binary form and its associated
|
||||||
|
documentation ("the Software").
|
||||||
|
|
||||||
|
2. Subject to the terms and conditions of this BeOpen Python License
|
||||||
|
Agreement, BeOpen hereby grants Licensee a non-exclusive,
|
||||||
|
royalty-free, world-wide license to reproduce, analyze, test, perform
|
||||||
|
and/or display publicly, prepare derivative works, distribute, and
|
||||||
|
otherwise use the Software alone or in any derivative version,
|
||||||
|
provided, however, that the BeOpen Python License is retained in the
|
||||||
|
Software, alone or in any derivative version prepared by Licensee.
|
||||||
|
|
||||||
|
3. BeOpen is making the Software available to Licensee on an "AS IS"
|
||||||
|
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
|
||||||
|
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||||
|
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
|
||||||
|
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||||
|
|
||||||
|
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
|
||||||
|
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
|
||||||
|
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
|
||||||
|
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||||
|
|
||||||
|
5. This License Agreement will automatically terminate upon a material
|
||||||
|
breach of its terms and conditions.
|
||||||
|
|
||||||
|
6. This License Agreement shall be governed by and interpreted in all
|
||||||
|
respects by the law of the State of California, excluding conflict of
|
||||||
|
law provisions. Nothing in this License Agreement shall be deemed to
|
||||||
|
create any relationship of agency, partnership, or joint venture
|
||||||
|
between BeOpen and Licensee. This License Agreement does not grant
|
||||||
|
permission to use BeOpen trademarks or trade names in a trademark
|
||||||
|
sense to endorse or promote products or services of Licensee, or any
|
||||||
|
third party. As an exception, the "BeOpen Python" logos available at
|
||||||
|
http://www.pythonlabs.com/logos.html may be used according to the
|
||||||
|
permissions granted on that web page.
|
||||||
|
|
||||||
|
7. By copying, installing or otherwise using the software, Licensee
|
||||||
|
agrees to be bound by the terms and conditions of this License
|
||||||
|
Agreement.
|
||||||
|
|
||||||
|
|
||||||
|
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
1. This LICENSE AGREEMENT is between the Corporation for National
|
||||||
|
Research Initiatives, having an office at 1895 Preston White Drive,
|
||||||
|
Reston, VA 20191 ("CNRI"), and the Individual or Organization
|
||||||
|
("Licensee") accessing and otherwise using Python 1.6.1 software in
|
||||||
|
source or binary form and its associated documentation.
|
||||||
|
|
||||||
|
2. Subject to the terms and conditions of this License Agreement, CNRI
|
||||||
|
hereby grants Licensee a nonexclusive, royalty-free, world-wide
|
||||||
|
license to reproduce, analyze, test, perform and/or display publicly,
|
||||||
|
prepare derivative works, distribute, and otherwise use Python 1.6.1
|
||||||
|
alone or in any derivative version, provided, however, that CNRI's
|
||||||
|
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
|
||||||
|
1995-2001 Corporation for National Research Initiatives; All Rights
|
||||||
|
Reserved" are retained in Python 1.6.1 alone or in any derivative
|
||||||
|
version prepared by Licensee. Alternately, in lieu of CNRI's License
|
||||||
|
Agreement, Licensee may substitute the following text (omitting the
|
||||||
|
quotes): "Python 1.6.1 is made available subject to the terms and
|
||||||
|
conditions in CNRI's License Agreement. This Agreement together with
|
||||||
|
Python 1.6.1 may be located on the internet using the following
|
||||||
|
unique, persistent identifier (known as a handle): 1895.22/1013. This
|
||||||
|
Agreement may also be obtained from a proxy server on the internet
|
||||||
|
using the following URL: http://hdl.handle.net/1895.22/1013".
|
||||||
|
|
||||||
|
3. In the event Licensee prepares a derivative work that is based on
|
||||||
|
or incorporates Python 1.6.1 or any part thereof, and wants to make
|
||||||
|
the derivative work available to others as provided herein, then
|
||||||
|
Licensee hereby agrees to include in any such work a brief summary of
|
||||||
|
the changes made to Python 1.6.1.
|
||||||
|
|
||||||
|
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
|
||||||
|
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
|
||||||
|
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||||
|
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
|
||||||
|
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||||
|
|
||||||
|
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||||
|
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||||
|
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
|
||||||
|
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||||
|
|
||||||
|
6. This License Agreement will automatically terminate upon a material
|
||||||
|
breach of its terms and conditions.
|
||||||
|
|
||||||
|
7. This License Agreement shall be governed by the federal
|
||||||
|
intellectual property law of the United States, including without
|
||||||
|
limitation the federal copyright law, and, to the extent such
|
||||||
|
U.S. federal law does not apply, by the law of the Commonwealth of
|
||||||
|
Virginia, excluding Virginia's conflict of law provisions.
|
||||||
|
Notwithstanding the foregoing, with regard to derivative works based
|
||||||
|
on Python 1.6.1 that incorporate non-separable material that was
|
||||||
|
previously distributed under the GNU General Public License (GPL), the
|
||||||
|
law of the Commonwealth of Virginia shall govern this License
|
||||||
|
Agreement only as to issues arising under or with respect to
|
||||||
|
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
|
||||||
|
License Agreement shall be deemed to create any relationship of
|
||||||
|
agency, partnership, or joint venture between CNRI and Licensee. This
|
||||||
|
License Agreement does not grant permission to use CNRI trademarks or
|
||||||
|
trade name in a trademark sense to endorse or promote products or
|
||||||
|
services of Licensee, or any third party.
|
||||||
|
|
||||||
|
8. By clicking on the "ACCEPT" button where indicated, or by copying,
|
||||||
|
installing or otherwise using Python 1.6.1, Licensee agrees to be
|
||||||
|
bound by the terms and conditions of this License Agreement.
|
||||||
|
|
||||||
|
ACCEPT
|
||||||
|
|
||||||
|
|
||||||
|
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
|
||||||
|
The Netherlands. All rights reserved.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and distribute this software and its
|
||||||
|
documentation for any purpose and without fee is hereby granted,
|
||||||
|
provided that the above copyright notice appear in all copies and that
|
||||||
|
both that copyright notice and this permission notice appear in
|
||||||
|
supporting documentation, and that the name of Stichting Mathematisch
|
||||||
|
Centrum or CWI not be used in advertising or publicity pertaining to
|
||||||
|
distribution of the software without specific, written prior
|
||||||
|
permission.
|
||||||
|
|
||||||
|
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||||
|
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||||
|
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
|
||||||
|
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||||
|
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
PERFORMANCE OF THIS SOFTWARE.
|
||||||
123
venv/Lib/site-packages/aiohappyeyeballs-2.6.1.dist-info/METADATA
Normal file
123
venv/Lib/site-packages/aiohappyeyeballs-2.6.1.dist-info/METADATA
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
Metadata-Version: 2.3
|
||||||
|
Name: aiohappyeyeballs
|
||||||
|
Version: 2.6.1
|
||||||
|
Summary: Happy Eyeballs for asyncio
|
||||||
|
License: PSF-2.0
|
||||||
|
Author: J. Nick Koston
|
||||||
|
Author-email: nick@koston.org
|
||||||
|
Requires-Python: >=3.9
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: Natural Language :: English
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Topic :: Software Development :: Libraries
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Classifier: License :: OSI Approved :: Python Software Foundation License
|
||||||
|
Project-URL: Bug Tracker, https://github.com/aio-libs/aiohappyeyeballs/issues
|
||||||
|
Project-URL: Changelog, https://github.com/aio-libs/aiohappyeyeballs/blob/main/CHANGELOG.md
|
||||||
|
Project-URL: Documentation, https://aiohappyeyeballs.readthedocs.io
|
||||||
|
Project-URL: Repository, https://github.com/aio-libs/aiohappyeyeballs
|
||||||
|
Description-Content-Type: text/markdown
|
||||||
|
|
||||||
|
# aiohappyeyeballs
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/aio-libs/aiohappyeyeballs/actions/workflows/ci.yml?query=branch%3Amain">
|
||||||
|
<img src="https://img.shields.io/github/actions/workflow/status/aio-libs/aiohappyeyeballs/ci-cd.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >
|
||||||
|
</a>
|
||||||
|
<a href="https://aiohappyeyeballs.readthedocs.io">
|
||||||
|
<img src="https://img.shields.io/readthedocs/aiohappyeyeballs.svg?logo=read-the-docs&logoColor=fff&style=flat-square" alt="Documentation Status">
|
||||||
|
</a>
|
||||||
|
<a href="https://codecov.io/gh/aio-libs/aiohappyeyeballs">
|
||||||
|
<img src="https://img.shields.io/codecov/c/github/aio-libs/aiohappyeyeballs.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://python-poetry.org/">
|
||||||
|
<img src="https://img.shields.io/badge/packaging-poetry-299bd7?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAASCAYAAABrXO8xAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJJSURBVHgBfZLPa1NBEMe/s7tNXoxW1KJQKaUHkXhQvHgW6UHQQ09CBS/6V3hKc/AP8CqCrUcpmop3Cx48eDB4yEECjVQrlZb80CRN8t6OM/teagVxYZi38+Yz853dJbzoMV3MM8cJUcLMSUKIE8AzQ2PieZzFxEJOHMOgMQQ+dUgSAckNXhapU/NMhDSWLs1B24A8sO1xrN4NECkcAC9ASkiIJc6k5TRiUDPhnyMMdhKc+Zx19l6SgyeW76BEONY9exVQMzKExGKwwPsCzza7KGSSWRWEQhyEaDXp6ZHEr416ygbiKYOd7TEWvvcQIeusHYMJGhTwF9y7sGnSwaWyFAiyoxzqW0PM/RjghPxF2pWReAowTEXnDh0xgcLs8l2YQmOrj3N7ByiqEoH0cARs4u78WgAVkoEDIDoOi3AkcLOHU60RIg5wC4ZuTC7FaHKQm8Hq1fQuSOBvX/sodmNJSB5geaF5CPIkUeecdMxieoRO5jz9bheL6/tXjrwCyX/UYBUcjCaWHljx1xiX6z9xEjkYAzbGVnB8pvLmyXm9ep+W8CmsSHQQY77Zx1zboxAV0w7ybMhQmfqdmmw3nEp1I0Z+FGO6M8LZdoyZnuzzBdjISicKRnpxzI9fPb+0oYXsNdyi+d3h9bm9MWYHFtPeIZfLwzmFDKy1ai3p+PDls1Llz4yyFpferxjnyjJDSEy9CaCx5m2cJPerq6Xm34eTrZt3PqxYO1XOwDYZrFlH1fWnpU38Y9HRze3lj0vOujZcXKuuXm3jP+s3KbZVra7y2EAAAAAASUVORK5CYII=" alt="Poetry">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/astral-sh/ruff">
|
||||||
|
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/pre-commit/pre-commit">
|
||||||
|
<img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square" alt="pre-commit">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://pypi.org/project/aiohappyeyeballs/">
|
||||||
|
<img src="https://img.shields.io/pypi/v/aiohappyeyeballs.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
|
||||||
|
</a>
|
||||||
|
<img src="https://img.shields.io/pypi/pyversions/aiohappyeyeballs.svg?style=flat-square&logo=python&logoColor=fff" alt="Supported Python versions">
|
||||||
|
<img src="https://img.shields.io/pypi/l/aiohappyeyeballs.svg?style=flat-square" alt="License">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Documentation**: <a href="https://aiohappyeyeballs.readthedocs.io" target="_blank">https://aiohappyeyeballs.readthedocs.io </a>
|
||||||
|
|
||||||
|
**Source Code**: <a href="https://github.com/aio-libs/aiohappyeyeballs" target="_blank">https://github.com/aio-libs/aiohappyeyeballs </a>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[Happy Eyeballs](https://en.wikipedia.org/wiki/Happy_Eyeballs)
|
||||||
|
([RFC 8305](https://www.rfc-editor.org/rfc/rfc8305.html))
|
||||||
|
|
||||||
|
## Use case
|
||||||
|
|
||||||
|
This library exists to allow connecting with
|
||||||
|
[Happy Eyeballs](https://en.wikipedia.org/wiki/Happy_Eyeballs)
|
||||||
|
([RFC 8305](https://www.rfc-editor.org/rfc/rfc8305.html))
|
||||||
|
when you
|
||||||
|
already have a list of addrinfo and not a DNS name.
|
||||||
|
|
||||||
|
The stdlib version of `loop.create_connection()`
|
||||||
|
will only work when you pass in an unresolved name which
|
||||||
|
is not a good fit when using DNS caching or resolving
|
||||||
|
names via another method such as `zeroconf`.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install this via pip (or your favourite package manager):
|
||||||
|
|
||||||
|
`pip install aiohappyeyeballs`
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[aiohappyeyeballs is licensed under the same terms as cpython itself.](https://github.com/python/cpython/blob/main/LICENSE)
|
||||||
|
|
||||||
|
## Example usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
addr_infos = await loop.getaddrinfo("example.org", 80)
|
||||||
|
|
||||||
|
socket = await start_connection(addr_infos)
|
||||||
|
socket = await start_connection(addr_infos, local_addr_infos=local_addr_infos, happy_eyeballs_delay=0.2)
|
||||||
|
|
||||||
|
transport, protocol = await loop.create_connection(
|
||||||
|
MyProtocol, sock=socket, ...)
|
||||||
|
|
||||||
|
# Remove the first address for each family from addr_info
|
||||||
|
pop_addr_infos_interleave(addr_info, 1)
|
||||||
|
|
||||||
|
# Remove all matching address from addr_info
|
||||||
|
remove_addr_infos(addr_info, "dead::beef::")
|
||||||
|
|
||||||
|
# Convert a local_addr to local_addr_infos
|
||||||
|
local_addr_infos = addr_to_addr_infos(("127.0.0.1",0))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
This package contains code from cpython and is licensed under the same terms as cpython itself.
|
||||||
|
|
||||||
|
This package was created with
|
||||||
|
[Copier](https://copier.readthedocs.io/) and the
|
||||||
|
[browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)
|
||||||
|
project template.
|
||||||
|
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
aiohappyeyeballs-2.6.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
aiohappyeyeballs-2.6.1.dist-info/LICENSE,sha256=Oy-B_iHRgcSZxZolbI4ZaEVdZonSaaqFNzv7avQdo78,13936
|
||||||
|
aiohappyeyeballs-2.6.1.dist-info/METADATA,sha256=NSXlhJwAfi380eEjAo7BQ4P_TVal9xi0qkyZWibMsVM,5915
|
||||||
|
aiohappyeyeballs-2.6.1.dist-info/RECORD,,
|
||||||
|
aiohappyeyeballs-2.6.1.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
||||||
|
aiohappyeyeballs/__init__.py,sha256=x7kktHEtaD9quBcWDJPuLeKyjuVAI-Jj14S9B_5hcTs,361
|
||||||
|
aiohappyeyeballs/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
aiohappyeyeballs/__pycache__/_staggered.cpython-312.pyc,,
|
||||||
|
aiohappyeyeballs/__pycache__/impl.cpython-312.pyc,,
|
||||||
|
aiohappyeyeballs/__pycache__/types.cpython-312.pyc,,
|
||||||
|
aiohappyeyeballs/__pycache__/utils.cpython-312.pyc,,
|
||||||
|
aiohappyeyeballs/_staggered.py,sha256=edfVowFx-P-ywJjIEF3MdPtEMVODujV6CeMYr65otac,6900
|
||||||
|
aiohappyeyeballs/impl.py,sha256=Dlcm2mTJ28ucrGnxkb_fo9CZzLAkOOBizOt7dreBbXE,9681
|
||||||
|
aiohappyeyeballs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
aiohappyeyeballs/types.py,sha256=YZJIAnyoV4Dz0WFtlaf_OyE4EW7Xus1z7aIfNI6tDDQ,425
|
||||||
|
aiohappyeyeballs/utils.py,sha256=on9GxIR0LhEfZu8P6Twi9hepX9zDanuZM20MWsb3xlQ,3028
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: poetry-core 2.1.1
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
14
venv/Lib/site-packages/aiohappyeyeballs/__init__.py
Normal file
14
venv/Lib/site-packages/aiohappyeyeballs/__init__.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
__version__ = "2.6.1"
|
||||||
|
|
||||||
|
from .impl import start_connection
|
||||||
|
from .types import AddrInfoType, SocketFactoryType
|
||||||
|
from .utils import addr_to_addr_infos, pop_addr_infos_interleave, remove_addr_infos
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"AddrInfoType",
|
||||||
|
"SocketFactoryType",
|
||||||
|
"addr_to_addr_infos",
|
||||||
|
"pop_addr_infos_interleave",
|
||||||
|
"remove_addr_infos",
|
||||||
|
"start_connection",
|
||||||
|
)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user