Bootstrap Python rewrite skeleton
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
.git
|
||||
.gitignore
|
||||
.pytest_cache
|
||||
.venv
|
||||
__pycache__
|
||||
*.pyc
|
||||
data
|
||||
openapi
|
||||
src
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
APP_NAME=Home Automation Backend (Python)
|
||||
APP_ENV=development
|
||||
APP_DEBUG=true
|
||||
APP_HOST=0.0.0.0
|
||||
APP_PORT=8000
|
||||
DATABASE_URL=sqlite:///./data/app.db
|
||||
TICKTICK_CLIENT_ID=
|
||||
TICKTICK_CLIENT_SECRET=
|
||||
TICKTICK_REDIRECT_URI=http://localhost:8000/ticktick/auth/callback
|
||||
TICKTICK_TOKEN=
|
||||
HOME_ASSISTANT_BASE_URL=http://localhost:8123
|
||||
HOME_ASSISTANT_AUTH_TOKEN=
|
||||
HOME_ASSISTANT_ACTION_TASK_PROJECT_ID=
|
||||
|
||||
+5
-34
@@ -1,37 +1,8 @@
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
|
||||
temp_data/
|
||||
|
||||
# py file for branch switching
|
||||
.venv
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
config.yaml
|
||||
bin/
|
||||
*.db
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
data/
|
||||
openapi/
|
||||
|
||||
cover.html
|
||||
Vendored
+12
-27
@@ -1,35 +1,20 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Package",
|
||||
"type": "go",
|
||||
"name": "Launch Python App",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"name": "Launch Poo Reverse",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/src/helper/poo_recorder_helper/main.go",
|
||||
"module": "uvicorn",
|
||||
"args": [
|
||||
"reverse"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Launch Home Automation",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/src/main.go",
|
||||
"args": [
|
||||
"serve"
|
||||
]
|
||||
"app.main:app",
|
||||
"--reload",
|
||||
"--host",
|
||||
"0.0.0.0",
|
||||
"--port",
|
||||
"8000"
|
||||
],
|
||||
"jinja": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY app ./app
|
||||
COPY alembic ./alembic
|
||||
COPY alembic.ini ./
|
||||
COPY scripts ./scripts
|
||||
COPY README.md ./
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -1,3 +1,177 @@
|
||||
# Home Automation Backend
|
||||
|
||||

|
||||
这是当前 `home-automation` 项目的 Python 重构基础骨架。当前仓库仍保留 Go 版本作为事实基线,而这个 Python 部分的目标是为后续逐模块迁移提供稳定工程基础。
|
||||
|
||||
为便于清理仓库,重构开始前就存在的 Go 实现和相关资产已经统一移动到 `legacy/go-backend/`。这样在 Python 重构完成后,可以按目录整体删除旧实现。
|
||||
|
||||
当前阶段只包含:
|
||||
|
||||
- FastAPI 基础应用骨架
|
||||
- 环境变量配置体系
|
||||
- SQLite + SQLAlchemy + Alembic 基础设施
|
||||
- 极简 server-side templates
|
||||
- pytest 测试基础
|
||||
- OpenAPI 导出脚本
|
||||
- Docker / Compose 基础骨架
|
||||
|
||||
当前阶段明确不包含:
|
||||
|
||||
- TickTick 业务逻辑迁移
|
||||
- Home Assistant 业务逻辑迁移
|
||||
- poo records 业务迁移
|
||||
- location / life trajectory 业务迁移
|
||||
- Notion 模块
|
||||
|
||||
Notion 在 Go 版本中仍然存在,但已被明确视为 legacy / removed scope,不进入新的 Python 系统目标。
|
||||
|
||||
旧 Go 代码位置:
|
||||
|
||||
- `legacy/go-backend/src/`
|
||||
- `legacy/go-backend/helper/`
|
||||
- `legacy/go-backend/.github/workflows/`
|
||||
|
||||
## 当前目录
|
||||
|
||||
Python 骨架的主要目录如下:
|
||||
|
||||
- `app/`: FastAPI 应用代码
|
||||
- `alembic/`: Alembic migration 环境
|
||||
- `tests/`: pytest 测试
|
||||
- `docs/`: 架构说明与迁移文档
|
||||
- `scripts/`: 辅助脚本,例如 OpenAPI 导出
|
||||
|
||||
## 依赖管理
|
||||
|
||||
项目现在采用 `pip-tools` 管理依赖:
|
||||
|
||||
- 生产依赖源文件:`requirements.in`
|
||||
- 开发依赖源文件:`dev-requirements.in`
|
||||
- 编译产物:
|
||||
- `requirements.txt`
|
||||
- `dev-requirements.txt`
|
||||
|
||||
更新依赖时建议使用:
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install pip-tools
|
||||
pip-compile requirements.in
|
||||
pip-compile dev-requirements.in
|
||||
```
|
||||
|
||||
如果要升级某个依赖,可以用:
|
||||
|
||||
```bash
|
||||
pip-compile --upgrade-package fastapi requirements.in
|
||||
pip-compile dev-requirements.in
|
||||
```
|
||||
|
||||
## 本地启动
|
||||
|
||||
建议使用 Python 3.11 或以上版本。
|
||||
|
||||
1. 创建虚拟环境并安装依赖
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r dev-requirements.txt
|
||||
```
|
||||
|
||||
2. 准备环境变量
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
3. 启动服务
|
||||
|
||||
```bash
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
启动后可访问:
|
||||
|
||||
- 应用首页:`http://localhost:8000/`
|
||||
- 健康检查:`http://localhost:8000/status`
|
||||
- Swagger UI:`http://localhost:8000/docs`
|
||||
- ReDoc:`http://localhost:8000/redoc`
|
||||
|
||||
## 数据库与 Alembic
|
||||
|
||||
当前默认数据库使用 SQLite。
|
||||
|
||||
- 默认数据库地址:`sqlite:///./data/app.db`
|
||||
- 数据目录:`./data/`
|
||||
|
||||
初始化 migration 环境后,可继续添加模型并生成迁移:
|
||||
|
||||
```bash
|
||||
alembic revision --autogenerate -m "init tables"
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
这一轮尚未引入业务表,因此 Alembic 目前主要是基础设施就绪状态。
|
||||
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
pytest
|
||||
```
|
||||
|
||||
当前测试包含:
|
||||
|
||||
- app 基本启动测试
|
||||
- `/status` endpoint 测试
|
||||
|
||||
## OpenAPI 导出
|
||||
|
||||
FastAPI 默认会暴露 OpenAPI。若需要导出静态 schema 文件,可运行:
|
||||
|
||||
```bash
|
||||
python scripts/export_openapi.py
|
||||
```
|
||||
|
||||
输出文件会写到:
|
||||
|
||||
- `openapi/openapi.json`
|
||||
- `openapi/openapi.yaml`
|
||||
|
||||
## 容器启动
|
||||
|
||||
1. 准备环境变量文件
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. 启动容器
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
默认端口:
|
||||
|
||||
- `8000:8000`
|
||||
|
||||
SQLite 持久化目录:
|
||||
|
||||
- 本地 `./data`
|
||||
- 容器内 `/app/data`
|
||||
|
||||
## 后续迁移建议
|
||||
|
||||
后续可以在当前骨架上逐步迁移这些模块:
|
||||
|
||||
- TickTick integration
|
||||
- Home Assistant integration
|
||||
- poo records
|
||||
- location / life trajectory
|
||||
|
||||
建议继续参考:
|
||||
|
||||
- [当前系统盘点](docs/current-system-inventory.md)
|
||||
- [Python 重构方案](docs/python-rewrite-plan.md)
|
||||
- [迁移风险清单](docs/migration-risks.md)
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
[alembic]
|
||||
script_location = alembic
|
||||
prepend_sys_path = .
|
||||
sqlalchemy.url = sqlite:///./data/app.db
|
||||
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
This directory contains the Alembic migration environment for the Python rewrite skeleton.
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
|
||||
from app.config import get_settings
|
||||
from app.models.base import Base
|
||||
|
||||
config = context.config
|
||||
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
settings = get_settings()
|
||||
config.set_main_option("sqlalchemy.url", settings.database_url)
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
"""Application package for the Python rewrite skeleton."""
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
"""API package."""
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
"""Route modules."""
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from app.config import Settings
|
||||
from app.dependencies import get_app_settings
|
||||
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).resolve().parents[2] / "templates"))
|
||||
router = APIRouter(tags=["pages"])
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
def home(request: Request, settings: Settings = Depends(get_app_settings)) -> HTMLResponse:
|
||||
context = {
|
||||
"app_name": settings.app_name,
|
||||
"app_env": settings.app_env,
|
||||
"notion_status": "Legacy scope, removed from the Python rewrite target.",
|
||||
}
|
||||
return templates.TemplateResponse(request, "home.html", context)
|
||||
@@ -0,0 +1,11 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.schemas.health import StatusResponse
|
||||
|
||||
router = APIRouter(tags=["system"])
|
||||
|
||||
|
||||
@router.get("/status", response_model=StatusResponse)
|
||||
def get_status() -> StatusResponse:
|
||||
return StatusResponse(status="ok")
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import computed_field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
app_name: str = "Home Automation Backend (Python)"
|
||||
app_env: str = "development"
|
||||
app_debug: bool = False
|
||||
app_host: str = "0.0.0.0"
|
||||
app_port: int = 8000
|
||||
|
||||
database_url: str = "sqlite:///./data/app.db"
|
||||
|
||||
ticktick_client_id: str = ""
|
||||
ticktick_client_secret: str = ""
|
||||
ticktick_redirect_uri: str = ""
|
||||
ticktick_token: str = ""
|
||||
|
||||
home_assistant_base_url: str = ""
|
||||
home_assistant_auth_token: str = ""
|
||||
home_assistant_action_task_project_id: str = ""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
case_sensitive=False,
|
||||
)
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def is_development(self) -> bool:
|
||||
return self.app_env.lower() == "development"
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def sqlite_path(self) -> Path | None:
|
||||
prefix = "sqlite:///"
|
||||
if not self.database_url.startswith(prefix):
|
||||
return None
|
||||
raw_path = self.database_url[len(prefix) :]
|
||||
return Path(raw_path)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
from collections.abc import Generator
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
||||
|
||||
from app.config import get_settings
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
connect_args: dict[str, object] = {}
|
||||
if settings.database_url.startswith("sqlite"):
|
||||
connect_args["check_same_thread"] = False
|
||||
|
||||
engine = create_engine(settings.database_url, connect_args=connect_args)
|
||||
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, class_=Session)
|
||||
|
||||
|
||||
def get_db_session() -> Generator[Session, None, None]:
|
||||
session = SessionLocal()
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
from collections.abc import Generator
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import Settings, get_settings
|
||||
from app.db import get_db_session
|
||||
|
||||
|
||||
def get_app_settings() -> Settings:
|
||||
return get_settings()
|
||||
|
||||
|
||||
def get_db() -> Generator[Session, None, None]:
|
||||
yield from get_db_session()
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
"""External integration placeholders for future migration."""
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.config import Settings
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class HomeAssistantClient:
|
||||
settings: Settings
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
return bool(self.settings.home_assistant_base_url and self.settings.home_assistant_auth_token)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.config import Settings
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class TickTickClient:
|
||||
settings: Settings
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
return bool(self.settings.ticktick_client_id and self.settings.ticktick_client_secret)
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from app.api.routes import pages, status
|
||||
from app.config import get_settings
|
||||
|
||||
|
||||
def ensure_runtime_dirs() -> None:
|
||||
settings = get_settings()
|
||||
if settings.sqlite_path is not None:
|
||||
settings.sqlite_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
ensure_runtime_dirs()
|
||||
yield
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
settings = get_settings()
|
||||
app = FastAPI(
|
||||
title=settings.app_name,
|
||||
debug=settings.app_debug,
|
||||
version="0.1.0",
|
||||
lifespan=lifespan,
|
||||
description=(
|
||||
"Python rewrite skeleton for the home automation backend. "
|
||||
"This stage provides only the foundation for future module migration."
|
||||
),
|
||||
)
|
||||
|
||||
static_dir = Path(__file__).parent / "static"
|
||||
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
||||
|
||||
app.include_router(status.router)
|
||||
app.include_router(pages.router)
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
"""SQLAlchemy models package."""
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from app.db import Base
|
||||
|
||||
__all__ = ["Base"]
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
"""Pydantic schemas package."""
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class StatusResponse(BaseModel):
|
||||
status: str
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
"""Service layer package."""
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from app.config import Settings
|
||||
|
||||
|
||||
def build_status_payload(settings: Settings) -> dict[str, str]:
|
||||
return {"status": "ok", "environment": settings.app_env}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
:root {
|
||||
--bg: #f4f1ea;
|
||||
--panel: rgba(255, 255, 255, 0.88);
|
||||
--text: #1f2933;
|
||||
--muted: #5b6875;
|
||||
--accent: #2d6a4f;
|
||||
--border: rgba(31, 41, 51, 0.08);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(45, 106, 79, 0.18), transparent 28%),
|
||||
linear-gradient(160deg, #f7f4ee 0%, #ece6d8 100%);
|
||||
}
|
||||
|
||||
.shell {
|
||||
width: min(880px, calc(100% - 32px));
|
||||
margin: 48px auto;
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 32px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 24px;
|
||||
background: var(--panel);
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: 0 20px 60px rgba(31, 41, 51, 0.12);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 8px;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 16px;
|
||||
font-size: clamp(2rem, 4vw, 3.2rem);
|
||||
}
|
||||
|
||||
.lead {
|
||||
margin: 0 0 24px;
|
||||
line-height: 1.7;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.meta div {
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid rgba(31, 41, 51, 0.06);
|
||||
}
|
||||
|
||||
.meta dt {
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.meta dd {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.shell {
|
||||
margin: 24px auto;
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}{{ app_name }}{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='styles.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ app_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<p class="eyebrow">Python Rewrite Skeleton</p>
|
||||
<h1>{{ app_name }}</h1>
|
||||
<p class="lead">
|
||||
这是当前 Go 后端的 Python 重构基础骨架。此阶段仅提供应用入口、配置、数据库、
|
||||
测试、模板和容器化基础,不包含业务逻辑迁移。
|
||||
</p>
|
||||
<dl class="meta">
|
||||
<div>
|
||||
<dt>运行环境</dt>
|
||||
<dd>{{ app_env }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>健康检查</dt>
|
||||
<dd><a href="/status">/status</a></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>OpenAPI</dt>
|
||||
<dd><a href="/docs">/docs</a></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Notion</dt>
|
||||
<dd>{{ notion_status }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-r requirements.in
|
||||
|
||||
httpx>=0.28,<1.0
|
||||
pip-tools>=7.4,<8.0
|
||||
pytest>=8.3,<9.0
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.13
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile dev-requirements.in
|
||||
#
|
||||
alembic==1.18.4
|
||||
# via -r requirements.in
|
||||
annotated-types==0.7.0
|
||||
# via pydantic
|
||||
anyio==4.13.0
|
||||
# via
|
||||
# httpx
|
||||
# starlette
|
||||
# watchfiles
|
||||
build==1.4.3
|
||||
# via pip-tools
|
||||
certifi==2026.2.25
|
||||
# via
|
||||
# httpcore
|
||||
# httpx
|
||||
click==8.3.2
|
||||
# via
|
||||
# pip-tools
|
||||
# uvicorn
|
||||
fastapi==0.115.14
|
||||
# via -r requirements.in
|
||||
greenlet==3.4.0
|
||||
# via sqlalchemy
|
||||
h11==0.16.0
|
||||
# via
|
||||
# httpcore
|
||||
# uvicorn
|
||||
httpcore==1.0.9
|
||||
# via httpx
|
||||
httptools==0.7.1
|
||||
# via uvicorn
|
||||
httpx==0.28.1
|
||||
# via -r dev-requirements.in
|
||||
idna==3.11
|
||||
# via
|
||||
# anyio
|
||||
# httpx
|
||||
iniconfig==2.3.0
|
||||
# via pytest
|
||||
jinja2==3.1.6
|
||||
# via -r requirements.in
|
||||
mako==1.3.11
|
||||
# via alembic
|
||||
markupsafe==3.0.3
|
||||
# via
|
||||
# jinja2
|
||||
# mako
|
||||
packaging==26.1
|
||||
# via
|
||||
# build
|
||||
# pytest
|
||||
# wheel
|
||||
pip-tools==7.5.3
|
||||
# via -r dev-requirements.in
|
||||
pluggy==1.6.0
|
||||
# via pytest
|
||||
pydantic==2.13.2
|
||||
# via
|
||||
# fastapi
|
||||
# pydantic-settings
|
||||
pydantic-core==2.46.2
|
||||
# via pydantic
|
||||
pydantic-settings==2.13.1
|
||||
# via -r requirements.in
|
||||
pygments==2.20.0
|
||||
# via pytest
|
||||
pyproject-hooks==1.2.0
|
||||
# via
|
||||
# build
|
||||
# pip-tools
|
||||
pytest==8.4.2
|
||||
# via -r dev-requirements.in
|
||||
python-dotenv==1.2.2
|
||||
# via
|
||||
# pydantic-settings
|
||||
# uvicorn
|
||||
python-multipart==0.0.26
|
||||
# via -r requirements.in
|
||||
pyyaml==6.0.3
|
||||
# via
|
||||
# -r requirements.in
|
||||
# uvicorn
|
||||
sqlalchemy==2.0.49
|
||||
# via
|
||||
# -r requirements.in
|
||||
# alembic
|
||||
starlette==0.46.2
|
||||
# via fastapi
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# alembic
|
||||
# fastapi
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
# sqlalchemy
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.2
|
||||
# via
|
||||
# pydantic
|
||||
# pydantic-settings
|
||||
uvicorn[standard]==0.44.0
|
||||
# via -r requirements.in
|
||||
uvloop==0.22.1
|
||||
# via uvicorn
|
||||
watchfiles==1.1.1
|
||||
# via uvicorn
|
||||
websockets==16.0
|
||||
# via uvicorn
|
||||
wheel==0.46.3
|
||||
# via pip-tools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# pip
|
||||
# setuptools
|
||||
@@ -0,0 +1,16 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "8000:8000"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
DATABASE_URL: sqlite:////app/data/app.db
|
||||
APP_HOST: 0.0.0.0
|
||||
APP_PORT: 8000
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# Python 骨架架构概览
|
||||
|
||||
本文档说明当前 Python skeleton 的职责边界与目录组织。它描述的是“后续迁移承载体”,不是完整业务实现。
|
||||
|
||||
## 当前目标
|
||||
|
||||
这一轮的目标是提供一个稳定、轻量、可持续扩展的基础工程,使后续可以逐步迁移:
|
||||
|
||||
- TickTick integration
|
||||
- Home Assistant integration
|
||||
- poo records
|
||||
- location / life trajectory
|
||||
|
||||
## 目录设计
|
||||
|
||||
### `app/`
|
||||
|
||||
应用核心代码目录。
|
||||
|
||||
- `main.py`
|
||||
- FastAPI app factory
|
||||
- lifespan
|
||||
- 基础路由注册
|
||||
- `config.py`
|
||||
- 环境变量驱动的 settings
|
||||
- `db.py`
|
||||
- SQLAlchemy engine / session / Base
|
||||
- `dependencies.py`
|
||||
- 通用依赖注入
|
||||
- `api/`
|
||||
- HTTP routes
|
||||
- `models/`
|
||||
- SQLAlchemy models
|
||||
- `schemas/`
|
||||
- Pydantic schemas
|
||||
- `services/`
|
||||
- 业务服务层
|
||||
- `integrations/`
|
||||
- 外部系统适配层占位
|
||||
- `templates/`
|
||||
- Jinja2 模板
|
||||
- `static/`
|
||||
- 极简静态资源
|
||||
|
||||
### `alembic/`
|
||||
|
||||
数据库 migration 基础设施。当前尚未迁入业务表,但迁移链路已就绪。
|
||||
|
||||
### `tests/`
|
||||
|
||||
pytest 测试目录。后续可以在这里自然扩展:
|
||||
|
||||
- unit tests
|
||||
- mock tests
|
||||
- integration tests
|
||||
|
||||
### `scripts/`
|
||||
|
||||
辅助脚本目录。当前包含 OpenAPI 导出脚本。
|
||||
|
||||
## 当前约束
|
||||
|
||||
- 当前只搭骨架,不迁业务逻辑
|
||||
- 当前数据库继续使用 SQLite
|
||||
- 当前不引入前后端分离
|
||||
- 当前不设计 Notion 模块
|
||||
|
||||
## 关于 Notion
|
||||
|
||||
Notion 在 Go 版本中仍是现状模块,但在 Python 重构中已经明确属于 removed scope。
|
||||
|
||||
因此当前 Python skeleton:
|
||||
|
||||
- 不提供 Notion integration 模块
|
||||
- 不提供 Notion schema
|
||||
- 不预留 Notion 相关业务流
|
||||
|
||||
如果未来需要回顾其历史作用,应继续参考 Go 版本和现有迁移盘点文档,而不是在 Python 骨架中保留它。
|
||||
|
||||
@@ -0,0 +1,557 @@
|
||||
# 当前系统盘点
|
||||
|
||||
本文档用于盘点当前 branch 上的 Go 实现,并将其作为后续 Python 重构的唯一事实基线。
|
||||
|
||||
## 范围与基线
|
||||
|
||||
- 当前事实基线:`legacy/go-backend/src/` 下的 Go 代码
|
||||
- 不纳入当前基线:更早的 Python 版本
|
||||
- 主入口:[`legacy/go-backend/src/cmd/serve.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/cmd/serve.go:62)
|
||||
|
||||
## 系统概览
|
||||
|
||||
当前应用是一个单进程 Go HTTP 服务,具备以下特征:
|
||||
|
||||
- 暴露少量 REST API
|
||||
- 使用本地 SQLite 做持久化
|
||||
- 调用 Home Assistant API 和 webhook
|
||||
- 通过 OAuth 和 REST API 集成 TickTick
|
||||
- 当前仍依赖 Notion 做 poo 记录同步
|
||||
- 内置一个每日执行的定时同步任务
|
||||
|
||||
进程启动后会先读取 YAML 配置文件,再初始化 Notion 与 TickTick 工具层、初始化各业务组件自己的 SQLite 数据库、注册路由、启动调度器,最后在配置的端口上提供 HTTP 服务。可参考 [`src/cmd/serve.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/cmd/serve.go:65) 和 [`src/cmd/serve.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/cmd/serve.go:104)。
|
||||
|
||||
## API 盘点
|
||||
|
||||
### `GET /status`
|
||||
|
||||
- 路由定义:[`src/cmd/serve.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/cmd/serve.go:106)
|
||||
- 用途:基础存活检查
|
||||
- 请求参数:无
|
||||
- 请求体:无
|
||||
- 响应:纯文本 `OK`
|
||||
- 鉴权:当前代码中无鉴权
|
||||
- 调用方类型:通用健康检查,可能用于本地监控或 supervisor 级别的探活
|
||||
|
||||
### `GET /poo/latest`
|
||||
|
||||
- 路由定义:[`src/cmd/serve.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/cmd/serve.go:110)
|
||||
- 处理函数:[`pooRecorder.HandleNotifyLatestPoo`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/pooRecorder/pooRecorder.go:87)
|
||||
- 用途:将最新一条 poo 状态重新发布到 Home Assistant 的 sensor state
|
||||
- 请求参数:无
|
||||
- 请求体:无
|
||||
- 响应:
|
||||
- 成功:空响应体,默认 HTTP 200
|
||||
- 失败:通过 `http.Error(...)` 返回文本错误信息
|
||||
- 鉴权:当前代码中无鉴权
|
||||
- 外部调用方:
|
||||
- 会被 `POST /homeassistant/publish` 间接触发,当 `target=poo_recorder` 且 `action=get_latest` 时,代码会通过内部 HTTP 请求访问 `http://localhost:{port}/poo/latest`,见 [`src/components/homeassistant/homeassistant.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/homeassistant/homeassistant.go:110)
|
||||
- 副作用:
|
||||
- 从 `poo_records` 读取最新一条记录
|
||||
- 调用 Home Assistant `/api/states/{entity_id}` 更新 sensor 状态,见 [`src/util/homeassistantutil/homeassistantutil.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/util/homeassistantutil/homeassistantutil.go:65)
|
||||
|
||||
### `POST /poo/record`
|
||||
|
||||
- 路由定义:[`src/cmd/serve.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/cmd/serve.go:111)
|
||||
- 处理函数:[`pooRecorder.HandleRecordPoo`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/pooRecorder/pooRecorder.go:57)
|
||||
- 用途:记录一条 poo 事件,同时镜像到 Notion、刷新 Home Assistant sensor,并可选触发一个 Home Assistant webhook
|
||||
- 请求体 JSON:
|
||||
- `status: string`
|
||||
- `latitude: string`
|
||||
- `longitude: string`
|
||||
- 请求校验:
|
||||
- JSON decoder 开启了 `DisallowUnknownFields`
|
||||
- 如果配置里缺少 `pooRecorder.tableId`,请求会直接失败,虽然从纯本地 DB 角度看本来仍有可能写入成功
|
||||
- 响应:
|
||||
- 成功:空响应体,默认 HTTP 200
|
||||
- 请求错误:返回 decoder 错误文本,HTTP 400
|
||||
- 服务端错误:返回错误文本,HTTP 500
|
||||
- 鉴权:当前代码中无鉴权
|
||||
- 外部调用方:大概率是 Home Assistant、移动端 shortcut、或手工调用;代码中没有明确写死调用方
|
||||
- 副作用:
|
||||
- 向 SQLite `poo_records` 插入一条记录
|
||||
- 异步向 Notion 追加一行
|
||||
- 同步发布最新 Home Assistant sensor 状态
|
||||
- 如果存在 `pooRecorder.webhookId`,异步触发一个 Home Assistant webhook
|
||||
|
||||
### `POST /homeassistant/publish`
|
||||
|
||||
- 路由定义:[`src/cmd/serve.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/cmd/serve.go:112)
|
||||
- 处理函数:[`HomeAssistant.HandleHaMessage`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/homeassistant/homeassistant.go:36)
|
||||
- 用途:接收自动化消息,根据 `target` 和 `action` 做分发
|
||||
- 请求体 JSON:
|
||||
- `target: string`
|
||||
- `action: string`
|
||||
- `content: string`
|
||||
- 请求校验:
|
||||
- JSON decoder 开启了 `DisallowUnknownFields`
|
||||
- 响应:
|
||||
- 成功路径:空响应体,默认 HTTP 200
|
||||
- 失败路径:通常为空响应体并返回 HTTP 500;TickTick auth 回调是单独的接口,不在这里
|
||||
- 鉴权:当前代码中无鉴权
|
||||
- 外部调用方:设计意图上是给 Home Assistant automation 消息调用
|
||||
|
||||
当前代码支持的消息契约如下:
|
||||
|
||||
- `target=poo_recorder`, `action=get_latest`
|
||||
- 转发到本地 `GET /poo/latest`
|
||||
- 见 [`src/components/homeassistant/homeassistant.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/homeassistant/homeassistant.go:72)
|
||||
|
||||
- `target=location_recorder`, `action=record`
|
||||
- `content` 预期是一个 JSON 风格字符串,实际很可能使用单引号
|
||||
- 当前代码会用 `strings.ReplaceAll(message.Content, "'", "\"")` 做归一化
|
||||
- 然后转发到本地 `POST /location/record`
|
||||
- 见 [`src/components/homeassistant/homeassistant.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/homeassistant/homeassistant.go:82)
|
||||
|
||||
- `target=ticktick`, `action=create_action_task`
|
||||
- `content` 预期可解析为:
|
||||
- `action: string`
|
||||
- `due_hour: int`
|
||||
- 当前代码会忽略调用方传来的 `title` 字段,而是把 `action` 映射为 TickTick task title
|
||||
- 到期时间的计算方式是:取 `now + due_hour` 后所在日期的“次日零点”,再转成 TickTick 使用的时间格式
|
||||
- 最终在配置指定的 TickTick project 中创建任务
|
||||
- 见 [`src/components/homeassistant/homeassistant.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/homeassistant/homeassistant.go:124)
|
||||
|
||||
不支持的 `target` 或 `action` 会返回 HTTP 500,并打 warning 日志。相关测试在 [`src/components/homeassistant/homeassistant_test.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/homeassistant/homeassistant_test.go:68)。
|
||||
|
||||
### `POST /location/record`
|
||||
|
||||
- 路由定义:[`src/cmd/serve.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/cmd/serve.go:114)
|
||||
- 处理函数:[`locationRecorder.HandleRecordLocation`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/locationRecorder/locationRecorder.go:43)
|
||||
- 用途:记录人的位置点,用于人生轨迹 / movement history
|
||||
- 请求体 JSON:
|
||||
- `person: string`
|
||||
- `latitude: string`
|
||||
- `longitude: string`
|
||||
- `altitude: string`,从请求结构上看是可选,但代码里即使为空也会被解析成 `0`
|
||||
- 请求校验:
|
||||
- JSON decoder 开启了 `DisallowUnknownFields`
|
||||
- 数值解析错误会被忽略;如果 `latitude` / `longitude` / `altitude` 不是合法数字,当前实现会静默落成 `0`
|
||||
- 响应:
|
||||
- 成功:空响应体,默认 HTTP 200
|
||||
- 请求错误:返回 decoder 错误文本,HTTP 400
|
||||
- 鉴权:当前代码中无鉴权
|
||||
- 外部调用方:
|
||||
- 可被任意客户端直接调用
|
||||
- 也会被 `POST /homeassistant/publish` 间接触发
|
||||
- 副作用:
|
||||
- 向 SQLite `location` 表插入一条记录
|
||||
|
||||
### `GET /ticktick/auth/code`
|
||||
|
||||
- 路由定义:[`src/cmd/serve.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/cmd/serve.go:116)
|
||||
- 处理函数:[`TicktickUtilImpl.HandleAuthCode`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/util/ticktickutil/ticktickutil.go:103)
|
||||
- 用途:TickTick OAuth redirect callback
|
||||
- Query 参数:
|
||||
- `state`
|
||||
- `code`
|
||||
- 响应:
|
||||
- 成功:纯文本 `Authorization successful`
|
||||
- 失败:纯文本错误信息,HTTP 400 或 500
|
||||
- 鉴权:
|
||||
- 通过 OAuth `state` 与进程内保存的 `authState` 做校验
|
||||
- 没有额外的 session 或用户级鉴权
|
||||
- 外部调用方:TickTick OAuth redirect
|
||||
- 副作用:
|
||||
- 用 authorization code 换取 access token
|
||||
- 通过 `viper.WriteConfig()` 把 `ticktick.token` 写回 YAML 配置文件
|
||||
|
||||
## 外部集成盘点
|
||||
|
||||
### TickTick
|
||||
|
||||
- 在 Python 重构中的状态:应保留
|
||||
- 主要文件:
|
||||
- [`src/util/ticktickutil/ticktickutil.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/util/ticktickutil/ticktickutil.go:1)
|
||||
- [`src/components/homeassistant/homeassistant.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/homeassistant/homeassistant.go:100)
|
||||
- 当前职责:
|
||||
- 初始化 TickTick 鉴权状态
|
||||
- 当 token 缺失时启动 OAuth 授权
|
||||
- 接收 OAuth callback 并持久化 token
|
||||
- 读取 project 下的 tasks
|
||||
- 若不存在同名任务,则创建新任务
|
||||
- 连接方式:
|
||||
- OAuth authorization code flow
|
||||
- 调用 `https://ticktick.com/oauth/token`
|
||||
- 调用 `https://api.ticktick.com/open/v1/...`
|
||||
- 依赖的配置项:
|
||||
- `ticktick.clientId`
|
||||
- `ticktick.clientSecret`
|
||||
- `ticktick.redirectUri`
|
||||
- `ticktick.token`
|
||||
- 关键实现依赖:
|
||||
- 原生 `net/http`
|
||||
- `viper`,同时承担配置读取和配置回写
|
||||
- 迁移高风险点:
|
||||
- OAuth callback 的 `state` 只保存在进程内;如果服务在授权开始和回调完成之间重启,流程会断
|
||||
- token 直接写回 YAML 配置文件,虽然简单,但运维上比较脆弱
|
||||
- 去重逻辑只按 task title 精确匹配
|
||||
- due date 的计算语义是隐含在代码里的,重构前应先冻结
|
||||
- `Init()` 会在启动时积极检查配置,且在 token 缺失时打印手动授权 URL;Python 版需要明确是否仍要在启动阶段卡住这一流程
|
||||
|
||||
### Home Assistant
|
||||
|
||||
- 在 Python 重构中的状态:应保留
|
||||
- 主要文件:
|
||||
- [`src/components/homeassistant/homeassistant.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/homeassistant/homeassistant.go:1)
|
||||
- [`src/util/homeassistantutil/homeassistantutil.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/util/homeassistantutil/homeassistantutil.go:1)
|
||||
- 当前职责:
|
||||
- 接收来自 Home Assistant automations 的命令 envelope
|
||||
- 将命令转发给本地模块
|
||||
- 把 sensor state 发布回 Home Assistant
|
||||
- 在 poo 记录后触发 Home Assistant webhook
|
||||
- 连接方式:
|
||||
- 入站 webhook 风格 JSON 接口:`POST /homeassistant/publish`
|
||||
- 出站 REST 调用:Home Assistant `/api/states/{entity_id}`
|
||||
- 出站 webhook 调用:`/api/webhook/{webhook_id}`
|
||||
- 出站调用使用 bearer token
|
||||
- 依赖的配置项:
|
||||
- `homeassistant.ip`
|
||||
- `homeassistant.port`
|
||||
- `homeassistant.authToken`
|
||||
- `homeassistant.actionTaskProjectId`
|
||||
- `pooRecorder.webhookId`
|
||||
- `pooRecorder.sensorEntityName`
|
||||
- `pooRecorder.sensorFriendlyName`
|
||||
- 关键实现依赖:
|
||||
- 原生 `net/http`
|
||||
- 通过 `localhost:{port}` 发起自调用,而不是直接走函数调用
|
||||
- 迁移高风险点:
|
||||
- 入站 `/homeassistant/publish` 当前没有鉴权
|
||||
- 当前命令 envelope 里的 `content` 是字符串,且常带单引号,现有客户端可能依赖这种非标准格式
|
||||
- 模块间当前是通过自调用 HTTP 和 1 秒 timeout 编排的
|
||||
- sensor 发布和 webhook 触发都属于强副作用行为,需要在兼容性测试里单独覆盖
|
||||
|
||||
### Notion
|
||||
|
||||
- 在 Python 重构中的状态:当前存在,但按已知目标不计划默认保留
|
||||
- 主要文件:
|
||||
- [`src/util/notion/notion.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/util/notion/notion.go:1)
|
||||
- [`src/components/pooRecorder/pooRecorder.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/pooRecorder/pooRecorder.go:191)
|
||||
- helper CLI:[`src/helper/poo_recorder_helper/cmd/reverse.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/helper/poo_recorder_helper/cmd/reverse.go:21)
|
||||
- 当前职责:
|
||||
- 使用 config token 初始化 Notion client
|
||||
- 读取 / 写入 poo 记录对应的表格行
|
||||
- 每日做 SQLite 和 Notion 的双向同步
|
||||
- 提供一个反转 Notion 表顺序的辅助 CLI
|
||||
- 连接方式:
|
||||
- 通过 `github.com/jomei/notionapi` 调用 Notion API
|
||||
- token 鉴权
|
||||
- 依赖的配置项:
|
||||
- `notion.token`
|
||||
- `pooRecorder.tableId`
|
||||
- 关键实现依赖:
|
||||
- `github.com/jomei/notionapi`
|
||||
- 迁移高风险点:
|
||||
- 当前服务启动时如果缺少 `notion.token` 会直接退出,即便 Notion 并不是系统所有功能都需要的基础能力
|
||||
- `POST /poo/record` 当前要求 `pooRecorder.tableId` 存在,并会异步镜像到 Notion
|
||||
- 每日定时同步会同时改写 Notion 和 SQLite,若 Python 版移除这一行为,数据一致性预期会发生变化
|
||||
|
||||
## 数据库与 Schema 盘点
|
||||
|
||||
### 数据库类型
|
||||
|
||||
- 当前使用 SQLite 做组件级持久化
|
||||
- poo recorder 中显式导入了 SQLite driver,见 [`src/components/pooRecorder/pooRecorder.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/pooRecorder/pooRecorder.go:20)
|
||||
- location recorder 也通过 driver 名 `sqlite` 打开 SQLite,见 [`src/components/locationRecorder/locationRecorder.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/locationRecorder/locationRecorder.go:80)
|
||||
|
||||
### Poo recorder 数据库
|
||||
|
||||
- 配置项:`pooRecorder.dbPath`
|
||||
- 默认路径:`pooRecorder.db`
|
||||
- migration 机制:
|
||||
- 手写 `PRAGMA user_version`
|
||||
- 当前有效版本可以认为是 `1`
|
||||
- 目前只实现了 `0 -> 1`
|
||||
- 表:
|
||||
- `poo_records`
|
||||
- schema 定义见 [`src/components/pooRecorder/pooRecorder.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/pooRecorder/pooRecorder.go:162)
|
||||
- 字段:
|
||||
- `timestamp TEXT PRIMARY KEY`
|
||||
- `status TEXT NOT NULL`
|
||||
- `latitude REAL NOT NULL`
|
||||
- `longitude REAL NOT NULL`
|
||||
- 核心用途:
|
||||
- 用于查询最新 poo 状态并发布到 Home Assistant sensor
|
||||
- 作为本地 poo 历史的持久化来源
|
||||
- 作为 Notion 双向同步的本地数据源和数据汇
|
||||
- 明显核心字段:
|
||||
- `timestamp`
|
||||
- `status`
|
||||
- `latitude`
|
||||
- `longitude`
|
||||
- 可能属于历史包袱 / 后续需要再判断的点:
|
||||
- 当前实现与 Notion 表行结构高度耦合
|
||||
- 时间戳是字符串,格式为 `2006-01-02T15:04Z07:00`,不是带秒的完整 RFC3339
|
||||
- API 请求模型接受的经纬度是字符串,因此 Python 版的类型规范化要小心兼容
|
||||
|
||||
### Location recorder 数据库
|
||||
|
||||
- 配置项:`locationRecorder.dbPath`
|
||||
- 默认路径:`location_recorder.db`
|
||||
- migration 机制:
|
||||
- 手写 `PRAGMA user_version`
|
||||
- 当前版本 `2`
|
||||
- 已实现 migration:
|
||||
- `0 -> 1`:建表
|
||||
- `1 -> 2`:把旧 datetime 字符串改写成 RFC3339 UTC
|
||||
- 表:
|
||||
- `location`
|
||||
- schema 定义见 [`src/components/locationRecorder/locationRecorder.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/locationRecorder/locationRecorder.go:115)
|
||||
- 字段:
|
||||
- `person TEXT NOT NULL`
|
||||
- `datetime TEXT NOT NULL`
|
||||
- `latitude REAL NOT NULL`
|
||||
- `longitude REAL NOT NULL`
|
||||
- `altitude REAL`
|
||||
- 主键 `(person, datetime)`
|
||||
- 核心用途:
|
||||
- 持久化人生轨迹 / 位置点记录
|
||||
- 明显核心字段:
|
||||
- `person`
|
||||
- `datetime`
|
||||
- `latitude`
|
||||
- `longitude`
|
||||
- 可能属于历史包袱 / 后续需要再判断的点:
|
||||
- `altitude` 在语义上是可选,但当前入站解析会把缺失和非法值一起压成 `0`
|
||||
- 当前没有查询 API,这张表目前主要承担“只写不读”的存储角色
|
||||
|
||||
### 跨模块数据库观察
|
||||
|
||||
- 当前没有统一的共享 schema;每个组件各自打开自己的 SQLite 文件
|
||||
- 没有使用 ORM
|
||||
- 没有统一 migration 框架
|
||||
- 除主键外,没有看到额外索引
|
||||
- `poo` 的常规写入和异步 Notion 镜像之间没有事务保证,一致性更接近 best-effort
|
||||
|
||||
## 业务模块拆分
|
||||
|
||||
### 1. HTTP 外壳 / 应用启动层
|
||||
|
||||
- 职责:
|
||||
- 读取配置
|
||||
- 设置日志级别
|
||||
- 管理 scheduler 生命周期
|
||||
- 注册路由
|
||||
- 处理优雅退出
|
||||
- 主要文件:[`src/cmd/serve.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/cmd/serve.go:62)
|
||||
- 依赖:
|
||||
- 所有业务模块
|
||||
- 迁移判断:
|
||||
- 这部分后续应成为 FastAPI 的 app 装配层
|
||||
- 可以在早期先迁为“薄壳”
|
||||
|
||||
### 2. Poo recorder
|
||||
|
||||
- 职责:
|
||||
- 接收 poo 记录
|
||||
- 持久化本地 poo 历史
|
||||
- 向 Home Assistant 发布最新状态 sensor
|
||||
- 触发可选 Home Assistant webhook
|
||||
- 与 Notion 做同步
|
||||
- 主要文件:[`src/components/pooRecorder/pooRecorder.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/pooRecorder/pooRecorder.go:50)
|
||||
- 依赖:
|
||||
- SQLite
|
||||
- Home Assistant util
|
||||
- Notion util
|
||||
- scheduler
|
||||
- 迁移判断:
|
||||
- 这是功能上重要、但耦合也最重的模块
|
||||
- 适合在设计上先拆成:
|
||||
- poo API / service
|
||||
- Home Assistant 发布适配层
|
||||
- legacy Notion sync adapter
|
||||
|
||||
### 3. Location recorder
|
||||
|
||||
- 职责:
|
||||
- 接收位置更新
|
||||
- 持久化人生轨迹点
|
||||
- 主要文件:[`src/components/locationRecorder/locationRecorder.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/locationRecorder/locationRecorder.go:39)
|
||||
- 依赖:
|
||||
- SQLite
|
||||
- 迁移判断:
|
||||
- 相对独立
|
||||
- 很适合作为优先迁移对象
|
||||
- 但需要先明确数值校验规则,因为当前实现会把非法数字静默压成 `0`
|
||||
|
||||
### 4. Home Assistant 命令路由层
|
||||
|
||||
- 职责:
|
||||
- 接收命令 envelope
|
||||
- 根据 target/action 调度 poo、location、ticktick 行为
|
||||
- 主要文件:[`src/components/homeassistant/homeassistant.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/homeassistant/homeassistant.go:36)
|
||||
- 依赖:
|
||||
- 本地服务端口
|
||||
- TickTick util
|
||||
- 迁移判断:
|
||||
- 它是外部 automations 的关键契约
|
||||
- 应在迁移早中期就被冻结和复刻
|
||||
|
||||
### 5. TickTick adapter
|
||||
|
||||
- 职责:
|
||||
- OAuth callback
|
||||
- project / task REST 操作
|
||||
- 任务去重
|
||||
- 主要文件:[`src/util/ticktickutil/ticktickutil.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/util/ticktickutil/ticktickutil.go:81)
|
||||
- 依赖:
|
||||
- TickTick API
|
||||
- 可写配置文件
|
||||
- 迁移判断:
|
||||
- 作为内部 adapter 相对独立
|
||||
- 复杂度中等,主要难点是 OAuth 和 token 持久化
|
||||
|
||||
### 6. Home Assistant 出站 client
|
||||
|
||||
- 职责:
|
||||
- 发布 sensor state
|
||||
- 触发 webhook
|
||||
- 主要文件:[`src/util/homeassistantutil/homeassistantutil.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/util/homeassistantutil/homeassistantutil.go:30)
|
||||
- 依赖:
|
||||
- Home Assistant API/token
|
||||
- 迁移判断:
|
||||
- 小而独立
|
||||
- 很适合作为较早迁移的适配层
|
||||
|
||||
### 7. Notion adapter 与辅助 CLI
|
||||
|
||||
- 职责:
|
||||
- 读写 Notion table rows
|
||||
- 维护 poo 相关表格的辅助操作
|
||||
- 主要文件:
|
||||
- [`src/util/notion/notion.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/util/notion/notion.go:14)
|
||||
- [`src/helper/poo_recorder_helper/cmd/reverse.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/helper/poo_recorder_helper/cmd/reverse.go:21)
|
||||
- 迁移判断:
|
||||
- 当前存在,但按已知方向应视为 planned non-migration
|
||||
- 更适合被标记为 legacy 模块,而不是直接带进 Python 主体
|
||||
|
||||
### 8. 辅助 helper CLI
|
||||
|
||||
- `src/helper/poo_recorder_helper`
|
||||
- 用于 Notion 表反转的运维辅助工具
|
||||
- `src/helper/location_recorder`
|
||||
- 当前基本还是脚手架,没有实质业务逻辑
|
||||
- 迁移判断:
|
||||
- 二者都不是后端重构的核心目标
|
||||
- `poo_recorder_helper` 应随着 Notion 一并视作 legacy
|
||||
|
||||
## 运行方式与部署形态
|
||||
|
||||
### 配置方式
|
||||
|
||||
- 配置文件名:`config.yaml`
|
||||
- 搜索路径:
|
||||
- 当前工作目录
|
||||
- `$HOME/.config/home-automation`
|
||||
- 配置加载代码:[`src/cmd/serve.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/cmd/serve.go:65)
|
||||
- 开启了 `viper.WatchConfig()`
|
||||
|
||||
代码中实际出现的配置项包括:
|
||||
|
||||
- `port`
|
||||
- `logLevel`
|
||||
- `notion.token`
|
||||
- `ticktick.clientId`
|
||||
- `ticktick.clientSecret`
|
||||
- `ticktick.redirectUri`
|
||||
- `ticktick.token`
|
||||
- `homeassistant.ip`
|
||||
- `homeassistant.port`
|
||||
- `homeassistant.authToken`
|
||||
- `homeassistant.actionTaskProjectId`
|
||||
- `pooRecorder.tableId`
|
||||
- `pooRecorder.webhookId`
|
||||
- `pooRecorder.sensorEntityName`
|
||||
- `pooRecorder.sensorFriendlyName`
|
||||
- `pooRecorder.dbPath`
|
||||
- `locationRecorder.dbPath`
|
||||
|
||||
### 进程模型
|
||||
|
||||
- 单个二进制,通过 Cobra 子命令 `serve` 启动
|
||||
- 监听 `SIGINT` / `SIGTERM` 做优雅退出
|
||||
- scheduler 与 HTTP server 在同一进程内运行
|
||||
- 路由层使用 `gorilla/mux`
|
||||
|
||||
### 定时任务
|
||||
|
||||
- 每天 `0 5 * * *` 执行一次 poo records 与 Notion 的同步
|
||||
- 定义在 [`src/components/pooRecorder/pooRecorder.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/pooRecorder/pooRecorder.go:180)
|
||||
|
||||
### 被动接收式接口
|
||||
|
||||
- 上面列出的入站 REST API
|
||||
- TickTick OAuth callback endpoint
|
||||
|
||||
### 运行时依赖的外部服务
|
||||
|
||||
- Home Assistant HTTP API 与 webhook
|
||||
- TickTick OAuth 与 REST API
|
||||
- 当前 poo 流程仍依赖 Notion API
|
||||
- 本地可写文件系统,用于配置文件和 SQLite DB
|
||||
|
||||
### 当前本地 / 服务部署形态
|
||||
|
||||
- 安装脚本会构建 Go 二进制,并安装到 `$HOME/.local/home-automation-backend`
|
||||
- 使用 Supervisor 管理进程
|
||||
- 生成的 supervisor 配置最终执行 `{binary} serve`
|
||||
- 参考:
|
||||
- [`helper/install.sh`](/home/tianyu/workspace/home-automation/legacy/go-backend/helper/install.sh:45)
|
||||
- [`helper/home_automation_backend_template.conf`](/home/tianyu/workspace/home-automation/legacy/go-backend/helper/home_automation_backend_template.conf:1)
|
||||
|
||||
### 容器化情况
|
||||
|
||||
- 这一轮代码扫描中没有发现 `Dockerfile` 或 compose 文件
|
||||
- 当前部署形态是 supervisor-based,而不是 container-based
|
||||
|
||||
## 测试与文档现状
|
||||
|
||||
### 测试现状
|
||||
|
||||
- 只发现一个测试文件:[`src/components/homeassistant/homeassistant_test.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/homeassistant/homeassistant_test.go:1)
|
||||
- 当前测试覆盖:
|
||||
- 入站命令 JSON 解码
|
||||
- target/action 路由分发
|
||||
- 转发到 poo/location handler 的行为
|
||||
- TickTick task 创建委托
|
||||
- 错误日志和失败路径
|
||||
- 当前没有覆盖:
|
||||
- poo recorder 的 DB 行为
|
||||
- location recorder 的 DB 行为
|
||||
- TickTick OAuth 流程
|
||||
- Home Assistant 出站发布
|
||||
- Notion sync 逻辑
|
||||
- 启动与配置加载
|
||||
- scheduler 行为
|
||||
|
||||
### 本轮测试执行情况
|
||||
|
||||
- 我尝试在 `legacy/go-backend/src/` 下执行 `go test ./...`
|
||||
- 但当前会话环境里没有安装 `go` 命令,因此无法实际运行测试
|
||||
- 所以这轮关于测试的判断,基于静态阅读,而不是实际执行结果
|
||||
|
||||
### 文档现状
|
||||
|
||||
- 仓库里的 `README.md` 基本只有标题和 badge,内容非常少
|
||||
- 没有用户可读的 API 文档
|
||||
- 没有 schema 文档
|
||||
- 没有 Home Assistant / TickTick 的契约说明文档
|
||||
- 没有关于配置项、OAuth 初始化、数据库文件位置的运维文档
|
||||
|
||||
### 后续最需要补齐的文档
|
||||
|
||||
- Home Assistant 命令 envelope 与支持的 action
|
||||
- 各 API 的 request / response 契约
|
||||
- TickTick OAuth 初始化与 token 持久化方式
|
||||
- 数据库归属、用途与保留策略
|
||||
- Notion 下线 / 不迁移说明
|
||||
|
||||
## 对 Python 重构特别重要的事实
|
||||
|
||||
- 当前 API 行为整体比较“轻响应、重副作用”,很多成功请求返回的都是空响应体
|
||||
- 当前所有入站 API 都没有看到鉴权
|
||||
- 当前系统本地真相来源是 SQLite,但 poo 数据还同时与 Notion 同步
|
||||
- `notion.token` 现在不是可选项,缺失时服务会在 `initUtil()` 阶段直接退出
|
||||
- Home Assistant 命令路由当前是通过本地 HTTP 自调用实现的,而不是直接服务层调用
|
||||
- TickTick callback 会改写应用本身使用的 YAML 配置文件
|
||||
@@ -0,0 +1,40 @@
|
||||
# Migration Notes
|
||||
|
||||
本文档记录 Python skeleton 阶段的迁移说明,帮助后续继续推进时快速恢复上下文。
|
||||
|
||||
## 当前阶段完成内容
|
||||
|
||||
- 建立 FastAPI 应用骨架
|
||||
- 建立环境变量配置体系
|
||||
- 接入 SQLAlchemy 与 Alembic
|
||||
- 建立 Jinja2 模板基础
|
||||
- 建立 pytest 基础设施
|
||||
- 建立 Docker / Compose 基础骨架
|
||||
- 建立 OpenAPI 导出脚本
|
||||
|
||||
## 当前阶段未做内容
|
||||
|
||||
- 未迁移 TickTick 业务逻辑
|
||||
- 未迁移 Home Assistant 业务逻辑
|
||||
- 未迁移 poo records
|
||||
- 未迁移 location / life trajectory
|
||||
- 未实现真实 OAuth 流程
|
||||
- 未做数据迁移
|
||||
|
||||
## 后续建议顺序
|
||||
|
||||
建议继续沿用既有迁移文档中的顺序:
|
||||
|
||||
1. 先迁 `location recorder`
|
||||
2. 再迁 Home Assistant 出站适配层
|
||||
3. 再迁 TickTick adapter
|
||||
4. 再迁 Home Assistant 命令网关
|
||||
5. 最后迁 `poo recorder`
|
||||
|
||||
## 开发约束提醒
|
||||
|
||||
- 保持对当前 Go 外部行为的兼容意识
|
||||
- 不要把旧 Python 版本当作设计基线
|
||||
- 不要重新引入 Notion 作为 Python 主系统能力
|
||||
- 在迁业务模块时,优先补 contract tests
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
# 迁移风险清单
|
||||
|
||||
本文档列出将当前 Go 后端重构为 Python 时的主要风险点。这里的风险判断,默认都是以“当前 Go 行为需要尽量兼容”为前提。
|
||||
|
||||
## 最高风险区域
|
||||
|
||||
### 1. `POST /homeassistant/publish` 存在隐式行为契约
|
||||
|
||||
风险:
|
||||
|
||||
- 当前 Home Assistant 网关使用 `target`、`action`、`content` 这种 envelope
|
||||
- `content` 不是标准嵌套 JSON,而是字符串化 payload
|
||||
- 有些 payload 明显依赖单引号 pseudo-JSON,再在代码中做归一化
|
||||
|
||||
为什么重要:
|
||||
|
||||
- 如果 Python 版过早改成“严格、干净、标准化的嵌套 JSON”,现有 Home Assistant automations 可能直接失效
|
||||
|
||||
当前证据:
|
||||
|
||||
- [`src/components/homeassistant/homeassistant.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/homeassistant/homeassistant.go:82)
|
||||
- 测试见 [`src/components/homeassistant/homeassistant_test.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/homeassistant/homeassistant_test.go:129)
|
||||
|
||||
缓解建议:
|
||||
|
||||
- 在重构前先收集真实线上 payload 样例
|
||||
- 第一阶段保留兼容性解析
|
||||
- 为所有当前支持的 `target/action` 增加回归测试
|
||||
|
||||
### 2. TickTick OAuth 与 token 持久化流程
|
||||
|
||||
风险:
|
||||
|
||||
- OAuth `state` 当前只保存在进程内
|
||||
- token 获取后会直接写回 YAML 配置文件
|
||||
|
||||
为什么重要:
|
||||
|
||||
- Python 重构很容易在不自觉的情况下改变操作流程
|
||||
- token 持久化语义一变,可能会带来难排查的鉴权失败
|
||||
|
||||
当前证据:
|
||||
|
||||
- callback 与配置回写逻辑见 [`src/util/ticktickutil/ticktickutil.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/util/ticktickutil/ticktickutil.go:103)
|
||||
- 授权 URL 初始化见 [`src/util/ticktickutil/ticktickutil.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/util/ticktickutil/ticktickutil.go:275)
|
||||
|
||||
缓解建议:
|
||||
|
||||
- 在编码前先确定 token storage 方案
|
||||
- 保持 callback 契约稳定
|
||||
- 在 staging 环境用真实 TickTick app 做端到端验证
|
||||
|
||||
### 3. Poo recorder 的副作用比 API 表面看起来更复杂
|
||||
|
||||
风险:
|
||||
|
||||
- `POST /poo/record` 不只是写一条 DB
|
||||
- 它还会镜像到 Notion、发布 Home Assistant sensor、并且可能触发 Home Assistant webhook
|
||||
|
||||
为什么重要:
|
||||
|
||||
- 即使 Python 版 API 看起来兼容,如果漏掉这些副作用,也会导致真实自动化行为偏差
|
||||
|
||||
当前证据:
|
||||
|
||||
- [`src/components/pooRecorder/pooRecorder.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/pooRecorder/pooRecorder.go:57)
|
||||
- [`src/components/pooRecorder/pooRecorder.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/pooRecorder/pooRecorder.go:97)
|
||||
- [`src/components/pooRecorder/pooRecorder.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/pooRecorder/pooRecorder.go:339)
|
||||
|
||||
缓解建议:
|
||||
|
||||
- 把 endpoint 契约和 side-effect 契约分开写清楚
|
||||
- 在 Python 中通过显式 service / adapter 接口承接这些行为
|
||||
- 用 mock Home Assistant / TickTick / Notion 的方式做测试
|
||||
|
||||
### 4. 移除 Notion 会改变当前运行预期
|
||||
|
||||
风险:
|
||||
|
||||
- Notion 虽然已经被识别为“不计划继续保留”,但它现在并不是边缘代码
|
||||
- 它当前参与启动、请求处理以及每日同步
|
||||
|
||||
为什么重要:
|
||||
|
||||
- 去掉 Notion 会实质改变数据流
|
||||
- 也可能影响历史镜像、人工运维方式以及启动要求
|
||||
|
||||
当前证据:
|
||||
|
||||
- 启动时强依赖 `notion.token`,见 [`src/cmd/serve.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/cmd/serve.go:41)
|
||||
- 每日同步逻辑见 [`src/components/pooRecorder/pooRecorder.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/pooRecorder/pooRecorder.go:191)
|
||||
- helper CLI 见 [`src/helper/poo_recorder_helper/cmd/reverse.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/helper/poo_recorder_helper/cmd/reverse.go:21)
|
||||
|
||||
缓解建议:
|
||||
|
||||
- 在文档里显式说明 Notion 将被下线 / 不迁移
|
||||
- 先决定是否需要一次性历史导出或回填
|
||||
- 确保移除 Notion 后,`pooRecorder.tableId` 和 `notion.token` 不再阻塞服务启动
|
||||
|
||||
## 中等风险区域
|
||||
|
||||
### 5. SQLite 兼容性与时间戳格式
|
||||
|
||||
风险:
|
||||
|
||||
- 当前代码把时间戳以文本形式存储
|
||||
- `location` 和 `poo` 两个模块使用的时间格式并不相同
|
||||
|
||||
为什么重要:
|
||||
|
||||
- Python 重构若擅自统一时间格式,可能会破坏旧 DB 的可兼容读取
|
||||
|
||||
当前证据:
|
||||
|
||||
- poo 时间戳写入逻辑见 [`src/components/pooRecorder/pooRecorder.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/pooRecorder/pooRecorder.go:344)
|
||||
- location 时间戳写入逻辑见 [`src/components/locationRecorder/locationRecorder.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/locationRecorder/locationRecorder.go:61)
|
||||
|
||||
缓解建议:
|
||||
|
||||
- 先决定 Python 第一阶段是否直接复用现有 DB 文件
|
||||
- 如果要复用,就要保留当前时间戳序列化行为
|
||||
- 为数据层建立回归样例
|
||||
|
||||
### 6. 输入校验行为可能与 FastAPI 默认习惯冲突
|
||||
|
||||
风险:
|
||||
|
||||
- FastAPI / Pydantic 通常更倾向严格校验
|
||||
- 当前 Go 代码的校验行为并不一致:
|
||||
- 有些接口拒绝 unknown fields
|
||||
- `location` 数值解析错误会被静默忽略
|
||||
- 很多成功响应是空响应体
|
||||
|
||||
为什么重要:
|
||||
|
||||
- 更“正确”的校验,也可能是破坏兼容性的改动
|
||||
|
||||
当前证据:
|
||||
|
||||
- 多个 handler 都开启了严格字段检查
|
||||
- `location` 的静默 float parsing 见 [`src/components/locationRecorder/locationRecorder.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/locationRecorder/locationRecorder.go:54)
|
||||
|
||||
缓解建议:
|
||||
|
||||
- 先明确哪些怪异行为是必须兼容的,哪些可以修正
|
||||
- 第一阶段如果要保持兼容,可以在 Python 里用自定义校验逻辑模拟当前行为
|
||||
|
||||
### 7. 定时任务行为漂移
|
||||
|
||||
风险:
|
||||
|
||||
- 当前应用内嵌了一个每天 `0 5 * * *` 执行的 Notion 同步任务
|
||||
|
||||
为什么重要:
|
||||
|
||||
- 如果 Python 版仍保留类似行为,时区、执行时机、幂等性处理差异都可能导致重复或漏同步
|
||||
|
||||
当前证据:
|
||||
|
||||
- [`src/components/pooRecorder/pooRecorder.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/pooRecorder/pooRecorder.go:180)
|
||||
|
||||
缓解建议:
|
||||
|
||||
- 如果 Notion 被移除,就应有意识地同步移除 scheduler 相关逻辑,并写清楚原因
|
||||
- 如果在过渡期暂时保留,就要明确 timezone 与幂等语义
|
||||
|
||||
### 8. self-HTTP 编排改成 direct service calls 的差异
|
||||
|
||||
风险:
|
||||
|
||||
- 当前 Home Assistant 网关通过调用 `localhost` 上的本地接口来驱动其它模块
|
||||
|
||||
为什么重要:
|
||||
|
||||
- 改成直接函数 / service 调用本身是合理的,但可能改变 timeout、错误传播和日志行为
|
||||
|
||||
当前证据:
|
||||
|
||||
- [`src/components/homeassistant/homeassistant.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/homeassistant/homeassistant.go:88)
|
||||
- [`src/components/homeassistant/homeassistant.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/homeassistant/homeassistant.go:115)
|
||||
|
||||
缓解建议:
|
||||
|
||||
- 对外 HTTP 行为保持不变
|
||||
- 但在内部重写后,补足状态码和失败语义测试
|
||||
|
||||
## 风险较低但仍重要的区域
|
||||
|
||||
### 9. 部署模型变化
|
||||
|
||||
风险:
|
||||
|
||||
- 当前部署方式是 supervisor-based
|
||||
- 未来目标是容器化
|
||||
|
||||
为什么重要:
|
||||
|
||||
- 启动文件路径、配置文件写入位置、token persistence 方式,在容器环境下都可能出问题
|
||||
|
||||
当前证据:
|
||||
|
||||
- 安装脚本见 [`helper/install.sh`](/home/tianyu/workspace/home-automation/legacy/go-backend/helper/install.sh:45)
|
||||
|
||||
缓解建议:
|
||||
|
||||
- 把运行时状态从镜像内容中解耦
|
||||
- 预先定义 DB/config 是否需要挂载 volume
|
||||
- 在 cutover 前先写清楚 container env vars 与文件挂载约定
|
||||
|
||||
### 10. 现有测试过少
|
||||
|
||||
风险:
|
||||
|
||||
- 当前大多数模块没有自动化测试
|
||||
|
||||
为什么重要:
|
||||
|
||||
- 没有安全网时,重构很容易改坏行为而不自知
|
||||
|
||||
当前证据:
|
||||
|
||||
- 当前只发现 [`src/components/homeassistant/homeassistant_test.go`](/home/tianyu/workspace/home-automation/legacy/go-backend/src/components/homeassistant/homeassistant_test.go:1)
|
||||
|
||||
缓解建议:
|
||||
|
||||
- 把 contract test 建设视作迁移工作的一部分,而不是迁移后的补票
|
||||
- 每迁一个模块,就同步补该模块的测试
|
||||
|
||||
## 总结
|
||||
|
||||
这次重构最大的风险,不是“Go 改 Python”本身,而是几个隐藏得很深的行为契约:
|
||||
|
||||
- Home Assistant 命令 payload 的真实格式
|
||||
- TickTick 的 OAuth / token 生命周期
|
||||
- poo recorder 的一组副作用行为
|
||||
- 当前仍活跃、但计划下线的 Notion 耦合
|
||||
|
||||
只要先把这些契约写清楚、测清楚,再开始 Python 实现,整个重构路线就会可控很多。
|
||||
@@ -0,0 +1,314 @@
|
||||
# Python 重构方案
|
||||
|
||||
本文档基于当前 Go 实现,给出迁移到 Python + FastAPI 的设计输入与建议顺序。本文档只讨论迁移方案,不代表已经开始实现。
|
||||
|
||||
## 重构原则
|
||||
|
||||
- 以当前 Go 实现为唯一事实来源
|
||||
- 先保持对外行为兼容,再考虑内部清理和优化
|
||||
- 明确区分“行为兼容”和“内部实现升级”
|
||||
- 默认不把 Notion 纳入新的 Python 主版本目标,除非后续重新决策
|
||||
- 尽量按模块迁移,并在模块之间建立清晰契约
|
||||
|
||||
## 目标形态
|
||||
|
||||
建议的 Python 目标架构:
|
||||
|
||||
- 用 FastAPI 提供 HTTP 路由,并自然生成 OpenAPI
|
||||
- 业务逻辑按模块拆分到 service layer
|
||||
- 外部系统对接放到 adapter layer
|
||||
- SQLite 访问放到 repository layer
|
||||
- 配置采用显式 settings model
|
||||
- scheduler 是否内嵌在应用进程内,需要在启动语义明确后再决定
|
||||
- 仅保留极轻量的服务端页面,用于 OAuth 跳转或简单配置
|
||||
|
||||
## 建议的 Python 模块边界
|
||||
|
||||
### 应用外壳层
|
||||
|
||||
- 职责:
|
||||
- 读取 settings
|
||||
- 依赖注入与对象装配
|
||||
- 路由注册
|
||||
- lifespan hooks
|
||||
- scheduler 启停
|
||||
- 在 FastAPI 中可对应:
|
||||
- `main.py` 或 app factory
|
||||
- settings 类
|
||||
|
||||
### Poo 领域模块
|
||||
|
||||
- 职责:
|
||||
- 校验 poo record 输入
|
||||
- 持久化 poo record
|
||||
- 查询 latest poo
|
||||
- 通过接口触发外部副作用
|
||||
- 建议把副作用依赖抽象为端口:
|
||||
- `PooRepository`
|
||||
- `HomeAssistantPublisher`
|
||||
- `HomeAssistantWebhookClient`
|
||||
- 如有需要,可保留临时 `LegacyPooMirror` 作为 Notion 过渡适配器
|
||||
|
||||
### Location 领域模块
|
||||
|
||||
- 职责:
|
||||
- 校验并持久化位置点
|
||||
- 可保持简洁:
|
||||
- `LocationRepository`
|
||||
- `LocationService`
|
||||
|
||||
### Home Assistant 命令网关
|
||||
|
||||
- 职责:
|
||||
- 暴露 `/homeassistant/publish`
|
||||
- 解析 `target/action/content`
|
||||
- 将命令分发到内部服务
|
||||
- 兼容性注意:
|
||||
- 第一阶段应保留当前 `content` 的处理习惯,包括对字符串 payload 的兼容解析
|
||||
|
||||
### TickTick 集成模块
|
||||
|
||||
- 职责:
|
||||
- OAuth start / callback
|
||||
- token 存储
|
||||
- task 查询
|
||||
- 去重
|
||||
- task 创建
|
||||
- 建议:
|
||||
- 把 token persistence 抽象成独立能力,而不是把“改写配置文件”直接塞进业务逻辑
|
||||
|
||||
### Home Assistant 出站适配层
|
||||
|
||||
- 职责:
|
||||
- 发布 sensor state
|
||||
- 触发 webhook
|
||||
- 该层小而独立,适合较早迁移
|
||||
|
||||
### Legacy Notion 适配层
|
||||
|
||||
- 职责:
|
||||
- 只在分析或过渡阶段表示当前行为
|
||||
- 默认建议:
|
||||
- 不放入 Python 第一版正式目标
|
||||
- 如 cutover 期间确有需要,可以 feature flag 或独立迁移工具的方式暂存
|
||||
|
||||
## 实现前需要冻结的兼容契约
|
||||
|
||||
在正式编码前,建议先把以下当前行为写成明确契约:
|
||||
|
||||
### API 契约
|
||||
|
||||
- `POST /poo/record` 的请求字段与当前“成功时空响应体”的行为
|
||||
- `POST /location/record` 的请求字段与数值解析行为
|
||||
- `POST /homeassistant/publish` 的 envelope 格式与支持的 `target/action`
|
||||
- `GET /ticktick/auth/code` 的成功 / 失败语义
|
||||
|
||||
### 副作用契约
|
||||
|
||||
- `POST /poo/record` 在什么时机会发布 Home Assistant sensor
|
||||
- `POST /poo/record` 在什么条件下会触发 Home Assistant webhook
|
||||
- Home Assistant 消息如何映射为 TickTick task title 与 due date
|
||||
- Home Assistant sensor payload 的结构
|
||||
|
||||
### 持久化契约
|
||||
|
||||
- 当前 SQLite 表名与主键
|
||||
- 当前磁盘上的时间戳格式
|
||||
- Python 第一阶段是否直接复用现有 DB 文件,还是做显式迁移
|
||||
|
||||
## 建议的迁移决策
|
||||
|
||||
### 决策 1:第一阶段保持对外 API 形状不变
|
||||
|
||||
原因:
|
||||
|
||||
- 当前 API 面很小
|
||||
- 保持兼容能显著降低切换风险
|
||||
- 即使保持兼容,FastAPI 仍然可以生成 OpenAPI 文档
|
||||
|
||||
### 决策 2:把内部 self-HTTP 改成直接服务调用
|
||||
|
||||
原因:
|
||||
|
||||
- 当前 Go 代码中的 `localhost` 自调用,本质上是内部编排手段
|
||||
- 这不是一个必须暴露给外部的契约
|
||||
- Python 版改为直接函数 / service 调用,可以提升清晰度和可测试性
|
||||
|
||||
### 决策 3:先继续使用 SQLite
|
||||
|
||||
原因:
|
||||
|
||||
- 当前系统已经使用 SQLite
|
||||
- 数据模型规模很小
|
||||
- PostgreSQL 更适合作为 parity 之后的下一阶段演进
|
||||
|
||||
### 决策 4:默认不迁 Notion,但要明确记录影响
|
||||
|
||||
原因:
|
||||
|
||||
- 你已经明确表示 Notion 很可能不继续保留
|
||||
- 当前 Notion 不是“代码里有但没在用”,而是真正参与运行逻辑
|
||||
- 所以不能静默删除,而要在方案中写清楚删掉后有什么影响
|
||||
|
||||
### 决策 5:把 token / auth persistence 做成显式设计
|
||||
|
||||
原因:
|
||||
|
||||
- 当前 TickTick token 处理虽然可用,但运维上比较脆弱
|
||||
- Python 重构是一个把这件事规范化的机会
|
||||
|
||||
## 建议迁移顺序
|
||||
|
||||
### Phase 0:盘点与契约确认
|
||||
|
||||
- 完成当前系统 inventory
|
||||
- 确认哪些当前行为是“必须兼容的契约”,哪些只是历史偶然实现
|
||||
- 明确把 Notion 标为 non-migration scope,除非后续重新决定
|
||||
|
||||
### Phase 1:Python 骨架与通用基础设施
|
||||
|
||||
- 建立 FastAPI app shell
|
||||
- 定义 settings / config model
|
||||
- 定义日志方案
|
||||
- 定义 SQLite 访问方式
|
||||
- 定义测试框架与 fixture 策略
|
||||
- 定义 OpenAPI 生成与导出方式
|
||||
|
||||
这一阶段不需要大量迁移业务逻辑,只要搭好后续模块可持续迁入的基础即可。
|
||||
|
||||
### Phase 2:先迁最独立、最稳定的业务模块
|
||||
|
||||
推荐优先迁移:`location recorder`
|
||||
|
||||
原因:
|
||||
|
||||
- 独立 SQLite 表
|
||||
- 没有复杂外部副作用
|
||||
- 没有 OAuth
|
||||
- 没有 scheduler
|
||||
|
||||
这一阶段的交付物可以包括:
|
||||
|
||||
- `POST /location/record`
|
||||
- 与现有 SQLite 兼容的写入逻辑
|
||||
- 校验与 repository 的单元测试
|
||||
- 基于临时 SQLite 的 integration test
|
||||
|
||||
### Phase 3:迁移 Home Assistant 出站适配层
|
||||
|
||||
原因:
|
||||
|
||||
- 功能面小
|
||||
- 能为后面的 poo 迁移做铺垫
|
||||
|
||||
这一阶段的交付物可以包括:
|
||||
|
||||
- sensor publish client
|
||||
- webhook trigger client
|
||||
- 针对请求格式与错误处理的 mock tests
|
||||
|
||||
### Phase 4:迁移 TickTick adapter
|
||||
|
||||
原因:
|
||||
|
||||
- 相对自洽
|
||||
- 在完成 Home Assistant 命令网关前就需要它
|
||||
|
||||
这一阶段的交付物可以包括:
|
||||
|
||||
- OAuth callback endpoint
|
||||
- token persistence abstraction
|
||||
- task 创建与去重行为
|
||||
- 基于 mock HTTP 的集成式测试
|
||||
|
||||
### Phase 5:迁移 Home Assistant 命令网关
|
||||
|
||||
原因:
|
||||
|
||||
- 这是外部 automations 的核心编排入口
|
||||
- 在 location 与 TickTick adapter 准备好后,网关迁移会顺很多
|
||||
|
||||
这一阶段的交付物可以包括:
|
||||
|
||||
- `/homeassistant/publish`
|
||||
- 兼容当前 `target/action` 的分发逻辑
|
||||
- 用进程内 service 调用替代 self-HTTP
|
||||
- 把现有 Go 测试场景迁成 Python contract tests
|
||||
|
||||
### Phase 6:迁移 poo recorder 核心,但默认不带 Notion
|
||||
|
||||
原因:
|
||||
|
||||
- 这是最复杂的模块
|
||||
- 它既有本地 DB,又有 Home Assistant 副作用,当前还耦合 Notion
|
||||
|
||||
建议拆成两个子阶段:
|
||||
|
||||
- phase 6a:
|
||||
- 本地 poo DB
|
||||
- latest poo 查询
|
||||
- sensor publish
|
||||
- 可选 webhook trigger
|
||||
- `/poo/record`
|
||||
- `/poo/latest`
|
||||
|
||||
- phase 6b:
|
||||
- 如果 cutover 期间必须保留旧逻辑,再做一个临时 legacy Notion 兼容层
|
||||
|
||||
### Phase 7:运维加固与切换验证
|
||||
|
||||
- 做 Go / Python 路由级契约比对
|
||||
- 用现有 SQLite 文件或其副本做兼容验证
|
||||
- 在 staging 环境手动验证 TickTick OAuth
|
||||
- 用真实 Home Assistant automation payload 做验证
|
||||
- 导出 OpenAPI YAML
|
||||
- 再补容器化与部署方案
|
||||
|
||||
## 哪些模块适合先迁,哪些适合后迁
|
||||
|
||||
### 适合优先迁移
|
||||
|
||||
- location recorder
|
||||
- Home Assistant 出站 client
|
||||
- TickTick adapter
|
||||
|
||||
### 更适合后迁
|
||||
|
||||
- Home Assistant 命令网关
|
||||
- poo recorder 核心
|
||||
|
||||
### 现状存在,但建议不迁
|
||||
|
||||
- Notion sync adapter
|
||||
- `poo_recorder_helper` 的 Notion 表反转 CLI
|
||||
- `location_recorder` helper CLI 脚手架
|
||||
|
||||
## 建议的验证策略
|
||||
|
||||
### Contract tests
|
||||
|
||||
- 基于当前 Go 行为建立 request / response fixtures
|
||||
- 先把现有 `homeassistant` 测试案例迁成 Python
|
||||
- 补上 `poo` 与 `location` API 的契约测试
|
||||
|
||||
### Integration tests
|
||||
|
||||
- 每个模块使用临时 SQLite DB
|
||||
- Home Assistant 与 TickTick 出站流量通过 mock HTTP 替代
|
||||
- 若仍保留 scheduler,则为其补定时行为测试
|
||||
|
||||
### 手工 staging 验证
|
||||
|
||||
- TickTick OAuth callback
|
||||
- Home Assistant sensor 更新
|
||||
- Home Assistant webhook 触发
|
||||
- 当前真实自动化 payload 样例
|
||||
|
||||
## 开始实现前仍需明确的问题
|
||||
|
||||
- Python 第一阶段是否还要保留“缺少 `notion.token` 就启动失败”的行为,还是直接把 Notion 变成可关闭能力?
|
||||
- `POST /location/record` 是否要继续保留“非法数字静默变成 0”的兼容行为?
|
||||
- TickTick token 在第一阶段是否继续写回 YAML,还是立即切到独立 token store?
|
||||
- 当前 Home Assistant automations 是否真实依赖 `content` 中的单引号 pseudo-JSON?
|
||||
|
||||
这些问题不影响当前 inventory,但会影响第一阶段“兼容到什么程度”的具体定义。
|
||||
@@ -0,0 +1,18 @@
|
||||
# Legacy Code
|
||||
|
||||
这个目录用于收纳 Python 重构开始之前就已存在的旧实现与配套资产,方便在重构完成后整块删除。
|
||||
|
||||
当前已迁入:
|
||||
|
||||
- `go-backend/src/`
|
||||
- 旧 Go 后端实现
|
||||
- `go-backend/helper/`
|
||||
- 旧 Go 部署与辅助脚本
|
||||
- `go-backend/.github/workflows/`
|
||||
- 旧 Go 版本对应的 GitHub Actions workflows
|
||||
|
||||
原则上:
|
||||
|
||||
- 新的 Python 实现继续在仓库根目录的 `app/`、`tests/`、`alembic/` 等目录演进
|
||||
- 旧 Go 代码只作为迁移参考,不再作为新实现的结构基础
|
||||
- 当 Python 重构完成并验证稳定后,可以考虑整块删除 `legacy/go-backend/`
|
||||
@@ -0,0 +1,28 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=68", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-automation-python"
|
||||
version = "0.1.0"
|
||||
description = "Python rewrite skeleton for the home automation backend."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = [
|
||||
"app",
|
||||
"app.api",
|
||||
"app.api.routes",
|
||||
"app.integrations",
|
||||
"app.models",
|
||||
"app.schemas",
|
||||
"app.services",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["."]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
@@ -0,0 +1,8 @@
|
||||
alembic>=1.14,<2.0
|
||||
fastapi>=0.115,<0.116
|
||||
jinja2>=3.1,<4.0
|
||||
pydantic-settings>=2.6,<3.0
|
||||
python-multipart>=0.0.12,<1.0
|
||||
pyyaml>=6.0,<7.0
|
||||
sqlalchemy>=2.0,<3.0
|
||||
uvicorn[standard]>=0.32,<1.0
|
||||
@@ -0,0 +1,78 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.13
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile requirements.in
|
||||
#
|
||||
alembic==1.18.4
|
||||
# via -r requirements.in
|
||||
annotated-types==0.7.0
|
||||
# via pydantic
|
||||
anyio==4.13.0
|
||||
# via
|
||||
# starlette
|
||||
# watchfiles
|
||||
click==8.3.2
|
||||
# via uvicorn
|
||||
fastapi==0.115.14
|
||||
# via -r requirements.in
|
||||
greenlet==3.4.0
|
||||
# via sqlalchemy
|
||||
h11==0.16.0
|
||||
# via uvicorn
|
||||
httptools==0.7.1
|
||||
# via uvicorn
|
||||
idna==3.11
|
||||
# via anyio
|
||||
jinja2==3.1.6
|
||||
# via -r requirements.in
|
||||
mako==1.3.11
|
||||
# via alembic
|
||||
markupsafe==3.0.3
|
||||
# via
|
||||
# jinja2
|
||||
# mako
|
||||
pydantic==2.13.2
|
||||
# via
|
||||
# fastapi
|
||||
# pydantic-settings
|
||||
pydantic-core==2.46.2
|
||||
# via pydantic
|
||||
pydantic-settings==2.13.1
|
||||
# via -r requirements.in
|
||||
python-dotenv==1.2.2
|
||||
# via
|
||||
# pydantic-settings
|
||||
# uvicorn
|
||||
python-multipart==0.0.26
|
||||
# via -r requirements.in
|
||||
pyyaml==6.0.3
|
||||
# via
|
||||
# -r requirements.in
|
||||
# uvicorn
|
||||
sqlalchemy==2.0.49
|
||||
# via
|
||||
# -r requirements.in
|
||||
# alembic
|
||||
starlette==0.46.2
|
||||
# via fastapi
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# alembic
|
||||
# fastapi
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
# sqlalchemy
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.2
|
||||
# via
|
||||
# pydantic
|
||||
# pydantic-settings
|
||||
uvicorn[standard]==0.44.0
|
||||
# via -r requirements.in
|
||||
uvloop==0.22.1
|
||||
# via uvicorn
|
||||
watchfiles==1.1.1
|
||||
# via uvicorn
|
||||
websockets==16.0
|
||||
# via uvicorn
|
||||
@@ -0,0 +1,27 @@
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from app.main import create_app
|
||||
|
||||
|
||||
def main() -> None:
|
||||
app = create_app()
|
||||
output_dir = Path("openapi")
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
schema = app.openapi()
|
||||
|
||||
json_path = output_dir / "openapi.json"
|
||||
yaml_path = output_dir / "openapi.yaml"
|
||||
|
||||
json_path.write_text(__import__("json").dumps(schema, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
yaml_path.write_text(yaml.safe_dump(schema, allow_unicode=True, sort_keys=False), encoding="utf-8")
|
||||
|
||||
print(f"Wrote {json_path}")
|
||||
print(f"Wrote {yaml_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import create_app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
return create_app()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(app):
|
||||
with TestClient(app) as test_client:
|
||||
yield test_client
|
||||
@@ -0,0 +1,13 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
def test_app_starts(client: TestClient) -> None:
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_status_endpoint(client: TestClient) -> None:
|
||||
response = client.get("/status")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ok"}
|
||||
|
||||
Reference in New Issue
Block a user