Skip to content

Commit

Permalink
Initial Inventory Endpoints (#15)
Browse files Browse the repository at this point in the history
This MR implements the Inventory endpoints as described in the API Design.

Specifically,

GET endpoints for retrieving all stock (/inventory), specific stock items (/inventory/stock/{stock_id}) and ingredients (/inventory/ingredient/{ingredient_id}).

A POST endpoint for creating new stock items (/inventory), ensuring that ingredients exist before allowing stock creation.

Basic Streamlit pages have been generated.

closes: #10
  • Loading branch information
PaulJWright authored Aug 5, 2024
1 parent e811b5e commit 7706671
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 10 deletions.
63 changes: 63 additions & 0 deletions streamlit_app/pages/create_stock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from datetime import datetime

import requests
import streamlit as st


def create_stock_item(payload):
try:
# Post data to FastAPI endpoint
response = requests.post("http://fastapi:8000/inventory", json=payload)
response.raise_for_status()
return response.json(), None
except requests.exceptions.RequestException as e:
return None, str(e)


def add_stock_page():
st.title("Add Stock Item")

# Define the form for stock creation
with st.form(key="add_stock_form"):
st.header("Stock Information")

# Input fields for stock data
ingredient_id = st.number_input("Ingredient ID", min_value=1, step=1)
stock_quantity = st.number_input(
"Quantity", min_value=0.0, format="%.2f"
) # Float with 2 decimal places
stock_unit = st.selectbox(
"Unit", ["liter", "deciliter", "centiliter", "milliliter"]
) # Example units
cost = st.number_input(
"Cost per Unit", min_value=0.0, format="%.2f"
) # Float with 2 decimal places
delivery_date = st.date_input(
"Delivery Date", min_value=datetime(2000, 1, 1)
) # Date input

# Create the stock data payload
payload = {
"stock": {
"ingredient_id": ingredient_id,
"unit": stock_unit,
"quantity": stock_quantity,
"cost": cost,
"delivery_date": delivery_date.isoformat(),
# Convert date to ISO format string
}
}

# Submit button
submit_button = st.form_submit_button(label="Add Stock")

if submit_button:
result, error = create_stock_item(payload)
if error:
st.error(f"Failed to add stock: {error}")
else:
st.success(f"Stock item added successfully: {result}")


if __name__ == "__main__":
add_stock_page()
41 changes: 41 additions & 0 deletions streamlit_app/pages/ingredients.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import pandas as pd
import requests
import streamlit as st


def display_ingredient_items():
st.title("Ingredient Items Report")

ingredient_id = st.number_input("Enter Ingredient ID:", min_value=1, step=1)

if st.button("Get Ingredient"):
try:
# Fetch ingredient data from the FastAPI endpoint
response = requests.get(
f"http://fastapi:8000/inventory/ingredient/{ingredient_id}"
)
response.raise_for_status()
data = response.json()

# Extract items from the response data
ingredient_items = data.get("items", [])
df_ingredient = pd.DataFrame(ingredient_items)

# Check if the DataFrame is empty
if df_ingredient.empty:
st.write(f"No data found for Ingredient ID {ingredient_id}.")
else:
# Ensure 'delivery_date' is treated as a datetime object for sorting
df_ingredient["delivery_date"] = pd.to_datetime(
df_ingredient["delivery_date"]
)
# Sort the DataFrame by 'delivery_date'
df_ingredient_sorted = df_ingredient.sort_values(by="delivery_date")
st.table(df_ingredient_sorted)

except requests.exceptions.RequestException as e:
st.write("Failed to connect to FastAPI:", e)


if __name__ == "__main__":
display_ingredient_items()
30 changes: 30 additions & 0 deletions streamlit_app/pages/report_stock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import pandas as pd
import requests
import streamlit as st


def display_stock_items():
st.title("Stock Items Report")

try:
# Fetch stock data from the FastAPI endpoint
response = requests.get("http://fastapi:8000/inventory/")
response.raise_for_status()
data = response.json()

# Extract items from the response data
stock_items = data.get("items", [])
df_stock = pd.DataFrame(stock_items)

# Check if the DataFrame is empty
if df_stock.empty:
st.write("No stock data available.")
else:
st.table(df_stock)

except requests.exceptions.RequestException as e:
st.write("Failed to connect to FastAPI:", e)


if __name__ == "__main__":
display_stock_items()
79 changes: 78 additions & 1 deletion weird_salads/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@

from weird_salads.api.schemas import (
CreateOrderSchema,
CreateStockSchema,
GetMenuItemAvailabilitySchema,
GetMenuItemSchema,
GetOrderSchema,
GetOrdersSchema,
GetSimpleMenuSchema,
GetStockItemSchema,
GetStockSchema,
)
from weird_salads.inventory.inventory_service.exceptions import (
IngredientNotFoundError,
MenuItemNotFoundError,
StockItemNotFoundError,
)
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
Expand Down Expand Up @@ -61,6 +68,76 @@ def get_availability(item_id: int):
)


@app.get("/inventory", response_model=GetStockSchema, tags=["Inventory"])
def get_stock():
with UnitOfWork() as unit_of_work:
repo = MenuRepository(unit_of_work.session)
inventory_service = MenuService(repo)
results = inventory_service.list_stock()
return {"items": [result.dict() for result in results]}


@app.get(
"/inventory/stock/{stock_id}", response_model=GetStockItemSchema, tags=["Inventory"]
)
def get_stock_item(stock_id: str):
try:
with UnitOfWork() as unit_of_work:
repo = MenuRepository(unit_of_work.session)
inventory_service = MenuService(repo)
order = inventory_service.get_stock_item(stock_id=stock_id)
return order
except StockItemNotFoundError:
raise HTTPException(
status_code=404, detail=f"Stock Item with ID {stock_id} not found"
)


@app.get(
"/inventory/ingredient/{ingredient_id}",
response_model=GetStockSchema,
tags=["Inventory"],
)
def get_ingredient(ingredient_id: int):
try:
with UnitOfWork() as unit_of_work:
repo = MenuRepository(unit_of_work.session)
inventory_service = MenuService(repo)
ingredient = inventory_service.get_ingredient(ingredient_id=ingredient_id)
return {"items": [record.dict() for record in ingredient]}
except IngredientNotFoundError:
raise HTTPException(
status_code=404, detail=f"Ingredient Item with ID {ingredient_id} not found"
)


@app.post(
"/inventory",
status_code=status.HTTP_201_CREATED,
response_model=GetStockItemSchema,
tags=["Inventory"],
)
def create_stock(payload: CreateStockSchema):
with UnitOfWork() as unit_of_work:
repo = MenuRepository(unit_of_work.session)
stock_service = MenuService(repo)
stock_item = payload.model_dump()["stock"]
stock_item["unit"] = stock_item["unit"].value # necessary?

# Check if the ingredient exists
ingredient = stock_service.get_ingredient(stock_item["ingredient_id"])
if ingredient is None:
raise HTTPException(
status_code=404,
detail=f"Ingredient with ID {stock_item['ingredient_id']} not found",
)

order = stock_service.ingest_stock(stock_item)
unit_of_work.commit()
return_payload = order.dict()
return return_payload


# Orders
@app.get(
"/order",
Expand Down
28 changes: 28 additions & 0 deletions weird_salads/api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,31 @@ class GetOrdersSchema(BaseModel):

class Config:
extra = "forbid"


class StockSchema(BaseModel):
ingredient_id: int
unit: UnitOfMeasure
quantity: Annotated[float, Field(ge=0.0, strict=True)]
cost: Annotated[float, Field(ge=0.0, strict=True)]
# expiry_date: datetime
delivery_date: Optional[datetime] = datetime.now(timezone.utc)
created_on: Optional[datetime] = datetime.now(timezone.utc)

class Config:
extra = "forbid"


class GetStockItemSchema(StockSchema):
id: str


class GetStockSchema(BaseModel):
items: List[GetStockItemSchema]


class CreateStockSchema(BaseModel):
stock: StockSchema

class Config:
extra = "forbid"
8 changes: 8 additions & 0 deletions weird_salads/inventory/inventory_service/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,11 @@ class MenuItemNotFoundError(Exception):

class UnitConversionError(Exception):
pass


class StockItemNotFoundError(Exception):
pass


class IngredientNotFoundError(Exception):
pass
14 changes: 9 additions & 5 deletions weird_salads/inventory/inventory_service/inventory_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
from typing import Any, Dict, List

from weird_salads.api.schemas import UnitOfMeasure
from weird_salads.inventory.inventory_service.exceptions import MenuItemNotFoundError
from weird_salads.inventory.inventory_service.exceptions import (
IngredientNotFoundError,
MenuItemNotFoundError,
StockItemNotFoundError,
)
from weird_salads.inventory.inventory_service.inventory import (
MenuItem,
MenuItemIngredient,
Expand Down Expand Up @@ -133,19 +137,19 @@ def get_recipe_item_availability(self, item_id: int) -> Dict[str, Any]:
# ---- ingredient_id queries
def get_ingredient(self, ingredient_id: int):
ingredient_item = self.menu_repository.get_ingredient(ingredient_id)
if ingredient_item is not None:
if ingredient_item:
return ingredient_item
raise ValueError(f"items with id {ingredient_id} not found") # fix
raise IngredientNotFoundError(f"items with id {ingredient_id} not found") # fix

def ingest_stock(self, item):
return self.stock_repository.add_stock(item)
return self.menu_repository.add_stock(item)

# ---- stock_id queries
def get_stock_item(self, stock_id: str):
stock_item = self.menu_repository.get_stock(stock_id)
if stock_item is not None:
return stock_item
raise ValueError(f"stock with id {stock_id} not found") # fix
raise StockItemNotFoundError(f"stock with id {stock_id} not found") # fix

def list_stock(self): # needs options for filtering
return self.menu_repository.list_stock()
8 changes: 4 additions & 4 deletions weird_salads/inventory/repository/inventory_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,18 +106,18 @@ def _get_ingredient(self, id: int):

def get_ingredient(self, id: int) -> List[StockItem]:
ingredients = self._get_ingredient(id)
if ingredients is not None:
if ingredients: # is not None:
return [StockItem(**ingredient.dict()) for ingredient in ingredients]

def _get_stock(self, id: str):
return self.session.query(StockModel).filter(StockModel.id == id).first()

def get_stock(self, id_: str):
order = self._get(id)
def get_stock(self, id: str):
order = self._get_stock(id)
if order is not None:
return StockItem(**order.dict())

def list_stock(self, limit=None):
def list_stock(self, limit=None): # need to implement limits
query = self.session.query(StockModel)
records = query.all()
return [StockItem(**record.dict()) for record in records]
Expand Down

0 comments on commit 7706671

Please sign in to comment.