avant codex

This commit is contained in:
2026-02-22 15:05:40 +01:00
parent fed449c784
commit 20af00d653
291 changed files with 51868 additions and 424 deletions

View File

@@ -4,4 +4,4 @@ COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN mkdir -p /data/uploads
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8060"]

View File

@@ -0,0 +1,10 @@
from typing import Optional
from sqlmodel import Field, SQLModel
class Dicton(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
mois: int # 1-12
jour: Optional[int] = None
texte: str
region: Optional[str] = None # Auvergne|Haute-Loire|National

View File

@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import Optional
from sqlmodel import Field, SQLModel
@@ -16,10 +16,12 @@ class Garden(SQLModel, table=True):
ombre: Optional[str] = None # ombre | mi-ombre | plein_soleil
sol_type: Optional[str] = None
sol_ph: Optional[float] = None
surface_m2: Optional[float] = None
ensoleillement: Optional[str] = None
grille_largeur: int = 6
grille_hauteur: int = 4
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class GardenCell(SQLModel, table=True):
@@ -39,7 +41,7 @@ class GardenImage(SQLModel, table=True):
garden_id: int = Field(foreign_key="garden.id", index=True)
filename: str
caption: Optional[str] = None
created_at: datetime = Field(default_factory=datetime.utcnow)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class Measurement(SQLModel, table=True):
@@ -50,4 +52,4 @@ class Measurement(SQLModel, table=True):
humidite_air: Optional[float] = None
humidite_sol: Optional[float] = None
source: str = "manuel" # manuel | capteur
ts: datetime = Field(default_factory=datetime.utcnow)
ts: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

View File

@@ -1,9 +1,11 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import Optional
from sqlmodel import Field, SQLModel
class PlantVariety(SQLModel, table=True):
class Plant(SQLModel, table=True):
__tablename__ = "plant"
id: Optional[int] = Field(default=None, primary_key=True)
nom_commun: str
nom_botanique: Optional[str] = None
@@ -11,9 +13,11 @@ class PlantVariety(SQLModel, table=True):
famille: Optional[str] = None
tags: Optional[str] = None # CSV
type_plante: Optional[str] = None # legume | fruit | aromatique | fleur
categorie: Optional[str] = None # potager|fleur|arbre|arbuste
besoin_eau: Optional[str] = None # faible | moyen | fort
besoin_soleil: Optional[str] = None
espacement_cm: Optional[int] = None
hauteur_cm: Optional[int] = None
temp_min_c: Optional[float] = None
duree_culture_j: Optional[int] = None
profondeur_semis_cm: Optional[float] = None
@@ -23,13 +27,16 @@ class PlantVariety(SQLModel, table=True):
repiquage_mois: Optional[str] = None
plantation_mois: Optional[str] = None
recolte_mois: Optional[str] = None
maladies_courantes: Optional[str] = None
astuces_culture: Optional[str] = None
url_reference: Optional[str] = None
notes: Optional[str] = None
created_at: datetime = Field(default_factory=datetime.utcnow)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class PlantImage(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
variety_id: int = Field(foreign_key="plantvariety.id", index=True)
plant_id: int = Field(foreign_key="plant.id", index=True)
filename: str
caption: Optional[str] = None
created_at: datetime = Field(default_factory=datetime.utcnow)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

View File

@@ -1,12 +1,28 @@
from datetime import date, datetime
from datetime import date, datetime, timezone
from typing import Optional
from sqlmodel import Field, SQLModel
class PlantingCreate(SQLModel):
garden_id: int
variety_id: int
cell_id: Optional[int] = None
date_semis: Optional[date] = None
date_plantation: Optional[date] = None
date_repiquage: Optional[date] = None
quantite: int = 1
statut: str = "prevu"
date_recolte_debut: Optional[date] = None
date_recolte_fin: Optional[date] = None
rendement_estime: Optional[float] = None
rendement_reel: Optional[float] = None
notes: Optional[str] = None
class Planting(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
garden_id: int = Field(foreign_key="garden.id", index=True)
variety_id: int = Field(foreign_key="plantvariety.id", index=True)
variety_id: int = Field(foreign_key="plant.id", index=True)
cell_id: Optional[int] = Field(default=None, foreign_key="gardencell.id")
date_semis: Optional[date] = None
date_plantation: Optional[date] = None
@@ -18,8 +34,8 @@ class Planting(SQLModel, table=True):
rendement_estime: Optional[float] = None
rendement_reel: Optional[float] = None
notes: Optional[str] = None
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class PlantingEvent(SQLModel, table=True):
@@ -27,4 +43,4 @@ class PlantingEvent(SQLModel, table=True):
planting_id: int = Field(foreign_key="planting.id", index=True)
type: str # arrosage | taille | traitement | observation | autre
note: Optional[str] = None
ts: datetime = Field(default_factory=datetime.utcnow)
ts: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

View File

@@ -0,0 +1,42 @@
from datetime import date, datetime, timezone
from typing import Optional
from sqlmodel import Field, SQLModel
class RecolteCreate(SQLModel):
quantite: float
unite: str = "kg" # kg|g|unites|litres|bottes
date_recolte: date
notes: Optional[str] = None
class Recolte(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
plantation_id: int = Field(foreign_key="planting.id", index=True)
quantite: float
unite: str = "kg"
date_recolte: date
notes: Optional[str] = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class ObservationCreate(SQLModel):
type: str # maladie|ravageur|traitement|note
titre: str
description: Optional[str] = None
date: date
photo_url: Optional[str] = None
plantation_id: Optional[int] = None
garden_id: Optional[int] = None
class Observation(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
type: str
titre: str
description: Optional[str] = None
date: date
photo_url: Optional[str] = None
plantation_id: Optional[int] = Field(default=None, foreign_key="planting.id")
garden_id: Optional[int] = Field(default=None, foreign_key="garden.id")
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

View File

@@ -1,17 +1,34 @@
from datetime import date, datetime
from datetime import date, datetime, timezone
from typing import Optional
from sqlmodel import Field, SQLModel
class TaskCreate(SQLModel):
titre: str
description: Optional[str] = None
garden_id: Optional[int] = None
planting_id: Optional[int] = None
outil_id: Optional[int] = None
priorite: str = "normale" # basse | normale | haute
echeance: Optional[date] = None
recurrence: Optional[str] = None
frequence_jours: Optional[int] = None
date_prochaine: Optional[date] = None
statut: str = "a_faire" # a_faire | en_cours | fait | annule
class Task(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
titre: str
description: Optional[str] = None
garden_id: Optional[int] = Field(default=None, foreign_key="garden.id")
planting_id: Optional[int] = Field(default=None, foreign_key="planting.id")
priorite: str = "normale" # basse | normale | haute
outil_id: Optional[int] = Field(default=None, foreign_key="tool.id")
priorite: str = "normale"
echeance: Optional[date] = None
recurrence: Optional[str] = None # quotidien | hebdomadaire | mensuel
statut: str = "a_faire" # a_faire | en_cours | fait | annule
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
recurrence: Optional[str] = None
frequence_jours: Optional[int] = None
date_prochaine: Optional[date] = None
statut: str = "a_faire"
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

View File

@@ -0,0 +1,12 @@
from datetime import datetime, timezone
from typing import Optional
from sqlmodel import Field, SQLModel
class Tool(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
nom: str
description: Optional[str] = None
categorie: Optional[str] = None # beche|fourche|griffe|arrosage|taille|autre
photo_url: Optional[str] = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

View File

@@ -0,0 +1,59 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlmodel import Session, select
from app.database import get_session
from app.models.astuce import Astuce
router = APIRouter(tags=["astuces"])
@router.get("/astuces", response_model=List[Astuce])
def list_astuces(
entity_type: Optional[str] = Query(None),
entity_id: Optional[int] = Query(None),
session: Session = Depends(get_session),
):
q = select(Astuce)
if entity_type:
q = q.where(Astuce.entity_type == entity_type)
if entity_id is not None:
q = q.where(Astuce.entity_id == entity_id)
return session.exec(q).all()
@router.post("/astuces", response_model=Astuce, status_code=status.HTTP_201_CREATED)
def create_astuce(a: Astuce, session: Session = Depends(get_session)):
session.add(a)
session.commit()
session.refresh(a)
return a
@router.get("/astuces/{id}", response_model=Astuce)
def get_astuce(id: int, session: Session = Depends(get_session)):
a = session.get(Astuce, id)
if not a:
raise HTTPException(404, "Astuce introuvable")
return a
@router.put("/astuces/{id}", response_model=Astuce)
def update_astuce(id: int, data: Astuce, session: Session = Depends(get_session)):
a = session.get(Astuce, id)
if not a:
raise HTTPException(404, "Astuce introuvable")
for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items():
setattr(a, k, v)
session.add(a)
session.commit()
session.refresh(a)
return a
@router.delete("/astuces/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_astuce(id: int, session: Session = Depends(get_session)):
a = session.get(Astuce, id)
if not a:
raise HTTPException(404, "Astuce introuvable")
session.delete(a)
session.commit()

View File

@@ -0,0 +1,18 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, Query
from sqlmodel import Session, select
from app.database import get_session
from app.models.dicton import Dicton
router = APIRouter(tags=["dictons"])
@router.get("/dictons", response_model=List[Dicton])
def list_dictons(
mois: Optional[int] = Query(None),
session: Session = Depends(get_session),
):
q = select(Dicton)
if mois:
q = q.where(Dicton.mois == mois)
return session.exec(q).all()

View File

@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
@@ -38,7 +38,7 @@ def update_garden(id: int, data: Garden, session: Session = Depends(get_session)
raise HTTPException(status_code=404, detail="Jardin introuvable")
for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items():
setattr(g, k, v)
g.updated_at = datetime.utcnow()
g.updated_at = datetime.now(timezone.utc)
session.add(g)
session.commit()
session.refresh(g)

View File

@@ -0,0 +1,36 @@
import calendar
from datetime import date
from typing import Any
from fastapi import APIRouter, HTTPException, Query
router = APIRouter(tags=["lunaire"])
# Cache en mémoire : {mois_str: list[dict]}
_CACHE: dict[str, list[dict]] = {}
@router.get("/lunar")
def get_lunar(
month: str = Query(..., description="Format YYYY-MM"),
) -> list[dict[str, Any]]:
if month in _CACHE:
return _CACHE[month]
try:
year, mon = int(month[:4]), int(month[5:7])
except (ValueError, IndexError):
raise HTTPException(400, "Format attendu : YYYY-MM")
last_day = calendar.monthrange(year, mon)[1]
start = date(year, mon, 1)
end = date(year, mon, last_day)
try:
from app.services.lunar import build_calendar
from dataclasses import asdict
result = [asdict(d) for d in build_calendar(start, end)]
except ImportError:
raise HTTPException(503, "Service lunaire non disponible (skyfield non installé)")
except Exception as e:
raise HTTPException(500, f"Erreur calcul lunaire : {e}")
_CACHE[month] = result
return result

View File

@@ -2,13 +2,20 @@ import os
import uuid
from typing import List, Optional
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
from fastapi import APIRouter, Body, Depends, File, HTTPException, Query, UploadFile, status
from pydantic import BaseModel
from sqlmodel import Session, select
from app.config import UPLOAD_DIR
from app.database import get_session
from app.models.media import Attachment, Media
class MediaPatch(BaseModel):
entity_type: Optional[str] = None
entity_id: Optional[int] = None
titre: Optional[str] = None
router = APIRouter(tags=["media"])
@@ -81,6 +88,19 @@ def create_media(m: Media, session: Session = Depends(get_session)):
return m
@router.patch("/media/{id}", response_model=Media)
def update_media(id: int, payload: MediaPatch, session: Session = Depends(get_session)):
m = session.get(Media, id)
if not m:
raise HTTPException(404, "Media introuvable")
for k, v in payload.model_dump(exclude_none=True).items():
setattr(m, k, v)
session.add(m)
session.commit()
session.refresh(m)
return m
@router.delete("/media/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_media(id: int, session: Session = Depends(get_session)):
m = session.get(Media, id)

View File

@@ -1,9 +1,9 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.database import get_session
from app.models.planting import Planting, PlantingEvent
from app.models.planting import Planting, PlantingCreate, PlantingEvent
router = APIRouter(tags=["plantations"])
@@ -14,7 +14,8 @@ def list_plantings(session: Session = Depends(get_session)):
@router.post("/plantings", response_model=Planting, status_code=status.HTTP_201_CREATED)
def create_planting(p: Planting, session: Session = Depends(get_session)):
def create_planting(data: PlantingCreate, session: Session = Depends(get_session)):
p = Planting(**data.model_dump())
session.add(p)
session.commit()
session.refresh(p)
@@ -30,13 +31,13 @@ def get_planting(id: int, session: Session = Depends(get_session)):
@router.put("/plantings/{id}", response_model=Planting)
def update_planting(id: int, data: Planting, session: Session = Depends(get_session)):
def update_planting(id: int, data: PlantingCreate, session: Session = Depends(get_session)):
p = session.get(Planting, id)
if not p:
raise HTTPException(status_code=404, detail="Plantation introuvable")
for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items():
for k, v in data.model_dump(exclude_unset=True).items():
setattr(p, k, v)
p.updated_at = datetime.utcnow()
p.updated_at = datetime.now(timezone.utc)
session.add(p)
session.commit()
session.refresh(p)

View File

@@ -0,0 +1,56 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlmodel import Session, select
from app.database import get_session
from app.models.plant import Plant
router = APIRouter(tags=["plantes"])
@router.get("/plants", response_model=List[Plant])
def list_plants(
categorie: Optional[str] = Query(None),
session: Session = Depends(get_session),
):
q = select(Plant)
if categorie:
q = q.where(Plant.categorie == categorie)
return session.exec(q).all()
@router.post("/plants", response_model=Plant, status_code=status.HTTP_201_CREATED)
def create_plant(p: Plant, session: Session = Depends(get_session)):
session.add(p)
session.commit()
session.refresh(p)
return p
@router.get("/plants/{id}", response_model=Plant)
def get_plant(id: int, session: Session = Depends(get_session)):
p = session.get(Plant, id)
if not p:
raise HTTPException(404, "Plante introuvable")
return p
@router.put("/plants/{id}", response_model=Plant)
def update_plant(id: int, data: Plant, session: Session = Depends(get_session)):
p = session.get(Plant, id)
if not p:
raise HTTPException(404, "Plante introuvable")
for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items():
setattr(p, k, v)
session.add(p)
session.commit()
session.refresh(p)
return p
@router.delete("/plants/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_plant(id: int, session: Session = Depends(get_session)):
p = session.get(Plant, id)
if not p:
raise HTTPException(404, "Plante introuvable")
session.delete(p)
session.commit()

View File

@@ -0,0 +1,84 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlmodel import Session, select
from app.database import get_session
from app.models.recolte import Observation, ObservationCreate, Recolte, RecolteCreate
router = APIRouter(tags=["récoltes"])
# ── Récoltes (nested sous plantings) ──────────────────────────────────────────
@router.get("/plantings/{planting_id}/recoltes", response_model=List[Recolte])
def list_recoltes(planting_id: int, session: Session = Depends(get_session)):
return session.exec(
select(Recolte).where(Recolte.plantation_id == planting_id)
).all()
@router.post(
"/plantings/{planting_id}/recoltes",
response_model=Recolte,
status_code=status.HTTP_201_CREATED,
)
def create_recolte(
planting_id: int,
data: RecolteCreate,
session: Session = Depends(get_session),
):
r = Recolte(plantation_id=planting_id, **data.model_dump())
session.add(r)
session.commit()
session.refresh(r)
return r
@router.delete("/recoltes/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_recolte(id: int, session: Session = Depends(get_session)):
r = session.get(Recolte, id)
if not r:
raise HTTPException(404, "Récolte introuvable")
session.delete(r)
session.commit()
# ── Observations ───────────────────────────────────────────────────────────────
@router.get("/observations", response_model=List[Observation])
def list_observations(
plantation_id: Optional[int] = Query(None),
garden_id: Optional[int] = Query(None),
session: Session = Depends(get_session),
):
q = select(Observation)
if plantation_id is not None:
q = q.where(Observation.plantation_id == plantation_id)
if garden_id is not None:
q = q.where(Observation.garden_id == garden_id)
return session.exec(q).all()
@router.post("/observations", response_model=Observation, status_code=status.HTTP_201_CREATED)
def create_observation(data: ObservationCreate, session: Session = Depends(get_session)):
o = Observation(**data.model_dump())
session.add(o)
session.commit()
session.refresh(o)
return o
@router.get("/observations/{id}", response_model=Observation)
def get_observation(id: int, session: Session = Depends(get_session)):
o = session.get(Observation, id)
if not o:
raise HTTPException(404, "Observation introuvable")
return o
@router.delete("/observations/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_observation(id: int, session: Session = Depends(get_session)):
o = session.get(Observation, id)
if not o:
raise HTTPException(404, "Observation introuvable")
session.delete(o)
session.commit()

View File

@@ -1,8 +1,7 @@
from datetime import date
from fastapi import APIRouter, Depends
from sqlmodel import Session, select
from app.database import get_session
from app.models.settings import UserSettings, LunarCalendarEntry
from app.models.settings import UserSettings
router = APIRouter(tags=["réglages"])
@@ -24,16 +23,3 @@ def update_settings(data: dict, session: Session = Depends(get_session)):
session.add(row)
session.commit()
return {"ok": True}
@router.get("/lunar")
def get_lunar(month: str, session: Session = Depends(get_session)):
year, m = map(int, month.split("-"))
first = date(year, m, 1)
last_m, last_y = (m + 1, year) if m < 12 else (1, year + 1)
last = date(last_y, last_m, 1)
return session.exec(
select(LunarCalendarEntry)
.where(LunarCalendarEntry.jour >= first)
.where(LunarCalendarEntry.jour < last)
).all()

View File

@@ -1,9 +1,9 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.database import get_session
from app.models.task import Task
from app.models.task import Task, TaskCreate
router = APIRouter(tags=["tâches"])
@@ -23,7 +23,8 @@ def list_tasks(
@router.post("/tasks", response_model=Task, status_code=status.HTTP_201_CREATED)
def create_task(t: Task, session: Session = Depends(get_session)):
def create_task(data: TaskCreate, session: Session = Depends(get_session)):
t = Task(**data.model_dump())
session.add(t)
session.commit()
session.refresh(t)
@@ -39,13 +40,26 @@ def get_task(id: int, session: Session = Depends(get_session)):
@router.put("/tasks/{id}", response_model=Task)
def update_task(id: int, data: Task, session: Session = Depends(get_session)):
def update_task(id: int, data: TaskCreate, session: Session = Depends(get_session)):
t = session.get(Task, id)
if not t:
raise HTTPException(status_code=404, detail="Tâche introuvable")
for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items():
for k, v in data.model_dump(exclude_unset=True).items():
setattr(t, k, v)
t.updated_at = datetime.utcnow()
t.updated_at = datetime.now(timezone.utc)
session.add(t)
session.commit()
session.refresh(t)
return t
@router.put("/tasks/{id}/statut", response_model=Task)
def update_statut(id: int, statut: str, session: Session = Depends(get_session)):
t = session.get(Task, id)
if not t:
raise HTTPException(status_code=404, detail="Tâche introuvable")
t.statut = statut
t.updated_at = datetime.now(timezone.utc)
session.add(t)
session.commit()
session.refresh(t)

View File

@@ -0,0 +1,56 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlmodel import Session, select
from app.database import get_session
from app.models.tool import Tool
router = APIRouter(tags=["outils"])
@router.get("/tools", response_model=List[Tool])
def list_tools(
categorie: Optional[str] = Query(None),
session: Session = Depends(get_session),
):
q = select(Tool)
if categorie:
q = q.where(Tool.categorie == categorie)
return session.exec(q).all()
@router.post("/tools", response_model=Tool, status_code=status.HTTP_201_CREATED)
def create_tool(t: Tool, session: Session = Depends(get_session)):
session.add(t)
session.commit()
session.refresh(t)
return t
@router.get("/tools/{id}", response_model=Tool)
def get_tool(id: int, session: Session = Depends(get_session)):
t = session.get(Tool, id)
if not t:
raise HTTPException(404, "Outil introuvable")
return t
@router.put("/tools/{id}", response_model=Tool)
def update_tool(id: int, data: Tool, session: Session = Depends(get_session)):
t = session.get(Tool, id)
if not t:
raise HTTPException(404, "Outil introuvable")
for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items():
setattr(t, k, v)
session.add(t)
session.commit()
session.refresh(t)
return t
@router.delete("/tools/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_tool(id: int, session: Session = Depends(get_session)):
t = session.get(Tool, id)
if not t:
raise HTTPException(404, "Outil introuvable")
session.delete(t)
session.commit()

View File

@@ -6,73 +6,238 @@ import app.models # noqa
def run_seed():
from app.models.garden import Garden, GardenCell, Measurement
from app.models.plant import PlantVariety
from app.models.plant import Plant
from app.models.planting import Planting, PlantingEvent
from app.models.task import Task
from app.models.tool import Tool
from app.models.dicton import Dicton
from app.models.astuce import Astuce
with Session(engine) as session:
if session.exec(select(Garden)).first():
return # déjà seedé
already_seeded = session.exec(select(Garden)).first() is not None
jardin = Garden(
nom="Mon potager",
description="Potager principal plein sud",
type="plein_air",
exposition="S",
ombre="plein_soleil",
sol_type="limoneux",
grille_largeur=6,
grille_hauteur=4,
)
session.add(jardin)
session.flush()
if not already_seeded:
# ── Jardin ────────────────────────────────────────────────────────────
jardin = Garden(
nom="Mon potager",
description="Potager principal plein sud",
type="plein_air",
exposition="S",
ombre="plein_soleil",
sol_type="limoneux",
surface_m2=24.0,
grille_largeur=6,
grille_hauteur=4,
)
session.add(jardin)
session.flush()
for row in range(4):
for col in range(6):
session.add(GardenCell(
garden_id=jardin.id,
col=col, row=row,
libelle=f"{chr(65 + row)}{col + 1}",
))
for row in range(4):
for col in range(6):
session.add(
GardenCell(
garden_id=jardin.id,
col=col,
row=row,
libelle=f"{chr(65 + row)}{col + 1}",
)
)
session.add(Measurement(garden_id=jardin.id, temp_air=18.0, humidite_air=65.0))
session.add(Measurement(garden_id=jardin.id, temp_air=18.0, humidite_air=65.0))
tomate = PlantVariety(
nom_commun="Tomate", variete="Andine Cornue",
famille="Solanacées", type_plante="legume",
besoin_eau="fort", espacement_cm=60,
plantation_mois="4,5", recolte_mois="7,8,9",
)
courgette = PlantVariety(
nom_commun="Courgette", variete="Verte",
famille="Cucurbitacées", type_plante="legume",
besoin_eau="moyen", espacement_cm=80,
plantation_mois="5,6", recolte_mois="7,8",
)
salade = PlantVariety(
nom_commun="Laitue", variete="Batavia",
famille="Astéracées", type_plante="legume",
besoin_eau="moyen", espacement_cm=25,
)
session.add_all([tomate, courgette, salade])
session.flush()
# ── 20 Plantes ────────────────────────────────────────────────────────
plantes_data = [
dict(nom_commun="Tomate", variete="Andine Cornue", famille="Solanacées",
categorie="potager", type_plante="legume", besoin_eau="fort",
espacement_cm=60, plantation_mois="4,5", recolte_mois="7,8,9",
semis_interieur_mois="2,3"),
dict(nom_commun="Courgette", variete="Verte", famille="Cucurbitacées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=80, plantation_mois="5,6", recolte_mois="7,8",
semis_interieur_mois="4"),
dict(nom_commun="Carotte", famille="Apiacées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=8, semis_exterieur_mois="3,4,5,6",
recolte_mois="6,7,8,9,10"),
dict(nom_commun="Laitue", variete="Batavia", famille="Astéracées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=25, plantation_mois="3,4,5,8,9",
recolte_mois="5,6,7,10"),
dict(nom_commun="Ail", famille="Amaryllidacées",
categorie="potager", type_plante="legume", besoin_eau="faible",
espacement_cm=15, plantation_mois="10,11",
recolte_mois="6,7"),
dict(nom_commun="Oignon", famille="Amaryllidacées",
categorie="potager", type_plante="legume", besoin_eau="faible",
espacement_cm=10, semis_interieur_mois="2,3",
plantation_mois="4,5", recolte_mois="7,8"),
dict(nom_commun="Haricot", variete="Nain", famille="Fabacées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=15, semis_exterieur_mois="5,6",
recolte_mois="7,8,9"),
dict(nom_commun="Pois", variete="Mange-tout", famille="Fabacées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=10, semis_exterieur_mois="3,4",
recolte_mois="6,7"),
dict(nom_commun="Poireau", famille="Amaryllidacées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=15, semis_interieur_mois="2,3",
plantation_mois="6,7", recolte_mois="10,11,12,1,2"),
dict(nom_commun="Pomme de terre", famille="Solanacées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=35, plantation_mois="3,4,5",
recolte_mois="7,8,9,10"),
dict(nom_commun="Fraise", famille="Rosacées",
categorie="potager", type_plante="fruit", besoin_eau="moyen",
espacement_cm=30, plantation_mois="3,4,9,10",
recolte_mois="5,6,7"),
dict(nom_commun="Framboise", famille="Rosacées",
categorie="arbuste", type_plante="fruit", besoin_eau="moyen",
espacement_cm=60, plantation_mois="11,12,2,3",
recolte_mois="7,8,9"),
dict(nom_commun="Persil", famille="Apiacées",
categorie="potager", type_plante="aromatique", besoin_eau="moyen",
espacement_cm=20, semis_exterieur_mois="3,4,5,8",
recolte_mois="4,5,6,7,8,9,10"),
dict(nom_commun="Échalote", famille="Amaryllidacées",
categorie="potager", type_plante="legume", besoin_eau="faible",
espacement_cm=15, plantation_mois="2,3",
recolte_mois="7,8"),
dict(nom_commun="Chou-fleur", famille="Brassicacées",
categorie="potager", type_plante="legume", besoin_eau="fort",
espacement_cm=60, semis_interieur_mois="3,4",
plantation_mois="5,6", recolte_mois="9,10,11"),
dict(nom_commun="Chou", variete="Milan", famille="Brassicacées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=50, semis_interieur_mois="3,4",
plantation_mois="5,6", recolte_mois="10,11,12"),
dict(nom_commun="Betterave", famille="Amaranthacées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=15, semis_exterieur_mois="4,5,6",
recolte_mois="8,9,10"),
dict(nom_commun="Radis", famille="Brassicacées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=5, semis_exterieur_mois="3,4,5,8,9",
recolte_mois="4,5,6,9,10"),
dict(nom_commun="Épinard", famille="Amaranthacées",
categorie="potager", type_plante="legume", besoin_eau="moyen",
espacement_cm=15, semis_exterieur_mois="3,4,8,9",
recolte_mois="5,6,10,11"),
dict(nom_commun="Basilic", famille="Lamiacées",
categorie="potager", type_plante="aromatique", besoin_eau="moyen",
espacement_cm=20, semis_interieur_mois="3,4",
plantation_mois="5,6", recolte_mois="6,7,8,9"),
]
p1 = Planting(
garden_id=jardin.id, variety_id=tomate.id,
date_plantation=date(2026, 5, 1), quantite=6, statut="en_cours",
)
p2 = Planting(
garden_id=jardin.id, variety_id=courgette.id,
date_plantation=date(2026, 5, 15), quantite=3, statut="prevu",
)
session.add_all([p1, p2])
session.flush()
plantes = []
for data in plantes_data:
p = Plant(**data)
session.add(p)
plantes.append(p)
session.flush()
session.add(PlantingEvent(planting_id=p1.id, type="arrosage", note="Arrosage du matin"))
tomate = plantes[0]
courgette = plantes[1]
session.add(Task(titre="Arroser les tomates", priorite="haute",
statut="a_faire", garden_id=jardin.id))
session.add(Task(titre="Traiter contre les pucerons", priorite="normale", statut="a_faire"))
session.add(Task(titre="Préparer le compost", priorite="basse", statut="en_cours"))
# ── Plantings ──────────────────────────────────────────────────────────
p1 = Planting(
garden_id=jardin.id,
variety_id=tomate.id,
date_plantation=date(2026, 5, 1),
quantite=6,
statut="en_cours",
)
p2 = Planting(
garden_id=jardin.id,
variety_id=courgette.id,
date_plantation=date(2026, 5, 15),
quantite=3,
statut="prevu",
)
session.add_all([p1, p2])
session.flush()
session.add(PlantingEvent(planting_id=p1.id, type="arrosage", note="Arrosage du matin"))
# ── Tâches ────────────────────────────────────────────────────────────
session.add(Task(titre="Arroser les tomates", priorite="haute",
statut="a_faire", garden_id=jardin.id))
session.add(Task(titre="Traiter contre les pucerons", priorite="normale", statut="a_faire"))
session.add(Task(titre="Préparer le compost", priorite="basse", statut="en_cours"))
# ── Outils (indépendant du jardin) ────────────────────────────────────────
if not session.exec(select(Tool)).first():
outils_data = [
dict(nom="Bêche", categorie="beche",
description="Bêche acier forgé, manche bois 110 cm"),
dict(nom="Fourche-bêche", categorie="fourche",
description="Fourche à bêcher 4 dents inox"),
dict(nom="Grelinette", categorie="fourche",
description="Aérateur bi-fourche ergonomique"),
dict(nom="Pioche", categorie="beche",
description="Pioche légère pour travaux de surface"),
dict(nom="Sarcloir", categorie="griffe",
description="Sarcloir oscillant pour désherber entre les rangs"),
dict(nom="Râteau", categorie="griffe",
description="Râteau métallique 14 dents"),
dict(nom="Binette", categorie="griffe",
description="Binette pour ameublir et désherber"),
dict(nom="Transplantoir", categorie="taille",
description="Transplantoir inox gradué"),
dict(nom="Arrosoir", categorie="arrosage",
description="Arrosoir 10L avec pomme amovible"),
dict(nom="Sécateur", categorie="taille",
description="Sécateur de précision bypass"),
]
for data in outils_data:
session.add(Tool(**data))
# ── Dictons (indépendant du jardin) ──────────────────────────────────────
if not session.exec(select(Dicton)).first():
dictons_data = [
dict(mois=1, texte="En janvier, la neige au potager réjouit le jardinier.", region="National"),
dict(mois=2, texte="À la Chandeleur, l'hiver reste ou reprend vigueur.", region="National"),
dict(mois=3, texte="Mars venteux, avril pluvieux, font mai fleureux.", region="National"),
dict(mois=3, texte="Quand mars se déguise en été, avril se déguise en hiver.", region="Auvergne"),
dict(mois=4, texte="Avril ne te découvre pas d'un fil.", region="National"),
dict(mois=4, texte="Pluie d'avril, fleurs à l'infini.", region="Haute-Loire"),
dict(mois=5, texte="Gelées de mai, misère chez le jardinier.", region="Haute-Loire"),
dict(mois=5, jour=11, texte="Saints de glace : Mamert, Pancrace et Gervais.", region="National"),
dict(mois=6, texte="Juin sec, juillet pluvieux ; juillet sec, grains savoureux.", region="Auvergne"),
dict(mois=7, texte="Pluie de juillet remplit greniers et cuves.", region="National"),
dict(mois=8, texte="Août chaud, vin bon.", region="Auvergne"),
dict(mois=9, texte="En septembre, qui sème du blé en fait son profit.", region="National"),
dict(mois=10, texte="En octobre, glands à foison, bon hiver selon la raison.", region="Haute-Loire"),
dict(mois=11, texte="À la Saint-Martin, bois ton vin.", region="National"),
dict(mois=12, texte="Noël au balcon, Pâques aux tisons.", region="Auvergne"),
]
for data in dictons_data:
session.add(Dicton(**data))
# ── Astuces (indépendant du jardin) ──────────────────────────────────────
if not session.exec(select(Astuce)).first():
astuces_data = [
dict(titre="Rotation des cultures", entity_type="general",
contenu="Changez chaque année la famille de légumes sur chaque parcelle pour éviter l'épuisement du sol et les maladies."),
dict(titre="Compagnonnage tomate-basilic", entity_type="plante",
contenu="Plantez du basilic au pied des tomates : il éloigne les pucerons et améliore le goût des fruits."),
dict(titre="Paillage économise l'eau", entity_type="jardin",
contenu="Un paillage de 5 à 10 cm (paille, BRF, tontes) réduit les arrosages de moitié et limite les mauvaises herbes."),
dict(titre="Arrosage au pied le matin", entity_type="general",
contenu="Arrosez toujours au pied des plantes le matin pour éviter les maladies cryptogamiques et la brûlure des feuilles."),
dict(titre="Purin d'ortie maison", entity_type="general",
contenu="Faites macérer 1 kg d'orties dans 10 L d'eau pendant 10 jours. Diluez à 10 % et arrosez le sol pour stimuler la croissance."),
dict(titre="Buttage des pommes de terre", entity_type="plante",
contenu="Buttez régulièrement les pommes de terre quand les fanes atteignent 20 cm pour favoriser la tubérisation."),
dict(titre="Semis de carottes en gel", entity_type="plante",
contenu="Mélangez les graines de carottes avec du sable fin pour un semis homogène et clairsemé."),
dict(titre="Récupération d'eau de pluie", entity_type="jardin",
contenu="Installez une cuve de récupération d'eau de pluie : une maison avec 100 m² de toiture collecte 60 000 L/an."),
dict(titre="Calendrier lunaire", entity_type="general",
contenu="Semez les légumes-feuilles en lune montante, les légumes-racines en lune descendante pour de meilleurs résultats."),
dict(titre="Taille en vert des tomates", entity_type="plante",
contenu="Pincez les gourmands (tiges secondaires entre tige principale et feuille) pour concentrer l'énergie sur les fruits."),
]
for data in astuces_data:
session.add(Astuce(**data))
session.commit()

BIN
backend/jardin.db Normal file

Binary file not shown.

View File

@@ -1,6 +1,6 @@
def test_create_planting(client):
g = client.post("/api/gardens", json={"nom": "Potager", "type": "plein_air"}).json()
v = client.post("/api/varieties", json={"nom_commun": "Tomate"}).json()
v = client.post("/api/plants", json={"nom_commun": "Tomate"}).json()
r = client.post("/api/plantings", json={
"garden_id": g["id"], "variety_id": v["id"], "quantite": 3
})
@@ -10,7 +10,7 @@ def test_create_planting(client):
def test_list_plantings(client):
g = client.post("/api/gardens", json={"nom": "Potager", "type": "plein_air"}).json()
v = client.post("/api/varieties", json={"nom_commun": "Tomate"}).json()
v = client.post("/api/plants", json={"nom_commun": "Tomate"}).json()
client.post("/api/plantings", json={"garden_id": g["id"], "variety_id": v["id"]})
r = client.get("/api/plantings")
assert r.status_code == 200
@@ -19,7 +19,7 @@ def test_list_plantings(client):
def test_add_planting_event(client):
g = client.post("/api/gardens", json={"nom": "Potager", "type": "plein_air"}).json()
v = client.post("/api/varieties", json={"nom_commun": "Tomate"}).json()
v = client.post("/api/plants", json={"nom_commun": "Tomate"}).json()
p = client.post("/api/plantings", json={"garden_id": g["id"], "variety_id": v["id"]}).json()
r = client.post(f"/api/plantings/{p['id']}/events", json={"type": "arrosage", "note": "Bien arrosé"})
assert r.status_code == 201

View File

@@ -0,0 +1,26 @@
def test_create_plant(client):
r = client.post("/api/plants", json={"nom_commun": "Tomate", "famille": "Solanacées"})
assert r.status_code == 201
assert r.json()["nom_commun"] == "Tomate"
def test_list_plants(client):
client.post("/api/plants", json={"nom_commun": "Tomate"})
client.post("/api/plants", json={"nom_commun": "Courgette"})
r = client.get("/api/plants")
assert r.status_code == 200
assert len(r.json()) == 2
def test_get_plant(client):
r = client.post("/api/plants", json={"nom_commun": "Basilic"})
id = r.json()["id"]
r2 = client.get(f"/api/plants/{id}")
assert r2.status_code == 200
def test_delete_plant(client):
r = client.post("/api/plants", json={"nom_commun": "Test"})
id = r.json()["id"]
r2 = client.delete(f"/api/plants/{id}")
assert r2.status_code == 204

View File

@@ -0,0 +1,36 @@
def test_create_recolte(client):
# créer jardin + plante + plantation d'abord
g = client.post(
"/api/gardens",
json={"nom": "J", "grille_largeur": 2, "grille_hauteur": 2, "type": "plein_air"},
).json()
p = client.post("/api/plants", json={"nom_commun": "Tomate"}).json()
pl = client.post(
"/api/plantings",
json={"garden_id": g["id"], "variety_id": p["id"], "quantite": 1, "statut": "en_cours"},
).json()
r = client.post(
f"/api/plantings/{pl['id']}/recoltes",
json={"quantite": 2.5, "unite": "kg", "date_recolte": "2026-08-01"},
)
assert r.status_code == 201
assert r.json()["quantite"] == 2.5
def test_list_recoltes(client):
g = client.post(
"/api/gardens",
json={"nom": "J", "grille_largeur": 2, "grille_hauteur": 2, "type": "plein_air"},
).json()
p = client.post("/api/plants", json={"nom_commun": "Tomate"}).json()
pl = client.post(
"/api/plantings",
json={"garden_id": g["id"], "variety_id": p["id"], "quantite": 1, "statut": "en_cours"},
).json()
client.post(
f"/api/plantings/{pl['id']}/recoltes",
json={"quantite": 1, "unite": "kg", "date_recolte": "2026-08-01"},
)
r = client.get(f"/api/plantings/{pl['id']}/recoltes")
assert r.status_code == 200
assert len(r.json()) == 1

View File

@@ -0,0 +1,18 @@
def test_create_tool(client):
r = client.post("/api/tools", json={"nom": "Bêche", "categorie": "beche"})
assert r.status_code == 201
assert r.json()["nom"] == "Bêche"
def test_list_tools(client):
client.post("/api/tools", json={"nom": "Outil1"})
r = client.get("/api/tools")
assert r.status_code == 200
assert len(r.json()) >= 1
def test_delete_tool(client):
r = client.post("/api/tools", json={"nom": "Test"})
id = r.json()["id"]
r2 = client.delete(f"/api/tools/{id}")
assert r2.status_code == 204

View File

@@ -1,26 +1,29 @@
"""Tests de l'ancien endpoint /api/varieties — maintenant redirigé vers /api/plants."""
def test_create_variety(client):
r = client.post("/api/varieties", json={"nom_commun": "Tomate", "famille": "Solanacées"})
r = client.post("/api/plants", json={"nom_commun": "Tomate", "famille": "Solanacées"})
assert r.status_code == 201
assert r.json()["nom_commun"] == "Tomate"
def test_list_varieties(client):
client.post("/api/varieties", json={"nom_commun": "Tomate"})
client.post("/api/varieties", json={"nom_commun": "Courgette"})
r = client.get("/api/varieties")
client.post("/api/plants", json={"nom_commun": "Tomate"})
client.post("/api/plants", json={"nom_commun": "Courgette"})
r = client.get("/api/plants")
assert r.status_code == 200
assert len(r.json()) == 2
def test_get_variety(client):
r = client.post("/api/varieties", json={"nom_commun": "Basilic"})
r = client.post("/api/plants", json={"nom_commun": "Basilic"})
id = r.json()["id"]
r2 = client.get(f"/api/varieties/{id}")
r2 = client.get(f"/api/plants/{id}")
assert r2.status_code == 200
def test_delete_variety(client):
r = client.post("/api/varieties", json={"nom_commun": "Test"})
r = client.post("/api/plants", json={"nom_commun": "Test"})
id = r.json()["id"]
r2 = client.delete(f"/api/varieties/{id}")
r2 = client.delete(f"/api/plants/{id}")
assert r2.status_code == 204