munch-ease-backend/main.py

575 lines
18 KiB
Python
Raw Normal View History

2024-01-13 07:42:23 +00:00
import datetime
2025-10-18 03:26:42 +00:00
import os
from typing import Annotated, Dict, List, Optional
2024-01-08 00:45:22 +00:00
2025-10-18 03:26:42 +00:00
import aiosqlite
from fastapi import Cookie, Depends, FastAPI, Query
2024-04-25 02:03:30 +00:00
from fastapi.encoders import jsonable_encoder
2025-10-18 03:26:42 +00:00
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
2024-01-07 00:33:40 +00:00
2025-10-18 03:26:42 +00:00
import db
import ingredients
import meals
import persons
import products
import recipes
import shopping
2024-01-07 00:33:40 +00:00
2025-10-18 03:26:42 +00:00
app = FastAPI()
DATABASE_PATH = os.environ.get("DOOF_DB", "./data/doof.sqlite")
2024-01-07 00:33:40 +00:00
2024-01-08 00:38:17 +00:00
# Dependency to create SQLite connection
async def get_db():
2024-09-21 01:22:30 +00:00
sql_db = await db.connect(DATABASE_PATH)
2024-01-08 00:38:17 +00:00
try:
yield sql_db
finally:
2024-01-13 01:54:04 +00:00
await sql_db.close()
2024-01-08 00:38:17 +00:00
2025-10-18 03:26:42 +00:00
async def cookie_person(
user_id: Annotated[int, Cookie(alias="user_id")], conn: aiosqlite.Connection = Depends(get_db)
) -> Optional[persons.Person]:
2024-04-25 04:57:39 +00:00
return await persons.get_by_id(conn, user_id)
2024-04-25 02:03:30 +00:00
2025-10-18 03:26:42 +00:00
@app.get("/api/recipes/parse", response_model=None)
async def parse_recipe_handler(
url: str, conn: aiosqlite.Connection = Depends(get_db), person=Depends(cookie_person)
) -> recipes.Recipe | JSONResponse:
2024-05-19 11:22:30 +00:00
parsed = await recipes.parse_recipe(conn, person, url)
2024-01-13 05:40:10 +00:00
if not parsed:
2025-10-18 03:26:42 +00:00
return JSONResponse(status_code=400, content={"message": "Recipe not found"})
2024-01-13 05:40:10 +00:00
return parsed
2024-01-08 00:45:22 +00:00
2025-10-18 03:26:42 +00:00
@app.get("/api/recipes/ingredients/parse")
2025-10-18 03:26:42 +00:00
async def parse_ingredients(
lines: Annotated[List[str], Query(alias="ingredients", title="Array of ingredients to parse")],
conn: aiosqlite.Connection = Depends(get_db),
) -> List[ingredients.Ingredient]:
2024-05-21 12:12:09 +00:00
had_links = False
result = []
for line in lines:
ingredient = await ingredients.parse_ingredient_from_link(conn, line)
if ingredient:
result.append(ingredient)
had_links = True
continue
ingredient = ingredients.parse_ingredient_from_nlp(line)
if ingredient:
result.append(ingredient)
continue
if had_links:
await conn.commit()
2024-01-17 08:36:54 +00:00
await ingredients.match_existing_products(conn, result)
return result
2024-01-08 00:38:17 +00:00
2025-10-18 03:26:42 +00:00
2024-01-13 01:54:04 +00:00
class ProductUrl(BaseModel):
url: str
2025-10-18 03:26:42 +00:00
tags: List[str] = Field(default_factory=list)
2024-01-13 01:54:04 +00:00
@app.post("/api/products")
2025-10-18 03:26:42 +00:00
async def create_product(
url: ProductUrl, conn: aiosqlite.Connection = Depends(get_db)
) -> Optional[products.Product]:
2024-01-13 07:44:48 +00:00
return await products.get_or_create(conn, url.url, url.tags)
2024-01-13 05:40:10 +00:00
2025-10-18 03:26:42 +00:00
async def load_full_recipe(conn: aiosqlite.Connection, id: int) -> Optional[recipes.Recipe]:
2024-01-13 07:44:48 +00:00
r = await recipes.find_recipe_by_id(conn, id)
2024-01-13 06:59:35 +00:00
if not r:
2024-01-13 07:42:23 +00:00
return None
2024-01-13 06:59:35 +00:00
r.ingredients = []
2024-01-17 07:21:16 +00:00
async for ingredient in ingredients.find_ingredients_by_recipe_id(conn, id):
2024-01-13 06:59:35 +00:00
r.ingredients.append(ingredient)
2025-10-18 03:26:42 +00:00
if r.created_by_id is not None:
r.created_by = await persons.get_by_id(conn, r.created_by_id)
2024-04-25 04:57:39 +00:00
2024-01-13 06:59:35 +00:00
return r
2025-10-18 03:26:42 +00:00
@app.get("/api/recipes")
2025-10-18 03:26:42 +00:00
async def get_recipes(
q: Optional[str] = None, conn: aiosqlite.Connection = Depends(get_db)
) -> List[recipes.Recipe]:
2024-01-13 08:44:07 +00:00
result = []
if q:
async for recipe in recipes.find_recipes_by_name(conn, q):
result.append(recipe)
else:
async for recipe in recipes.get_all(conn):
result.append(recipe)
2024-01-13 08:44:07 +00:00
for recipe in result:
recipe.ingredients = []
2024-01-17 07:21:16 +00:00
async for ingredient in ingredients.find_ingredients_by_recipe_id(conn, recipe.id):
recipe.ingredients.append(ingredient)
2025-10-18 03:26:42 +00:00
2024-01-13 08:44:07 +00:00
return result
2025-10-18 03:26:42 +00:00
@app.get("/api/recipes/{recipe_id}", response_model=None)
async def get_recipe(
recipe_id: int, conn: aiosqlite.Connection = Depends(get_db)
) -> recipes.Recipe | JSONResponse:
2024-01-13 07:42:23 +00:00
r = await load_full_recipe(conn, recipe_id)
if not r:
2025-10-18 03:26:42 +00:00
return JSONResponse(status_code=404, content={"message": "Recipe not found"})
2024-01-13 07:42:23 +00:00
return r
2025-10-18 03:26:42 +00:00
@app.post("/api/recipes", response_model=None)
async def create_recipe(
recipe: recipes.Recipe,
conn: aiosqlite.Connection = Depends(get_db),
user: persons.Person = Depends(cookie_person),
) -> recipes.Recipe | JSONResponse:
2024-05-20 10:09:57 +00:00
if not recipe.ingredients:
2025-10-18 03:26:42 +00:00
return JSONResponse(
status_code=400, content={"message": "Recipe must have at least one ingredient"}
)
2024-05-20 10:09:57 +00:00
if recipe.id >= 0:
await recipes.hide_recipe(conn, recipe.id, user)
recipe.based_on_recipe = recipe.id
recipe.id = 0
2025-10-18 03:26:42 +00:00
2024-05-20 10:09:57 +00:00
recipe.created_by_id = user.id
await recipes.insert_recipe(conn, recipe)
for ingredient in recipe.ingredients:
ingredient.recipe_id = recipe.id
if ingredient.product:
ingredient.product_id = ingredient.product.id
2025-10-18 03:26:42 +00:00
2024-01-17 08:36:54 +00:00
await ingredients.insert_ingredient(conn, ingredient)
2024-04-28 03:57:02 +00:00
2024-01-13 05:40:10 +00:00
await conn.commit()
2025-10-18 03:26:42 +00:00
2024-05-20 10:09:57 +00:00
return recipe
2024-01-13 07:42:23 +00:00
2025-10-18 03:26:42 +00:00
@app.delete("/recipes/{recipe_id}", response_model=None)
async def delete_recipe(
recipe_id: int,
conn: aiosqlite.Connection = Depends(get_db),
user: persons.Person = Depends(cookie_person),
) -> recipes.Recipe | JSONResponse:
2024-05-02 12:25:11 +00:00
recipe = await recipes.find_recipe_by_id(conn, recipe_id)
if not recipe:
2025-10-18 03:26:42 +00:00
return JSONResponse(status_code=404, content={"message": "Recipe not found"})
2024-05-02 12:25:11 +00:00
await recipes.hide_recipe(conn, recipe_id, user)
await conn.commit()
return recipe
2025-10-18 03:26:42 +00:00
@app.get("/api/meals/upcoming")
2025-10-18 03:26:42 +00:00
async def get_upcoming_meals(
date_from: Annotated[datetime.datetime, Query(alias="from")],
to: datetime.datetime,
conn: aiosqlite.Connection = Depends(get_db),
) -> List[meals.Meal]:
2024-01-13 07:42:23 +00:00
result = []
2024-05-25 02:33:41 +00:00
async for meal in meals.find_upcoming_meals_by_date_range(conn, date_from, to):
2024-01-17 11:43:16 +00:00
await meals.load_recipes(conn, meal)
await meals.load_extra_ingredients(conn, meal)
2024-04-30 11:21:59 +00:00
await meals.load_participants(conn, meal)
2024-01-13 07:42:23 +00:00
result.append(meal)
return result
2025-10-18 03:26:42 +00:00
@app.get("/api/meals/{meal_id}", response_model=None)
async def get_meal(
meal_id: int, conn: aiosqlite.Connection = Depends(get_db)
) -> meals.Meal | JSONResponse:
2024-01-17 10:39:48 +00:00
meal = await meals.find_meal_by_id(conn, meal_id)
if not meal:
2025-10-18 03:26:42 +00:00
return JSONResponse(status_code=404, content={"message": "Meal not found"})
2024-01-17 10:39:48 +00:00
return meal
2025-10-18 03:26:42 +00:00
2024-05-04 05:06:48 +00:00
def get_duplicates(items: List[meals.Person]) -> set[str]:
2025-10-18 03:26:42 +00:00
seen: set[int] = set()
duplicates: set[str] = set()
2024-05-04 05:06:48 +00:00
for item in items:
if item.id in seen:
duplicates.add(item.name)
seen.add(item.id)
return duplicates
2025-10-18 03:26:42 +00:00
def validate_meal(meal: meals.Meal) -> Optional[JSONResponse]:
2024-04-25 04:57:39 +00:00
if not meal.chefs:
2025-10-18 03:26:42 +00:00
return JSONResponse(
status_code=400, content={"message": "Meal must have at least one chef"}
)
2024-01-13 07:42:23 +00:00
if not meal.cleanup:
2025-10-18 03:26:42 +00:00
return JSONResponse(
status_code=400, content={"message": "Meal must have at least one cleanup person"}
)
2024-01-13 07:42:23 +00:00
if not meal.consumers:
2025-10-18 03:26:42 +00:00
return JSONResponse(
status_code=400, content={"message": "Meal must have at least one consumer"}
)
2024-01-17 07:21:16 +00:00
if len(meal.recipes) == 0 and len(meal.extra_ingredients) == 0:
2025-10-18 03:26:42 +00:00
return JSONResponse(
status_code=400, content={"message": "Meal must have at least one recipe or ingredient"}
)
2024-05-04 05:06:48 +00:00
duplicates = get_duplicates(meal.chefs)
if duplicates:
2025-10-18 03:26:42 +00:00
return JSONResponse(
status_code=400, content={"message": f'Duplicate chef: {", ".join(duplicates)}'}
)
2024-05-04 05:06:48 +00:00
duplicates = get_duplicates(meal.cleanup)
if duplicates:
2025-10-18 03:26:42 +00:00
return JSONResponse(
status_code=400,
content={"message": f'Duplicate cleanup person: {", ".join(duplicates)}'},
)
2024-05-04 05:06:48 +00:00
duplicates = get_duplicates(meal.consumers)
if duplicates:
2025-10-18 03:26:42 +00:00
return JSONResponse(
status_code=400, content={"message": f'Duplicate consumer: {", ".join(duplicates)}'}
)
zero_servings = [r for r in meal.recipes if r.servings == 0]
if zero_servings:
2025-10-18 03:26:42 +00:00
return JSONResponse(
status_code=400, content={"message": "Recipe servings must be greater than 0"}
)
2024-05-04 05:06:48 +00:00
return None
2025-10-18 03:26:42 +00:00
@app.post("/api/meals", response_model=None)
async def create_meal(
meal: meals.Meal, conn: aiosqlite.Connection = Depends(get_db)
) -> meals.Meal | JSONResponse:
2024-05-04 05:06:48 +00:00
validation_response = validate_meal(meal)
if validation_response:
return validation_response
2024-01-17 07:21:16 +00:00
2024-01-13 07:42:23 +00:00
await meals.insert_meal(conn, meal)
await conn.commit()
2024-01-13 08:44:07 +00:00
return meal
2025-10-18 03:26:42 +00:00
@app.put("/api/meals/{meal_id}", response_model=None)
async def update_meal(
meal_id: int, meal: meals.Meal, conn: aiosqlite.Connection = Depends(get_db)
) -> meals.Meal | JSONResponse:
2024-05-02 11:20:52 +00:00
if meal.id != meal_id:
2025-10-18 03:26:42 +00:00
return JSONResponse(
status_code=400, content={"message": "Meal ID in URL does not match meal ID in body"}
)
2024-05-02 11:20:52 +00:00
existing = await meals.find_meal_by_id(conn, meal_id)
if not existing:
2025-10-18 03:26:42 +00:00
return JSONResponse(status_code=404, content={"message": "Meal not found"})
2024-05-04 05:06:48 +00:00
validation_response = validate_meal(meal)
if validation_response:
return validation_response
2024-05-02 11:20:52 +00:00
await meals.update_meal(conn, meal)
await conn.commit()
return await get_meal(meal_id, conn)
2025-10-18 03:26:42 +00:00
@app.post("/api/meals/{meal_id}/consumed", response_model=None)
async def mark_consumed(
meal_id: int,
consumed_date: Optional[datetime.datetime] = None,
conn: aiosqlite.Connection = Depends(get_db),
person: persons.Person = Depends(cookie_person),
) -> meals.Meal | JSONResponse:
2024-10-14 05:59:05 +00:00
if consumed_date and not consumed_date.tzinfo:
2025-10-18 03:26:42 +00:00
return JSONResponse(
status_code=400, content={"message": "Consumed date must include timezone"}
)
2024-05-27 11:55:47 +00:00
meal = await meals.find_meal_by_id(conn, meal_id)
2024-05-25 02:33:41 +00:00
if not meal:
2025-10-18 03:26:42 +00:00
return JSONResponse(status_code=404, content={"message": "Meal not found"})
2024-05-25 02:33:41 +00:00
2024-10-14 05:59:05 +00:00
await meals.mark_consumed(conn, meal, consumed_date or datetime.datetime.now().astimezone())
await shopping.remove_request(conn, person, meal=meal)
2025-10-18 03:26:42 +00:00
2024-05-25 02:33:41 +00:00
await conn.commit()
return meal
2025-10-18 03:26:42 +00:00
@app.delete("/api/meals/{meal_id}", response_model=None)
async def delete_meal(
meal_id: int,
conn: aiosqlite.Connection = Depends(get_db),
person: persons.Person = Depends(cookie_person),
) -> meals.Meal | JSONResponse:
2024-01-17 11:43:16 +00:00
meal = await meals.find_meal_by_id(conn, meal_id)
if not meal:
2025-10-18 03:26:42 +00:00
return JSONResponse(status_code=404, content={"message": "Meal not found"})
2024-01-17 11:43:16 +00:00
await shopping.remove_request(conn, person, meal=meal)
2024-01-17 11:43:16 +00:00
await meals.delete_meal(conn, meal.id)
2024-10-13 08:31:37 +00:00
2024-01-17 11:43:16 +00:00
await conn.commit()
return meal
2024-01-17 10:39:48 +00:00
2025-10-18 03:26:42 +00:00
class CurrentShoppingList(BaseModel):
outstanding_items: List[shopping.ShoppingListItem]
requested_meals: List[shopping.ShoppingListItem]
2025-10-18 03:26:42 +00:00
purchased_items: List[shopping.ShoppingListItem] = Field(default_factory=list)
ingredients_lookup: Dict[int, ingredients.Ingredient] = Field(default_factory=dict)
meals_lookup: Dict[int, meals.Meal] = Field(default_factory=dict)
shopping_list_lookup: Dict[int, shopping.ShoppingList] = Field(default_factory=dict)
recipes_lookup: Dict[int, recipes.Recipe] = Field(default_factory=dict)
2024-05-18 07:05:01 +00:00
@app.get("/api/shopping/current")
2025-10-18 03:26:42 +00:00
async def get_current_shopping_list(
conn: aiosqlite.Connection = Depends(get_db),
) -> CurrentShoppingList:
(
outstanding_requests,
purchased_requests,
meal_requests,
meals_lookup,
recipes_lookup,
ingredients_lookup,
) = await shopping.get_outstanding_requests(conn)
other_shopping_list_ids = {item.list_id for item in purchased_requests}
2025-10-18 03:26:42 +00:00
shopping_list_lookup = {}
for list_id in other_shopping_list_ids:
if list_id is not None:
sl = await shopping.load_shopping_list(conn, list_id)
if sl is not None:
shopping_list_lookup[list_id] = sl
# Add any additional items from shopping lists to the existing lookups
additional_items = [item for sl in shopping_list_lookup.values() for item in sl.items]
if additional_items:
2025-10-18 03:26:42 +00:00
await shopping.to_lookups(
conn, additional_items, meals_lookup, recipes_lookup, ingredients_lookup
)
return CurrentShoppingList(
outstanding_items=outstanding_requests,
requested_meals=meal_requests,
purchased_items=purchased_requests,
meals_lookup=meals_lookup,
shopping_list_lookup=shopping_list_lookup,
ingredients_lookup=ingredients_lookup,
2025-10-18 03:26:42 +00:00
recipes_lookup=recipes_lookup,
)
2025-10-18 03:26:42 +00:00
class PurchasedShoppingList(BaseModel):
list: shopping.ShoppingList
2025-10-18 03:26:42 +00:00
meals_lookup: Dict[int, meals.Meal] = Field(default_factory=dict)
ingredients_lookup: Dict[int, ingredients.Ingredient] = Field(default_factory=dict)
recipes_lookup: Dict[int, recipes.Recipe] = Field(default_factory=dict)
2024-05-19 03:43:06 +00:00
2025-10-18 03:26:42 +00:00
@app.get("/api/shopping/{list_id}", response_model=None)
async def get_shopping_list(
list_id: int, conn: aiosqlite.Connection = Depends(get_db)
) -> PurchasedShoppingList | JSONResponse:
shopping_list = await shopping.load_shopping_list(conn, list_id)
if not shopping_list:
2025-10-18 03:26:42 +00:00
return JSONResponse(status_code=404, content={"message": "Shopping list not found"})
meals_lookup, recipes_lookup, ingredients_lookup = await shopping.to_lookups(
conn, shopping_list.items
)
return PurchasedShoppingList(
list=shopping_list,
meals_lookup=meals_lookup,
recipes_lookup=recipes_lookup,
ingredients_lookup=ingredients_lookup,
)
2024-05-19 03:43:06 +00:00
@app.post("/api/shopping/")
2025-10-18 03:26:42 +00:00
async def purchase_ingredients(
shopping_list: shopping.ShoppingList,
conn: aiosqlite.Connection = Depends(get_db),
person: persons.Person = Depends(cookie_person),
) -> PurchasedShoppingList:
shopping_list = shopping.ShoppingList(
purchased_by=person, items=shopping_list.items, store_name=shopping_list.store_name
)
await shopping.purchase(conn, shopping_list)
2024-05-19 03:43:06 +00:00
await conn.commit()
result = PurchasedShoppingList(list=shopping_list)
2025-10-18 03:26:42 +00:00
await shopping.to_lookups(
conn,
shopping_list.items,
result.meals_lookup,
result.recipes_lookup,
result.ingredients_lookup,
)
return result
2025-10-18 03:26:42 +00:00
@app.get("/api/shopping/current/me/ingredients")
2025-10-18 03:26:42 +00:00
async def get_my_shopping_list(
conn: aiosqlite.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)
) -> List[ingredients.Ingredient]:
return await shopping.get_persons_requests(conn, person.id)
2024-05-18 07:05:01 +00:00
2025-10-18 03:26:42 +00:00
@app.post("/api/shopping/current/me/ingredients")
2025-10-18 03:26:42 +00:00
async def sync_my_shopping_list(
requests: List[ingredients.Ingredient],
conn: aiosqlite.Connection = Depends(get_db),
person: persons.Person = Depends(cookie_person),
) -> List[ingredients.Ingredient]:
def isMatching(a: ingredients.Ingredient, b: ingredients.Ingredient) -> bool:
return a.id == b.id or a.line == b.line
my_shopping_list = await shopping.get_persons_requests(conn, person.id)
to_remove = [r for r in my_shopping_list if not any(isMatching(r, req) for req in requests)]
to_add = [req for req in requests if not any(isMatching(req, r) for r in my_shopping_list)]
for r in to_remove:
await shopping.remove_request(conn, person, ingredient=r)
2025-10-18 03:26:42 +00:00
for r in to_add:
if r.id < 0:
await ingredients.insert_ingredient(conn, r)
await shopping.request(conn, person, ingredient=r)
2024-05-18 07:05:01 +00:00
await conn.commit()
return await get_my_shopping_list(conn, person)
2024-05-18 07:05:01 +00:00
2025-10-18 03:26:42 +00:00
2024-05-18 07:05:01 +00:00
class MealIdWrapper(BaseModel):
meal_id: int
2025-10-18 03:26:42 +00:00
@app.post("/api/shopping/current/meals/me", response_model=None)
async def request_meal(
r: MealIdWrapper,
conn: aiosqlite.Connection = Depends(get_db),
person: persons.Person = Depends(cookie_person),
) -> shopping.ShoppingListItem | JSONResponse:
2024-05-18 07:05:01 +00:00
meal = await meals.find_meal_by_id(conn, r.meal_id)
if not meal:
2025-10-18 03:26:42 +00:00
return JSONResponse(status_code=404, content={"message": "Meal not found"})
response = await shopping.request(conn, person, meal=meal)
2024-05-18 07:05:01 +00:00
await conn.commit()
return response
2025-10-18 03:26:42 +00:00
@app.delete("/api/shopping/current/meals/{meal_id}", response_model=None)
async def unrequest_meal(
meal_id: int,
conn: aiosqlite.Connection = Depends(get_db),
person: persons.Person = Depends(cookie_person),
) -> dict | JSONResponse:
2024-05-18 07:05:01 +00:00
meal = await meals.find_meal_by_id(conn, meal_id)
if not meal:
2025-10-18 03:26:42 +00:00
return JSONResponse(status_code=404, content={"message": "Meal not found"})
2024-05-18 07:05:01 +00:00
await shopping.remove_request(conn, person, meal=meal)
2024-05-18 07:05:01 +00:00
await conn.commit()
return {}
2025-10-18 03:26:42 +00:00
@app.get("/api/persons")
2025-10-18 03:26:42 +00:00
async def get_persons(
q: Optional[str] = None, conn: aiosqlite.Connection = Depends(get_db)
) -> List[meals.Person]:
2024-05-04 04:22:19 +00:00
query = persons.search_by_name(conn, q) if q else persons.get_all(conn)
2024-01-13 08:44:07 +00:00
result = []
2024-05-04 04:22:19 +00:00
async for person in query:
2024-01-13 08:44:07 +00:00
result.append(person)
return result
2025-10-18 03:26:42 +00:00
@app.post("/api/persons")
2025-10-18 03:26:42 +00:00
async def create_person(
person: persons.Person, conn: aiosqlite.Connection = Depends(get_db)
) -> persons.Person:
2024-04-28 03:37:01 +00:00
await persons.insert_person(conn, person)
2024-01-13 08:44:07 +00:00
await conn.commit()
2024-04-25 02:03:30 +00:00
return person
2025-10-18 03:26:42 +00:00
2024-04-25 02:03:30 +00:00
class LoginBody(BaseModel):
username: str
2025-10-18 03:26:42 +00:00
@app.post("/api/auth/login", response_model=None)
async def login(
data: LoginBody, conn: aiosqlite.Connection = Depends(get_db)
) -> persons.Person | JSONResponse:
2024-04-25 02:03:30 +00:00
person = await persons.get_by_name(conn, data.username)
if not person:
2025-10-18 03:26:42 +00:00
return JSONResponse(status_code=404, content={"message": "Person not found"})
2024-04-25 02:03:30 +00:00
response = JSONResponse(content=jsonable_encoder(person))
2025-10-18 03:26:42 +00:00
response.set_cookie(key="user_id", value=str(person.id))
2024-04-28 02:15:19 +00:00
return response
2025-10-18 03:26:42 +00:00
@app.post("/api/auth/refresh")
2024-04-28 02:15:19 +00:00
async def current_user(user: persons.Person = Depends(cookie_person)) -> persons.Person:
return user
2025-10-18 03:26:42 +00:00
if os.environ.get("DOOF_PROD", False):
from fastapi.staticfiles import StaticFiles
2025-10-18 03:26:42 +00:00
app.mount("/", StaticFiles(directory="./front-dist", html=True), name="front-dist")
else:
# Proxy the request to the frontend development server
2025-10-18 03:26:42 +00:00
import httpx
from starlette.background import BackgroundTask
from starlette.requests import Request
from starlette.responses import StreamingResponse
client = httpx.AsyncClient(base_url="http://localhost:8080/")
async def _reverse_proxy(request: Request):
2025-10-18 03:26:42 +00:00
url = httpx.URL(path=request.url.path, query=request.url.query.encode("utf-8"))
rp_req = client.build_request(
request.method, url, headers=request.headers.raw, content=request.stream()
)
rp_resp = await client.send(rp_req, stream=True)
return StreamingResponse(
rp_resp.aiter_raw(),
status_code=rp_resp.status_code,
headers=rp_resp.headers,
background=BackgroundTask(rp_resp.aclose),
)
2025-10-18 03:26:42 +00:00
app.add_route("/{path:path}", _reverse_proxy, ["GET", "POST"])