first commit

This commit is contained in:
2026-03-06 17:57:44 +08:00
commit 6685bdb9a8
41 changed files with 3891 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
logs/
resources/
output.md
+1
View File
@@ -0,0 +1 @@
3.11
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2026 chakcy
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+58
View File
@@ -0,0 +1,58 @@
# chongming
[TOC]
作者: chakcy
版本: 0.1.0
## 运行项目
uv 环境
```shell
uv run serve
```
纯 python 环境
```shell
python -m venv .venv
source .venv/bin/activate # windwows 命令为 .venv\Scripts\activate
pip install -r requirements.txt
pip install -e .
serve
```
## 打包项目
uv 环境
```shell
uv run build
```
纯 python 环境
```shell
source .venv/bin/activate # windwows 命令为 .venv\Scripts\activate
build
```
打包出来的文件在 build 目录下
```text
build
├── resources
│   ├── chongming.ico
│   ├── plugins.mbank
│   └── app.mbank
├── config.toml
├── db.sqlite3
└── chongming # windows chongming.exe
```
运行打包后的项目
```shell
cd build
chongming # windows chongming.exe
```
+80
View File
@@ -0,0 +1,80 @@
[default]
app.name = "chongming"
app.version = "chongming"
app.description = "基于 FastAPI 和 Tortoise-ORM 的 chongming 打包脚手架"
app.debug = false
env = "development"
[database]
type = "sqlite"
url = "sqlite+aiosqlite:///./database.db"
generate_schemas = true # 生产环境使用迁移工具
[database.sqlite]
echo = true
pool_size = 20
max_overflow = 10
pool_timeout = 30
pool_recycle = 300
pool_pre_ping = true
connect_args.timeout = 15
connect_args.check_same_thread = false
[security]
secret_key = "your-secret-key-change-in-production"
algorithm = "HS256"
access_token_expire_minutes = 30
refresh_token_expire_days = 7
jwt_cache_db_path = "jwt_cache"
max_sessions_per_user = 5
[development]
env = "development"
debug = true
[development.server]
host = "127.0.0.1"
port = 8000
reload = true
[development.logging]
level = "DEBUG"
file = "logs/dev.log"
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
[development.cors]
allow_origins = ["*"]
allow_credentials = true
allow_methods = ["*"]
allow_headers = ["*"]
[production]
env = "production"
debug = false
[production.server]
host = "0.0.0.0"
port = 8000
reload = false
workers = 4
[production.logging]
level = "INFO"
file = "logs/prod.log"
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
max_size = 10485760 # 10MB
backup_count = 5 # 最多保存 10 个日志文件
[production.cors]
allow_origins = ["https://yourdomain.com", "http://localhost:8000", "http://127.0.0.1:8000"]
allow_credentials = true
allow_methods = ["GET", "POST", "PUT", "DELETE"]
allow_headers = ["Authorization", "Content-Type"]
[production.module_system]
type = "module-bank"
path = ["resources/app.mbank", "resources/plugins.mbank"]
[production.file_system]
type = "svfs"
path = "resources/application.vfs"
Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

+78
View File
@@ -0,0 +1,78 @@
[default]
app.name = "chongming"
app.version = "0.1.0"
app.description = "基于 FastAPI 的 chongming 打包脚手架"
app.debug = false
env = "production"
[database]
type = "sqlite"
url = "sqlite+aiosqlite:///./database.db"
generate_schemas = true # 生产环境使用迁移工具
[database.sqlite]
echo = true
pool_size = 20
max_overflow = 10
pool_timeout = 30
pool_recycle = 300
pool_pre_ping = true
connect_args.timeout = 15
connect_args.check_same_thread = false
[security]
secret_key = "your-secret-key-change-in-production"
algorithm = "HS256"
access_token_expire_minutes = 30
refresh_token_expire_days = 7
jwt_cache_db_path = "jwt_cache"
max_sessions_per_user = 5
[development]
env = "development"
debug = true
[development.server]
host = "127.0.0.1"
port = 8001
reload = true
[development.logging]
level = "DEBUG"
file = "logs/dev.log"
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
[development.cors]
allow_origins = ["*"]
allow_credentials = true
allow_methods = ["*"]
allow_headers = ["*"]
[production]
env = "production"
debug = false
[production.server]
host = "0.0.0.0"
port = 8000
reload = false
workers = 4
[production.logging]
level = "INFO"
file = "logs/prod.log"
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
[production.cors]
allow_origins = ["https://yourdomain.com", "http://localhost:8000", "http://127.0.0.1:8000"]
allow_credentials = true
allow_methods = ["GET", "POST", "PUT", "DELETE"]
allow_headers = ["Authorization", "Content-Type"]
[production.module_system]
type = "module-bank"
path = ["resources/app.mbank", "resources/plugins.mbank"]
[production.file_system]
type = "svfs"
path = "resources/application.vfs"
+67
View File
@@ -0,0 +1,67 @@
# main.py - 修复 reload 问题
#!/usr/bin/env python3
"""
FastAPI + Tortoise-ORM + chongming 示例
主入口文件,适配 chongming 打包系统
"""
import sys
from utils.launch import launch
from multiprocessing import freeze_support
import server
def app_run(app_config: dict, default_config: dict):
"""
应用运行函数 - 适配模块银行环境
Args:
app_config: 应用配置
default_config: 默认配置
"""
import uvicorn
from app.core.logger import setup_logging, get_logger # type: ignore
setup_logging(app_config["logging"])
logger = get_logger("app")
# 获取服务器配置
server_config = app_config.get("server", {})
host = server_config.get("host", "0.0.0.0")
port = server_config.get("port", 8000)
# 打包环境强制禁用重载
is_frozen = getattr(sys, "frozen", False)
reload = False if is_frozen else server_config.get("reload", False)
workers = server_config.get("workers", 4)
# 显示启动信息
env = app_config.get("env", "development")
logger.info("=" * 50)
logger.info(f"🚀 启动 {default_config.get('app.name', 'FastAPI 应用')}")
logger.info(f"📦 版本: {default_config.get('app.version', '1.0.0')}")
logger.info(f"🌍 环境: {env}")
logger.info(f"📍 地址: http://{host}:{port}")
logger.info(f"📚 文档: http://{host}:{port}/docs")
logger.info(f"🔧 调试: {'启用' if app_config.get('debug', False) else '禁用'}")
logger.info(f"🔄 重载: {'启用' if reload else '禁用'}")
logger.info(f"👥 Workers: {workers}")
logger.info("=" * 50)
uvicorn.run(
"server:app",
host=host,
port=port,
reload=reload,
workers=workers,
log_level="debug" if app_config.get("debug", False) else "info",
access_log=True,
)
def main():
freeze_support()
launch(app_run, "production")
if __name__ == "__main__":
main()
+19
View File
@@ -0,0 +1,19 @@
# server.py
import sys
from utils.launch import init_module_bank
from utils.config import ConfigLoader
config = ConfigLoader("config.toml")
init_module_bank(config)
# 从数据库导入真正的 app 模块
try:
import app # type: ignore # 此时会从 SQLite 数据库加载
app_instance = app.app # 获取 FastAPI 实例
except ImportError as e:
print(f"无法从数据库导入 app 模块: {e}")
sys.exit(1)
# 暴露 ASGI 应用变量,供 uvicorn 使用
app = app_instance
View File
+88
View File
@@ -0,0 +1,88 @@
# config_loader.py - 读取config.toml的示例代码
import os
import sys
from pathlib import Path
from typing import Any, Dict, Optional
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
class ConfigLoader:
"""TOML配置文件加载器"""
def __init__(self, config_path: Optional[str] = None):
"""
初始化配置加载器
Args:
config_path: 配置文件路径,如果为None则自动查找
"""
if config_path is None:
# 自动查找配置文件
config_path = self._find_config_file()
self.config_path = Path(config_path)
self._config: Dict[str, Any] = {}
self.load()
def _find_config_file(self) -> str:
"""自动查找配置文件"""
possible_paths = [
"config.toml",
"config/config.toml",
"~/.config/myapp/config.toml",
"/etc/myapp/config.toml",
]
for path in possible_paths:
expanded_path = os.path.expanduser(path)
if os.path.exists(expanded_path):
return expanded_path
raise FileNotFoundError("未找到配置文件")
def load(self) -> None:
"""加载配置文件"""
if not self.config_path.exists():
raise FileNotFoundError(f"配置文件不存在: {self.config_path}")
with open(self.config_path, "r", encoding="utf-8") as f:
self._config = tomllib.loads(f.read())
def get(self, key: str, default: Any = None) -> Any:
"""获取配置值,支持点号分隔的路径"""
keys = key.split(".")
value = self._config
try:
for k in keys:
value = value[k]
return value
except (KeyError, TypeError):
return default
def get_section(self, section: str) -> Dict[str, Any]:
"""获取整个配置节"""
return self._config.get(section, {})
def reload(self) -> None:
"""重新加载配置文件"""
self.load()
def __getitem__(self, key: str) -> Any:
"""支持字典式访问"""
return self.get(key)
def __contains__(self, key: str) -> bool:
"""检查配置是否存在"""
return self.get(key) is not None
@property
def all(self) -> Dict[str, Any]:
"""获取所有配置"""
return self._config
+32
View File
@@ -0,0 +1,32 @@
from .config import ConfigLoader
import os
from module_bank import PythonToSQLite
from typing import Callable
key = None
def init_module_bank(config):
MODULE_BANK_PATH_LIST = config["production"]["module_system"]["path"]
for module_path in MODULE_BANK_PATH_LIST:
if os.path.exists(module_path) == False:
raise ValueError(f"模块路径不存在: {module_path}")
packer_list = [
PythonToSQLite(module_path, key=key) for module_path in MODULE_BANK_PATH_LIST
]
for packer in packer_list:
modules = packer.list_modules()
for module in modules:
print(f"{module['module_name']} {'[包]' if module['is_package'] else ''}")
packer.install_importer(key=key)
def launch(run_fun: Callable[[dict, dict], None], run_mode: str):
config = ConfigLoader("config.toml")
# 获取对应运行模式的配置
default_config = config.get("default", {})
app_config = config.get(run_mode, {})
run_fun(app_config, default_config)
+45
View File
@@ -0,0 +1,45 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "chongming"
version = "0.1.0"
description = "基于 modules-bank 的 fastapi 打包脚手架"
readme = "README.md"
requires-python = ">=3.8"
dependencies = [
"aiosqlite",
"bcrypt==4.0.1",
"fastapi[uvicorn]",
"module-bank",
"passlib[bcrypt]",
"pyjwt",
"sqlite-vfs",
"sqlitedict",
"sqlmodel[aiosqlite]",
"tomli",
"typing-extensions",
"uvicorn",
]
[[tool.uv.index]]
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
default = true
[[tool.uv.index]]
url = "https://mirrors.ustc.edu.cn/pypi/web/simple/"
[dependency-groups]
dev = [
"pyarmor",
"pyinstaller",
]
[tool.hatch.build.targets.wheel]
packages = ["src/chongming"]
[project.scripts]
build = "chongming.scripts.build:build"
serve = "chongming:serve"
+9
View File
@@ -0,0 +1,9 @@
{
"venvPath": ".",
"venv": ".venv",
"pythonVersion": "3.11",
"extraPaths": [
"./",
"./.venv/lib/python3.12/site-packages"
]
}
+16
View File
@@ -0,0 +1,16 @@
aerich
fastapi[uvicorn]
module-bank==0.1.1
passlib
pydantic-settings
pydantic[email]
sqlite-vfs
toml
tortoise-orm
uvicorn[standard]
sqlitedict
pyjwt
bcrypt==4.0.1
pyinstaller
aerich
requests
+19
View File
@@ -0,0 +1,19 @@
import uvicorn
from .app.core.config import load_config
from .app.core.logger import setup_logging, get_logger
def serve():
config = load_config()
env = config["default"]["env"]
server_config = config[env]["server"]
log_config = config[env]["logging"]
setup_logging(log_config)
logget = get_logger("app")
logget.info(f"启动服务: {server_config}")
logget.info(f"启动日志: {log_config}")
uvicorn.run("chongming.app:app", **server_config)
if __name__ == "__main__":
serve()
+105
View File
@@ -0,0 +1,105 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from sqlmodel import SQLModel, text
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.exc import OperationalError
from .core.config import get_config
from .core.jwt_cache import get_jwt_cache
from .core.logger import get_logger
from .api import api_router
config = get_config()
logger = get_logger("app")
try:
env = config["default"]["env"]
except KeyError: # pragma: no cover
raise ValueError("配置不存在")
# 生命周期管理
@asynccontextmanager
async def lifespan(app: FastAPI):
from plugins import hello
hello()
database = config["database"]
database_type = database["type"]
database_config = database.get(database_type, None)
if database_config is None:
raise ValueError("配置不存在")
# 启动时:创建引擎和会话工厂,初始化数据库表
engine = create_async_engine(database["url"], **database_config)
async_session_maker = async_sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
# 将资源存储到 app.state
app.state.engine = engine
app.state.async_session_maker = async_session_maker
# 创建表(异步方式)
if env == "development":
async with engine.begin() as conn:
if database_type == "sqlite":
await conn.execute(text("PRAGMA journal_mode=WAL"))
await conn.execute(text("PRAGMA synchronous=NORMAL"))
try:
await conn.run_sync(
lambda conn: SQLModel.metadata.create_all(
bind=conn, checkfirst=True
)
)
except OperationalError as e:
if "already exists" in str(e):
raise
yield # 应用运行期间
# 关闭时:释放引擎
await engine.dispose()
get_jwt_cache().close()
cors = config[env].get("cors", {})
# 创建 FastAPI 应用
app = FastAPI(
title=config["default"]["app"]["name"],
version=config["default"]["app"]["version"],
description=config["default"]["app"]["description"],
docs_url="/docs" if config[env]["debug"] else None,
redoc_url="/redoc" if config[env]["debug"] else None,
openapi_url="/openapi.json" if config[env]["debug"] else None,
lifespan=lifespan,
)
# 添加 CORS 中间件
app.add_middleware(CORSMiddleware, **cors)
app.include_router(api_router, prefix="/api/v1")
# 根路由
@app.get("/", summary="根路径")
async def root():
"""根路径,返回应用信息"""
return {
"message": f"欢迎使用 {config["default"]["app"]["name"]}",
"version": config["default"]["app"]["version"],
"docs": "/docs" if config[env]["debug"] else None,
"health": "/api/v1/health",
}
# 全局异常处理器
@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
"""全局异常处理器"""
logger.error(f"全局异常: {exc}")
return {
"error": "内部服务器错误",
"detail": str(exc) if config[env]["debug"] else "请查看服务器日志",
}
+18
View File
@@ -0,0 +1,18 @@
"""
API 路由
"""
from fastapi import APIRouter
from .routers import todo, auth
# 创建主路由
api_router = APIRouter()
# 注册子路由
api_router.include_router(todo.router, prefix="/todos", tags=["todo"])
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
# 导出路由
__all__ = ["api_router"]
+31
View File
@@ -0,0 +1,31 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from ..service.auth import get_auth_service
from ..core.jwt_cache import TokenData
security = HTTPBearer()
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
) -> TokenData:
"""
获取当前用户依赖项
验证访问令牌并返回用户信息
"""
auth_service = get_auth_service()
token = credentials.credentials
token_data = await auth_service.validate_access_token(token)
if token_data is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的访问令牌",
headers={"WWW-Authenticate": "Bearer"},
)
return token_data
+134
View File
@@ -0,0 +1,134 @@
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from ...core.database import get_session
from ...core.jwt_cache import get_jwt_cache, TokenData
from ...core.security import verify_password
from ...service.user import UserService
from ...service.auth import get_auth_service
from ...model.user import UserCreate, UserRead, UserLogin
from ..deps import get_current_user
router = APIRouter(tags=["authentication"])
security = HTTPBearer()
@router.post("/register", response_model=UserRead)
async def register(
user_data: UserCreate,
session: AsyncSession = Depends(get_session),
):
print(user_data.password)
existing = await UserService.get_user_by_email(user_data.email, session)
if existing:
raise HTTPException(status_code=400, detail="邮箱已被注册")
user = await UserService.create_user(
session=session,
email=user_data.email,
password=user_data.password,
username=user_data.username,
full_name=user_data.full_name,
)
return user
@router.post("/login")
async def login(
user_data: UserLogin,
request: Request,
session: AsyncSession = Depends(get_session),
):
user = await UserService.get_user_by_email(user_data.email, session)
if not user:
raise HTTPException(status_code=400, detail="邮箱或密码错误")
if not verify_password(user_data.password, user.hashed_password):
raise HTTPException(status_code=400, detail="邮箱或密码错误")
if not user.is_active:
raise HTTPException(status_code=400, detail="用户被禁用")
cache = get_jwt_cache()
user_data_dict = {
"sub": str(user.id),
"email": user.email,
"username": user.username,
}
token = cache.create_token(
user_id=str(user.id),
user_data=user_data_dict,
token_type="access",
device_id=None,
user_agent=request.headers.get("user-agent") if request else None,
ip_address=request.client.host if request else None, # type: ignore
)
return {"access_token": token, "token_type": "bearer"}
@router.post("/refresh", response_model=dict, summary="刷新访问令牌")
async def refresh_token(
request: Request,
credentials: HTTPAuthorizationCredentials = Depends(security),
session: AsyncSession = Depends(get_session),
):
auth_service = get_auth_service()
refresh_token = credentials.credentials
new_access_token = await auth_service.refresh_access_token(
refresh_token, session, request
)
if not new_access_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的刷新令牌",
headers={"WWW-Authenticate": "Bearer"},
)
return {"access_token": new_access_token, "token_type": "bearer"}
@router.post("/logout")
async def logout(
credentials: HTTPAuthorizationCredentials = Depends(security),
):
auth_service = get_auth_service()
token = credentials.credentials
await auth_service.logout(token)
return {"message": "登出成功"}
@router.post("/logout-all", summary="等出所有设备")
async def logout_all(current_user: TokenData = Depends(get_current_user)):
auth_service = get_auth_service()
await auth_service.logout_all(current_user.user_id)
return {"message": "所有设备已登出"}
@router.get("/sessions", summary="获取用户会话")
async def get_sessions(current_user: TokenData = Depends(get_current_user)):
"""获取用户的所有活跃会话"""
auth_service = get_auth_service()
sessions = await auth_service.get_user_sessions(current_user.user_id)
return {
"user_id": current_user.user_id,
"sessions": sessions,
"count": len(sessions),
}
@router.get("/me", response_model=UserRead, summary="获取当前用户信息")
async def get_current_user_info(
session: AsyncSession = Depends(get_session),
current_user: TokenData = Depends(get_current_user),
):
"""获取当前登录用户的信息"""
user = await UserService.get_user_by_id(int(current_user.user_id), session)
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
return user
+59
View File
@@ -0,0 +1,59 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from ...model.todo import TodoCreate, TodoUpdate, TodoRead
from ...core.database import get_session
from ...service.todo import TodoService
from ...model.user import User
from ..deps import get_current_user
router = APIRouter()
# -------------------- 路由 --------------------
@router.post("/", response_model=TodoRead, dependencies=[Depends(get_current_user)])
async def create_todo(
todo: TodoCreate,
session: AsyncSession = Depends(get_session),
):
return await TodoService.create_todo(todo, session)
@router.get(
"/", response_model=List[TodoRead], dependencies=[Depends(get_current_user)]
)
async def read_todos(
offset: int = 0, limit: int = 100, session: AsyncSession = Depends(get_session)
):
return await TodoService.get_todos(offset, limit, session)
@router.get(
"/{todo_id}", response_model=TodoRead, dependencies=[Depends(get_current_user)]
)
async def read_todo(todo_id: int, session: AsyncSession = Depends(get_session)):
todo = await TodoService.get_todo_by_id(todo_id, session)
if not todo:
raise HTTPException(status_code=404, detail="Todo not found")
return todo
@router.patch(
"/{todo_id}", response_model=TodoRead, dependencies=[Depends(get_current_user)]
)
async def update_todo(
todo_id: int, todo_update: TodoUpdate, session: AsyncSession = Depends(get_session)
):
todo = await TodoService.update_todo(todo_id, todo_update, session)
return todo
@router.delete("/{todo_id}", dependencies=[Depends(get_current_user)])
async def delete_todo(todo_id: int, session: AsyncSession = Depends(get_session)):
is_deleted = await TodoService.delete_todo(todo_id, session)
if not is_deleted:
raise HTTPException(status_code=404, detail="Todo not found")
return {"ok": True}
View File
+125
View File
@@ -0,0 +1,125 @@
import sys
from pathlib import Path
from typing import TypedDict, Literal, Optional, cast
if sys.version_info >= (3, 11):
import tomllib
from typing import Required, NotRequired
else:
import tomli as tomllib
from typing_extensions import Required, NotRequired
# 设置 env 为 development 或 production
class DefaultApp(TypedDict):
name: str
version: str
description: str
debug: bool
class Default(TypedDict):
app: DefaultApp
env: Literal["production", "development"]
class SqliteConnectionArgs(TypedDict, total=False):
timeout: int
check_same_thread: bool
class DatabaseSqlite(TypedDict, total=False):
echo: bool
pool_size: int
max_overflow: int
pool_timeout: int
pool_recycle: int
pool_pre_ping: bool
connect_args: SqliteConnectionArgs
class Database(TypedDict, total=False):
type: Required[Literal["sqlite", "mysql", "postgresql"]]
url: Required[str]
generate_schemas: bool
sqlite: DatabaseSqlite
mysql: dict
postgresql: dict
class Security(TypedDict):
secret_key: str
algorithm: Literal["HS256"]
access_token_expire_minutes: int
jwt_cache_db_path: str
refresh_token_expire_days: int
max_sessions_per_user: int
class Server(TypedDict, total=False):
host: Required[str]
port: Required[int]
reload: bool
workers: int
class Logging(TypedDict):
level: str
file: str
format: str
max_size: int
backup_count: int
class Cors(TypedDict, total=False):
allow_origins: list[str]
allow_credentials: bool
allow_methods: list[str]
allow_headers: list[str]
class ModuleSystem(TypedDict, total=False):
type: str
path: list[str]
class FileSystem(TypedDict, total=False):
type: str
path: list[str]
class Env(TypedDict, total=False):
debug: Required[bool]
server: Required[Server]
logging: Required[Logging]
cors: Cors
module_system: ModuleSystem
file_system: FileSystem
class Config(TypedDict):
default: Default
database: Database
security: Security
development: Env
production: Env
def load_config(config_path: Path = Path("config.toml")) -> Config:
"""从 TOML 文件加载配置"""
if not config_path.exists():
raise FileNotFoundError(f"配置文件不存在: {config_path}")
with open(config_path, "rb") as f:
return cast(Config, tomllib.load(f))
_config: Optional[Config] = None
def get_config(config_path: str = "config.toml") -> Config:
global _config
if _config is None:
_config = load_config(Path(config_path))
return _config
+11
View File
@@ -0,0 +1,11 @@
from typing import AsyncGenerator
from fastapi import Request
from sqlalchemy.ext.asyncio import AsyncSession
# 获取数据库会话的依赖
async def get_session(request: Request) -> AsyncGenerator[AsyncSession, None]:
async_session_maker = request.app.state.async_session_maker
async with async_session_maker() as session:
yield session
+459
View File
@@ -0,0 +1,459 @@
import jwt
import json
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List, TypedDict
from sqlitedict import SqliteDict
from pydantic import BaseModel
from dataclasses import dataclass, asdict
# from pydantic import BaseModel
import hashlib
import threading
from pathlib import Path
from .config import get_config
@dataclass
class TokenCacheData:
"""Token 缓存数据结构"""
token_hash: str
user_id: str
device_id: Optional[str] = None
user_agent: Optional[str] = None
ip_address: Optional[str] = None
created_at: Optional[str] = None
expires_at: Optional[str] = None
last_used: Optional[str] = None
is_active: bool = True
payload_data: Optional[Dict[str, Any]] = None
token_type: str = "access" # access 或 refresh
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.utcnow().isoformat()
if self.last_used is None:
self.last_used = datetime.utcnow().isoformat()
class JWTCache:
"""JWT Token 缓存管理器"""
def __init__(self, db_path: Optional[str] = None):
self.config = get_config()
if db_path is None:
db_path = self.config["security"]["jwt_cache_db_path"]
db_file_path = Path(db_path)
db_file_path.mkdir(parents=True, exist_ok=True)
self.lock = threading.RLock()
# 主 Token 缓存
self.token_db = SqliteDict(
rf"{db_path}/tokens.cache",
tablename="tokens",
autocommit=True,
encode=self._json_encode,
decode=self._json_decode,
journal_mode="WAL",
)
# 黑名单缓存
self.blacklist_db = SqliteDict(
rf"{db_path}/blacklist.cache",
tablename="blacklist",
autocommit=True,
encode=json.dumps,
decode=json.loads,
journal_mode="WAL",
)
# 用户会话索引
self.user_sessions_db = SqliteDict(
rf"{db_path}/user_sessions.cache",
tablename="user_sessions",
autocommit=True,
encode=json.dumps,
decode=json.loads,
journal_mode="WAL",
)
self.secret_key = self.config["security"]["secret_key"]
self.algorithm = self.config["security"]["algorithm"]
def _json_encode(self, obj):
"""自定义 JSON 编码器,处理 datetime 对象"""
def convert_datetime(o):
if isinstance(o, datetime):
return o.isoformat()
raise TypeError(
f"Object of type {o.__class__.__name__} is not JSON serializable"
)
return json.dumps(obj, default=convert_datetime)
def _json_decode(self, s):
"""自定义 JSON 解码器"""
return json.loads(s)
def _hash_token(self, token: str) -> str:
"""Token 哈希化(避免存储明文 Token)"""
return hashlib.sha256(token.encode()).hexdigest()
def create_token(
self,
user_id: str,
user_data: Dict[str, Any],
token_type: str = "access",
device_id: Optional[str] = None,
user_agent: Optional[str] = None,
ip_address: Optional[str] = None,
) -> str:
"""
创建并缓存 JWT Token
Args:
user_id: 用户ID
user_data: 用户数据
token_type: Token 类型(access 或 refresh)
device_id: 设备ID
user_agent: 用户代理
ip_address: IP地址
Returns:
JWT Token 字符串
"""
with self.lock:
# 计算过期时间
if token_type == "access":
expire_minutes = self.config["security"]["access_token_expire_minutes"]
expires_delta = timedelta(minutes=expire_minutes)
else:
expire_days = self.config["security"]["refresh_token_expire_days"]
expires_delta = timedelta(days=expire_days)
expire = datetime.utcnow() + expires_delta
# 构建 payload
payload = {
"sub": user_id,
"exp": expire,
"iat": datetime.utcnow(),
"type": token_type,
"data": user_data,
"device_id": device_id,
"jti": self._generate_jti(), # JWT ID,唯一标识
}
# 生成 JWT Token
token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
if isinstance(token, bytes):
token = token.decode("utf-8")
# 创建缓存数据
token_hash = self._hash_token(token)
cache_data = TokenCacheData(
token_hash=token_hash,
user_id=user_id,
device_id=device_id,
user_agent=user_agent,
ip_address=ip_address,
expires_at=expire.isoformat(), # 转换为字符串
payload_data=payload,
token_type=token_type,
)
# 存储到缓存
self.token_db[token_hash] = asdict(cache_data)
# 更新用户会话索引
self._update_user_session_index(user_id, token_hash, token_type)
# 检查用户会话数限制
self._enforce_session_limit(user_id)
return token
def _generate_jti(self) -> str:
"""生成唯一的 JWT ID"""
import uuid
return str(uuid.uuid4())
def _update_user_session_index(
self, user_id: str, token_hash: str, token_type: str
):
"""更新用户会话索引"""
with self.lock:
key = f"{user_id}:{token_type}"
user_tokens = self.user_sessions_db.get(key, [])
# 添加新 token
user_tokens.append(token_hash)
# 只保留最近的一定数量的 token
max_tokens = 100 # 每个用户每种类型最多保留100个 token
if len(user_tokens) > max_tokens:
user_tokens = user_tokens[-max_tokens:]
self.user_sessions_db[key] = user_tokens
def _enforce_session_limit(self, user_id: str):
"""强制用户会话限制"""
max_sessions = self.config["security"]["max_sessions_per_user"]
access_key = f"{user_id}:access"
refresh_key = f"{user_id}:refresh"
# 检查 access token 数量
access_tokens = self.user_sessions_db.get(access_key, [])
if len(access_tokens) > max_sessions:
# 删除最早的 token
tokens_to_remove = access_tokens[:-max_sessions]
for token_hash in tokens_to_remove:
if token_hash in self.token_db:
del self.token_db[token_hash]
# 更新索引
self.user_sessions_db[access_key] = access_tokens[-max_sessions:]
# 检查 refresh token 数量
refresh_tokens = self.user_sessions_db.get(refresh_key, [])
if len(refresh_tokens) > max_sessions:
tokens_to_remove = refresh_tokens[:-max_sessions]
for token_hash in tokens_to_remove:
if token_hash in self.token_db:
del self.token_db[token_hash]
self.user_sessions_db[refresh_key] = refresh_tokens[-max_sessions:]
def validate_token(
self, token: str, token_type: str = "access"
) -> Optional[Dict[str, Any]]:
"""
验证 JWT Token
Args:
token: JWT Token
token_type: 期望的 Token 类型
Returns:
验证成功返回用户数据,失败返回 None
"""
try:
# 解码验证 Token
payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
# 检查 Token 类型
print(f"payload: {payload}")
if payload.get("type") != token_type:
return None
user_id = payload.get("sub")
jti = payload.get("jti")
if not user_id or not jti:
return None
# 检查黑名单
token_hash = self._hash_token(token)
if self._is_blacklisted(token_hash):
return None
# 检查缓存
with self.lock:
cache_data = self.token_db.get(token_hash)
if not cache_data:
return None
# 检查 Token 是否激活
if not cache_data.get("is_active", True):
return None
# 检查是否过期
expires_at = datetime.fromisoformat(cache_data["expires_at"])
if datetime.utcnow() > expires_at:
# 标记为不活跃
cache_data["is_active"] = False
self.token_db[token_hash] = cache_data
return None
# 更新最后使用时间
cache_data["last_used"] = datetime.utcnow().isoformat()
self.token_db[token_hash] = cache_data
return {
"user_id": user_id,
"device_id": cache_data.get("device_id"),
"payload": payload,
"cache_data": cache_data,
}
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
except Exception:
return None
def add_to_blacklist(self, token: str, reason: str = "logout"):
"""添加 Token 到黑名单"""
token_hash = self._hash_token(token)
expire_time = datetime.utcnow() + timedelta(days=7) # 黑名单保留7天
self.blacklist_db[token_hash] = {
"reason": reason,
"blacklisted_at": datetime.utcnow().isoformat(),
"expires_at": expire_time.isoformat(),
}
def _is_blacklisted(self, token_hash: str) -> bool:
"""检查 Token 是否在黑名单中"""
with self.lock:
if token_hash not in self.blacklist_db:
return False
blacklist_data = self.blacklist_db[token_hash]
expires_at = datetime.fromisoformat(blacklist_data["expires_at"])
# 如果黑名单条目已过期,删除它
if datetime.utcnow() > expires_at:
del self.blacklist_db[token_hash]
return False
return True
def invalidate_token(self, token: str):
"""使单个 Token 失效"""
with self.lock:
token_hash = self._hash_token(token)
# 添加到黑名单
self.add_to_blacklist(token)
# 标记为不活跃
if token_hash in self.token_db:
cache_data = self.token_db[token_hash]
cache_data["is_active"] = False
self.token_db[token_hash] = cache_data
def invalidate_user_tokens(self, user_id: str, token_type: Optional[str] = None):
"""使用户的所有 Token 失效"""
with self.lock:
if token_type:
keys = [f"{user_id}:{token_type}"]
else:
keys = [f"{user_id}:access", f"{user_id}:refresh"]
for key in keys:
token_hashes = self.user_sessions_db.get(key, [])
for token_hash in token_hashes:
# 添加到黑名单
# 注意:这里我们只有 token_hash,没有原始 token
# 所以只能标记为不活跃
if token_hash in self.token_db:
cache_data = self.token_db[token_hash]
cache_data["is_active"] = False
self.token_db[token_hash] = cache_data
# 清空用户会话索引
self.user_sessions_db[key] = []
def get_user_sessions(self, user_id: str) -> List[Dict[str, Any]]:
"""获取用户的所有活跃会话"""
with self.lock:
sessions = []
for token_type in ["access", "refresh"]:
key = f"{user_id}:{token_type}"
token_hashes = self.user_sessions_db.get(key, [])
for token_hash in token_hashes:
if token_hash in self.token_db:
cache_data = self.token_db[token_hash]
# 检查是否活跃且未过期
if cache_data.get("is_active", True):
expires_at = datetime.fromisoformat(
cache_data["expires_at"]
)
if datetime.utcnow() <= expires_at:
sessions.append(
{
"token_type": token_type,
"device_id": cache_data.get("device_id"),
"user_agent": cache_data.get("user_agent"),
"ip_address": cache_data.get("ip_address"),
"created_at": cache_data["created_at"],
"last_used": cache_data["last_used"],
"expires_at": cache_data["expires_at"],
}
)
return sessions
def cleanup_expired(self):
"""清理过期数据"""
with self.lock:
cleaned_count = 0
current_time = datetime.utcnow()
# 清理过期 Token
for token_hash, data in list(self.token_db.items()):
expires_at = datetime.fromisoformat(data["expires_at"])
if current_time > expires_at:
del self.token_db[token_hash]
cleaned_count += 1
# 清理过期黑名单
for token_hash, data in list(self.blacklist_db.items()):
expires_at = datetime.fromisoformat(data["expires_at"])
if current_time > expires_at:
del self.blacklist_db[token_hash]
cleaned_count += 1
return cleaned_count
def close(self):
with self.lock:
"""关闭所有数据库连接"""
self.token_db.close()
self.blacklist_db.close()
self.user_sessions_db.close()
# 全局缓存实例
_jwt_cache = None
def get_jwt_cache() -> JWTCache:
"""获取 JWT 缓存实例(单例模式)"""
global _jwt_cache
if _jwt_cache is None:
_jwt_cache = JWTCache()
return _jwt_cache
# Token 响应模型
class TokenResponse(BaseModel):
"""Token 响应模型"""
access_token: str
refresh_token: str
token_type: str = "bearer"
expires_in: int
refresh_expires_in: int
# Token 数据模型
class TokenData(BaseModel):
"""Token 数据模型"""
user_id: str
username: Optional[str] = None
email: Optional[str] = None
scopes: List[str] = []
+64
View File
@@ -0,0 +1,64 @@
import logging
from pathlib import Path
from .config import Logging
from logging.handlers import RotatingFileHandler
class ExcludeWatchFilesFilter(logging.Filter):
def filter(self, record):
# 如果日志记录来自 watchfiles 模块,则不记录(返回 False)
return not record.name.startswith("watchfiles")
def setup_logging(log_config: Logging):
log_level = getattr(logging, log_config["level"], logging.INFO)
log_format = log_config["format"]
# 创建文件夹
log_file_path = Path(log_config["file"])
log_file_dir = log_file_path.parent
if log_file_dir != Path("."):
log_file_dir.mkdir(parents=True, exist_ok=True)
# 设置特定日志级别的过滤
logging.getLogger("watchfiles").setLevel(logging.WARNING)
# 配置根日志记录器
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
# 创建格式化器
formatter = logging.Formatter(log_format)
# 创建轮转文件处理器 - 限制文件大小为 10MB,保留 5 个备份文件
max_bytes = log_config.get("max_size", 10 * 1024 * 1024) # 默认 10MB
backup_count = log_config.get("backup_count", 5) # 默认保留 5 个备份
file_handler = RotatingFileHandler(
log_config["file"],
encoding="utf-8",
maxBytes=max_bytes,
backupCount=backup_count,
)
file_handler.setFormatter(formatter)
file_handler.addFilter(ExcludeWatchFilesFilter())
# 创建控制台处理器并添加过滤器
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
console_handler.addFilter(ExcludeWatchFilesFilter())
# 添加处理器到根日志记录器
root_logger.addHandler(file_handler)
root_logger.addHandler(console_handler)
print(
f"日志系统已配置 - 级别: {logging.getLevelName(log_level)}, 文件: {log_config['file']} "
f"(最大:{max_bytes / 1024 / 1024:.1f}MB, 备份:{backup_count})"
)
def get_logger(name: str) -> logging.Logger:
logger = logging.getLogger(name)
# 为每个logger添加过滤器
logger.addFilter(ExcludeWatchFilesFilter())
return logger
+14
View File
@@ -0,0 +1,14 @@
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""验证密码"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""密码哈希"""
return pwd_context.hash(password)
View File
+28
View File
@@ -0,0 +1,28 @@
from typing import Optional
from sqlmodel import SQLModel, Field
# 定义模型
class TodoBase(SQLModel):
title: str
description: Optional[str] = None
completed: bool = False
class Todo(TodoBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
class TodoCreate(TodoBase):
pass
class TodoRead(TodoBase):
id: int
class TodoUpdate(SQLModel):
title: Optional[str] = None
description: Optional[str] = None
completed: Optional[bool] = None
+36
View File
@@ -0,0 +1,36 @@
from typing import Optional
from sqlmodel import SQLModel, Field
from pydantic import BaseModel
class UserBase(SQLModel):
email: str = Field(unique=True, index=True)
username: Optional[str] = Field(default=None, max_length=50)
full_name: Optional[str] = Field(default=None, max_length=100)
is_active: bool = Field(default=True)
is_superuser: bool = Field(default=False)
created_at: Optional[str] = Field(default=None) # 可自动填充,此处略
class User(UserBase, table=True):
"""用户模型"""
id: Optional[int] = Field(default=None, primary_key=True)
hashed_password: str
class UserCreate(UserBase):
"""用户创建模型"""
password: str
class UserRead(UserBase):
"""用户读取模型"""
id: int
class UserLogin(BaseModel):
email: str
password: str
+166
View File
@@ -0,0 +1,166 @@
"""
认证服务 - 处理用户认证和 Token 管理
"""
from typing import Optional
from fastapi import Request
from passlib.context import CryptContext
from sqlalchemy.ext.asyncio import AsyncSession
from ..model.user import User
from .user import UserService
from ..core.jwt_cache import get_jwt_cache, TokenResponse, TokenData
from ..core.config import get_config
class AuthService:
"""认证服务"""
def __init__(self):
self.jwt_cache = get_jwt_cache()
self.settings = get_config()
self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
async def authenticate_user(
self, email: str, password: str, session: AsyncSession
) -> Optional[User]:
"""验证用户凭据"""
user = await UserService.get_user_by_email(email, session)
if not user:
return None
if not self.pwd_context.verify(password, user.hashed_password):
return None
return user
async def create_tokens(
self,
user: User,
request: Optional[Request] = None,
device_id: Optional[str] = None,
) -> TokenResponse:
"""为用户创建访问和刷新令牌"""
# 获取用户数据
user_data = {
"username": user.username,
"email": user.email,
"is_superuser": user.is_superuser,
}
# 获取设备信息
user_agent = None
ip_address = None
if request:
user_agent = request.headers.get("user-agent")
ip_address = request.client.host if request.client else None
# 创建访问令牌
access_token = self.jwt_cache.create_token(
user_id=str(user.id),
user_data=user_data,
token_type="access",
device_id=device_id,
user_agent=user_agent,
ip_address=ip_address,
)
# 创建刷新令牌
refresh_token = self.jwt_cache.create_token(
user_id=str(user.id),
user_data=user_data,
token_type="refresh",
device_id=device_id,
user_agent=user_agent,
ip_address=ip_address,
)
return TokenResponse(
access_token=access_token,
refresh_token=refresh_token,
expires_in=self.settings["security"]["access_token_expire_minutes"] * 60,
refresh_expires_in=getattr(self.settings, "refresh_token_expire_days", 7)
* 24
* 60
* 60,
)
async def validate_access_token(self, token: str) -> Optional[TokenData]:
"""验证访问令牌"""
validation = self.jwt_cache.validate_token(token, token_type="access")
if not validation:
return None
return TokenData(
user_id=validation["user_id"],
username=validation["payload"]["data"].get("username"),
email=validation["payload"]["data"].get("email"),
scopes=[], # 可以根据需要添加权限范围
)
async def refresh_access_token(
self,
refresh_token: str,
session: AsyncSession,
request: Optional[Request] = None,
) -> Optional[str]:
"""使用刷新令牌获取新的访问令牌"""
validation = self.jwt_cache.validate_token(refresh_token, token_type="access")
if not validation:
return None
user_id = validation["user_id"]
# 获取用户
user = await UserService.get_user_by_id(int(user_id), session)
if not user:
return None
# 创建新的访问令牌
user_data = {
"username": user.username,
"email": user.email,
"is_superuser": user.is_superuser,
}
device_id = validation.get("device_id")
user_agent = None
ip_address = None
if request:
user_agent = request.headers.get("user-agent")
ip_address = request.client.host if request.client else None
new_access_token = self.jwt_cache.create_token(
user_id=user_id,
user_data=user_data,
token_type="access",
device_id=device_id,
user_agent=user_agent,
ip_address=ip_address,
)
return new_access_token
async def logout(self, token: str):
"""用户登出 - 使令牌失效"""
self.jwt_cache.invalidate_token(token)
async def logout_all(self, user_id: str):
"""使用户的所有令牌失效"""
self.jwt_cache.invalidate_user_tokens(str(user_id))
async def get_user_sessions(self, user_id: str):
"""获取用户的活跃会话"""
return self.jwt_cache.get_user_sessions(str(user_id))
# 全局认证服务实例
_auth_service = None
def get_auth_service() -> AuthService:
"""获取认证服务实例(单例模式)"""
global _auth_service
if _auth_service is None:
_auth_service = AuthService()
return _auth_service
+65
View File
@@ -0,0 +1,65 @@
from typing import Sequence, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import select
from ..model.todo import TodoCreate, Todo, TodoUpdate
class TodoService:
@staticmethod
async def create_todo(todo: TodoCreate, session: AsyncSession) -> Todo:
db_todo = Todo.model_validate(todo)
session.add(db_todo)
await session.commit()
await session.refresh(db_todo)
return db_todo
@staticmethod
async def get_todos(
offset: int,
limit: int,
session: AsyncSession,
) -> Sequence[Todo]:
result = await session.execute(select(Todo).offset(offset).limit(limit))
todos = result.scalars().all()
return todos
@staticmethod
async def get_todo_by_id(
todo_id: int,
session: AsyncSession,
) -> Optional[Todo]:
todo = await session.get(Todo, todo_id)
return todo
@staticmethod
async def update_todo(
todo_id: int,
todo_update: TodoUpdate,
session: AsyncSession,
) -> Optional[Todo]:
todo = await session.get(Todo, todo_id)
if not todo:
return None
update_data = todo_update.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(todo, key, value)
session.add(todo)
await session.commit()
await session.refresh(todo)
return todo
@staticmethod
async def delete_todo(
todo_id: int,
session: AsyncSession,
) -> bool:
todo = await session.get(Todo, todo_id)
if not todo:
return False
await session.delete(todo)
await session.commit()
return True
+28
View File
@@ -0,0 +1,28 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Optional
from ..model.user import User
from ..core.security import get_password_hash
class UserService:
@staticmethod
async def get_user_by_email(email: str, session: AsyncSession) -> Optional[User]:
result = await session.execute(select(User).where(User.email == email)) # type: ignore
return result.scalar_one_or_none()
@staticmethod
async def get_user_by_id(user_id: int, session: AsyncSession) -> Optional[User]:
return await session.get(User, user_id)
@staticmethod
async def create_user(
session: AsyncSession, email: str, password: str, **kwargs
) -> User:
hashed = get_password_hash(password)
user = User(email=email, hashed_password=hashed, **kwargs)
session.add(user)
await session.commit()
await session.refresh(user)
return user
View File
+253
View File
@@ -0,0 +1,253 @@
from module_bank import PythonToSQLite
from module_bank.encryption import Encryption
import os
from pathlib import Path
import shutil
import subprocess
import sys
import asyncio
def create_admin():
try:
cmd = [
sys.executable,
"../src/chongming/scripts/create_admin.py",
]
print(f"正在运行 create_admin.py: {' '.join(cmd)}")
# 创建包含 UTF-8 编码的环境变量
env = os.environ.copy()
env["PYTHONIOENCODING"] = "utf-8"
env["PYTHONUTF8"] = "1"
result = subprocess.run(
cmd,
cwd="./build",
capture_output=True,
text=True,
encoding="utf-8", # 明确指定编码
errors="replace", # 替换无法解码的字符
env=env, # 传递修改后的环境变量
)
if result.returncode == 0:
print("创建管理员用户成功")
print(result.stdout)
else:
print("创建管理员用户失败")
# 安全地打印错误信息,避免编码问题
try:
print("错误信息:", result.stderr)
except UnicodeEncodeError:
print(
"错误信息:", result.stderr.encode("ascii", "ignore").decode("ascii")
)
except Exception as e:
print("创建管理员用户失败")
print(f"异常: {e}")
def run_pyarmor_obfuscate():
# pyarmor obfuscate --output=obfuscated_script.py original_script.py
obfuscate_files = [
("main.py", "./"),
("server.py", "./"),
("utils/launch.py", "./utils"),
("utils/config.py", "./utils"),
]
for file, output in obfuscate_files:
cmd = [
"pyarmor",
"gen",
f"--output={output}",
file,
]
result = subprocess.run(cmd, cwd="./build", capture_output=True, text=True)
if result.returncode != 0:
print(f"打包文件: {file} 失败")
print(result.stderr)
return
else:
print(result.stdout)
print(f"打包文件: {file}")
def run_pyinstaller():
"""运行 PyInstaller 打包成可执行文件"""
try:
# 构建 PyInstaller 命令
cmd = [
sys.executable,
"-m",
"PyInstaller",
"--onefile",
"--name",
"chongming",
"--distpath",
".",
"--workpath",
"./build_temp",
"--specpath",
"./specs",
"--clean",
# 隐藏导入
"--hidden-import",
"utils",
"--hidden-import",
"utils.launch",
"--hidden-import",
"utils.config",
"--hidden-import",
"server",
"--hidden-import",
"module_bank",
"--hidden-import",
"fastapi",
"--hidden-import",
"fastapi.middleware.cors",
"--hidden-import",
"uvicorn",
"--hidden-import",
"sqlmodel",
"--hidden-import",
"aiosqlite",
"--hidden-import",
"pyjwt",
"--hidden-import",
"sqlitedict",
"--hidden-import",
"passlib",
"--hidden-import",
"passlib.context",
"--hidden-import",
"passlib.handlers.bcrypt",
"--hidden-import",
"jwt",
# 其他选项
"--icon",
"../../public/chongming.ico",
"main.py",
]
print(f"正在运行 PyInstaller: {' '.join(cmd)}")
result = subprocess.run(cmd, cwd="./build", capture_output=True, text=True)
if result.returncode == 0:
print("PyInstaller 打包成功!")
print(result.stdout)
else:
print("PyInstaller 打包失败!")
print("错误信息:", result.stderr)
except FileNotFoundError:
print("错误:未找到 PyInstaller,请先安装 PyInstaller:pip install pyinstaller")
except Exception as e:
print(f"PyInstaller 执行出错: {e}")
async def init_database():
from sqlalchemy.ext.asyncio import create_async_engine
from ..app.core.config import get_config
from sqlmodel import SQLModel, text
from sqlalchemy.exc import OperationalError
database = get_config()["database"]
database_type = database["type"]
database_config = database.get(database_type, None)
if database_config is None:
raise ValueError("配置不存在")
engine = create_async_engine(
"sqlite+aiosqlite:///./build/database.db", **database_config
)
async with engine.begin() as conn:
if database_type == "sqlite":
await conn.execute(text("PRAGMA journal_mode=WAL"))
await conn.execute(text("PRAGMA synchronous=NORMAL"))
try:
await conn.run_sync(
lambda conn: SQLModel.metadata.create_all(bind=conn, checkfirst=True)
)
except OperationalError as e:
if "already exists" in str(e):
raise
async def main():
# 打包模块
key = Encryption.generate_key()
resources_path = Path("resources")
if not resources_path.exists():
resources_path.mkdir()
app_packer = PythonToSQLite("./resources/app.mbank", key)
app_packer.pack_directory("src/chongming/app", "app")
app_packer.verify_package_structure()
app_packer.delete_source_code(None)
plugin_packer = PythonToSQLite("./resources/plugins.mbank", key)
plugin_packer.pack_directory("src/plugins", "plugins")
plugin_packer.verify_package_structure()
plugin_packer.delete_source_code(None)
# 创建 build 目录
build_path = Path("build")
if build_path.exists() and build_path.is_dir():
shutil.rmtree("build")
os.makedirs("build", exist_ok=True)
# 复制 utils 文件夹到 build 文件夹
utils_path = Path("utils")
if utils_path.exists():
target_utils_path = Path("build") / "utils"
shutil.copytree(utils_path, target_utils_path, dirs_exist_ok=True)
print(f"已复制 utils 文件夹到 {target_utils_path}")
# 复制 resources 文件夹到 build 文件夹
resources_path = Path("resources")
if resources_path.exists():
target_resources_path = Path("build") / "resources"
shutil.copytree(resources_path, target_resources_path, dirs_exist_ok=True)
print(f"已复制 resources 文件夹到 {target_resources_path}")
print("模块打包完成")
# 复制 main.py 到 build 文件夹
shutil.copy("public/main.py", "build")
shutil.copy("public/server.py", "build")
# 复制 config.toml 到 build 文件夹
shutil.copy("public/config.toml", "build")
# 复制 utils 文件夹到 build 文件夹
shutil.copytree("public/utils", "build/utils")
with open("./build/utils/launch.py", "r", encoding="utf-8") as f:
context = f.read()
context = context.replace("key = None", f"key = '{key}'")
with open("./build/utils/launch.py", "w", encoding="utf-8") as f:
f.write(context)
# 混淆
run_pyarmor_obfuscate()
# 运行 PyInstaller
run_pyinstaller()
# 清理临时文件
shutil.rmtree(r"build/build_temp", ignore_errors=True)
shutil.rmtree(r"build/specs", ignore_errors=True)
shutil.rmtree(r"build/utils", ignore_errors=True)
shutil.rmtree(r"build/pyarmor_runtime_000000", ignore_errors=True)
os.remove(r"build/main.py")
os.remove(r"build/server.py")
# os.remove(r"build/config.toml")
print("构建完成")
# 初始化数据库
await init_database()
def build():
asyncio.run(main())
+12
View File
@@ -0,0 +1,12 @@
import os
import shutil
# 需要遍历的目录
root_dir = "./"
# 遍历目录
for dirpath, dirnames, filenames in os.walk(root_dir):
if "__pycache__" in dirnames:
# 获取 __pycache__ 目录的全路径
pycache_dir = os.path.join(dirpath, "__pycache__")
# 删除目录
shutil.rmtree(pycache_dir)
print(f"Removed: {pycache_dir}")
+36
View File
@@ -0,0 +1,36 @@
import os
import sys
def generate_markdown_from_py_files(directory, output_file):
with open(output_file, "w", encoding="utf-8") as md_file:
for root, dirs, files in os.walk(directory):
# 排除 venv 目录
dirs[:] = [d for d in dirs if d != ".venv"]
dirs[:] = [d for d in dirs if d != ".vscode"]
# dirs[:] = [d for d in dirs if d != "scripts"]
dirs[:] = [d for d in dirs if d != "build"]
for file in files:
if (
file.endswith(".py")
or file.endswith(".rs")
or file.endswith(".toml")
or file.endswith(".md")
):
file_path = os.path.join(root, file)
if file == "output.md":
continue
md_file.write(f"`{file_path}`\n")
md_file.write("```python\n")
with open(file_path, "r", encoding="utf-8") as py_file:
md_file.write(py_file.read())
md_file.write("\n```\n\n")
if __name__ == "__main__":
# 指定目录和输出文件名
target_directory = sys.argv[1] # 替换为你的目标目录
output_markdown_file = "output.md" # 输出的 Markdown 文件名
generate_markdown_from_py_files(target_directory, output_markdown_file)
print(f"Markdown 文件已生成:{output_markdown_file}")
+2
View File
@@ -0,0 +1,2 @@
def hello():
print("Hello World")
Generated
+1711
View File
File diff suppressed because it is too large Load Diff