Recipe corpus (#108): - Add _MAIN_INGREDIENT_SIGNALS to tag_inferrer.py (Chicken/Beef/Pork/Fish/Pasta/ Vegetables/Eggs/Legumes/Grains/Cheese) — infers main:* tags from ingredient names - Update browser_domains.py main_ingredient categories to use main:* tag queries instead of raw food terms; recipe_browser_fts now has full 3.19M row coverage (was ~1.2K before backfill) Bug fixes: - Fix community posts response shape (#96): add total/page/page_size fields - Fix export endpoint arg types (#92) - Fix household invite store leak (#93) - Fix receipts endpoint issues - Fix saved_recipes endpoint - Add session endpoint (app/api/endpoints/session.py) Shopping list: - Add migration 033_shopping_list.sql - Add shopping schemas (app/models/schemas/shopping.py) - Add ShoppingView.vue, ShoppingItemRow.vue, shopping.ts store Frontend: - InventoryList, RecipesView, RecipeDetailPanel polish - App.vue routing updates for shopping view Docs: - Add user-facing docs under docs/ (getting-started, user-guide, reference) - Add screenshots
79 lines
2.6 KiB
Python
79 lines
2.6 KiB
Python
"""Export endpoints — CSV and JSON export of user data."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import csv
|
|
import io
|
|
import json
|
|
from datetime import datetime, timezone
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from fastapi.responses import StreamingResponse
|
|
|
|
from app.db.session import get_store
|
|
from app.db.store import Store
|
|
|
|
router = APIRouter(prefix="/export", tags=["export"])
|
|
|
|
|
|
@router.get("/receipts/csv")
|
|
async def export_receipts_csv(store: Store = Depends(get_store)):
|
|
receipts = await asyncio.to_thread(store.list_receipts, 1000, 0)
|
|
output = io.StringIO()
|
|
fields = ["id", "filename", "status", "created_at", "updated_at"]
|
|
writer = csv.DictWriter(output, fieldnames=fields, extrasaction="ignore")
|
|
writer.writeheader()
|
|
writer.writerows(receipts)
|
|
output.seek(0)
|
|
return StreamingResponse(
|
|
iter([output.getvalue()]),
|
|
media_type="text/csv",
|
|
headers={"Content-Disposition": "attachment; filename=receipts.csv"},
|
|
)
|
|
|
|
|
|
@router.get("/inventory/csv")
|
|
async def export_inventory_csv(store: Store = Depends(get_store)):
|
|
items = await asyncio.to_thread(store.list_inventory)
|
|
output = io.StringIO()
|
|
fields = ["id", "product_name", "barcode", "category", "quantity", "unit",
|
|
"location", "expiration_date", "status", "created_at"]
|
|
writer = csv.DictWriter(output, fieldnames=fields, extrasaction="ignore")
|
|
writer.writeheader()
|
|
writer.writerows(items)
|
|
output.seek(0)
|
|
return StreamingResponse(
|
|
iter([output.getvalue()]),
|
|
media_type="text/csv",
|
|
headers={"Content-Disposition": "attachment; filename=inventory.csv"},
|
|
)
|
|
|
|
|
|
@router.get("/json")
|
|
async def export_full_json(store: Store = Depends(get_store)):
|
|
"""Export full pantry inventory + saved recipes as a single JSON file.
|
|
|
|
Intended for data portability — users can import this into another
|
|
Kiwi instance or keep it as an offline backup.
|
|
"""
|
|
inventory, saved = await asyncio.gather(
|
|
asyncio.to_thread(store.list_inventory),
|
|
asyncio.to_thread(store.get_saved_recipes),
|
|
)
|
|
|
|
export_doc = {
|
|
"kiwi_export": {
|
|
"version": "1.0",
|
|
"exported_at": datetime.now(timezone.utc).isoformat(),
|
|
"inventory": [dict(row) for row in inventory],
|
|
"saved_recipes": [dict(row) for row in saved],
|
|
}
|
|
}
|
|
|
|
body = json.dumps(export_doc, default=str, indent=2)
|
|
filename = f"kiwi-export-{datetime.now(timezone.utc).strftime('%Y%m%d')}.json"
|
|
return StreamingResponse(
|
|
iter([body]),
|
|
media_type="application/json",
|
|
headers={"Content-Disposition": f"attachment; filename={filename}"},
|
|
)
|