Skip to content

Commit

Permalink
Feature/menu implementation (#14)
Browse files Browse the repository at this point in the history
This MR addresses several key updates:

1. FastAPI Application (app.py): Introduced endpoints for retrieving a list of menu items, details for specific items, and availability information. This enhances the API’s capability to serve detailed menu and stock data to clients.

```
/menu/
/menu/{item_id}
/menu/{item_id}/availability
```

2. Schemas: Added new and updated existing schemas to include detailed information on menu items, ingredients, and their availability. This ensures that the API responses are comprehensive and aligned with the new backend logic.
Inventory Service (inventory_service.py):

3. Enhanced Functionality: Added methods for calculating ingredient availability, converting units, and fetching stock data. These improvements enable more accurate and flexible management of menu item stock and availability.
  • Loading branch information
PaulJWright authored Aug 5, 2024
1 parent 5890d05 commit e811b5e
Show file tree
Hide file tree
Showing 10 changed files with 687 additions and 12 deletions.
7 changes: 4 additions & 3 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ COPY ../database/ /app/database
# https://setuptools-scm.readthedocs.io/en/latest/usage/
# https://stackoverflow.com/questions/77572077/using-setuptools-scm-pretend-version-for-package-version-inside-docker-with-git
ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_MY_PACKAGE=0.0
RUN pip install --root-user-action=ignore --no-cache-dir .
RUN pip install --root-user-action=ignore .

ARG LOCATION_ID
ARG SEED_LOCATION_ID
ARG SEED_QUANTITY
# Command to run FastAPI using Uvicorn
# CMD ["uvicorn", "weird_salads.api.app:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["sh", "-c", "alembic upgrade head && python weird_salads/utils/database/seed_db.py --location_id ${LOCATION_ID} --base_path data/ && uvicorn weird_salads.api.app:app --host 0.0.0.0 --port 8000"]
CMD ["sh", "-c", "alembic upgrade head && python weird_salads/utils/database/seed_db.py --location_id ${SEED_LOCATION_ID} --quantity ${SEED_QUANTITY} --base_path data/ && uvicorn weird_salads.api.app:app --host 0.0.0.0 --port 8000"]
# CMD ["sleep", "365d"]
3 changes: 2 additions & 1 deletion docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ services:
- ../data/:/app/data/ # Mount the data
environment:
- DATABASE_URL=sqlite:///data/orders.db
- LOCATION_ID=1 # put as an environment variable
- SEED_LOCATION_ID=1 # location id for DB seeding
- SEED_QUANTITY=7.4 # quantity of ingredients for DB seeding

streamlit:
build:
Expand Down
85 changes: 85 additions & 0 deletions streamlit_app/pages/menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import pandas as pd
import requests
import streamlit as st


# Function to fetch menu items
def fetch_menu_items():
try:
response = requests.get("http://fastapi:8000/menu/")
response.raise_for_status()
data = response.json()
return data.get("items", [])
except requests.exceptions.RequestException as e:
st.write("Failed to connect to FastAPI:", e)
return []
except Exception as e:
st.write("An error occurred:", e)
return []


# Function to fetch availability of a specific menu item
def fetch_item_availability(item_id):
try:
response = requests.get(
f"http://fastapi:8000/menu/{item_id}/availability"
) # Update endpoint if necessary
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
st.write("Failed to connect to FastAPI:", e)
return None
except Exception as e:
st.write("An error occurred:", e)
return None


# Function to display menu items
def display_menu():
st.header("Menu")

menu_items = fetch_menu_items()

# Initialize session state for tracking the current order and status
if "current_order" not in st.session_state:
st.session_state.current_order = None
st.session_state.order_status = ""

if menu_items:
# Create a DataFrame to display menu items
df = pd.DataFrame(menu_items)

# Filter out items that are not on the menu
df = df[df["on_menu"]]
df["price"] = df["price"].apply(lambda x: f"${x:.2f}")

st.write("### Menu Items")

for idx, row in df.iterrows():
cols = st.columns([3, 1, 2])

with cols[0]:
st.write(f"{row['name']} ({row['price']})")

with cols[1]:
button_key = f"order_{row['id']}"
if st.button("Order", key=button_key):
# Fetch availability when button is clicked
availability = fetch_item_availability(row["id"])
if availability and availability.get("available_portions", 0) >= 1:
st.session_state.current_order = row["name"]
st.session_state.order_status = "Order success!"
else:
st.session_state.order_status = "Sorry, that's out of stock"

with cols[2]:
if st.session_state.current_order == row["name"]:
st.write(f"Ordered: {row['name']}")
st.write(st.session_state.order_status)

else:
st.write("No menu items found.")


if __name__ == "__main__":
display_menu()
58 changes: 55 additions & 3 deletions weird_salads/api/app.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,67 @@
from fastapi import FastAPI
from fastapi import FastAPI, HTTPException
from starlette import status

from weird_salads.api.schemas import CreateOrderSchema # noqa
from weird_salads.api.schemas import GetOrderSchema, GetOrdersSchema
from weird_salads.api.schemas import (
CreateOrderSchema,
GetMenuItemAvailabilitySchema,
GetMenuItemSchema,
GetOrderSchema,
GetOrdersSchema,
GetSimpleMenuSchema,
)
from weird_salads.inventory.inventory_service.exceptions import MenuItemNotFoundError
from weird_salads.inventory.inventory_service.inventory_service import MenuService
from weird_salads.inventory.repository.inventory_repository import MenuRepository
from weird_salads.orders.orders_service.orders_service import OrdersService
from weird_salads.orders.repository.orders_repository import OrdersRepository
from weird_salads.utils.unit_of_work import UnitOfWork

app = FastAPI()


# Menu
@app.get("/menu", response_model=GetSimpleMenuSchema, tags=["Menu"])
def get_menu():
with UnitOfWork() as unit_of_work:
repo = MenuRepository(unit_of_work.session)
inventory_service = MenuService(repo)
results = inventory_service.list_menu()
return {"items": [result.dict() for result in results]}


@app.get("/menu/{item_id}", response_model=GetMenuItemSchema, tags=["Menu"])
def get_order(item_id: int):
try:
with UnitOfWork() as unit_of_work:
repo = MenuRepository(unit_of_work.session)
inventory_service = MenuService(repo)
order = inventory_service.get_item(item_id=item_id)
return order
except MenuItemNotFoundError:
raise HTTPException(
status_code=404, detail=f"Menu Item with ID {item_id} not found"
)


@app.get(
"/menu/{item_id}/availability",
response_model=GetMenuItemAvailabilitySchema,
tags=["Menu"],
)
def get_availability(item_id: int):
try:
with UnitOfWork() as unit_of_work:
repo = MenuRepository(unit_of_work.session)
inventory_service = MenuService(repo)
order = inventory_service.get_recipe_item_availability(item_id=item_id)
return order # Ensure `order` is an instance of `GetRecipeItemSchema`
except MenuItemNotFoundError:
raise HTTPException(
status_code=404, detail=f"Menu Item with ID {item_id} not found"
)


# Orders
@app.get(
"/order",
response_model=GetOrdersSchema,
Expand Down
125 changes: 122 additions & 3 deletions weird_salads/api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,134 @@
Pydantic Schemas for API
"""

from datetime import datetime
from typing import List
from datetime import datetime, timezone
from enum import Enum
from typing import List, Optional

from pydantic import BaseModel
from pydantic import BaseModel, Field, field_validator
from typing_extensions import Annotated

# from typing_extensions import Annotated


# =================================
# Menu-related Schema for the API
# =================================
class UnitOfMeasure(str, Enum):
"""
Enum for units of measure
"""

liter = "liter"
deciliter = "deciliter"
centiliter = "centiliter"
milliliter = "milliliter"


class SimpleMenuItemSchema(BaseModel):
"""
Simple Menu Item (an overview).
"""

name: str
description: Optional[str] = None
price: Annotated[float, Field(ge=0.0, strict=True)]
created_on: datetime = datetime.now(timezone.utc)
on_menu: bool = True

class Config:
extra = "forbid"

@field_validator("price")
def quantity_non_nullable(cls, value):
assert value is not None, "price may not be None"
return value


# GET, now includes id...
class GetSimpleMenuItemSchema(SimpleMenuItemSchema):
id: int


# GET Menu (list of Menu items)
class GetSimpleMenuSchema(BaseModel):
"""
Menu (GET)
"""

items: List[GetSimpleMenuItemSchema]

class Config:
extra = "forbid"


# Ingredient-related things
# -------------------------


class IngredientItemSchema(BaseModel):
name: str
description: Optional[str] = None

class Config:
extra = "forbid"


class GetIngredientItemSchema(IngredientItemSchema):
id: int

class Config:
extra = "forbid"


# Schema for MenuItemIngredient
class MenuItemIngredientSchema(BaseModel):
quantity: float
unit: UnitOfMeasure
ingredient: GetIngredientItemSchema

class Config:
extra = "forbid"


# GET, now includes id and ingredients...
class GetMenuItemSchema(GetSimpleMenuItemSchema):
"""
Menu Item detail (GET)
"""

id: int
ingredients: List[
MenuItemIngredientSchema
] # Include ingredients with the menu item


# Stock-related things
# --------------------


class MenuItemAvailabilitySchema(BaseModel):
ingredient: GetIngredientItemSchema
required_quantity: Annotated[float, Field(ge=0.0, strict=True)]
available_quantity: Annotated[float, Field(ge=0.0, strict=True)]
unit: UnitOfMeasure

class Config:
extra = "forbid"


# Schema for menu item availability
class GetMenuItemAvailabilitySchema(GetSimpleMenuItemSchema):
available_portions: int = 0
ingredient_availability: List[MenuItemAvailabilitySchema]

class Config:
extra = "forbid"


# =================================
# Orders-related Schema for the API
# =================================
class CreateOrderSchema(BaseModel):
menu_id: int

Expand Down
6 changes: 6 additions & 0 deletions weird_salads/inventory/inventory_service/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class MenuItemNotFoundError(Exception):
pass


class UnitConversionError(Exception):
pass
Loading

0 comments on commit e811b5e

Please sign in to comment.