Bootstrap Python rewrite skeleton
This commit is contained in:
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user