From 883299272f8fe5f4a435d582969e896d2040466b Mon Sep 17 00:00:00 2001 From: gilles Date: Sun, 22 Feb 2026 12:20:32 +0100 Subject: [PATCH] feat(backend): champs identified_* sur Media pour stocker l'identification Co-Authored-By: Claude Sonnet 4.6 --- backend/app/migrate.py | 71 +++++++++++++++++++++++++++++++++++++ backend/app/models/media.py | 28 +++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 backend/app/migrate.py create mode 100644 backend/app/models/media.py diff --git a/backend/app/migrate.py b/backend/app/migrate.py new file mode 100644 index 0000000..8006d3b --- /dev/null +++ b/backend/app/migrate.py @@ -0,0 +1,71 @@ +"""Migration automatique SQLite — détecte et ajoute les colonnes manquantes.""" +import logging +from sqlalchemy import text +from app.database import engine + +logger = logging.getLogger(__name__) + +# Définition des colonnes attendues par table : {table: [(col, type_sql, default)]} +EXPECTED_COLUMNS: dict[str, list[tuple[str, str, str | None]]] = { + "plant": [ + ("categorie", "TEXT", None), + ("hauteur_cm", "INTEGER", None), + ("maladies_courantes", "TEXT", None), + ("astuces_culture", "TEXT", None), + ("url_reference", "TEXT", None), + ], + "garden": [ + ("surface_m2", "REAL", None), + ("ensoleillement", "TEXT", None), + ], + "task": [ + ("frequence_jours", "INTEGER", None), + ("date_prochaine", "TEXT", None), + ("outil_id", "INTEGER", None), + ], + "plantvariety": [ + # ancien nom de table → migration vers "plant" si présente + ], + "media": [ + ("identified_species", "TEXT", None), + ("identified_common", "TEXT", None), + ("identified_confidence", "REAL", None), + ("identified_source", "TEXT", None), + ], +} + + +def _existing_columns(conn, table: str) -> set[str]: + rows = conn.execute(text(f"PRAGMA table_info({table})")).fetchall() + return {row[1] for row in rows} + + +def _existing_tables(conn) -> set[str]: + rows = conn.execute(text("SELECT name FROM sqlite_master WHERE type='table'")).fetchall() + return {row[0] for row in rows} + + +def run_migrations() -> None: + with engine.connect() as conn: + tables = _existing_tables(conn) + + # Renommage plantvariety → plant si l'ancienne table existe et la nouvelle non + if "plantvariety" in tables and "plant" not in tables: + conn.execute(text("ALTER TABLE plantvariety RENAME TO plant")) + conn.commit() + logger.info("Migration: plantvariety renommée en plant") + tables = _existing_tables(conn) + + for table, columns in EXPECTED_COLUMNS.items(): + if table not in tables or not columns: + continue + existing = _existing_columns(conn, table) + for col_name, col_type, default in columns: + if col_name not in existing: + if default is not None: + sql = f"ALTER TABLE {table} ADD COLUMN {col_name} {col_type} DEFAULT {default}" + else: + sql = f"ALTER TABLE {table} ADD COLUMN {col_name} {col_type}" + conn.execute(text(sql)) + conn.commit() + logger.info(f"Migration: colonne {table}.{col_name} ajoutée") diff --git a/backend/app/models/media.py b/backend/app/models/media.py new file mode 100644 index 0000000..49a4db4 --- /dev/null +++ b/backend/app/models/media.py @@ -0,0 +1,28 @@ +from datetime import datetime, timezone +from typing import Optional +from sqlmodel import Field, SQLModel + + +class Media(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + entity_type: str # jardin|plante|outil|plantation + entity_id: int + url: str + thumbnail_url: Optional[str] = None + titre: Optional[str] = None + # Identification automatique + identified_species: Optional[str] = None + identified_common: Optional[str] = None + identified_confidence: Optional[float] = None + identified_source: Optional[str] = None # "plantnet" | "yolo" | "cache" + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + + +class Attachment(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + entity_type: str + entity_id: int + type: str # pdf|url|note + titre: Optional[str] = None + contenu: Optional[str] = None + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))