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/
env/
ENV/
*.egg-info/
.pytest_cache/
# 项目特定
chromedriver
@@ -20,6 +22,7 @@ debug_screenshot.png
# 运行时文件
sessions/
*.lock
!/uv.lock
*.log
*.pid
backend.pid
+5 -7
View File
@@ -27,6 +27,7 @@
### 环境要求
- Python 3.9+
- uv
- Node.js 16+
- Chrome 浏览器
@@ -34,11 +35,8 @@
```bash
# 后端
python -m venv venv
venv\Scripts\activate # Windows
source venv/bin/activate # Linux/Mac
pip install -r apps/backend/requirements.txt
python main.py backend
uv sync
uv run python main.py backend
# 前端
cd apps/frontend
@@ -46,7 +44,7 @@ npm install
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
python main.py backend-daemon
uv run python main.py backend-daemon
python main.py frontend-daemon
python main.py status
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
或使用虚拟环境:
PYTHONPATH=apps ./venv/bin/python apps/backend/scripts/create_admin.py
uv run python apps/backend/scripts/create_admin.py
"""
import sys
from pathlib import Path
@@ -7,8 +7,7 @@
- last_failed_login: 最后一次登录失败时间
运行方式:
PYTHONPATH=apps python -m backend.scripts.migrate_add_account_lockout
python -m backend.scripts.migrate_add_account_lockout
uv run python -m backend.scripts.migrate_add_account_lockout
"""
import sys
+2 -2
View File
@@ -36,8 +36,8 @@ User=username
# Example: /home/username/CheckInApp
WorkingDirectory=/path/to/CheckInApp
# Start backend using the Python project manager
ExecStart=/path/to/CheckInApp/venv/bin/python /path/to/CheckInApp/main.py backend --no-reload
# Start backend using the uv-managed Python project manager
ExecStart=/usr/bin/env uv run python /path/to/CheckInApp/main.py backend --no-reload
# Restart policy
Restart=on-failure
+6 -7
View File
@@ -6,6 +6,7 @@
- Ubuntu 20.04+ / CentOS 7+ / Windows Server
- Python 3.9+
- uv
- Node.js 16+
- Chrome / Chromium
- 2GB+ RAM
@@ -16,9 +17,11 @@
# Ubuntu
sudo apt update
sudo apt install -y python3 nodejs npm chromium-browser
curl -LsSf https://astral.sh/uv/install.sh | sh
# CentOS
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>
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
@@ -174,7 +173,7 @@ server {
```python
# 安装 redis
pip install redis
uv sync --extra redis
# 配置会话存储
REDIS_URL=redis://localhost:6379/0
+6 -14
View File
@@ -9,18 +9,10 @@
git clone <repository>
cd CheckInApp
# 创建虚拟环境
python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
# 安装后端依赖
uv sync
# 安装依赖
pip install -r apps/backend/requirements.txt
# 安装开发依赖
pip install pytest pytest-asyncio black flake8
python main.py backend
uv run python main.py backend
```
### 前端开发
@@ -214,7 +206,7 @@ export const useTagStore = defineStore('tag', {
# 手动创建脚本在 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
# 运行测试
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
- 函数添加类型注解
- API 路由使用 Pydantic 模型验证
+20 -18
View File
@@ -8,7 +8,6 @@ import os
import signal
import subprocess
import sys
import time
from pathlib import Path
@@ -16,8 +15,7 @@ REPO_ROOT = Path(__file__).resolve().parent
APPS_DIR = REPO_ROOT / "apps"
BACKEND_DIR = APPS_DIR / "backend"
FRONTEND_DIR = APPS_DIR / "frontend"
VENV_DIR = REPO_ROOT / "venv"
PYTHON_BIN = VENV_DIR / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
UV_BIN = os.environ.get("UV", "uv")
BACKEND_PID = REPO_ROOT / "backend.pid"
FRONTEND_PID = REPO_ROOT / "frontend.pid"
LOGS_DIR = REPO_ROOT / "logs"
@@ -41,11 +39,13 @@ def ensure_runtime_dirs() -> None:
def get_python() -> str:
if PYTHON_BIN.exists():
return str(PYTHON_BIN)
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:
try:
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
except ModuleNotFoundError as exc:
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
print("backend.main:app import OK")
return 0
@@ -121,9 +121,7 @@ def start_backend_daemon(args: argparse.Namespace) -> int:
return 0
BACKEND_PID.unlink(missing_ok=True)
cmd = [
get_python(),
str(REPO_ROOT / "main.py"),
cmd = backend_manager_command(
"backend",
"--host",
args.host,
@@ -132,17 +130,21 @@ def start_backend_daemon(args: argparse.Namespace) -> int:
"--no-reload",
"--log-level",
args.log_level,
]
)
BACKEND_LOG.parent.mkdir(parents=True, exist_ok=True)
log_file = BACKEND_LOG.open("a", encoding="utf-8")
proc = subprocess.Popen(
cmd,
cwd=REPO_ROOT,
env=backend_env(),
stdout=log_file,
stderr=subprocess.STDOUT,
start_new_session=os.name != "nt",
)
try:
proc = subprocess.Popen(
cmd,
cwd=REPO_ROOT,
env=backend_env(),
stdout=log_file,
stderr=subprocess.STDOUT,
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")
print(f"backend: started pid {proc.pid}")
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