build(backend): manage dependencies with uv

BREAKING CHANGE: apps/backend/requirements.txt is no longer the backend dependency source. Use uv sync and uv run python main.py for backend setup and startup.
This commit is contained in:
2026-05-03 17:19:27 +08:00
parent d4d6f87730
commit f8dcf6e3d3
12 changed files with 2613 additions and 81 deletions
+3
View File
@@ -8,6 +8,8 @@ __pycache__/
venv/ venv/
env/ env/
ENV/ ENV/
*.egg-info/
.pytest_cache/
# 项目特定 # 项目特定
chromedriver chromedriver
@@ -20,6 +22,7 @@ debug_screenshot.png
# 运行时文件 # 运行时文件
sessions/ sessions/
*.lock *.lock
!/uv.lock
*.log *.log
*.pid *.pid
backend.pid backend.pid
+5 -7
View File
@@ -27,6 +27,7 @@
### 环境要求 ### 环境要求
- Python 3.9+ - Python 3.9+
- uv
- Node.js 16+ - Node.js 16+
- Chrome 浏览器 - Chrome 浏览器
@@ -34,11 +35,8 @@
```bash ```bash
# 后端 # 后端
python -m venv venv uv sync
venv\Scripts\activate # Windows uv run python main.py backend
source venv/bin/activate # Linux/Mac
pip install -r apps/backend/requirements.txt
python main.py backend
# 前端 # 前端
cd apps/frontend cd apps/frontend
@@ -46,7 +44,7 @@ npm install
npm run dev npm run dev
# 创建管理员 # 创建管理员
PYTHONPATH=apps python apps/backend/scripts/create_admin.py uv run python apps/backend/scripts/create_admin.py
``` ```
### 访问地址 ### 访问地址
@@ -57,7 +55,7 @@ PYTHONPATH=apps python apps/backend/scripts/create_admin.py
## 进程管理 ## 进程管理
```bash ```bash
python main.py backend-daemon uv run python main.py backend-daemon
python main.py frontend-daemon python main.py frontend-daemon
python main.py status python main.py status
python main.py stop [all|backend|frontend] python main.py stop [all|backend|frontend]
-27
View File
@@ -1,27 +0,0 @@
# Web Framework
fastapi>=0.115.12
uvicorn[standard]>=0.34.0
# Database
sqlalchemy>=2.0.36
# Validation & Settings
pydantic[email]>=2.10.6
pydantic-settings>=2.7.1
python-dotenv>=1.0.1
# Authentication & Security
pyjwt>=2.10.1
bcrypt>=4.2.2
slowapi>=0.1.9
# Task Scheduling
apscheduler>=3.10.4
croniter>=5.0.3
# Automation
selenium>=4.28.1
filelock>=3.16.1
# HTTP & Utilities
requests>=2.32.3
+1 -4
View File
@@ -3,10 +3,7 @@
创建管理员用户的脚本 创建管理员用户的脚本
使用方法: 使用方法:
PYTHONPATH=apps python apps/backend/scripts/create_admin.py uv run python apps/backend/scripts/create_admin.py
或使用虚拟环境:
PYTHONPATH=apps ./venv/bin/python apps/backend/scripts/create_admin.py
""" """
import sys import sys
from pathlib import Path from pathlib import Path
@@ -7,8 +7,7 @@
- last_failed_login: 最后一次登录失败时间 - last_failed_login: 最后一次登录失败时间
运行方式: 运行方式:
PYTHONPATH=apps python -m backend.scripts.migrate_add_account_lockout uv run python -m backend.scripts.migrate_add_account_lockout
python -m backend.scripts.migrate_add_account_lockout
""" """
import sys import sys
+2 -2
View File
@@ -36,8 +36,8 @@ User=username
# Example: /home/username/CheckInApp # Example: /home/username/CheckInApp
WorkingDirectory=/path/to/CheckInApp WorkingDirectory=/path/to/CheckInApp
# Start backend using the Python project manager # Start backend using the uv-managed Python project manager
ExecStart=/path/to/CheckInApp/venv/bin/python /path/to/CheckInApp/main.py backend --no-reload ExecStart=/usr/bin/env uv run python /path/to/CheckInApp/main.py backend --no-reload
# Restart policy # Restart policy
Restart=on-failure Restart=on-failure
+6 -7
View File
@@ -6,6 +6,7 @@
- Ubuntu 20.04+ / CentOS 7+ / Windows Server - Ubuntu 20.04+ / CentOS 7+ / Windows Server
- Python 3.9+ - Python 3.9+
- uv
- Node.js 16+ - Node.js 16+
- Chrome / Chromium - Chrome / Chromium
- 2GB+ RAM - 2GB+ RAM
@@ -16,9 +17,11 @@
# Ubuntu # Ubuntu
sudo apt update sudo apt update
sudo apt install -y python3 nodejs npm chromium-browser sudo apt install -y python3 nodejs npm chromium-browser
curl -LsSf https://astral.sh/uv/install.sh | sh
# CentOS # CentOS
sudo yum install -y python3 nodejs npm chromium sudo yum install -y python3 nodejs npm chromium
curl -LsSf https://astral.sh/uv/install.sh | sh
``` ```
## 生产部署 ## 生产部署
@@ -32,15 +35,11 @@ sudo yum install -y python3 nodejs npm chromium
git clone <repository> git clone <repository>
cd CheckInApp cd CheckInApp
# 创建虚拟环境
python3 -m venv venv
source venv/bin/activate
# 安装依赖 # 安装依赖
pip install -r apps/backend/requirements.txt uv sync
# 生产环境额外依赖 # 生产环境额外依赖
pip install gunicorn uv sync --extra production
# 配置环境变量 # 配置环境变量
cp .env.example .env cp .env.example .env
@@ -174,7 +173,7 @@ server {
```python ```python
# 安装 redis # 安装 redis
pip install redis uv sync --extra redis
# 配置会话存储 # 配置会话存储
REDIS_URL=redis://localhost:6379/0 REDIS_URL=redis://localhost:6379/0
+6 -14
View File
@@ -9,18 +9,10 @@
git clone <repository> git clone <repository>
cd CheckInApp cd CheckInApp
# 创建虚拟环境 # 安装后端依赖
python -m venv venv uv sync
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
# 安装依赖 uv run python main.py backend
pip install -r apps/backend/requirements.txt
# 安装开发依赖
pip install pytest pytest-asyncio black flake8
python main.py backend
``` ```
### 前端开发 ### 前端开发
@@ -214,7 +206,7 @@ export const useTagStore = defineStore('tag', {
# 手动创建脚本在 apps/backend/scripts/migrate_*.py # 手动创建脚本在 apps/backend/scripts/migrate_*.py
# 执行迁移 # 执行迁移
PYTHONPATH=apps python apps/backend/scripts/migrate_xxx.py uv run python apps/backend/scripts/migrate_xxx.py
``` ```
### 测试 ### 测试
@@ -232,7 +224,7 @@ def test_create_task():
assert task.is_active == True assert task.is_active == True
# 运行测试 # 运行测试
PYTHONPATH=apps pytest apps/backend/tests/ uv run pytest tests/
``` ```
#### 前端测试 #### 前端测试
@@ -257,7 +249,7 @@ npm run test
### 后端规范 ### 后端规范
- 使用 Black 格式化: `black apps/backend/` - 使用 Black 格式化: `uv run black apps/backend/`
- 遵循 PEP 8 - 遵循 PEP 8
- 函数添加类型注解 - 函数添加类型注解
- API 路由使用 Pydantic 模型验证 - API 路由使用 Pydantic 模型验证
+12 -10
View File
@@ -8,7 +8,6 @@ import os
import signal import signal
import subprocess import subprocess
import sys import sys
import time
from pathlib import Path from pathlib import Path
@@ -16,8 +15,7 @@ REPO_ROOT = Path(__file__).resolve().parent
APPS_DIR = REPO_ROOT / "apps" APPS_DIR = REPO_ROOT / "apps"
BACKEND_DIR = APPS_DIR / "backend" BACKEND_DIR = APPS_DIR / "backend"
FRONTEND_DIR = APPS_DIR / "frontend" FRONTEND_DIR = APPS_DIR / "frontend"
VENV_DIR = REPO_ROOT / "venv" UV_BIN = os.environ.get("UV", "uv")
PYTHON_BIN = VENV_DIR / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
BACKEND_PID = REPO_ROOT / "backend.pid" BACKEND_PID = REPO_ROOT / "backend.pid"
FRONTEND_PID = REPO_ROOT / "frontend.pid" FRONTEND_PID = REPO_ROOT / "frontend.pid"
LOGS_DIR = REPO_ROOT / "logs" LOGS_DIR = REPO_ROOT / "logs"
@@ -41,11 +39,13 @@ def ensure_runtime_dirs() -> None:
def get_python() -> str: def get_python() -> str:
if PYTHON_BIN.exists():
return str(PYTHON_BIN)
return sys.executable return sys.executable
def backend_manager_command(*args: str) -> list[str]:
return [UV_BIN, "run", "python", str(REPO_ROOT / "main.py"), *args]
def read_pid(path: Path) -> int | None: def read_pid(path: Path) -> int | None:
try: try:
return int(path.read_text(encoding="utf-8").strip()) return int(path.read_text(encoding="utf-8").strip())
@@ -93,7 +93,7 @@ def run_backend(args: argparse.Namespace) -> int:
import backend.main # noqa: F401 import backend.main # noqa: F401
except ModuleNotFoundError as exc: except ModuleNotFoundError as exc:
print(f"backend import path OK; missing dependency: {exc.name}", file=sys.stderr) print(f"backend import path OK; missing dependency: {exc.name}", file=sys.stderr)
print(f"install dependencies with: {get_python()} -m pip install -r apps/backend/requirements.txt", file=sys.stderr) print("install dependencies with: uv sync", file=sys.stderr)
return 2 return 2
print("backend.main:app import OK") print("backend.main:app import OK")
return 0 return 0
@@ -121,9 +121,7 @@ def start_backend_daemon(args: argparse.Namespace) -> int:
return 0 return 0
BACKEND_PID.unlink(missing_ok=True) BACKEND_PID.unlink(missing_ok=True)
cmd = [ cmd = backend_manager_command(
get_python(),
str(REPO_ROOT / "main.py"),
"backend", "backend",
"--host", "--host",
args.host, args.host,
@@ -132,9 +130,10 @@ def start_backend_daemon(args: argparse.Namespace) -> int:
"--no-reload", "--no-reload",
"--log-level", "--log-level",
args.log_level, args.log_level,
] )
BACKEND_LOG.parent.mkdir(parents=True, exist_ok=True) BACKEND_LOG.parent.mkdir(parents=True, exist_ok=True)
log_file = BACKEND_LOG.open("a", encoding="utf-8") log_file = BACKEND_LOG.open("a", encoding="utf-8")
try:
proc = subprocess.Popen( proc = subprocess.Popen(
cmd, cmd,
cwd=REPO_ROOT, cwd=REPO_ROOT,
@@ -143,6 +142,9 @@ def start_backend_daemon(args: argparse.Namespace) -> int:
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
start_new_session=os.name != "nt", start_new_session=os.name != "nt",
) )
except FileNotFoundError:
print("uv executable not found; install uv and run: uv sync", file=sys.stderr)
return 1
BACKEND_PID.write_text(str(proc.pid), encoding="utf-8") BACKEND_PID.write_text(str(proc.pid), encoding="utf-8")
print(f"backend: started pid {proc.pid}") print(f"backend: started pid {proc.pid}")
print(f"log: {BACKEND_LOG}") print(f"log: {BACKEND_LOG}")
+47
View File
@@ -0,0 +1,47 @@
[project]
name = "checkin-app-backend"
version = "0.1.0"
description = "FastAPI backend for CheckIn App"
readme = "README.md"
requires-python = ">=3.9"
dependencies = [
"apscheduler>=3.10.4",
"bcrypt>=4.2.2",
"croniter>=5.0.3",
"fastapi>=0.115.12",
"filelock>=3.16.1",
"pydantic[email]>=2.10.6",
"pydantic-settings>=2.7.1",
"pyjwt>=2.10.1",
"python-dotenv>=1.0.1",
"requests>=2.32.3",
"selenium>=4.28.1",
"slowapi>=0.1.9",
"sqlalchemy>=2.0.36",
"uvicorn[standard]>=0.34.0",
]
[project.optional-dependencies]
production = [
"gunicorn>=23.0.0",
]
redis = [
"redis>=5.0.0",
]
[dependency-groups]
dev = [
"black>=24.0.0",
"flake8>=7.0.0",
"pytest>=8.0.0",
"pytest-asyncio>=0.24.0",
]
[build-system]
requires = ["setuptools>=69"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["apps"]
include = ["backend*"]
namespaces = true
+61
View File
@@ -0,0 +1,61 @@
from __future__ import annotations
import builtins
import io
import importlib.util
import types
import unittest
from contextlib import redirect_stderr
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import Mock, patch
MAIN_PATH = Path(__file__).resolve().parents[1] / "main.py"
MAIN_SPEC = importlib.util.spec_from_file_location("main", MAIN_PATH)
assert MAIN_SPEC is not None and MAIN_SPEC.loader is not None
main = importlib.util.module_from_spec(MAIN_SPEC)
MAIN_SPEC.loader.exec_module(main)
class BackendManagerUvTests(unittest.TestCase):
def test_backend_check_missing_dependency_points_to_uv_sync(self) -> None:
original_import = builtins.__import__
def fake_import(name, globals=None, locals=None, fromlist=(), level=0):
if name == "backend.main":
raise ModuleNotFoundError("No module named 'fastapi'", name="fastapi")
return original_import(name, globals, locals, fromlist, level)
args = types.SimpleNamespace(check=True)
stderr = io.StringIO()
with patch("builtins.__import__", side_effect=fake_import), redirect_stderr(stderr):
exit_code = main.run_backend(args)
self.assertEqual(exit_code, 2)
self.assertIn("uv sync", stderr.getvalue())
self.assertNotIn("pip install -r", stderr.getvalue())
def test_backend_daemon_uses_uv_run_python(self) -> None:
args = types.SimpleNamespace(host="127.0.0.1", port=8000, log_level="info")
proc = Mock(pid=12345)
with TemporaryDirectory() as tmp_dir:
tmp = Path(tmp_dir)
with (
patch.object(main, "BACKEND_PID", tmp / "backend.pid"),
patch.object(main, "BACKEND_LOG", tmp / "backend.log"),
patch.object(main, "LOGS_DIR", tmp),
patch("subprocess.Popen", return_value=proc) as popen,
):
exit_code = main.start_backend_daemon(args)
self.assertEqual(exit_code, 0)
cmd = popen.call_args.args[0]
self.assertEqual(Path(cmd[0]).name, "uv")
self.assertEqual(cmd[1:3], ["run", "python"])
self.assertEqual(cmd[3:5], [str(main.REPO_ROOT / "main.py"), "backend"])
if __name__ == "__main__":
unittest.main()
Generated
+2461
View File
File diff suppressed because it is too large Load Diff