Backend (FastAPI): - REST API: auth, plants, beds, plantings - CRUD layer with CRUDBase - Pydantic v2 schemas for all entities - Alembic migration: complete schema + all enums - Seed data: 28 global plants + 15 compatibilities Frontend (Vue 3 + PrimeVue): - Axios client with JWT interceptor + auto-refresh - Pinia stores: auth, beds, plants - Views: Login, Beds, BedDetail, PlantLibrary - Components: AppLayout, BedForm, PlantingForm, PlantForm Docker: - docker-compose.yml (production) - docker-compose.dev.yml (development with hot-reload) - Nginx config with SPA fallback + API proxy - Multi-stage frontend Dockerfile - .env.example, .gitignore Version: 1.0.0-alpha
172 lines
8.6 KiB
Python
172 lines
8.6 KiB
Python
"""
|
||
Seed-Daten: Globale Pflanzenfamilien, Pflanzen und Kompatibilitäten.
|
||
Idempotent – kann mehrfach ausgeführt werden ohne Fehler.
|
||
"""
|
||
import asyncio
|
||
import uuid
|
||
|
||
from sqlalchemy import select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.db.session import AsyncSessionLocal
|
||
from app.models.plant import (
|
||
CompatibilityRating,
|
||
NutrientDemand,
|
||
Plant,
|
||
PlantCompatibility,
|
||
PlantFamily,
|
||
WaterDemand,
|
||
)
|
||
|
||
|
||
FAMILIES = [
|
||
{"name": "Solanaceae", "latin_name": "Solanaceae"},
|
||
{"name": "Kreuzblütler", "latin_name": "Brassicaceae"},
|
||
{"name": "Doldenblütler", "latin_name": "Apiaceae"},
|
||
{"name": "Hülsenfrüchtler", "latin_name": "Fabaceae"},
|
||
{"name": "Kürbisgewächse", "latin_name": "Cucurbitaceae"},
|
||
{"name": "Korbblütler", "latin_name": "Asteraceae"},
|
||
{"name": "Lauchgewächse", "latin_name": "Alliaceae"},
|
||
{"name": "Gänsefußgewächse", "latin_name": "Amaranthaceae"},
|
||
{"name": "Lippenblütler", "latin_name": "Lamiaceae"},
|
||
{"name": "Süßgräser", "latin_name": "Poaceae"},
|
||
]
|
||
|
||
# (name, latin_name, family_name, nutrient, water, spacing_cm, sow_start, sow_end, rest_years)
|
||
PLANTS = [
|
||
# Solanaceae
|
||
("Tomate", "Solanum lycopersicum", "Solanaceae", NutrientDemand.STARK, WaterDemand.MITTEL, 60, 3, 4, 3),
|
||
("Paprika", "Capsicum annuum", "Solanaceae", NutrientDemand.STARK, WaterDemand.MITTEL, 45, 2, 3, 3),
|
||
("Aubergine", "Solanum melongena", "Solanaceae", NutrientDemand.STARK, WaterDemand.MITTEL, 50, 2, 3, 3),
|
||
# Kreuzblütler
|
||
("Brokkoli", "Brassica oleracea var. italica", "Kreuzblütler", NutrientDemand.STARK, WaterDemand.MITTEL, 45, 3, 4, 4),
|
||
("Weißkohl", "Brassica oleracea var. capitata", "Kreuzblütler", NutrientDemand.STARK, WaterDemand.MITTEL, 50, 3, 4, 4),
|
||
("Kohlrabi", "Brassica oleracea var. gongylodes", "Kreuzblütler", NutrientDemand.MITTEL, WaterDemand.MITTEL, 22, 3, 7, 3),
|
||
("Radieschen", "Raphanus sativus", "Kreuzblütler", NutrientDemand.SCHWACH, WaterDemand.MITTEL, 5, 3, 8, 2),
|
||
# Doldenblütler
|
||
("Möhre", "Daucus carota", "Doldenblütler", NutrientDemand.MITTEL, WaterDemand.WENIG, 5, 3, 7, 3),
|
||
("Petersilie", "Petroselinum crispum", "Doldenblütler", NutrientDemand.MITTEL, WaterDemand.MITTEL, 18, 3, 5, 2),
|
||
("Sellerie", "Apium graveolens", "Doldenblütler", NutrientDemand.STARK, WaterDemand.VIEL, 28, 2, 3, 3),
|
||
# Hülsenfrüchtler
|
||
("Buschbohne", "Phaseolus vulgaris", "Hülsenfrüchtler", NutrientDemand.SCHWACH, WaterDemand.MITTEL, 12, 5, 7, 3),
|
||
("Erbse", "Pisum sativum", "Hülsenfrüchtler", NutrientDemand.SCHWACH, WaterDemand.MITTEL, 8, 3, 5, 3),
|
||
# Kürbisgewächse
|
||
("Gurke", "Cucumis sativus", "Kürbisgewächse", NutrientDemand.STARK, WaterDemand.VIEL, 60, 4, 5, 2),
|
||
("Zucchini", "Cucurbita pepo", "Kürbisgewächse", NutrientDemand.STARK, WaterDemand.VIEL, 90, 4, 5, 2),
|
||
("Kürbis", "Cucurbita maxima", "Kürbisgewächse", NutrientDemand.STARK, WaterDemand.VIEL, 180, 4, 5, 2),
|
||
# Korbblütler
|
||
("Kopfsalat", "Lactuca sativa", "Korbblütler", NutrientDemand.SCHWACH, WaterDemand.MITTEL, 22, 3, 8, 2),
|
||
("Feldsalat", "Valerianella locusta", "Korbblütler", NutrientDemand.SCHWACH, WaterDemand.WENIG, 8, 8, 9, 2),
|
||
# Lauchgewächse
|
||
("Zwiebel", "Allium cepa", "Lauchgewächse", NutrientDemand.MITTEL, WaterDemand.WENIG, 12, 3, 4, 3),
|
||
("Lauch", "Allium porrum", "Lauchgewächse", NutrientDemand.MITTEL, WaterDemand.MITTEL, 12, 2, 3, 3),
|
||
("Knoblauch", "Allium sativum", "Lauchgewächse", NutrientDemand.SCHWACH, WaterDemand.WENIG, 10, 10, 11, 4),
|
||
("Schnittlauch", "Allium schoenoprasum", "Lauchgewächse", NutrientDemand.SCHWACH, WaterDemand.WENIG, 10, 3, 4, 3),
|
||
# Gänsefußgewächse
|
||
("Mangold", "Beta vulgaris var. cicla", "Gänsefußgewächse", NutrientDemand.MITTEL, WaterDemand.MITTEL, 28, 3, 6, 3),
|
||
("Spinat", "Spinacia oleracea", "Gänsefußgewächse", NutrientDemand.MITTEL, WaterDemand.MITTEL, 12, 3, 9, 3),
|
||
("Rote Bete", "Beta vulgaris var. conditiva", "Gänsefußgewächse", NutrientDemand.MITTEL, WaterDemand.MITTEL, 10, 4, 6, 3),
|
||
# Lippenblütler
|
||
("Basilikum", "Ocimum basilicum", "Lippenblütler", NutrientDemand.SCHWACH, WaterDemand.MITTEL, 20, 4, 5, 2),
|
||
("Thymian", "Thymus vulgaris", "Lippenblütler", NutrientDemand.SCHWACH, WaterDemand.WENIG, 25, 3, 4, 2),
|
||
("Minze", "Mentha spicata", "Lippenblütler", NutrientDemand.SCHWACH, WaterDemand.VIEL, 30, 3, 4, 2),
|
||
("Oregano", "Origanum vulgare", "Lippenblütler", NutrientDemand.SCHWACH, WaterDemand.WENIG, 25, 3, 4, 2),
|
||
]
|
||
|
||
# (plant_a_name, plant_b_name, rating, reason)
|
||
COMPATIBILITIES = [
|
||
("Tomate", "Basilikum", CompatibilityRating.GUT, "Basilikum fördert das Tomatenwachstum und hält Schädlinge fern."),
|
||
("Tomate", "Möhre", CompatibilityRating.GUT, "Gute Nachbarn, gegenseitige Förderung."),
|
||
("Tomate", "Petersilie", CompatibilityRating.GUT, "Petersilie stärkt die Tomaten."),
|
||
("Tomate", "Brokkoli", CompatibilityRating.SCHLECHT, "Kohl hemmt das Tomatenwachstum."),
|
||
("Tomate", "Weißkohl", CompatibilityRating.SCHLECHT, "Kohl hemmt das Tomatenwachstum."),
|
||
("Möhre", "Zwiebel", CompatibilityRating.GUT, "Zwiebeln halten die Möhrenfliege fern."),
|
||
("Möhre", "Lauch", CompatibilityRating.GUT, "Lauch schützt die Möhre vor der Möhrenfliege."),
|
||
("Möhre", "Erbse", CompatibilityRating.GUT, "Erbsen lockern den Boden für Möhren."),
|
||
("Gurke", "Buschbohne", CompatibilityRating.GUT, "Klassische gute Nachbarschaft."),
|
||
("Weißkohl", "Sellerie", CompatibilityRating.GUT, "Sellerie hält die Kohlfliege fern."),
|
||
("Brokkoli", "Sellerie", CompatibilityRating.GUT, "Sellerie hält Kohlschädlinge fern."),
|
||
("Spinat", "Radieschen", CompatibilityRating.GUT, "Platzsparende Kombination, gegenseitig förderlich."),
|
||
("Zwiebel", "Buschbohne", CompatibilityRating.SCHLECHT, "Zwiebeln hemmen das Bohnenwachstum."),
|
||
("Zwiebel", "Erbse", CompatibilityRating.SCHLECHT, "Zwiebeln und Hülsenfrüchtler vertragen sich nicht."),
|
||
("Zwiebel", "Weißkohl", CompatibilityRating.SCHLECHT, "Konkurrenz um Nährstoffe."),
|
||
]
|
||
|
||
|
||
async def seed_initial_data(db: AsyncSession) -> None:
|
||
# 1. Plant families
|
||
family_map: dict[str, PlantFamily] = {}
|
||
for f in FAMILIES:
|
||
result = await db.execute(select(PlantFamily).where(PlantFamily.name == f["name"]))
|
||
existing = result.scalar_one_or_none()
|
||
if existing:
|
||
family_map[f["name"]] = existing
|
||
else:
|
||
obj = PlantFamily(id=uuid.uuid4(), **f)
|
||
db.add(obj)
|
||
await db.flush()
|
||
family_map[f["name"]] = obj
|
||
|
||
# 2. Global plants
|
||
plant_map: dict[str, Plant] = {}
|
||
for (name, latin, family_name, nutrient, water, spacing, sow_start, sow_end, rest) in PLANTS:
|
||
result = await db.execute(
|
||
select(Plant).where(Plant.name == name, Plant.tenant_id == None) # noqa: E711
|
||
)
|
||
existing = result.scalar_one_or_none()
|
||
if existing:
|
||
plant_map[name] = existing
|
||
else:
|
||
obj = Plant(
|
||
id=uuid.uuid4(),
|
||
tenant_id=None,
|
||
family_id=family_map[family_name].id,
|
||
name=name,
|
||
latin_name=latin,
|
||
nutrient_demand=nutrient,
|
||
water_demand=water,
|
||
spacing_cm=spacing,
|
||
sowing_start_month=sow_start,
|
||
sowing_end_month=sow_end,
|
||
rest_years=rest,
|
||
)
|
||
db.add(obj)
|
||
await db.flush()
|
||
plant_map[name] = obj
|
||
|
||
# 3. Compatibilities (both directions)
|
||
for (name_a, name_b, rating, reason) in COMPATIBILITIES:
|
||
if name_a not in plant_map or name_b not in plant_map:
|
||
continue
|
||
id_a = plant_map[name_a].id
|
||
id_b = plant_map[name_b].id
|
||
result = await db.execute(
|
||
select(PlantCompatibility).where(
|
||
PlantCompatibility.plant_id_a == id_a,
|
||
PlantCompatibility.plant_id_b == id_b,
|
||
)
|
||
)
|
||
if not result.scalar_one_or_none():
|
||
db.add(PlantCompatibility(id=uuid.uuid4(), plant_id_a=id_a, plant_id_b=id_b, rating=rating, reason=reason))
|
||
# Reverse direction
|
||
result = await db.execute(
|
||
select(PlantCompatibility).where(
|
||
PlantCompatibility.plant_id_a == id_b,
|
||
PlantCompatibility.plant_id_b == id_a,
|
||
)
|
||
)
|
||
if not result.scalar_one_or_none():
|
||
db.add(PlantCompatibility(id=uuid.uuid4(), plant_id_a=id_b, plant_id_b=id_a, rating=rating, reason=reason))
|
||
|
||
await db.commit()
|
||
print("Seed-Daten erfolgreich eingespielt.")
|
||
|
||
|
||
async def main() -> None:
|
||
async with AsyncSessionLocal() as db:
|
||
await seed_initial_data(db)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(main())
|