This commit is contained in:
2026-06-16 09:19:32 +03:00
parent 7f1516c9c9
commit 8f3ac70b20
43 changed files with 1644 additions and 4668 deletions
+19
View File
@@ -0,0 +1,19 @@
from sqlalchemy.engine import Engine
def dialect_name(engine: Engine) -> str:
return engine.dialect.name
def is_sqlite(engine: Engine) -> bool:
return dialect_name(engine) == "sqlite"
def is_postgresql(engine: Engine) -> bool:
return dialect_name(engine) == "postgresql"
def bool_literal(engine: Engine, value: bool = False) -> str:
if is_sqlite(engine):
return "1" if value else "0"
return "true" if value else "false"
+2 -1
View File
@@ -1,6 +1,7 @@
from sqlalchemy import inspect, text
from app.db.base import engine
from app.db.dialect import bool_literal
def run_migrations() -> None:
@@ -17,7 +18,7 @@ def run_migrations() -> None:
conn.execute(
text(
"ALTER TABLE pomodoro_sessions "
"ADD COLUMN completion_notified BOOLEAN DEFAULT 0"
f"ADD COLUMN completion_notified BOOLEAN DEFAULT {bool_literal(engine, False)}"
)
)
+2 -14
View File
@@ -4,7 +4,7 @@ from sqlalchemy import inspect, select, text
from sqlalchemy.orm import Session
from app.db.base import engine
from app.db.models import FitnessProfile
from app.db.models import FitnessProfile, StepLog
from app.fitness.calculators import DEFAULT_NEAT_KCAL, compute_targets, macro_targets
logger = logging.getLogger(__name__)
@@ -173,19 +173,7 @@ def run_fitness_migrations() -> None:
)
if "step_logs" not in inspector.get_table_names():
with engine.begin() as conn:
conn.execute(
text(
"CREATE TABLE step_logs ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"logged_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
"steps INTEGER DEFAULT 0, "
"active_calories FLOAT, "
"source VARCHAR(32) DEFAULT 'manual', "
"notes TEXT DEFAULT ''"
")"
)
)
StepLog.__table__.create(engine, checkfirst=True)
if "body_metrics" in inspector.get_table_names():
_add_column_if_missing(
+3 -30
View File
@@ -11,6 +11,7 @@ from app.auth.tokens import hash_token
from app.character.card import DEFAULT_CARD, normalize_card
from app.config import get_settings
from app.db.base import engine
from app.db.models import CharacterCard, User
logger = logging.getLogger(__name__)
@@ -55,39 +56,11 @@ def _add_column_if_missing(table: str, column: str, ddl: str) -> None:
def _ensure_users_table() -> None:
if _table_exists("users"):
return
with engine.begin() as conn:
conn.execute(
text(
"CREATE TABLE users ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"username VARCHAR(64) NOT NULL UNIQUE, "
"display_name VARCHAR(255) DEFAULT '', "
"api_token_hash VARCHAR(64) NOT NULL, "
"is_active BOOLEAN DEFAULT 1, "
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP"
")"
)
)
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_users_api_token_hash ON users (api_token_hash)"))
User.__table__.create(engine, checkfirst=True)
def _ensure_character_cards_table() -> None:
if _table_exists("character_cards"):
return
with engine.begin() as conn:
conn.execute(
text(
"CREATE TABLE character_cards ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"user_id INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE, "
"card_json TEXT DEFAULT '{}', "
"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP"
")"
)
)
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_character_cards_user_id ON character_cards (user_id)"))
CharacterCard.__table__.create(engine, checkfirst=True)
def _add_user_id_columns() -> None:
-299
View File
@@ -1,299 +0,0 @@
from datetime import datetime
from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
class ChatSession(Base):
__tablename__ = "chat_sessions"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(String(255), default="Новый чат")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
messages: Mapped[list["Message"]] = relationship(
back_populates="session", cascade="all, delete-orphan", order_by="Message.created_at"
)
class Message(Base):
__tablename__ = "messages"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
session_id: Mapped[int] = mapped_column(ForeignKey("chat_sessions.id", ondelete="CASCADE"), index=True)
role: Mapped[str] = mapped_column(String(32))
content: Mapped[str] = mapped_column(Text, default="")
tool_calls_json: Mapped[str | None] = mapped_column(Text, nullable=True)
reasoning_json: Mapped[str | None] = mapped_column(Text, nullable=True)
tool_call_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
session: Mapped["ChatSession"] = relationship(back_populates="messages")
class PomodoroCycle(Base):
__tablename__ = "pomodoro_cycles"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
work_duration_min: Mapped[int] = mapped_column(Integer, default=25)
short_break_min: Mapped[int] = mapped_column(Integer, default=5)
long_break_min: Mapped[int] = mapped_column(Integer, default=15)
sessions_until_long_break: Mapped[int] = mapped_column(Integer, default=4)
completed_work_sessions: Mapped[int] = mapped_column(Integer, default=0)
task_note: Mapped[str] = mapped_column(Text, default="")
auto_advance: Mapped[bool] = mapped_column(Boolean, default=True)
chat_notify_seq: Mapped[int] = mapped_column(Integer, default=0)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class PomodoroSession(Base):
__tablename__ = "pomodoro_sessions"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
status: Mapped[str] = mapped_column(String(32), default="idle")
phase: Mapped[str] = mapped_column(String(32), default="work")
duration_min: Mapped[int] = mapped_column(Integer, default=25)
task_note: Mapped[str] = mapped_column(Text, default="")
result: Mapped[str | None] = mapped_column(Text, nullable=True)
completed: Mapped[bool] = mapped_column(Boolean, default=False)
completion_notified: Mapped[bool] = mapped_column(Boolean, default=False)
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
paused_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
elapsed_seconds: Mapped[int] = mapped_column(Integer, default=0)
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class TaigaProject(Base):
__tablename__ = "taiga_projects"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
taiga_id: Mapped[int] = mapped_column(Integer, unique=True, index=True)
name: Mapped[str] = mapped_column(String(255))
slug: Mapped[str] = mapped_column(String(255), unique=True, index=True)
synced_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class ProjectBinding(Base):
__tablename__ = "project_bindings"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
taiga_slug: Mapped[str] = mapped_column(String(255), unique=True, index=True)
gitea_owner: Mapped[str] = mapped_column(String(255), default="")
gitea_repo: Mapped[str] = mapped_column(String(255), default="")
default_branch: Mapped[str] = mapped_column(String(64), default="main")
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class UserProfile(Base):
__tablename__ = "user_profile"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
data_json: Mapped[str] = mapped_column(Text, default="{}")
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class MemoryFact(Base):
__tablename__ = "memory_facts"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
category: Mapped[str] = mapped_column(String(64), default="fact", index=True)
content: Mapped[str] = mapped_column(Text)
source: Mapped[str] = mapped_column(String(32), default="user")
session_id: Mapped[int | None] = mapped_column(
ForeignKey("chat_sessions.id", ondelete="SET NULL"), nullable=True, index=True
)
importance: Mapped[int] = mapped_column(Integer, default=3)
active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class SessionSummary(Base):
__tablename__ = "session_summaries"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
session_id: Mapped[int] = mapped_column(
ForeignKey("chat_sessions.id", ondelete="CASCADE"), unique=True, index=True
)
summary: Mapped[str] = mapped_column(Text, default="")
message_count: Mapped[int] = mapped_column(Integer, default=0)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class FitnessProfile(Base):
__tablename__ = "fitness_profiles"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
sex: Mapped[str] = mapped_column(String(16), default="male")
age: Mapped[int] = mapped_column(Integer, default=30)
height_cm: Mapped[float] = mapped_column(Float, default=170.0)
weight_kg: Mapped[float] = mapped_column(Float, default=70.0)
activity_level: Mapped[str] = mapped_column(String(32), default="moderate")
goal: Mapped[str] = mapped_column(String(32), default="maintain")
target_weight_kg: Mapped[float | None] = mapped_column(Float, nullable=True)
weekly_workouts: Mapped[int] = mapped_column(Integer, default=3)
calorie_target: Mapped[float] = mapped_column(Float, default=2000.0)
protein_g: Mapped[float] = mapped_column(Float, default=140.0)
fat_g: Mapped[float] = mapped_column(Float, default=65.0)
carbs_g: Mapped[float] = mapped_column(Float, default=200.0)
water_l: Mapped[float] = mapped_column(Float, default=2.5)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class BodyMetric(Base):
__tablename__ = "body_metrics"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
recorded_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
weight_kg: Mapped[float] = mapped_column(Float)
body_fat_pct: Mapped[float | None] = mapped_column(Float, nullable=True)
chest_cm: Mapped[float | None] = mapped_column(Float, nullable=True)
waist_cm: Mapped[float | None] = mapped_column(Float, nullable=True)
notes: Mapped[str] = mapped_column(Text, default="")
class FoodLog(Base):
__tablename__ = "food_logs"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
logged_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
meal_type: Mapped[str] = mapped_column(String(32), default="snack")
description: Mapped[str] = mapped_column(Text, default="")
calories: Mapped[float] = mapped_column(Float, default=0)
protein_g: Mapped[float] = mapped_column(Float, default=0)
fat_g: Mapped[float] = mapped_column(Float, default=0)
carbs_g: Mapped[float] = mapped_column(Float, default=0)
source: Mapped[str] = mapped_column(String(32), default="llm")
estimated: Mapped[bool] = mapped_column(Boolean, default=True)
class WaterLog(Base):
__tablename__ = "water_logs"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
logged_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
amount_ml: Mapped[int] = mapped_column(Integer)
class WorkoutLog(Base):
__tablename__ = "workout_logs"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
logged_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
title: Mapped[str] = mapped_column(String(255), default="Тренировка")
notes: Mapped[str] = mapped_column(Text, default="")
duration_min: Mapped[int | None] = mapped_column(Integer, nullable=True)
exercises_json: Mapped[str] = mapped_column(Text, default="[]")
class FitnessReminder(Base):
__tablename__ = "fitness_reminders"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
kind: Mapped[str] = mapped_column(String(32))
hour: Mapped[int] = mapped_column(Integer, default=12)
minute: Mapped[int] = mapped_column(Integer, default=0)
interval_hours: Mapped[int | None] = mapped_column(Integer, nullable=True)
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
last_fired_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
class ShoppingList(Base):
__tablename__ = "shopping_lists"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(255), unique=True, index=True)
sort_order: Mapped[int] = mapped_column(Integer, default=0)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
items: Mapped[list["ShoppingListItem"]] = relationship(
back_populates="shopping_list",
cascade="all, delete-orphan",
order_by="ShoppingListItem.sort_order, ShoppingListItem.id",
)
class ShoppingListItem(Base):
__tablename__ = "shopping_list_items"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
list_id: Mapped[int] = mapped_column(
ForeignKey("shopping_lists.id", ondelete="CASCADE"), index=True
)
text: Mapped[str] = mapped_column(String(500))
quantity: Mapped[float | None] = mapped_column(Float, nullable=True)
unit: Mapped[str] = mapped_column(String(64), default="")
checked: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
sort_order: Mapped[int] = mapped_column(Integer, default=0)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
shopping_list: Mapped["ShoppingList"] = relationship(back_populates="items")
class Reminder(Base):
__tablename__ = "reminders"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(String(255))
notes: Mapped[str] = mapped_column(Text, default="")
due_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True)
all_day: Mapped[bool] = mapped_column(Boolean, default=False)
recurrence: Mapped[str] = mapped_column(String(16), default="none")
enabled: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
last_fired_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
timezone: Mapped[str] = mapped_column(String(64), default="Europe/Moscow")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class AssistantState(Base):
__tablename__ = "assistant_state"
key: Mapped[str] = mapped_column(String(128), primary_key=True)
value: Mapped[str] = mapped_column(Text, default="")
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class WorkItem(Base):
__tablename__ = "work_items"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
taiga_slug: Mapped[str] = mapped_column(String(255), index=True)
taiga_project_id: Mapped[int] = mapped_column(Integer)
taiga_story_id: Mapped[int] = mapped_column(Integer)
taiga_story_ref: Mapped[int] = mapped_column(Integer, index=True)
gitea_owner: Mapped[str] = mapped_column(String(255), default="")
gitea_repo: Mapped[str] = mapped_column(String(255), default="")
gitea_issue_number: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
suggested_branch: Mapped[str] = mapped_column(String(255), default="")
raw_text: Mapped[str] = mapped_column(Text, default="")
title: Mapped[str] = mapped_column(String(500), default="")
status: Mapped[str] = mapped_column(String(32), default="open")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
closed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)