mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 05:56:29 +00:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 模型验证
|
||||||
|
|||||||
@@ -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,17 +130,21 @@ 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")
|
||||||
proc = subprocess.Popen(
|
try:
|
||||||
cmd,
|
proc = subprocess.Popen(
|
||||||
cwd=REPO_ROOT,
|
cmd,
|
||||||
env=backend_env(),
|
cwd=REPO_ROOT,
|
||||||
stdout=log_file,
|
env=backend_env(),
|
||||||
stderr=subprocess.STDOUT,
|
stdout=log_file,
|
||||||
start_new_session=os.name != "nt",
|
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")
|
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}")
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user