add homekeeper stack

This commit is contained in:
2026-07-06 19:25:38 +02:00
commit 58983e6855
90 changed files with 6816 additions and 0 deletions
View File
+94
View File
@@ -0,0 +1,94 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import text
from sqlalchemy.orm import Session
from typing import Any
from app.database import get_session
from app.auth import get_current_user
router = APIRouter()
def _row(r) -> dict:
return dict(r._mapping)
@router.get("")
def list_actions(
colony_id: int = Query(...),
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("""
SELECT * FROM colony_actions WHERE colony_id = :colony_id
ORDER BY date::DATE DESC, created_at DESC
"""), {"colony_id": colony_id}).fetchall()
return [_row(r) for r in rows]
@router.post("", status_code=201)
def create_action(
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(text("""
INSERT INTO colony_actions (colony_id, date, time, type, notes, related_colony_id)
VALUES (:colony_id, :date, :time, :type, :notes, :related_colony_id)
RETURNING *
"""), {
"colony_id": body.get("colony_id"),
"date": body.get("date"),
"time": body.get("time"),
"type": body.get("type"),
"notes": body.get("notes"),
"related_colony_id": body.get("related_colony_id"),
}).fetchone()
session.commit()
return _row(row)
@router.get("/{id}")
def get_action(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(
text("SELECT * FROM colony_actions WHERE id = :id"), {"id": id}
).fetchone()
if row is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(row)
@router.put("/{id}")
def update_action(
id: int,
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
result = session.execute(text("""
UPDATE colony_actions SET date = :date, type = :type, notes = :notes
WHERE id = :id RETURNING *
"""), {
"id": id,
"date": body.get("date"),
"type": body.get("type"),
"notes": body.get("notes"),
}).fetchone()
session.commit()
if result is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(result)
@router.delete("/{id}", status_code=204)
def delete_action(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
):
session.execute(text("DELETE FROM colony_actions WHERE id = :id"), {"id": id})
session.commit()
+99
View File
@@ -0,0 +1,99 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import text
from sqlalchemy.orm import Session
from typing import Any
from app.database import get_session
from app.auth import get_current_user
router = APIRouter()
def _row(r) -> dict:
return dict(r._mapping)
@router.get("")
def list_apiaries(
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("""
SELECT a.*, COUNT(c.id) AS colony_count
FROM apiaries a
LEFT JOIN colonies c ON c.apiary_id = a.id AND c.closed_at IS NULL
GROUP BY a.id
ORDER BY a.name
""")).fetchall()
return [_row(r) for r in rows]
@router.get("/{id}")
def get_apiary(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(
text("SELECT * FROM apiaries WHERE id = :id"), {"id": id}
).fetchone()
if row is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(row)
@router.post("", status_code=201)
def create_apiary(
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(text("""
INSERT INTO apiaries (name, location_text, lat, lng, notes)
VALUES (:name, :location_text, :lat, :lng, :notes)
RETURNING *
"""), {
"name": body.get("name"),
"location_text": body.get("location_text"),
"lat": body.get("lat"),
"lng": body.get("lng"),
"notes": body.get("notes"),
}).fetchone()
session.commit()
return _row(row)
@router.put("/{id}")
def update_apiary(
id: int,
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
result = session.execute(text("""
UPDATE apiaries
SET name = :name, location_text = :location_text, lat = :lat, lng = :lng, notes = :notes
WHERE id = :id
RETURNING *
"""), {
"id": id,
"name": body.get("name"),
"location_text": body.get("location_text"),
"lat": body.get("lat"),
"lng": body.get("lng"),
"notes": body.get("notes"),
}).fetchone()
session.commit()
if result is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(result)
@router.delete("/{id}", status_code=204)
def delete_apiary(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
):
session.execute(text("DELETE FROM apiaries WHERE id = :id"), {"id": id})
session.commit()
+144
View File
@@ -0,0 +1,144 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import text
from sqlalchemy.orm import Session
from typing import Any
from app.database import get_session
from app.auth import get_current_user
router = APIRouter()
def _row(r) -> dict:
return dict(r._mapping)
@router.get("")
def list_checklists(
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("SELECT * FROM checklists ORDER BY name")).fetchall()
return [_row(r) for r in rows]
@router.post("", status_code=201)
def create_checklist(
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(text("""
INSERT INTO checklists (name, scope) VALUES (:name, :scope) RETURNING *
"""), {"name": body.get("name"), "scope": body.get("scope", "colony")}).fetchone()
session.commit()
return _row(row)
@router.delete("/{id}", status_code=204)
def delete_checklist(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
):
session.execute(text("DELETE FROM checklists WHERE id = :id"), {"id": id})
session.commit()
@router.get("/{id}/items")
def list_items(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("""
SELECT * FROM checklist_items WHERE checklist_id = :id ORDER BY position
"""), {"id": id}).fetchall()
return [_row(r) for r in rows]
@router.post("/{id}/items", status_code=201)
def create_item(
id: int,
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(text("""
INSERT INTO checklist_items (checklist_id, text, position)
VALUES (:checklist_id, :text, :position) RETURNING *
"""), {
"checklist_id": id,
"text": body.get("text"),
"position": body.get("position", 0),
}).fetchone()
session.commit()
return _row(row)
@router.delete("/items/{item_id}", status_code=204)
def delete_item(
item_id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
):
session.execute(text("DELETE FROM checklist_items WHERE id = :id"), {"id": item_id})
session.commit()
@router.get("/{id}/completions")
def list_completions(
id: int,
scope_id: int = Query(...),
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("""
SELECT cc.*, ci.text, ci.position
FROM checklist_completions cc
JOIN checklist_items ci ON ci.id = cc.item_id
WHERE cc.checklist_id = :checklist_id AND cc.scope_id = :scope_id
ORDER BY ci.position
"""), {"checklist_id": id, "scope_id": scope_id}).fetchall()
return [_row(r) for r in rows]
@router.post("/{id}/completions/toggle")
def toggle_completion(
id: int,
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
item_id = body.get("item_id")
scope_id = body.get("scope_id")
date = body.get("date")
completed_by = body.get("completed_by")
notes = body.get("notes")
existing = session.execute(text("""
SELECT id FROM checklist_completions
WHERE checklist_id = :checklist_id AND item_id = :item_id AND scope_id = :scope_id
"""), {"checklist_id": id, "item_id": item_id, "scope_id": scope_id}).fetchone()
if existing is not None:
session.execute(
text("DELETE FROM checklist_completions WHERE id = :id"),
{"id": existing._mapping["id"]},
)
session.commit()
return {"toggled": False}
else:
session.execute(text("""
INSERT INTO checklist_completions (checklist_id, item_id, scope_id, date, completed_by, notes)
VALUES (:checklist_id, :item_id, :scope_id, :date, :completed_by, :notes)
"""), {
"checklist_id": id,
"item_id": item_id,
"scope_id": scope_id,
"date": date,
"completed_by": completed_by,
"notes": notes,
})
session.commit()
return {"toggled": True}
+294
View File
@@ -0,0 +1,294 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import text
from sqlalchemy.orm import Session
from typing import Any
from app.database import get_session
from app.auth import get_current_user
router = APIRouter()
def _row(r) -> dict:
return dict(r._mapping)
@router.get("")
def list_colonies(
apiary_id: int | None = Query(default=None),
include_closed: bool = Query(default=False),
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
where = "1=1" if include_closed else "c.closed_at IS NULL"
params: dict[str, Any] = {}
if apiary_id is not None:
where += " AND c.apiary_id = :apiary_id"
params["apiary_id"] = apiary_id
rows = session.execute(text(f"""
SELECT
c.*,
a.name AS apiary_name,
p.name AS parent_name,
q.year AS queen_year,
q.breed AS queen_breed,
(SELECT MAX(i.date::DATE)
FROM inspections i
WHERE i.colony_id = c.id) AS last_inspection_date
FROM colonies c
LEFT JOIN apiaries a ON a.id = c.apiary_id
LEFT JOIN colonies p ON p.id = c.parent_colony_id
LEFT JOIN queens q ON q.id = c.current_queen_id
WHERE {where}
ORDER BY a.name, c.name
"""), params).fetchall()
return [_row(r) for r in rows]
@router.get("/log-placeholder", include_in_schema=False)
def _log_placeholder():
pass # just to force ordering; actual log route is below
@router.get("/{id}/log")
def colony_log(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("""
SELECT date, type, summary, ref_id, created_at FROM (
SELECT date, 'durchsicht' AS type,
TRIM(BOTH FROM CONCAT_WS(' ',
CASE WHEN brood_frames IS NOT NULL THEN brood_frames::TEXT || ' BW' END,
CASE WHEN honey_frames IS NOT NULL THEN honey_frames::TEXT || ' HW' END,
CASE WHEN colony_strength IS NOT NULL THEN 'Stärke ' || colony_strength::TEXT END,
CASE WHEN queen_seen = 1 THEN '♛ gesehen' END
)) AS summary,
id AS ref_id, created_at
FROM inspections WHERE colony_id = :colony_id
UNION ALL
SELECT date, 'kontrolle' AS type,
count::TEXT || ' Milben' ||
CASE WHEN date_from IS NOT NULL
THEN ' · ' || (date::DATE - date_from::DATE)::TEXT || ' Tage' ELSE '' END AS summary,
id AS ref_id, created_at
FROM varroa_counts WHERE colony_id = :colony_id
UNION ALL
SELECT date, 'behandlung' AS type,
product || CASE WHEN dosage IS NOT NULL AND dosage <> ''
THEN ' · ' || dosage ELSE '' END AS summary,
id AS ref_id, created_at
FROM varroa_treatments WHERE colony_id = :colony_id
UNION ALL
SELECT vc.date, 'nachkontrolle' AS type,
vt.product || CASE WHEN vc.result IS NOT NULL AND vc.result <> ''
THEN ': ' || vc.result ELSE '' END AS summary,
vc.id AS ref_id, vc.created_at
FROM varroa_controls vc
JOIN varroa_treatments vt ON vt.id = vc.treatment_id
WHERE vc.colony_id = :colony_id
UNION ALL
SELECT h.date, 'ernte' AS type,
h.weight_kg::TEXT || ' kg' ||
CASE WHEN h.type IS NOT NULL THEN ' · ' || h.type ELSE '' END AS summary,
h.id AS ref_id, h.created_at
FROM honey_harvests h WHERE h.colony_id = :colony_id
UNION ALL
SELECT ca.date, 'massnahme' AS type,
CASE ca.type
WHEN 'ablegerbildung' THEN 'Ablegerbildung'
WHEN 'zwischenbodenableger' THEN 'Zwischenbödenableger'
WHEN 'brutentnahme' THEN 'Brutentnahme'
WHEN 'standwechsel' THEN 'Standwechsel'
WHEN 'futterung' THEN 'Fütterung'
WHEN 'vereinigung' THEN 'Völkervereinigung'
ELSE 'Maßnahme'
END ||
CASE WHEN ca.notes IS NOT NULL AND ca.notes <> ''
THEN ': ' || ca.notes ELSE '' END AS summary,
ca.id AS ref_id, ca.created_at
FROM colony_actions ca WHERE ca.colony_id = :colony_id
) log
ORDER BY date::DATE DESC, created_at DESC
"""), {"colony_id": id}).fetchall()
return [_row(r) for r in rows]
@router.get("/{id}/lineage")
def colony_lineage(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("""
WITH RECURSIVE lineage(id, name, status, parent_colony_id, depth) AS (
SELECT id, name, status, parent_colony_id, 0
FROM colonies WHERE id = :colony_id
UNION ALL
SELECT c.id, c.name, c.status, c.parent_colony_id, l.depth + 1
FROM colonies c
JOIN lineage l ON c.id = l.parent_colony_id
)
SELECT * FROM lineage ORDER BY depth DESC
"""), {"colony_id": id}).fetchall()
return [_row(r) for r in rows]
@router.get("/{id}/descendants")
def colony_descendants(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("""
WITH RECURSIVE descendants(id, name, status, parent_colony_id, depth) AS (
SELECT id, name, status, parent_colony_id, 0
FROM colonies WHERE id = :colony_id
UNION ALL
SELECT c.id, c.name, c.status, c.parent_colony_id, d.depth + 1
FROM colonies c
JOIN descendants d ON c.parent_colony_id = d.id
)
SELECT * FROM descendants WHERE depth > 0 ORDER BY depth, name
"""), {"colony_id": id}).fetchall()
return [_row(r) for r in rows]
@router.get("/{id}/location-history")
def colony_location_history(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("""
SELECT cl.*, a.name AS apiary_name
FROM colony_locations cl
JOIN apiaries a ON a.id = cl.apiary_id
WHERE cl.colony_id = :colony_id
ORDER BY cl.moved_at DESC
"""), {"colony_id": id}).fetchall()
return [_row(r) for r in rows]
@router.get("/{id}")
def get_colony(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(text("""
SELECT c.*, a.name AS apiary_name, p.name AS parent_name
FROM colonies c
LEFT JOIN apiaries a ON a.id = c.apiary_id
LEFT JOIN colonies p ON p.id = c.parent_colony_id
WHERE c.id = :id
"""), {"id": id}).fetchone()
if row is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(row)
@router.post("", status_code=201)
def create_colony(
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(text("""
INSERT INTO colonies (name, apiary_id, status, parent_colony_id, notes)
VALUES (:name, :apiary_id, :status, :parent_colony_id, :notes)
RETURNING *
"""), {
"name": body.get("name"),
"apiary_id": body.get("apiary_id"),
"status": body.get("status", "wirtschaftsvolk"),
"parent_colony_id": body.get("parent_colony_id"),
"notes": body.get("notes"),
}).fetchone()
colony_id = row._mapping["id"]
session.execute(text("""
INSERT INTO colony_locations (colony_id, apiary_id) VALUES (:colony_id, :apiary_id)
"""), {"colony_id": colony_id, "apiary_id": body.get("apiary_id")})
session.commit()
return _row(row)
@router.put("/{id}")
def update_colony(
id: int,
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
# Check if apiary changed (to insert colony_location)
old = session.execute(
text("SELECT apiary_id FROM colonies WHERE id = :id"), {"id": id}
).fetchone()
result = session.execute(text("""
UPDATE colonies
SET name = :name, apiary_id = :apiary_id, status = :status, notes = :notes
WHERE id = :id
RETURNING *
"""), {
"id": id,
"name": body.get("name"),
"apiary_id": body.get("apiary_id"),
"status": body.get("status"),
"notes": body.get("notes"),
}).fetchone()
if result is None:
raise HTTPException(status_code=404, detail="Not found")
new_apiary_id = body.get("apiary_id")
if old is not None and old._mapping["apiary_id"] != new_apiary_id:
session.execute(text("""
INSERT INTO colony_locations (colony_id, apiary_id) VALUES (:colony_id, :apiary_id)
"""), {"colony_id": id, "apiary_id": new_apiary_id})
session.commit()
return _row(result)
@router.post("/{id}/close")
def close_colony(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
result = session.execute(text("""
UPDATE colonies SET status = 'eingegangen', closed_at = NOW()
WHERE id = :id RETURNING *
"""), {"id": id}).fetchone()
session.commit()
if result is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(result)
@router.post("/{id}/set-queen")
def set_queen(
id: int,
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
result = session.execute(text("""
UPDATE colonies SET current_queen_id = :queen_id WHERE id = :id RETURNING *
"""), {"id": id, "queen_id": body.get("queen_id")}).fetchone()
session.commit()
if result is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(result)
+136
View File
@@ -0,0 +1,136 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import text
from sqlalchemy.orm import Session
from typing import Any
from app.database import get_session
from app.auth import get_current_user
router = APIRouter()
def _row(r) -> dict:
return dict(r._mapping)
@router.get("/summary")
def harvest_summary(
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("""
SELECT
EXTRACT(YEAR FROM date::date)::INTEGER AS year,
type,
SUM(weight_kg) AS total_kg,
COUNT(*) AS n_harvests
FROM honey_harvests
GROUP BY year, type
ORDER BY year DESC, total_kg DESC
""")).fetchall()
return [_row(r) for r in rows]
@router.get("")
def list_harvests(
colony_id: int | None = Query(default=None),
apiary_id: int | None = Query(default=None),
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
if colony_id is not None:
rows = session.execute(text("""
SELECT h.*, c.name AS colony_name, a.name AS apiary_name
FROM honey_harvests h
LEFT JOIN colonies c ON c.id = h.colony_id
LEFT JOIN apiaries a ON a.id = h.apiary_id
WHERE h.colony_id = :colony_id
ORDER BY h.date DESC
"""), {"colony_id": colony_id}).fetchall()
elif apiary_id is not None:
rows = session.execute(text("""
SELECT h.*, c.name AS colony_name, a.name AS apiary_name
FROM honey_harvests h
LEFT JOIN colonies c ON c.id = h.colony_id
LEFT JOIN apiaries a ON a.id = h.apiary_id
WHERE h.apiary_id = :apiary_id
ORDER BY h.date DESC
"""), {"apiary_id": apiary_id}).fetchall()
else:
rows = session.execute(text("""
SELECT h.*, c.name AS colony_name, a.name AS apiary_name
FROM honey_harvests h
LEFT JOIN colonies c ON c.id = h.colony_id
LEFT JOIN apiaries a ON a.id = h.apiary_id
ORDER BY h.date DESC
""")).fetchall()
return [_row(r) for r in rows]
@router.post("", status_code=201)
def create_harvest(
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(text("""
INSERT INTO honey_harvests (date, weight_kg, colony_id, apiary_id, type, notes, time)
VALUES (:date, :weight_kg, :colony_id, :apiary_id, :type, :notes, :time)
RETURNING *
"""), {
"date": body.get("date"),
"weight_kg": body.get("weight_kg"),
"colony_id": body.get("colony_id"),
"apiary_id": body.get("apiary_id"),
"type": body.get("type"),
"notes": body.get("notes"),
"time": body.get("time"),
}).fetchone()
session.commit()
return _row(row)
@router.get("/{id}")
def get_harvest(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(
text("SELECT * FROM honey_harvests WHERE id = :id"), {"id": id}
).fetchone()
if row is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(row)
@router.put("/{id}")
def update_harvest(
id: int,
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
result = session.execute(text("""
UPDATE honey_harvests SET date = :date, weight_kg = :weight_kg, notes = :notes
WHERE id = :id RETURNING *
"""), {
"id": id,
"date": body.get("date"),
"weight_kg": body.get("weight_kg"),
"notes": body.get("notes"),
}).fetchone()
session.commit()
if result is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(result)
@router.delete("/{id}", status_code=204)
def delete_harvest(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
):
session.execute(text("DELETE FROM honey_harvests WHERE id = :id"), {"id": id})
session.commit()
+164
View File
@@ -0,0 +1,164 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import text
from sqlalchemy.orm import Session
from typing import Any
from app.database import get_session
from app.auth import get_current_user
router = APIRouter()
def _row(r) -> dict:
return dict(r._mapping)
@router.get("")
def list_inspections(
colony_id: int = Query(...),
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("""
SELECT * FROM inspections WHERE colony_id = :colony_id
ORDER BY date::DATE DESC, id DESC
"""), {"colony_id": colony_id}).fetchall()
return [_row(r) for r in rows]
@router.get("/last")
def last_inspection(
colony_id: int = Query(...),
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
):
row = session.execute(text("""
SELECT * FROM inspections WHERE colony_id = :colony_id
ORDER BY date::DATE DESC, id DESC LIMIT 1
"""), {"colony_id": colony_id}).fetchone()
if row is None:
return None
return _row(row)
@router.get("/{id}/traits")
def inspection_traits(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("""
SELECT * FROM inspection_traits WHERE inspection_id = :id
"""), {"id": id}).fetchall()
return [_row(r) for r in rows]
@router.get("/{id}")
def get_inspection(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(
text("SELECT * FROM inspections WHERE id = :id"), {"id": id}
).fetchone()
if row is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(row)
@router.post("", status_code=201)
def create_inspection(
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(text("""
INSERT INTO inspections
(colony_id, date, inspector, brood_frames, honey_frames, food_stores_kg,
colony_strength, queen_seen, eggs_seen, queen_cells_count, notes)
VALUES
(:colony_id, :date, :inspector, :brood_frames, :honey_frames, :food_stores_kg,
:colony_strength, :queen_seen, :eggs_seen, :queen_cells_count, :notes)
RETURNING *
"""), {
"colony_id": body.get("colony_id"),
"date": body.get("date"),
"inspector": body.get("inspector"),
"brood_frames": body.get("brood_frames"),
"honey_frames": body.get("honey_frames"),
"food_stores_kg": body.get("food_stores_kg"),
"colony_strength": body.get("colony_strength"),
"queen_seen": int(body.get("queen_seen", 0)),
"eggs_seen": int(body.get("eggs_seen", 0)),
"queen_cells_count": int(body.get("queen_cells_count", 0)),
"notes": body.get("notes"),
}).fetchone()
inspection_id = row._mapping["id"]
traits = body.get("traits", {})
for trait, score in (traits or {}).items():
if score is not None:
session.execute(text("""
INSERT INTO inspection_traits (inspection_id, trait, score)
VALUES (:inspection_id, :trait, :score)
"""), {"inspection_id": inspection_id, "trait": trait, "score": int(score)})
session.commit()
return _row(row)
@router.put("/{id}")
def update_inspection(
id: int,
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
result = session.execute(text("""
UPDATE inspections SET
date = :date, inspector = :inspector, brood_frames = :brood_frames,
honey_frames = :honey_frames, food_stores_kg = :food_stores_kg,
colony_strength = :colony_strength, queen_seen = :queen_seen,
eggs_seen = :eggs_seen, queen_cells_count = :queen_cells_count, notes = :notes
WHERE id = :id
RETURNING *
"""), {
"id": id,
"date": body.get("date"),
"inspector": body.get("inspector"),
"brood_frames": body.get("brood_frames"),
"honey_frames": body.get("honey_frames"),
"food_stores_kg": body.get("food_stores_kg"),
"colony_strength": body.get("colony_strength"),
"queen_seen": int(body.get("queen_seen", 0)),
"eggs_seen": int(body.get("eggs_seen", 0)),
"queen_cells_count": int(body.get("queen_cells_count", 0)),
"notes": body.get("notes"),
}).fetchone()
if result is None:
raise HTTPException(status_code=404, detail="Not found")
session.execute(
text("DELETE FROM inspection_traits WHERE inspection_id = :id"), {"id": id}
)
traits = body.get("traits", {})
for trait, score in (traits or {}).items():
if score is not None:
session.execute(text("""
INSERT INTO inspection_traits (inspection_id, trait, score)
VALUES (:inspection_id, :trait, :score)
"""), {"inspection_id": id, "trait": trait, "score": int(score)})
session.commit()
return _row(result)
@router.delete("/{id}", status_code=204)
def delete_inspection(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
):
session.execute(text("DELETE FROM inspections WHERE id = :id"), {"id": id})
session.commit()
+149
View File
@@ -0,0 +1,149 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import text
from sqlalchemy.orm import Session
from typing import Any
from app.database import get_session
from app.auth import get_current_user
router = APIRouter()
def _row(r) -> dict:
return dict(r._mapping)
@router.get("")
def list_lists(
include_closed: bool = Query(default=False),
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
where = "" if include_closed else "WHERE l.closed_at IS NULL"
order = "ORDER BY l.closed_at IS NOT NULL, l.created_at DESC" if include_closed else "ORDER BY l.created_at DESC"
rows = session.execute(text(f"""
SELECT
l.*,
COUNT(i.id) AS item_count,
COUNT(i.id) FILTER (WHERE i.checked) AS checked_count
FROM lk.lists l
LEFT JOIN lk.list_items i ON i.list_id = l.id
{where}
GROUP BY l.id
{order}
""")).fetchall()
return [_row(r) for r in rows]
@router.post("", status_code=201)
def create_list(
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(text("""
INSERT INTO lk.lists (name, type, t_shirt_size, notes)
VALUES (:name, :type, :t_shirt_size, :notes)
RETURNING *
"""), {
"name": body.get("name"),
"type": body.get("type"),
"t_shirt_size": body.get("t_shirt_size"),
"notes": body.get("notes"),
}).fetchone()
session.commit()
return _row(row)
@router.post("/{id}/close")
def close_list(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
result = session.execute(text("""
UPDATE lk.lists SET status = 'closed', closed_at = now()
WHERE id = :id RETURNING *
"""), {"id": id}).fetchone()
session.commit()
if result is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(result)
@router.post("/{id}/reopen")
def reopen_list(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
result = session.execute(text("""
UPDATE lk.lists SET status = 'open', closed_at = NULL
WHERE id = :id RETURNING *
"""), {"id": id}).fetchone()
session.commit()
if result is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(result)
@router.delete("/{id}", status_code=204)
def delete_list(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
):
session.execute(text("DELETE FROM lk.lists WHERE id = :id"), {"id": id})
session.commit()
@router.get("/{id}/items")
def list_items(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("""
SELECT * FROM lk.list_items WHERE list_id = :list_id ORDER BY position, created_at
"""), {"list_id": id}).fetchall()
return [_row(r) for r in rows]
@router.post("/{id}/items", status_code=201)
def create_item(
id: int,
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(text("""
INSERT INTO lk.list_items (list_id, text) VALUES (:list_id, :text) RETURNING *
"""), {"list_id": id, "text": body.get("text")}).fetchone()
session.commit()
return _row(row)
@router.patch("/items/{id}")
def update_item_checked(
id: int,
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
result = session.execute(text("""
UPDATE lk.list_items SET checked = :checked WHERE id = :id RETURNING *
"""), {"id": id, "checked": bool(body.get("checked", False))}).fetchone()
session.commit()
if result is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(result)
@router.delete("/items/{id}", status_code=204)
def delete_item(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
):
session.execute(text("DELETE FROM lk.list_items WHERE id = :id"), {"id": id})
session.commit()
+141
View File
@@ -0,0 +1,141 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import text
from sqlalchemy.orm import Session
from typing import Any
from app.database import get_session
from app.auth import get_current_user
router = APIRouter()
def _row(r) -> dict:
return dict(r._mapping)
@router.get("")
def list_queens(
colony_id: int | None = Query(default=None),
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
params: dict[str, Any] = {}
where = ""
if colony_id is not None:
where = "WHERE q.colony_id = :colony_id"
params["colony_id"] = colony_id
rows = session.execute(text(f"""
SELECT q.*, c.name AS colony_name, o.name AS origin_colony_name
FROM queens q
LEFT JOIN colonies c ON c.id = q.colony_id
LEFT JOIN colonies o ON o.id = q.origin_colony_id
{where}
ORDER BY q.introduced_at DESC
"""), params).fetchall()
return [_row(r) for r in rows]
@router.post("", status_code=201)
def create_queen(
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(text("""
INSERT INTO queens (colony_id, year, breed, origin_colony_id, marked_color, notes, introduced_at)
VALUES (:colony_id, :year, :breed, :origin_colony_id, :marked_color, :notes, :introduced_at)
RETURNING *
"""), {
"colony_id": body.get("colony_id"),
"year": body.get("year"),
"breed": body.get("breed"),
"origin_colony_id": body.get("origin_colony_id"),
"marked_color": body.get("marked_color"),
"notes": body.get("notes"),
"introduced_at": body.get("introduced_at"),
}).fetchone()
queen_id = row._mapping["id"]
colony_id = body.get("colony_id")
if colony_id is not None:
session.execute(text("""
UPDATE colonies SET current_queen_id = :queen_id WHERE id = :colony_id
"""), {"queen_id": queen_id, "colony_id": colony_id})
session.commit()
return _row(row)
@router.get("/{id}")
def get_queen(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(
text("SELECT * FROM queens WHERE id = :id"), {"id": id}
).fetchone()
if row is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(row)
@router.put("/{id}")
def update_queen(
id: int,
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
result = session.execute(text("""
UPDATE queens SET
colony_id = :colony_id, year = :year, breed = :breed,
origin_colony_id = :origin_colony_id, marked_color = :marked_color,
notes = :notes, introduced_at = :introduced_at, superseded_at = :superseded_at
WHERE id = :id
RETURNING *
"""), {
"id": id,
"colony_id": body.get("colony_id"),
"year": body.get("year"),
"breed": body.get("breed"),
"origin_colony_id": body.get("origin_colony_id"),
"marked_color": body.get("marked_color"),
"notes": body.get("notes"),
"introduced_at": body.get("introduced_at"),
"superseded_at": body.get("superseded_at"),
}).fetchone()
session.commit()
if result is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(result)
@router.post("/{id}/supersede")
def supersede_queen(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
result = session.execute(text("""
UPDATE queens SET superseded_at = CURRENT_DATE WHERE id = :id RETURNING *
"""), {"id": id}).fetchone()
if result is None:
raise HTTPException(status_code=404, detail="Not found")
colony_id = result._mapping.get("colony_id")
if colony_id is not None:
session.execute(text("""
UPDATE colonies SET current_queen_id = NULL
WHERE id = :colony_id AND current_queen_id = :queen_id
"""), {"colony_id": colony_id, "queen_id": id})
session.commit()
return _row(result)
@router.delete("/{id}", status_code=204)
def delete_queen(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
):
session.execute(text("DELETE FROM queens WHERE id = :id"), {"id": id})
session.commit()
+284
View File
@@ -0,0 +1,284 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import text
from sqlalchemy.orm import Session
from typing import Any
from app.database import get_session
from app.auth import get_current_user
router = APIRouter()
def _row(r) -> dict:
return dict(r._mapping)
# ---- Counts ----
@router.get("/counts")
def list_counts(
colony_id: int = Query(...),
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("""
SELECT * FROM varroa_counts WHERE colony_id = :colony_id ORDER BY date DESC
"""), {"colony_id": colony_id}).fetchall()
return [_row(r) for r in rows]
@router.post("/counts", status_code=201)
def create_count(
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(text("""
INSERT INTO varroa_counts (colony_id, date_from, date, method, count, notes, time)
VALUES (:colony_id, :date_from, :date, :method, :count, :notes, :time)
RETURNING *
"""), {
"colony_id": body.get("colony_id"),
"date_from": body.get("date_from"),
"date": body.get("date"),
"method": body.get("method"),
"count": body.get("count"),
"notes": body.get("notes"),
"time": body.get("time"),
}).fetchone()
session.commit()
return _row(row)
@router.get("/counts/{id}")
def get_count(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(
text("SELECT * FROM varroa_counts WHERE id = :id"), {"id": id}
).fetchone()
if row is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(row)
@router.put("/counts/{id}")
def update_count(
id: int,
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
result = session.execute(text("""
UPDATE varroa_counts
SET date_from = :date_from, date = :date, method = :method, count = :count, notes = :notes
WHERE id = :id
RETURNING *
"""), {
"id": id,
"date_from": body.get("date_from"),
"date": body.get("date"),
"method": body.get("method"),
"count": body.get("count"),
"notes": body.get("notes"),
}).fetchone()
session.commit()
if result is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(result)
@router.delete("/counts/{id}", status_code=204)
def delete_count(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
):
session.execute(text("DELETE FROM varroa_counts WHERE id = :id"), {"id": id})
session.commit()
# ---- Treatments ----
@router.get("/treatments")
def list_treatments(
colony_id: int = Query(...),
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("""
SELECT * FROM varroa_treatments WHERE colony_id = :colony_id ORDER BY date DESC
"""), {"colony_id": colony_id}).fetchall()
return [_row(r) for r in rows]
@router.post("/treatments", status_code=201)
def create_treatment(
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(text("""
INSERT INTO varroa_treatments (colony_id, date, product, method, dosage, notes, time)
VALUES (:colony_id, :date, :product, :method, :dosage, :notes, :time)
RETURNING *
"""), {
"colony_id": body.get("colony_id"),
"date": body.get("date"),
"product": body.get("product"),
"method": body.get("method"),
"dosage": body.get("dosage"),
"notes": body.get("notes"),
"time": body.get("time"),
}).fetchone()
session.commit()
return _row(row)
@router.get("/treatments/{id}")
def get_treatment(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(
text("SELECT * FROM varroa_treatments WHERE id = :id"), {"id": id}
).fetchone()
if row is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(row)
@router.put("/treatments/{id}")
def update_treatment(
id: int,
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
result = session.execute(text("""
UPDATE varroa_treatments
SET date = :date, product = :product, method = :method, dosage = :dosage, notes = :notes
WHERE id = :id
RETURNING *
"""), {
"id": id,
"date": body.get("date"),
"product": body.get("product"),
"method": body.get("method"),
"dosage": body.get("dosage"),
"notes": body.get("notes"),
}).fetchone()
session.commit()
if result is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(result)
@router.delete("/treatments/{id}", status_code=204)
def delete_treatment(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
):
session.execute(text("DELETE FROM varroa_treatments WHERE id = :id"), {"id": id})
session.commit()
# ---- Controls ----
@router.get("/controls")
def list_controls(
colony_id: int | None = Query(default=None),
treatment_id: int | None = Query(default=None),
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
if treatment_id is not None:
rows = session.execute(text("""
SELECT vc.*, vt.product, vt.date AS treatment_date
FROM varroa_controls vc
JOIN varroa_treatments vt ON vt.id = vc.treatment_id
WHERE vc.treatment_id = :treatment_id
ORDER BY vc.date DESC
"""), {"treatment_id": treatment_id}).fetchall()
elif colony_id is not None:
rows = session.execute(text("""
SELECT vc.*, vt.product, vt.date AS treatment_date
FROM varroa_controls vc
JOIN varroa_treatments vt ON vt.id = vc.treatment_id
WHERE vc.colony_id = :colony_id
ORDER BY vc.date DESC
"""), {"colony_id": colony_id}).fetchall()
else:
return []
return [_row(r) for r in rows]
@router.post("/controls", status_code=201)
def create_control(
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(text("""
INSERT INTO varroa_controls (treatment_id, colony_id, date, result, notes)
VALUES (:treatment_id, :colony_id, :date, :result, :notes)
RETURNING *
"""), {
"treatment_id": body.get("treatment_id"),
"colony_id": body.get("colony_id"),
"date": body.get("date"),
"result": body.get("result"),
"notes": body.get("notes"),
}).fetchone()
session.commit()
return _row(row)
@router.get("/controls/{id}")
def get_control(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(
text("SELECT * FROM varroa_controls WHERE id = :id"), {"id": id}
).fetchone()
if row is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(row)
@router.put("/controls/{id}")
def update_control(
id: int,
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
result = session.execute(text("""
UPDATE varroa_controls SET date = :date, result = :result, notes = :notes
WHERE id = :id RETURNING *
"""), {
"id": id,
"date": body.get("date"),
"result": body.get("result"),
"notes": body.get("notes"),
}).fetchone()
session.commit()
if result is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(result)
@router.delete("/controls/{id}", status_code=204)
def delete_control(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
):
session.execute(text("DELETE FROM varroa_controls WHERE id = :id"), {"id": id})
session.commit()