From 49c6a15f1641d53e4860611459b1b3799f1523c9 Mon Sep 17 00:00:00 2001 From: Omnia-Agabani Date: Thu, 20 Nov 2025 19:52:37 +0400 Subject: [PATCH 1/4] Fix CIs checks --- smart_pantry_manager/data/pantry_omnia.xlsx | Bin 5305 -> 5340 bytes smart_pantry_manager/pages/all_recipes.py | 103 +++++++----- .../pages/recommended_recipes.py | 156 +++++++----------- smart_pantry_manager/smart_pantry.py | 103 ++++++------ 4 files changed, 178 insertions(+), 184 deletions(-) diff --git a/smart_pantry_manager/data/pantry_omnia.xlsx b/smart_pantry_manager/data/pantry_omnia.xlsx index c5b3b2303e87a9aa6280945c4eaad247c8ac83c8..4b8d70b8cf5d18ebac7e7286bba2bd6ed4b124f7 100644 GIT binary patch delta 1237 zcmdm~c}J5cz?+#xgn@y9gTZI+M4p2jK66W=>tFtTJn^blz1m`#MeC+3`Y9+Havst3 z+Ou}i&w!*W@9s~YwwC!|Z}K0zx*Y9ZzM$V7{ukMMt|UhFZY%eEwIR`ckpkl+yG5ZZ z&$OI=c+Y*chW%s4rc3W%H7KllC8jgUs7u7>kdlsDS(wybjl=G5(i<-}*hZC1X_zjv zPA758%kyoPktYmGxlQYNoGsU+>*XJL`1Rw%%D8B=`XDZWaC4Z|}aFw{?+F>A_0HRkG`!_D)z-H#^4u*LtVdzT4mb zOWeb8i>>Z&^lwgNpO-|>jG3b>z{bEZN1cH|7#JQEIr^LbGHS62fdb^mF4^s~nHU&a zxEL5D7$*A*OV#&I%g>)}AaHCy*CXaBCX9!)nzk(IHOrj5E9`EZbphk*hMBb{XYKyx zTnduJ`C;&T+rgV*0twc@)JzWd!COo4P+LOxb<*r?QxzA;r8~uT=&R| z-`}*jsP4c&ops9%zpr_2``Ys1p;^neu*L^BtYn?W{5Y*|ADO+>RUYgm<(Kv!fc-CCU1#dh!xV2k8 z@EkH&mf!y1yFiUndz)uyQ||NinkR$j&hwM{&pwm4Ex*=UU2KZxyO3n(hw&|k=DP`g z(O`O2DzYe3p?FC^g54!EwJBwjJPi)D7%iQ_B3^IExol>O?c@o!DawwV*WO6Jxn6A2)KH`>=px&6R1$4jSY|0 z{d#lKa-E{*R~#>vh=21_=vtL6CdKFRxnbUY?#9~VQ=)4h&iuoUoVqt(=Tc;5@|ipN z7w>Z*wfQk$G$V-XAn+K(RTcCBbBl#+;Msrjb|G_+k`F@me5g5KvaPU1G)O2wcW=!P zV5Z>VWncgzIA~ye2h1kG{8L;~nUh+qSCN|&;LXS+!T>8~N}{*BzwSB!)F1}b&JEYd zz|g=b$T#_duq4Rl*TOQ4zb12t$bmQ-A~H9oi981dug)lm_Wo!0^A%9+G$R9p;N*`&BK1or`uZO> z;A#7RYD)F%WvSLKv3d%A3W|oDM|8dR=q~yhkaXqU{mawVG9T+Fht5vbE(Ej1744|s)N=Bvz4Wsb#r7QWRquaq%1_I?({cFq=n*QutUitZy*sHIb zJ}+8VR-?PBcSq(uwd?)=R$pLE{{Bt=ShvHr53lb(QAZAtlIWQ+bCd0B)j9iREf`lPRDWBT zS@w0a;!=}M3s3d`X#Q^d+~&rOlM_#;hcYer9u@Ah_K#5D9k#|(UMo6xJz|@Qa?Ti0o`cHyJrQ|s&B?&r0+ zwCG5gf`_mpR|V4@PTdQ47kodN$?Fq%&W5vn>i@65%68|7z7^iDDRXtf+nGt<=Gf0Z z_r2!nc^|(3$F^flFZPSya0r!DpL_ED=9T~Du4uHsHUFI(z27h2UnHmEYx}c^b$_Jq zpMK~ti%b6JN9j4)UH5|bD0^IT`LtNwcVm6#q?vBgdv)XN{bo9TZ3#LbB+Gx9{p4r0 z!zvDQyCk_bZ7#6YyCcxjbb7MuPRa0pSscz?W>4?#C_27E_ww(;tAAH7`1B;xCP6U& z_|ftwOZLbY?C6lrIAi;|@_g010!62gZ4a`iCumD|ojSHHc!unUNpb22PfwV4TK6&U z=BC-(x7PFP9)5A!_s7LD>pAnk7t0IiRIzZo^Hm@Ea{kzXr3J=L*AY*73}hgs}j{xlZ9 zv3|Dh;{9Eh{)3Y#J2c~N{>7!p%yf0eWLduFKx#7^e>5YAn=J4c#PtyL0duDb*>J$} z_vBkb<{%})!uIG1J3-h2Br{7`CgL;~14BiQesM`1FXO(iQex1 zy6XVY6=J|V!40Hgpn>rnF9Sn-QEE=Hz8;7~)i{|?M27MDWNi^SkWJn|j>+T<5lwlJ i>t?q(wMYX?p6kpE48kz;fOG?+ pd.DataFrame: """ - Load recipes from the SQLite database and normalize column names. - Returns a DataFrame with columns: Recipe, Ingredients, Instructions + Load recipes from SQLite database and normalize column names. + + Returns: + pd.DataFrame: Columns = Recipe, Ingredients, Instructions """ db_path = "smart_pantry_manager/data/Recipe_Dataset.sqlite" - - # Check if database file exists if not os.path.exists(db_path): st.error(f"โŒ Database file not found at: {db_path}") return pd.DataFrame(columns=["Recipe", "Ingredients", "Instructions"]) - # Connect to SQLite database conn = sqlite3.connect(db_path) - try: - # Load the table "recipes" df = pd.read_sql_query("SELECT * FROM recipes", conn) - except Exception as e: - st.error(f"Error reading database: {e}") + except Exception as err: + st.error(f"Error reading database: {err}") conn.close() return pd.DataFrame(columns=["Recipe", "Ingredients", "Instructions"]) - conn.close() # Normalize column names df.columns = [c.strip().lower() for c in df.columns] - # Rename columns if they exist rename_map = { "title": "Recipe", "cleaned_ingredients": "Ingredients", @@ -60,47 +63,59 @@ def load_recipes(): # Keep only required columns required_cols = ["Recipe", "Ingredients", "Instructions"] df = df[[col for col in required_cols if col in df.columns]] - return df -def format_ingredients(ingredients_str): +def format_ingredients(ingredients_str: str) -> list: """ - Convert ingredients from string representation of list to a Python list. + Convert ingredients string into a list. + + Args: + ingredients_str (str): String representation of ingredients + + Returns: + list: List of ingredients as strings """ + if not ingredients_str: + return [] + + s = ingredients_str.strip() try: - # Try to parse as a Python list - if ingredients_str.startswith("["): - return ast.literal_eval(ingredients_str) - # If it's comma-separated - elif "," in ingredients_str: - return [item.strip() for item in ingredients_str.split(",")] - else: - return [ingredients_str] - except: - # If parsing fails, return as single item list - return [ingredients_str] - - -recipes = load_recipes() - -# ---------- Display recipes ---------- -if recipes.empty: + if s.startswith("[") and s.endswith("]"): + parsed = ast.literal_eval(s) + if isinstance(parsed, (list, tuple)): + return [str(x).strip() for x in parsed if str(x).strip()] + if "," in s: + return [item.strip() for item in s.split(",") if item.strip()] + return [s] + except Exception: + # fallback for | or newline-separated strings + if "|" in s: + return [x.strip() for x in s.split("|") if x.strip()] + if "\n" in s: + return [x.strip() for x in s.split("\n") if x.strip()] + return [s] + + +recipes_df = load_recipes() + +# ---------- Display Recipes ---------- +if recipes_df.empty: st.info("No recipes found.") else: - search = st.text_input("๐Ÿ” Search for a recipe:") - filtered = ( - recipes[recipes["Recipe"].str.contains(search, case=False, na=False)] - if search - else recipes + search_term = st.text_input("๐Ÿ” Search for a recipe:") + filtered_df = ( + recipes_df[recipes_df["Recipe"].str.contains(search_term, case=False, na=False)] + if search_term + else recipes_df ) - for _, row in filtered.iterrows(): + for _, row in filtered_df.iterrows(): with st.expander(f"๐Ÿ“– {row['Recipe']}"): st.markdown("**๐Ÿง‚ Ingredients:**") - ingredients_list = format_ingredients(row["Ingredients"]) - for ingredient in ingredients_list: - st.write(f"โ€ข {ingredient}") + ing_list = format_ingredients(row.get("Ingredients") or "") + for ing in ing_list: + st.write(f"โ€ข {ing}") st.markdown("**๐Ÿ‘ฉโ€๐Ÿณ Instructions:**") - st.write(row["Instructions"]) + st.write(row.get("Instructions") or "No instructions available.") diff --git a/smart_pantry_manager/pages/recommended_recipes.py b/smart_pantry_manager/pages/recommended_recipes.py index cbbdae3..adb5658 100644 --- a/smart_pantry_manager/pages/recommended_recipes.py +++ b/smart_pantry_manager/pages/recommended_recipes.py @@ -1,6 +1,14 @@ -# recommended_recipes.py -# Optimized Recommended Recipes page for Smart Pantry Manager -# Date: 2025-11-20 +""" +recommended_recipes.py +Recommended Recipes Page for Smart Pantry Manager + +Features: +- Personalized recipe suggestions based on user's pantry +- Match % calculation and missing ingredient hints +- Streamlit UI with expandable recipe details + +Date: 2025-11-20 +""" import ast import os @@ -12,61 +20,61 @@ import pandas as pd import streamlit as st +# ---------- Page Setup ---------- st.set_page_config(page_title="Recommended Recipes", page_icon="๐Ÿณ", layout="wide") st.title("๐Ÿณ Recommended Recipes") st.caption("Discover recipes you can cook with what's already in your pantry!") -# ---------- Check username ---------- +# ---------- Check Username ---------- if "username" not in st.session_state or not st.session_state["username"]: st.warning("Please go to the Home page and enter your username first.") st.stop() username = st.session_state["username"] -USER_FILE = os.path.join( +user_file = os.path.join( "smart_pantry_manager", "data", f"pantry_{username.replace(' ', '_').lower()}.xlsx" ) -# ---------- Load pantry ---------- +# ---------- Load Pantry ---------- try: - pantry = pd.read_excel(USER_FILE) - # Normalize pantry product names - if "Product" in pantry.columns: - pantry["Product"] = pantry["Product"].astype(str).str.lower().str.strip() + pantry_df = pd.read_excel(user_file) + if "Product" in pantry_df.columns: + pantry_df["Product"] = pantry_df["Product"].astype(str).str.lower().str.strip() else: - pantry["Product"] = "" + pantry_df["Product"] = "" except FileNotFoundError: st.info("Your pantry is empty. Please add items on the Home page.") st.stop() -# remove duplicates and empty -pantry_products = sorted({p for p in pantry["Product"].tolist() if p and p.strip()}) - -# Build compiled regex patterns for strict whole-word matching -# e.g., pantry 'milk' -> regex '\bmilk\b' (case-insensitive) -pantry_regexes = [ - re.compile(rf"\b{re.escape(prod)}\b", flags=re.IGNORECASE) - for prod in pantry_products -] +# Remove duplicates and empty product names +pantry_products = sorted({p for p in pantry_df["Product"].tolist() if p and p.strip()}) -# ---------- Load recipes ---------- +# ---------- Load Recipes ---------- @st.cache_data def load_recipes() -> pd.DataFrame: + """ + Load recipes from SQLite and normalize columns. + Returns DataFrame with Recipe, Ingredients, Instructions + """ db_path = os.path.join("smart_pantry_manager", "data", "Recipe_Dataset.sqlite") if not os.path.exists(db_path): st.error( - "โš ๏ธ Recipes database not found. Please ensure Recipe_Dataset.sqlite is in smart_pantry_manager/data/." + "โš ๏ธ Recipes database not found. Ensure Recipe_Dataset.sqlite " + "is in smart_pantry_manager/data/." ) return pd.DataFrame(columns=["Recipe", "Ingredients", "Instructions"]) + conn = sqlite3.connect(db_path) try: df = pd.read_sql_query("SELECT * FROM recipes", conn) - except Exception as e: - st.error(f"Error reading recipes: {e}") + except Exception as err: + st.error(f"Error reading recipes: {err}") conn.close() return pd.DataFrame(columns=["Recipe", "Ingredients", "Instructions"]) conn.close() + # Normalize column names df.columns = [c.strip().lower() for c in df.columns] rename_map = { @@ -79,51 +87,44 @@ def load_recipes() -> pd.DataFrame: df.rename( columns={k: v for k, v in rename_map.items() if k in df.columns}, inplace=True ) - required_cols = ["Recipe", "Ingredients", "Instructions"] - for col in required_cols: + # Ensure required columns exist + for col in ["Recipe", "Ingredients", "Instructions"]: if col not in df.columns: df[col] = "" - # Keep required only - return df[required_cols] - + return df[["Recipe", "Ingredients", "Instructions"]] -recipes = load_recipes() -if recipes.empty: +recipes_df = load_recipes() +if recipes_df.empty: st.warning("No recipes available in the database.") st.stop() # ---------- Utilities ---------- def normalize_text(s: str) -> str: - """Normalize unicode artifacts and strip.""" + """Normalize unicode artifacts and strip spaces.""" if s is None: return "" - # fix weird combined characters from windows-1252/utf-8 issues s = str(s) s = unicodedata.normalize("NFKC", s) - # remove zero-width and weird control chars s = re.sub(r"[\u200b-\u200f\u2028\u2029]", "", s) return s.strip() def parse_ingredients(ingredients_str: str) -> List[str]: - """Parse ingredients stored as a Python list string into a list of cleaned ingredient strings.""" + """Parse ingredients string into a cleaned list of ingredients.""" if not ingredients_str: return [] s = normalize_text(ingredients_str) try: - # likely a list literal like "['1 cup milk', 'salt']" if s.startswith("[") and s.endswith("]"): parsed = ast.literal_eval(s) if isinstance(parsed, (list, tuple)): return [normalize_text(str(x)) for x in parsed if str(x).strip()] - # fallback: comma-separated if "," in s: - return [normalize_text(item) for item in s.split(",") if item.strip()] + return [normalize_text(x) for x in s.split(",") if x.strip()] return [s] except Exception: - # last-resort: try splitting by '|' or newline if "|" in s: return [normalize_text(x) for x in s.split("|") if x.strip()] if "\n" in s: @@ -133,38 +134,31 @@ def parse_ingredients(ingredients_str: str) -> List[str]: def strip_leading_qty(s: str) -> str: """ - Remove leading quantity & measurements to expose ingredient name for matching. - Example: '1 cup evaporated milk' -> 'evaporated milk' - This is intentionally conservative: we remove common leading patterns. + Remove leading quantity & units from ingredient to match pantry. + Example: '1 cup milk' -> 'milk' """ if not s: return "" s = s.lower() - # common patterns: numbers, fractions, parentheses, measurements at start - # remove leading parenthetical groups or leading numbers/fractions/measurements + s = re.sub(r"^\s*\(?\d+(?:[\/\u00BC-\u00BE\u2150-\u215E]?\d*)?\)?\s*", "", s) s = re.sub( - r"^\s*\(?\d+(?:[\/\u00BC-\u00BE\u2150-\u215E]?\d*)?\)?\s*", "", s - ) # leading numbers like "1", "1/2" - s = re.sub( - r"^\s*\d+(\.\d+)?\s*(cup|cups|tbsp|tbsp.|tbsps|tsp|tsp.|oz|lb|lbs|g|kg|ml|l)\b", + r"^\s*\d+(\.\d+)?\s*(cup|cups|tbsp|tbsp\.|tbsps|tsp|tsp\.|oz|lb|lbs|g|kg|ml|l)\b", "", s, ) - s = re.sub(r"^\s*(?:one|two|three|four|a|an)\s+", "", s) # words - # remove leftover leading measurement words + s = re.sub(r"^\s*(?:one|two|three|four|a|an)\s+", "", s) s = re.sub(r"^\s*\(?\d+[^a-zA-Z]*\)?\s*", "", s) - # strip extras s = re.sub(r"^[\-\โ€“\โ€”\s]+", "", s) return s.strip() -# Use st.cache_data for availability checks. -# Cache key will be the ingredients string and a tuple of pantry products (for hashing). @st.cache_data def cached_check_availability( recipe_ingredients: str, pantry_products_tuple: Tuple[str, ...] ) -> Tuple[float, List[str]]: - """Return (match_percent, missing_items_list) using strict whole-word matching.""" + """ + Return (match_percent, missing_items_list) using whole-word matching. + """ ingredients = parse_ingredients(recipe_ingredients) if not ingredients: return 0.0, [] @@ -173,7 +167,6 @@ def cached_check_availability( available_count = 0 missing_items = [] - # convert pantry regex list from pantry_products_tuple each call for safety regexes = [ re.compile(rf"\b{re.escape(p)}\b", flags=re.IGNORECASE) for p in pantry_products_tuple @@ -181,27 +174,17 @@ def cached_check_availability( for item in ingredients: item_norm = normalize_text(item).lower() - # strip leading qty to focus on name name_candidate = strip_leading_qty(item_norm) - # fallback to full item if strip produced emptiness text_to_search = name_candidate or item_norm - - matched = False - for rx in regexes: - if rx.search(text_to_search): - matched = True - break - + matched = any(rx.search(text_to_search) for rx in regexes) if matched: available_count += 1 else: - # record a cleaned short name as missing - # take last 3 words of the name candidate to make missing hint concise words = text_to_search.split() - short = " ".join(words[-3:]) if len(words) > 3 else " ".join(words) + short = " ".join(words[-3:] if len(words) > 3 else words) missing_items.append(short) - match_percentage = (available_count / total) * 100 if total > 0 else 0.0 + match_percentage = (available_count / total) * 100 if total else 0.0 return round(match_percentage, 1), missing_items @@ -219,26 +202,17 @@ def cached_check_availability( status_text = st.empty() results = [] -total_recipes = len(recipes) - -# Convert pantry_products to tuple for caching +total_recipes = len(recipes_df) pantry_key = tuple(pantry_products) -for idx, (_, row) in enumerate(recipes.iterrows()): - # Update progress (user requested not to throttle updates) - progress = (idx + 1) / total_recipes - progress_bar.progress(progress) +for idx, (_, row) in enumerate(recipes_df.iterrows()): + progress_bar.progress((idx + 1) / total_recipes) status_text.text(f"Processing recipe {idx + 1} of {total_recipes}...") - - # Safely get ingredients string ingredients_raw = normalize_text(row.get("Ingredients") or "") match_percent, missing = cached_check_availability(ingredients_raw, pantry_key) - if match_percent >= min_match: instr = normalize_text(row.get("Instructions") or "") - # Shorten instructions safely instr_preview = instr[:500] + "..." if len(instr) > 500 else instr - results.append( { "Recipe": row.get("Recipe") or "Unnamed Recipe", @@ -251,16 +225,14 @@ def cached_check_availability( } ) -# Clear progress indicators progress_bar.empty() status_text.empty() # Sort and limit results results_df = pd.DataFrame(results) if not results_df.empty: - results_df = results_df.sort_values(by="Match %", ascending=False).head( - int(max_recipes) - ) + results_df = results_df.sort_values(by="Match %", ascending=False) + results_df = results_df.head(int(max_recipes)) st.success(f"โœ… Found {len(results_df)} matching recipes!") st.write("### ๐Ÿ“‹ Recipe Match Overview") @@ -276,14 +248,12 @@ def cached_check_availability( "๐ŸŸข" if row["Match %"] >= 80 else "๐ŸŸก" if row["Match %"] >= 60 else "๐ŸŸ " ) with st.expander(f"{match_color} {row['Recipe']} โ€” {row['Match %']}% match"): - col1, col2 = st.columns([1, 2]) - with col1: + c1, c2 = st.columns([1, 2]) + with c1: st.markdown(f"**Match:** {row['Match %']}%") st.markdown(f"**Missing:** {row['Missing']}") - - # Ingredients: parse and display safely ing_list = parse_ingredients(row["Ingredients"] or "") - with col2: + with c2: st.markdown("**๐Ÿง‚ Ingredients:**") if ing_list: for ing in ing_list[:10]: @@ -292,10 +262,10 @@ def cached_check_availability( st.write(f"*...and {len(ing_list) - 10} more*") else: st.write("No ingredient data available.") - - st.markdown("**๐Ÿ‘ฉโ€๐Ÿณ Instructions:**") - st.write(row["Instructions"] or "No instructions available.") + st.markdown("**๐Ÿ‘ฉโ€๐Ÿณ Instructions:**") + st.write(row["Instructions"] or "No instructions available.") else: st.info( - f"No recipes found with at least {min_match}% match. Try lowering the minimum match percentage." + f"No recipes found with at least {min_match}% match. " + "Try lowering the minimum match percentage." ) diff --git a/smart_pantry_manager/smart_pantry.py b/smart_pantry_manager/smart_pantry.py index 58f35ac..0efe929 100644 --- a/smart_pantry_manager/smart_pantry.py +++ b/smart_pantry_manager/smart_pantry.py @@ -1,4 +1,3 @@ -# spell-checker: disable """ smart_pantry.py Smart Pantry Web App (Home Page Only) @@ -20,10 +19,13 @@ import pandas as pd import streamlit as st +# Constants +DATA_DIR = "smart_pantry_manager/data" +EXPIRY_SOON_DAYS = 3 + # ---------- Page Setup ---------- st.set_page_config(page_title="Smart Pantry Manager", page_icon="๐Ÿงบ", layout="centered") - st.title("๐Ÿ“ฑ Smart Pantry Manager ๐Ÿ“Š") st.subheader("Track your pantry items and discover what you can cook ๐Ÿ‘‡") @@ -35,47 +37,40 @@ st.warning("Please enter your name to start using the app.") st.stop() -# Store username globally for use in other pages st.session_state["username"] = username +os.makedirs(DATA_DIR, exist_ok=True) -# Create user-specific file path -os.makedirs("smart_pantry_manager/data", exist_ok=True) -USER_FILE = ( - f"smart_pantry_manager/data/pantry_{username.replace(' ', '_').lower()}.xlsx" -) +user_file = os.path.join(DATA_DIR, f"pantry_{username.replace(' ', '_').lower()}.xlsx") # ---------- Load Pantry ---------- @st.cache_data -def load_pantry(file_path): - """Load pantry data for a specific user or create an empty table.""" +def load_pantry(file_path: str) -> pd.DataFrame: + """Load pantry data for a user or create an empty table.""" try: df = pd.read_excel(file_path) df["Expiry Date"] = pd.to_datetime(df["Expiry Date"], errors="coerce") df["Days Left"] = (df["Expiry Date"] - datetime.now()).dt.days - - # ๐Ÿงน Auto-remove expired items + # Remove expired items automatically df = df[df["Days Left"] >= 0].reset_index(drop=True) return df except FileNotFoundError: - return pd.DataFrame( - columns=[ - "Product", - "Category", - "Quantity", - "Unit", - "Expiry Date", - "Days Left", - ] - ) + columns = [ + "Product", + "Category", + "Quantity", + "Unit", + "Expiry Date", + "Days Left", + ] + return pd.DataFrame(columns=columns) -data = load_pantry(USER_FILE) +pantry_data = load_pantry(user_file) # ---------- Add New Product ---------- st.header("โž• Add a New Product") - -product = st.text_input("Product name:") +product_name = st.text_input("Product name:") category = st.selectbox( "Category:", [ @@ -99,40 +94,46 @@ def load_pantry(file_path): ) quantity = st.number_input("Quantity (numeric or count):", min_value=0.0, step=0.1) unit = st.selectbox("Unit:", ["count", "g", "kg", "ml", "L", "cup", "tbsp", "tsp"]) -expiry = st.date_input("Expiry date:") +expiry_date = st.date_input("Expiry date:") if st.button("๐Ÿ’พ Save product"): - if product: + if product_name: with st.spinner("๐Ÿ’พ Saving product... please wait..."): - time.sleep(2) # simulate loading + time.sleep(1) today = datetime.now().date() - days_left = (expiry - today).days + days_left = (expiry_date - today).days new_row = { - "Product": product, + "Product": product_name, "Category": category, "Quantity": quantity, "Unit": unit, - "Expiry Date": expiry, + "Expiry Date": expiry_date, "Days Left": days_left, } - data = pd.concat([data, pd.DataFrame([new_row])], ignore_index=True) - data.to_excel(USER_FILE, index=False) + pantry_data = pd.concat( + [pantry_data, pd.DataFrame([new_row])], ignore_index=True + ) + pantry_data.to_excel(user_file, index=False) st.cache_data.clear() - st.success(f"โœ… {product} added successfully!") + st.success(f"โœ… {product_name} added successfully!") else: st.warning("Please enter a product name.") # ---------- Update Days Left ---------- -if not data.empty: - data["Expiry Date"] = pd.to_datetime(data["Expiry Date"], errors="coerce") +if not pantry_data.empty: + pantry_data["Expiry Date"] = pd.to_datetime( + pantry_data["Expiry Date"], errors="coerce" + ) today = pd.Timestamp(datetime.now().date()) - data["Days Left"] = (data["Expiry Date"] - today).dt.days + pantry_data["Days Left"] = (pantry_data["Expiry Date"] - today).dt.days # ---------- Alerts ---------- st.header("โš ๏ธ Expiry Alerts") -if not data.empty: - expired = data[data["Days Left"] < 0] - expiring_soon = data[(data["Days Left"] >= 0) & (data["Days Left"] <= 3)] +if not pantry_data.empty: + expired = pantry_data[pantry_data["Days Left"] <= 0] + expiring_soon = pantry_data[ + (pantry_data["Days Left"] > 0) & (pantry_data["Days Left"] <= EXPIRY_SOON_DAYS) + ] if not expired.empty: st.error("โŒ Some products have expired:") @@ -146,26 +147,34 @@ def load_pantry(file_path): st.header("๐Ÿ“ฆ Your Pantry Items") -def color_days(val): - """Color based on days left.""" +def color_days(val: int) -> str: + """Return background color based on days left.""" if val < 0: color = "#ff4d4d" - elif val <= 3: + elif val <= EXPIRY_SOON_DAYS: color = "#ffcc00" else: color = "#85e085" return f"background-color: {color}; color: black;" -if not data.empty: - styled_data = data.reset_index(drop=True).style.applymap( +if not pantry_data.empty: + # Sort by Days Left (ascending) so soon-to-expire items are first + display_data = pantry_data.sort_values(by="Days Left", ascending=True) + styled_data = display_data.reset_index(drop=True).style.applymap( color_days, subset=["Days Left"] ) st.dataframe(styled_data, use_container_width=True) else: st.info("Your pantry is empty. Add your first product above!") + # ---------- Manual Save ---------- if st.button("๐Ÿ”„ Save Changes"): - data.to_excel(USER_FILE, index=False) - st.success("Pantry data saved successfully!") + if not data.empty: + # Sort by Days Left (lowest first) before saving + data_to_save = data.sort_values(by="Days Left", ascending=True) + data_to_save.to_excel(USER_FILE, index=False) + st.success("Pantry data saved successfully (sorted by expiry)!") + else: + st.info("No items to save.") From 656f412e0d25bf8b3539bb0585bc0453a323bbb9 Mon Sep 17 00:00:00 2001 From: Omnia-Agabani Date: Thu, 20 Nov 2025 19:55:00 +0400 Subject: [PATCH 2/4] Fix CIs checks --- smart_pantry_manager/smart_pantry.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/smart_pantry_manager/smart_pantry.py b/smart_pantry_manager/smart_pantry.py index 0efe929..db5cfd3 100644 --- a/smart_pantry_manager/smart_pantry.py +++ b/smart_pantry_manager/smart_pantry.py @@ -171,10 +171,12 @@ def color_days(val: int) -> str: # ---------- Manual Save ---------- if st.button("๐Ÿ”„ Save Changes"): - if not data.empty: - # Sort by Days Left (lowest first) before saving - data_to_save = data.sort_values(by="Days Left", ascending=True) - data_to_save.to_excel(USER_FILE, index=False) + if "data" in globals() and not data.empty: + # Sort by Days Left ascending before saving + sorted_data = data.sort_values(by="Days Left", ascending=True) + # Ensure USER_FILE is defined + user_file = f"smart_pantry_manager/data/pantry_{username.replace(' ', '_').lower()}.xlsx" + sorted_data.to_excel(user_file, index=False) st.success("Pantry data saved successfully (sorted by expiry)!") else: st.info("No items to save.") From 4ba01f3ed6f6075a7b1912f553f6be5976378d3f Mon Sep 17 00:00:00 2001 From: Omnia-Agabani Date: Thu, 20 Nov 2025 20:24:03 +0400 Subject: [PATCH 3/4] Fix CIs checks --- smart_pantry_manager/smart_pantry.py | 122 ++++++++++++++------------- 1 file changed, 64 insertions(+), 58 deletions(-) diff --git a/smart_pantry_manager/smart_pantry.py b/smart_pantry_manager/smart_pantry.py index db5cfd3..69b4ecb 100644 --- a/smart_pantry_manager/smart_pantry.py +++ b/smart_pantry_manager/smart_pantry.py @@ -3,11 +3,13 @@ Smart Pantry Web App (Home Page Only) Features: -- Each user has a personal pantry (saved to Excel) -- Add, edit, and track products with expiry alerts -- Quantity + Unit input (supports numeric + count) -- Small loading animation when saving -- Optional intro demo video +- Personal pantry for each user, stored in Excel +- Add, edit, and track products with automatic expiry alerts +- Support for numeric quantities and units (e.g., g, kg, ml, count) +- Visual indicators for soon-to-expire and expired items +- Optional loading animation when saving +- Optional demo video to guide new users + Date: 29/10/2025 """ @@ -19,10 +21,6 @@ import pandas as pd import streamlit as st -# Constants -DATA_DIR = "smart_pantry_manager/data" -EXPIRY_SOON_DAYS = 3 - # ---------- Page Setup ---------- st.set_page_config(page_title="Smart Pantry Manager", page_icon="๐Ÿงบ", layout="centered") @@ -38,15 +36,18 @@ st.stop() st.session_state["username"] = username -os.makedirs(DATA_DIR, exist_ok=True) -user_file = os.path.join(DATA_DIR, f"pantry_{username.replace(' ', '_').lower()}.xlsx") +# Create user-specific file path +os.makedirs("smart_pantry_manager/data", exist_ok=True) +USER_FILE = ( + f"smart_pantry_manager/data/pantry_{username.replace(' ', '_').lower()}.xlsx" +) # ---------- Load Pantry ---------- @st.cache_data -def load_pantry(file_path: str) -> pd.DataFrame: - """Load pantry data for a user or create an empty table.""" +def load_pantry(file_path): + """Load pantry data for a specific user or create empty table.""" try: df = pd.read_excel(file_path) df["Expiry Date"] = pd.to_datetime(df["Expiry Date"], errors="coerce") @@ -55,22 +56,28 @@ def load_pantry(file_path: str) -> pd.DataFrame: df = df[df["Days Left"] >= 0].reset_index(drop=True) return df except FileNotFoundError: - columns = [ - "Product", - "Category", - "Quantity", - "Unit", - "Expiry Date", - "Days Left", - ] - return pd.DataFrame(columns=columns) + return pd.DataFrame( + columns=[ + "Product", + "Category", + "Quantity", + "Unit", + "Expiry Date", + "Days Left", + ] + ) + +data = load_pantry(USER_FILE) -pantry_data = load_pantry(user_file) +# Store in session_state +if "pantry_data" not in st.session_state: + st.session_state["pantry_data"] = data # ---------- Add New Product ---------- st.header("โž• Add a New Product") -product_name = st.text_input("Product name:") + +product = st.text_input("Product name:") category = st.selectbox( "Category:", [ @@ -94,46 +101,47 @@ def load_pantry(file_path: str) -> pd.DataFrame: ) quantity = st.number_input("Quantity (numeric or count):", min_value=0.0, step=0.1) unit = st.selectbox("Unit:", ["count", "g", "kg", "ml", "L", "cup", "tbsp", "tsp"]) -expiry_date = st.date_input("Expiry date:") +expiry = st.date_input("Expiry date:") if st.button("๐Ÿ’พ Save product"): - if product_name: + if product: with st.spinner("๐Ÿ’พ Saving product... please wait..."): - time.sleep(1) + time.sleep(2) today = datetime.now().date() - days_left = (expiry_date - today).days + days_left = (expiry - today).days new_row = { - "Product": product_name, + "Product": product, "Category": category, "Quantity": quantity, "Unit": unit, - "Expiry Date": expiry_date, + "Expiry Date": expiry, "Days Left": days_left, } - pantry_data = pd.concat( - [pantry_data, pd.DataFrame([new_row])], ignore_index=True - ) - pantry_data.to_excel(user_file, index=False) + df = st.session_state["pantry_data"] + df = pd.concat([df, pd.DataFrame([new_row])], ignore_index=True) + # Sort by Days Left + df = df.sort_values(by="Days Left", ascending=True).reset_index(drop=True) + df.to_excel(USER_FILE, index=False) + st.session_state["pantry_data"] = df st.cache_data.clear() - st.success(f"โœ… {product_name} added successfully!") + st.success(f"โœ… {product} added successfully!") else: st.warning("Please enter a product name.") # ---------- Update Days Left ---------- -if not pantry_data.empty: - pantry_data["Expiry Date"] = pd.to_datetime( - pantry_data["Expiry Date"], errors="coerce" - ) +if not st.session_state["pantry_data"].empty: + df = st.session_state["pantry_data"] + df["Expiry Date"] = pd.to_datetime(df["Expiry Date"], errors="coerce") today = pd.Timestamp(datetime.now().date()) - pantry_data["Days Left"] = (pantry_data["Expiry Date"] - today).dt.days + df["Days Left"] = (df["Expiry Date"] - today).dt.days + st.session_state["pantry_data"] = df # ---------- Alerts ---------- st.header("โš ๏ธ Expiry Alerts") -if not pantry_data.empty: - expired = pantry_data[pantry_data["Days Left"] <= 0] - expiring_soon = pantry_data[ - (pantry_data["Days Left"] > 0) & (pantry_data["Days Left"] <= EXPIRY_SOON_DAYS) - ] +df = st.session_state["pantry_data"] +if not df.empty: + expired = df[df["Days Left"] < 0] + expiring_soon = df[(df["Days Left"] >= 0) & (df["Days Left"] <= 3)] if not expired.empty: st.error("โŒ Some products have expired:") @@ -147,20 +155,20 @@ def load_pantry(file_path: str) -> pd.DataFrame: st.header("๐Ÿ“ฆ Your Pantry Items") -def color_days(val: int) -> str: - """Return background color based on days left.""" +def color_days(val): + """Color based on days left.""" if val < 0: color = "#ff4d4d" - elif val <= EXPIRY_SOON_DAYS: + elif val <= 3: color = "#ffcc00" else: color = "#85e085" return f"background-color: {color}; color: black;" -if not pantry_data.empty: - # Sort by Days Left (ascending) so soon-to-expire items are first - display_data = pantry_data.sort_values(by="Days Left", ascending=True) +if not df.empty: + # Sort by Days Left ascending for display + display_data = df.sort_values(by="Days Left", ascending=True) styled_data = display_data.reset_index(drop=True).style.applymap( color_days, subset=["Days Left"] ) @@ -168,15 +176,13 @@ def color_days(val: int) -> str: else: st.info("Your pantry is empty. Add your first product above!") - # ---------- Manual Save ---------- if st.button("๐Ÿ”„ Save Changes"): - if "data" in globals() and not data.empty: - # Sort by Days Left ascending before saving - sorted_data = data.sort_values(by="Days Left", ascending=True) - # Ensure USER_FILE is defined - user_file = f"smart_pantry_manager/data/pantry_{username.replace(' ', '_').lower()}.xlsx" - sorted_data.to_excel(user_file, index=False) + df = st.session_state.get("pantry_data") + if df is not None and not df.empty: + df_sorted = df.sort_values(by="Days Left", ascending=True) + df_sorted.to_excel(USER_FILE, index=False) + st.session_state["pantry_data"] = df_sorted st.success("Pantry data saved successfully (sorted by expiry)!") else: st.info("No items to save.") From 67071aa9227c4ab0945d06d7b98681b1b1fee1af Mon Sep 17 00:00:00 2001 From: AzzaOmer1 <103028535+AzzaOmer1@users.noreply.github.com> Date: Sat, 29 Nov 2025 13:52:16 +0300 Subject: [PATCH 4/4] fixed order of pages, expired items issue and search bar of recipes --- ...d_recipes.py => 1- recommended_recipes.py} | 0 .../{all_recipes.py => 2- all_recipes.py} | 42 +++- smart_pantry_manager/smart_pantry.py | 181 +++++++++++++----- 3 files changed, 171 insertions(+), 52 deletions(-) rename smart_pantry_manager/pages/{recommended_recipes.py => 1- recommended_recipes.py} (100%) rename smart_pantry_manager/pages/{all_recipes.py => 2- all_recipes.py} (75%) diff --git a/smart_pantry_manager/pages/recommended_recipes.py b/smart_pantry_manager/pages/1- recommended_recipes.py similarity index 100% rename from smart_pantry_manager/pages/recommended_recipes.py rename to smart_pantry_manager/pages/1- recommended_recipes.py diff --git a/smart_pantry_manager/pages/all_recipes.py b/smart_pantry_manager/pages/2- all_recipes.py similarity index 75% rename from smart_pantry_manager/pages/all_recipes.py rename to smart_pantry_manager/pages/2- all_recipes.py index cc397eb..dd17770 100644 --- a/smart_pantry_manager/pages/all_recipes.py +++ b/smart_pantry_manager/pages/2- all_recipes.py @@ -7,7 +7,7 @@ - Display recipe list with ingredients and instructions - Supports search/filtering -Date: 2025-11-20 +Date: 2025-11-27 """ import ast @@ -99,17 +99,49 @@ def format_ingredients(ingredients_str: str) -> list: recipes_df = load_recipes() -# ---------- Display Recipes ---------- -if recipes_df.empty: - st.info("No recipes found.") +# ---------- Search with suggestions ---------- +# text input for free keywords +search_term = st.text_input("๐Ÿ” Search for a recipe:") + +selected_recipe = None + +# build suggestion list when user types something +if search_term: + suggestion_list = ( + recipes_df[recipes_df["Recipe"].str.contains(search_term, case=False, na=False)] + .sort_values("Recipe")["Recipe"] + .tolist() + ) + # Show suggestions as clickable buttons instead of selectbox + for recipe in suggestion_list: + if st.button(f"๐Ÿ“„ {recipe}"): + selected_recipe = recipe + break # Stop after clicking one + +# Filter results +if selected_recipe: + filtered_df = recipes_df[recipes_df["Recipe"] == selected_recipe] +elif search_term: + filtered_df = recipes_df[ + recipes_df["Recipe"].str.contains(search_term, case=False, na=False) + ] +else: + filtered_df = recipes_df + +# show only the selected recipe OR search results +if selected_recipe and selected_recipe != "": + filtered_df = recipes_df[recipes_df["Recipe"] == selected_recipe] else: - search_term = st.text_input("๐Ÿ” Search for a recipe:") filtered_df = ( recipes_df[recipes_df["Recipe"].str.contains(search_term, case=False, na=False)] if search_term else recipes_df ) +# ---------- Display Recipes ---------- +if filtered_df.empty: + st.info("No recipes found.") +else: for _, row in filtered_df.iterrows(): with st.expander(f"๐Ÿ“– {row['Recipe']}"): st.markdown("**๐Ÿง‚ Ingredients:**") diff --git a/smart_pantry_manager/smart_pantry.py b/smart_pantry_manager/smart_pantry.py index 69b4ecb..b92729f 100644 --- a/smart_pantry_manager/smart_pantry.py +++ b/smart_pantry_manager/smart_pantry.py @@ -10,8 +10,7 @@ - Optional loading animation when saving - Optional demo video to guide new users - -Date: 29/10/2025 +Date: 27/11/2025 """ import os @@ -21,6 +20,26 @@ import pandas as pd import streamlit as st + +# ----- Styling Functions ----- +def style_expired(expired_products_df): + return expired_products_df.style.set_properties( + **{ + "background-color": "#EB4343", # Light Red + "color": "black", + } + ) + + +def style_expiring_soon(expiring_products_df): + return expiring_products_df.style.set_properties( + **{ + "background-color": "#F5DB76", # Light Yellow + "color": "black", + } + ) + + # ---------- Page Setup ---------- st.set_page_config(page_title="Smart Pantry Manager", page_icon="๐Ÿงบ", layout="centered") @@ -52,9 +71,11 @@ def load_pantry(file_path): df = pd.read_excel(file_path) df["Expiry Date"] = pd.to_datetime(df["Expiry Date"], errors="coerce") df["Days Left"] = (df["Expiry Date"] - datetime.now()).dt.days + # Remove expired items automatically - df = df[df["Days Left"] >= 0].reset_index(drop=True) + # df = df[df["Days Left"] >= 0].reset_index(drop=True) return df + except FileNotFoundError: return pd.DataFrame( columns=[ @@ -68,17 +89,17 @@ def load_pantry(file_path): ) -data = load_pantry(USER_FILE) +pantry_df = load_pantry(USER_FILE) # Store in session_state if "pantry_data" not in st.session_state: - st.session_state["pantry_data"] = data + st.session_state["pantry_data"] = pantry_df # ---------- Add New Product ---------- st.header("โž• Add a New Product") -product = st.text_input("Product name:") -category = st.selectbox( +product_name = st.text_input("Product name:") +product_category = st.selectbox( "Category:", [ "Uncategorized", @@ -96,60 +117,128 @@ def load_pantry(file_path): "Frozen Foods", "Canned Goods", "Spices & Seasonings", + "Milks & Alternatives", "Drinks", ], ) -quantity = st.number_input("Quantity (numeric or count):", min_value=0.0, step=0.1) -unit = st.selectbox("Unit:", ["count", "g", "kg", "ml", "L", "cup", "tbsp", "tsp"]) -expiry = st.date_input("Expiry date:") +product_quantity = st.number_input( + "Quantity (numeric or count):", min_value=0.0, step=0.1 +) +product_unit = st.selectbox( + "Unit:", ["count", "g", "kg", "ml", "L", "cup", "tbsp", "tsp"] +) +expiry_date = st.date_input("Expiry date:") if st.button("๐Ÿ’พ Save product"): - if product: + if product_name: with st.spinner("๐Ÿ’พ Saving product... please wait..."): time.sleep(2) today = datetime.now().date() - days_left = (expiry - today).days - new_row = { - "Product": product, - "Category": category, - "Quantity": quantity, - "Unit": unit, - "Expiry Date": expiry, + days_left = (expiry_date - today).days + new_product = { + "Product": product_name, + "Category": product_category, + "Quantity": product_quantity, + "Unit": product_unit, + "Expiry Date": expiry_date, "Days Left": days_left, } - df = st.session_state["pantry_data"] - df = pd.concat([df, pd.DataFrame([new_row])], ignore_index=True) + pantry_df = st.session_state["pantry_data"] + pantry_df = pd.concat( + [pantry_df, pd.DataFrame([new_product])], ignore_index=True + ) + # Sort by Days Left - df = df.sort_values(by="Days Left", ascending=True).reset_index(drop=True) - df.to_excel(USER_FILE, index=False) - st.session_state["pantry_data"] = df + pantry_df = pantry_df.sort_values( + by="Days Left", ascending=True + ).reset_index(drop=True) + pantry_df.to_excel(USER_FILE, index=False) + st.session_state["pantry_data"] = pantry_df st.cache_data.clear() - st.success(f"โœ… {product} added successfully!") + st.success(f"โœ… {product_name} added successfully!") else: st.warning("Please enter a product name.") # ---------- Update Days Left ---------- if not st.session_state["pantry_data"].empty: - df = st.session_state["pantry_data"] - df["Expiry Date"] = pd.to_datetime(df["Expiry Date"], errors="coerce") + pantry_df = st.session_state["pantry_data"] + pantry_df["Expiry Date"] = pd.to_datetime(pantry_df["Expiry Date"], errors="coerce") today = pd.Timestamp(datetime.now().date()) - df["Days Left"] = (df["Expiry Date"] - today).dt.days - st.session_state["pantry_data"] = df + pantry_df["Days Left"] = (pantry_df["Expiry Date"] - today).dt.days + st.session_state["pantry_data"] = pantry_df # ---------- Alerts ---------- -st.header("โš ๏ธ Expiry Alerts") -df = st.session_state["pantry_data"] -if not df.empty: - expired = df[df["Days Left"] < 0] - expiring_soon = df[(df["Days Left"] >= 0) & (df["Days Left"] <= 3)] +st.markdown("### โš ๏ธ Expiry Alerts") + +current_df = st.session_state["pantry_data"] # renamed + +if not current_df.empty: + expired_items = current_df[current_df["Days Left"] < 0].copy() # renamed + expired_items = expired_items.sort_values(by="Days Left") + expiring_items = current_df[ + (current_df["Days Left"] >= 0) & (current_df["Days Left"] <= 3) + ].copy() # renamed + + # ---------- Expired Products ---------- + if not expired_items.empty: + with st.expander("โŒ Expired Products (Click to view)", expanded=True): + st.dataframe( + style_expired(expired_items[["Product", "Expiry Date", "Days Left"]]), + use_container_width=True, + ) + col1, col2 = st.columns([2, 1]) + if col2.button("๐Ÿ—‘๏ธ Delete ALL expired"): + current_df = current_df[current_df["Days Left"] >= 0] + st.session_state["pantry_data"] = current_df + current_df.to_excel(USER_FILE, index=False) + + st.success("All expired products deleted!") + + # ---------- Expiring Soon ---------- + if not expiring_items.empty: + with st.expander("โฐ Expiring Soon (Within 3 days)", expanded=True): + st.dataframe( + style_expiring_soon( + expiring_items[["Product", "Expiry Date", "Days Left"]] + ), + use_container_width=True, + ) + + # ---------- Manage Section ---------- + st.markdown("---") + st.markdown("### ๐Ÿ›  Manage Products") + + colA, colB = st.columns([2, 3]) + selected_item = colA.selectbox("Choose a product:", current_df["Product"].unique()) + + if selected_item: + item_row = current_df[current_df["Product"] == selected_item].iloc[0] + colB.markdown(f"**Current:** {item_row['Quantity']} {item_row['Unit']}") + + new_quantity = colB.number_input( + "New quantity:", + min_value=0.0, + max_value=float(item_row["Quantity"]), + value=float(item_row["Quantity"]), + step=0.1, + ) + + col1, col2 = st.columns([1, 1]) + if col1.button("โœ”๏ธ Update Quantity"): + current_df.loc[current_df["Product"] == selected_item, "Quantity"] = ( + new_quantity + ) + st.session_state["pantry_data"] = current_df + current_df.to_excel(USER_FILE, index=False) + st.success("Quantity updated!") - if not expired.empty: - st.error("โŒ Some products have expired:") - st.table(expired[["Product", "Expiry Date", "Days Left"]]) + if col2.button("๐Ÿ—‘๏ธ Delete Product"): + current_df = current_df[current_df["Product"] != selected_item] + st.session_state["pantry_data"] = current_df + current_df.to_excel(USER_FILE, index=False) + st.success(f"{selected_item} deleted!") - if not expiring_soon.empty: - st.warning("โฐ Some products are expiring soon:") - st.table(expiring_soon[["Product", "Expiry Date", "Days Left"]]) +st.markdown("---") # ---------- Pantry Table ---------- st.header("๐Ÿ“ฆ Your Pantry Items") @@ -166,9 +255,8 @@ def color_days(val): return f"background-color: {color}; color: black;" -if not df.empty: - # Sort by Days Left ascending for display - display_data = df.sort_values(by="Days Left", ascending=True) +if not pantry_df.empty: + display_data = pantry_df.sort_values(by="Days Left", ascending=True) styled_data = display_data.reset_index(drop=True).style.applymap( color_days, subset=["Days Left"] ) @@ -178,11 +266,10 @@ def color_days(val): # ---------- Manual Save ---------- if st.button("๐Ÿ”„ Save Changes"): - df = st.session_state.get("pantry_data") - if df is not None and not df.empty: - df_sorted = df.sort_values(by="Days Left", ascending=True) - df_sorted.to_excel(USER_FILE, index=False) - st.session_state["pantry_data"] = df_sorted + if not pantry_df.empty: + pantry_df_sorted = pantry_df.sort_values(by="Days Left", ascending=True) + pantry_df_sorted.to_excel(USER_FILE, index=False) + st.session_state["pantry_data"] = pantry_df_sorted st.success("Pantry data saved successfully (sorted by expiry)!") else: st.info("No items to save.")