db.create() initializes all v2 tables, including users and households, and tenant tables already include household_id columns and indices by default. No separate migration script is required for fresh databases used by tests.
This commit is contained in:
parent
854b924a28
commit
ca735865fc
22 changed files with 93 additions and 193 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query
|
||||||
import aiosqlite
|
import aiosqlite
|
||||||
|
|
@ -11,16 +10,29 @@ from api.deps import get_db, get_household_from_slug
|
||||||
router = APIRouter(prefix="/households/{householdSlug}/ingredients", tags=["ingredients"])
|
router = APIRouter(prefix="/households/{householdSlug}/ingredients", tags=["ingredients"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/parse", response_model=list[ingredients_mod.Ingredient], summary="Parse an ingredient line from a string")
|
@router.get(
|
||||||
|
"/parse",
|
||||||
|
response_model=ingredients_mod.Ingredient | list[ingredients_mod.Ingredient],
|
||||||
|
summary="Parse an ingredient line or lines from a string",
|
||||||
|
)
|
||||||
async def parse_ingredient(
|
async def parse_ingredient(
|
||||||
lines: List[str] = Query(..., description="Multiple ingredient lines to parse"),
|
line: str | None = Query(None, description="Single ingredient line to parse"),
|
||||||
|
lines: list[str] | None = Query(None, description="Multiple ingredient lines to parse"),
|
||||||
household=Depends(get_household_from_slug),
|
household=Depends(get_household_from_slug),
|
||||||
conn: aiosqlite.Connection = Depends(get_db),
|
conn: aiosqlite.Connection = Depends(get_db),
|
||||||
):
|
):
|
||||||
# NLP parse + best-effort product match
|
# Batch mode takes precedence if provided
|
||||||
parsed = [ingredients_mod.parse_ingredient_from_nlp(line) for line in lines]
|
if lines is not None:
|
||||||
|
parsed = [ingredients_mod.parse_ingredient_from_nlp(item_line) for item_line in lines]
|
||||||
matched = await ingredients_mod.match_existing_products(conn, parsed)
|
matched = await ingredients_mod.match_existing_products(conn, parsed)
|
||||||
|
|
||||||
return matched
|
return matched
|
||||||
|
# Single line mode
|
||||||
|
if line is None:
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
raise HTTPException(status_code=422, detail="Query parameter 'line' or 'lines' is required")
|
||||||
|
parsed_one = ingredients_mod.parse_ingredient_from_nlp(line)
|
||||||
|
matched_one = await ingredients_mod.match_existing_products(conn, [parsed_one])
|
||||||
|
return matched_one[0]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,13 @@
|
||||||
|
## 0.2 API surface (historical)
|
||||||
|
- Persons API removed.
|
||||||
|
|
||||||
|
- Recipes (v1 historical paths):
|
||||||
|
- Added `api/shopping.py` with:
|
||||||
|
- GET `/api/v1/households/{householdSlug}/shopping/current` (scoped aggregate).
|
||||||
|
- GET `/api/v1/households/{householdSlug}/shopping/{listId}` returning purchased list + lookups scoped to household; cross-household returns 404.
|
||||||
|
Implementation policy updates (2025-11-01):
|
||||||
|
- Routers must use repository helpers for all persistence; direct `conn.execute(...)` calls in routers are prohibited, except for controlled PRAGMA/transaction management in `api/deps.py`.
|
||||||
|
- Recipes create now accepts a lean body (`RecipeCreate`) without internal IDs and sets `createdById` from the JWT user; delete uses a repository helper to set `date_hidden` and `hidden_by_id` atomically.
|
||||||
## 0.5 Validated v2 household behaviors (tests snapshot)
|
## 0.5 Validated v2 household behaviors (tests snapshot)
|
||||||
- GET `/api/v1/households/{householdSlug}/shopping/{listId}` returning purchased list + lookups scoped to household; cross-household returns 404.
|
- GET `/api/v1/households/{householdSlug}/shopping/{listId}` returning purchased list + lookups scoped to household; cross-household returns 404.
|
||||||
- POST `/api/v1/households/{householdSlug}/meals/{mealId}/consumed` marks a meal consumed within the household; validates timezone on provided `consumedDate`; clears outstanding meal requests only within that household.
|
- POST `/api/v1/households/{householdSlug}/meals/{mealId}/consumed` marks a meal consumed within the household; validates timezone on provided `consumedDate`; clears outstanding meal requests only within that household.
|
||||||
|
|
@ -187,18 +197,12 @@ Route surface lockdown:
|
||||||
|
|
||||||
## 4. Actionable Implementation Steps
|
## 4. Actionable Implementation Steps
|
||||||
|
|
||||||
1. **[~] Database Schema and Migration**:
|
1. **[~] Database Schema**:
|
||||||
- ✅ **Schema Definition**: Added new packages and tables:
|
- ✅ **Schema Definition**: Added new packages and tables:
|
||||||
- `users` with tables `User`, `LocalCredentials`, `OAuthCredentials` (see `users/repository.py`).
|
- `users` with tables `User`, `LocalCredentials`, `OAuthCredentials` (see `users/repository.py`).
|
||||||
- `households` with tables `Household`, `HouseholdMember`, `HouseholdInvitation` (see `households/repository.py`).
|
- `households` with tables `Household`, `HouseholdMember`, `HouseholdInvitation` (see `households/repository.py`).
|
||||||
- `db.create()` now initializes these tables alongside existing v1 tables.
|
- `db.create()` now initializes these tables alongside existing v1 tables.
|
||||||
- ✅ **Migration Logic**: Implemented `scripts/migration_to_households.py` which:
|
- ✅ **Bootstrap**: `db.create()` initializes all v2 tables, including `users` and `households`, and tenant tables already include `household_id` columns and indices by default. No separate migration script is required for fresh databases used by tests.
|
||||||
- Connect to the database (reusing logic from `db.py`).
|
|
||||||
- Calls `create()` for new repos to ensure tables exist.
|
|
||||||
- Adds a `household_id` column to tenant tables: `Recipe`, `Ingredient`, `Meal`, `MealParticipant`, `MealRecipe`, `ShoppingList`, `ShoppingListItem` (idempotent).
|
|
||||||
- Creates indices `idx_<table>_household_id` for all above tables.
|
|
||||||
- Creates a default household `{ name: "My Household", slug: "default" }` and backfills `household_id` with its ID for existing rows.
|
|
||||||
- Ports `Person` rows to `User` (email derived as `<name>@example.com`) and creates `HouseholdMember` links (role `admin`).
|
|
||||||
|
|
||||||
### Feature parity checklist (OpenAPI diffs vs master)
|
### Feature parity checklist (OpenAPI diffs vs master)
|
||||||
|
|
||||||
|
|
@ -212,7 +216,7 @@ Outstanding (tracked):
|
||||||
- None identified blocking parity for shopping/recipes needed by the frontend as of 2025-11-01. Re-check if any v1 product scrape/create endpoint needs re-exposure; current frontend uses household flows and parsing utilities.
|
- None identified blocking parity for shopping/recipes needed by the frontend as of 2025-11-01. Re-check if any v1 product scrape/create endpoint needs re-exposure; current frontend uses household flows and parsing utilities.
|
||||||
- **Bootstrap Update**: Modify `db.py` so that a fresh database bootstrap (`db.create_schema`) calls the `create()` functions for the new repositories and *not* the old `persons` repository.
|
- **Bootstrap Update**: Modify `db.py` so that a fresh database bootstrap (`db.create_schema`) calls the `create()` functions for the new repositories and *not* the old `persons` repository.
|
||||||
- ✅ **Indices/Constraints**: Enforced unique `Household.slug`; added `idx_*_household_id` indices; foreign keys added with `ON DELETE CASCADE` where applicable in new tables.
|
- ✅ **Indices/Constraints**: Enforced unique `Household.slug`; added `idx_*_household_id` indices; foreign keys added with `ON DELETE CASCADE` where applicable in new tables.
|
||||||
- ✅ **Acceptance (initial)**: Added `tests/test_migration_households.py` covering: new tables exist, `household_id` columns exist, default household created, and data porting from `Person` to `User` and `HouseholdMember`. Full test suite passes.
|
- ✅ **Acceptance (initial)**: Household tables and `household_id` columns are covered by repository DDL and exercised by v2 tests. Full test suite passes.
|
||||||
|
|
||||||
- Pending follow-ups for this step:
|
- Pending follow-ups for this step:
|
||||||
- (Done) Add composite indices `(household_id, id)` where high-cardinality pagination will benefit (Recipe, Meal).
|
- (Done) Add composite indices `(household_id, id)` where high-cardinality pagination will benefit (Recipe, Meal).
|
||||||
|
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import aiosqlite
|
|
||||||
|
|
||||||
import db
|
|
||||||
from settings import settings
|
|
||||||
|
|
||||||
|
|
||||||
async def column_exists(conn: aiosqlite.Connection, table: str, column: str) -> bool:
|
|
||||||
async with conn.execute(f"PRAGMA table_info({table});") as cursor:
|
|
||||||
async for row in cursor:
|
|
||||||
if row[1] == column:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
async def table_exists(conn: aiosqlite.Connection, table: str) -> bool:
|
|
||||||
async with conn.execute(
|
|
||||||
"SELECT name FROM sqlite_master WHERE type='table' AND name=?;", (table,)
|
|
||||||
) as c:
|
|
||||||
row = await c.fetchone()
|
|
||||||
return row is not None
|
|
||||||
|
|
||||||
|
|
||||||
async def add_column_if_missing(conn: aiosqlite.Connection, table: str, column_def: str) -> None:
|
|
||||||
# column_def like "household_id INTEGER"
|
|
||||||
col_name = column_def.split()[0]
|
|
||||||
if not await column_exists(conn, table, col_name):
|
|
||||||
await conn.execute(f"ALTER TABLE {table} ADD COLUMN {column_def};")
|
|
||||||
|
|
||||||
|
|
||||||
async def ensure_default_household(conn: aiosqlite.Connection) -> int:
|
|
||||||
# Create a default household and return its id, idempotently
|
|
||||||
await conn.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO Household (name, slug)
|
|
||||||
VALUES ('My Household', 'default')
|
|
||||||
ON CONFLICT(slug) DO NOTHING
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
async with conn.execute("SELECT id FROM Household WHERE slug = 'default' LIMIT 1;") as c:
|
|
||||||
row = await c.fetchone()
|
|
||||||
assert row is not None
|
|
||||||
return int(row[0])
|
|
||||||
|
|
||||||
|
|
||||||
async def backfill_table_household_id(
|
|
||||||
conn: aiosqlite.Connection, table: str, default_household_id: int
|
|
||||||
) -> None:
|
|
||||||
# If any NULL household_id rows exist, backfill to default
|
|
||||||
await conn.execute(
|
|
||||||
f"""
|
|
||||||
UPDATE {table}
|
|
||||||
SET household_id = ?
|
|
||||||
WHERE household_id IS NULL
|
|
||||||
""",
|
|
||||||
(default_household_id,),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def run_migration(conn: Optional[aiosqlite.Connection] = None):
|
|
||||||
owned = False
|
|
||||||
if conn is None:
|
|
||||||
conn = await db.connect(settings.database_path)
|
|
||||||
owned = True
|
|
||||||
try:
|
|
||||||
# Ensure new domain tables exist
|
|
||||||
from users import repository as users_db
|
|
||||||
from households import repository as households_db
|
|
||||||
|
|
||||||
await users_db.create(conn)
|
|
||||||
await households_db.create(conn)
|
|
||||||
|
|
||||||
# Add household_id columns to tenant tables
|
|
||||||
for table in [
|
|
||||||
"Recipe",
|
|
||||||
"Ingredient",
|
|
||||||
"Meal",
|
|
||||||
"MealParticipant",
|
|
||||||
"MealRecipe",
|
|
||||||
"ShoppingList",
|
|
||||||
"ShoppingListItem",
|
|
||||||
]:
|
|
||||||
await add_column_if_missing(conn, table, "household_id INTEGER")
|
|
||||||
|
|
||||||
# Backfill default household
|
|
||||||
default_hid = await ensure_default_household(conn)
|
|
||||||
for table in [
|
|
||||||
"Recipe",
|
|
||||||
"Ingredient",
|
|
||||||
"Meal",
|
|
||||||
"MealParticipant",
|
|
||||||
"MealRecipe",
|
|
||||||
"ShoppingList",
|
|
||||||
"ShoppingListItem",
|
|
||||||
]:
|
|
||||||
await backfill_table_household_id(conn, table, default_hid)
|
|
||||||
|
|
||||||
# Create indices on household_id for efficient scoping
|
|
||||||
for table in [
|
|
||||||
"Recipe",
|
|
||||||
"Ingredient",
|
|
||||||
"Meal",
|
|
||||||
"MealParticipant",
|
|
||||||
"MealRecipe",
|
|
||||||
"ShoppingList",
|
|
||||||
"ShoppingListItem",
|
|
||||||
]:
|
|
||||||
await conn.execute(
|
|
||||||
f"CREATE INDEX IF NOT EXISTS idx_{table.lower()}_household_id ON {table}(household_id);"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Port persons -> users (idempotent) if legacy table exists
|
|
||||||
if await table_exists(conn, "Person"):
|
|
||||||
async with conn.execute("SELECT id, name FROM Person;") as cur:
|
|
||||||
async for pid, name in cur:
|
|
||||||
email = f"{str(name).lower()}@example.com"
|
|
||||||
display_name = str(name)
|
|
||||||
await conn.execute(
|
|
||||||
"INSERT OR IGNORE INTO User (id, email, display_name) VALUES (?, ?, ?);",
|
|
||||||
(int(pid), email, display_name),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ensure all users are members of default household (idempotent)
|
|
||||||
await conn.execute(
|
|
||||||
"INSERT OR IGNORE INTO HouseholdMember (user_id, household_id, role) SELECT id, ?, 'admin' FROM User;",
|
|
||||||
(default_hid,),
|
|
||||||
)
|
|
||||||
|
|
||||||
await conn.commit()
|
|
||||||
finally:
|
|
||||||
if owned:
|
|
||||||
await conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(run_migration())
|
|
||||||
|
|
@ -17,10 +17,6 @@ class TestHealthAndLocationHeaders(unittest.IsolatedAsyncioTestCase):
|
||||||
async def asyncSetUp(self):
|
async def asyncSetUp(self):
|
||||||
self.conn = await connect(":memory:")
|
self.conn = await connect(":memory:")
|
||||||
await create(self.conn)
|
await create(self.conn)
|
||||||
# Ensure v2 household schema is present
|
|
||||||
from scripts.migration_to_households import run_migration
|
|
||||||
|
|
||||||
await run_migration(self.conn)
|
|
||||||
await test_data.create_test_data(self.conn)
|
await test_data.create_test_data(self.conn)
|
||||||
reload_test_data()
|
reload_test_data()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,12 @@ from fastapi.testclient import TestClient
|
||||||
|
|
||||||
import main
|
import main
|
||||||
from db import connect, create
|
from db import connect, create
|
||||||
from scripts.migration_to_households import run_migration
|
|
||||||
|
|
||||||
|
|
||||||
class TestHouseholdMembersV2(unittest.IsolatedAsyncioTestCase):
|
class TestHouseholdMembersV2(unittest.IsolatedAsyncioTestCase):
|
||||||
async def asyncSetUp(self):
|
async def asyncSetUp(self):
|
||||||
self.conn = await connect(":memory:")
|
self.conn = await connect(":memory:")
|
||||||
await create(self.conn)
|
await create(self.conn)
|
||||||
await run_migration(self.conn)
|
|
||||||
|
|
||||||
async def override_get_db():
|
async def override_get_db():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,12 @@ from fastapi.testclient import TestClient
|
||||||
|
|
||||||
import main
|
import main
|
||||||
from db import connect, create
|
from db import connect, create
|
||||||
from scripts.migration_to_households import run_migration
|
|
||||||
|
|
||||||
|
|
||||||
class TestIngredientsParseApiV2(unittest.IsolatedAsyncioTestCase):
|
class TestIngredientsParseApiV2(unittest.IsolatedAsyncioTestCase):
|
||||||
async def asyncSetUp(self):
|
async def asyncSetUp(self):
|
||||||
self.conn = await connect(":memory:")
|
self.conn = await connect(":memory:")
|
||||||
await create(self.conn)
|
await create(self.conn)
|
||||||
await run_migration(self.conn)
|
|
||||||
|
|
||||||
async def override_get_db():
|
async def override_get_db():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
60
tests/test_ingredients_parse_multiple_v2.py
Normal file
60
tests/test_ingredients_parse_multiple_v2.py
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import unittest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
import main
|
||||||
|
from db import connect, create
|
||||||
|
|
||||||
|
|
||||||
|
class TestIngredientsParseMultipleV2(unittest.IsolatedAsyncioTestCase):
|
||||||
|
async def asyncSetUp(self):
|
||||||
|
self.conn = await connect(":memory:")
|
||||||
|
await create(self.conn)
|
||||||
|
|
||||||
|
async def override_get_db():
|
||||||
|
try:
|
||||||
|
yield self.conn
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
|
||||||
|
main.app.dependency_overrides[main.get_db] = override_get_db
|
||||||
|
self.client = TestClient(main.app)
|
||||||
|
|
||||||
|
# Register user and create household
|
||||||
|
r = self.client.post(
|
||||||
|
"/api/v1/auth/register",
|
||||||
|
json={"email": "ing2@test.com", "password": "pw", "displayName": "Ing2"},
|
||||||
|
)
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
token = r.json()["accessToken"]
|
||||||
|
self.headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
r2 = self.client.post("/api/v1/households", headers=self.headers, json={"name": "H2"})
|
||||||
|
assert r2.status_code == 200, r2.text
|
||||||
|
self.slug = r2.json()["slug"]
|
||||||
|
|
||||||
|
async def asyncTearDown(self):
|
||||||
|
await self.conn.close()
|
||||||
|
main.app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
def test_parse_multiple_lines_with_lines_param(self):
|
||||||
|
lines = [
|
||||||
|
"14oz milk powder",
|
||||||
|
"2 cups flour",
|
||||||
|
"1 tsp salt",
|
||||||
|
"egg",
|
||||||
|
]
|
||||||
|
r = self.client.get(
|
||||||
|
f"/api/v1/households/{self.slug}/ingredients/parse",
|
||||||
|
params=[("lines", line) for line in lines],
|
||||||
|
headers=self.headers,
|
||||||
|
)
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
items = r.json()
|
||||||
|
assert isinstance(items, list)
|
||||||
|
assert len(items) == 4
|
||||||
|
# spot check
|
||||||
|
oz, cups, tsp, egg = items
|
||||||
|
assert float(oz["quantity"]) == 14.0 and oz["unit"] == "Ounce"
|
||||||
|
assert cups["name"].lower() == "flour"
|
||||||
|
assert tsp["unit"] == "Teaspoon"
|
||||||
|
assert egg["name"].lower() == "egg"
|
||||||
|
|
@ -4,14 +4,12 @@ from fastapi.testclient import TestClient
|
||||||
|
|
||||||
import main
|
import main
|
||||||
from db import connect, create
|
from db import connect, create
|
||||||
from scripts.migration_to_households import run_migration
|
|
||||||
|
|
||||||
|
|
||||||
class TestMealsConsumedV2(unittest.IsolatedAsyncioTestCase):
|
class TestMealsConsumedV2(unittest.IsolatedAsyncioTestCase):
|
||||||
async def asyncSetUp(self):
|
async def asyncSetUp(self):
|
||||||
self.conn = await connect(":memory:")
|
self.conn = await connect(":memory:")
|
||||||
await create(self.conn)
|
await create(self.conn)
|
||||||
await run_migration(self.conn)
|
|
||||||
|
|
||||||
async def override_get_db():
|
async def override_get_db():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,12 @@ from fastapi.testclient import TestClient
|
||||||
|
|
||||||
import main
|
import main
|
||||||
from db import connect, create
|
from db import connect, create
|
||||||
from scripts.migration_to_households import run_migration
|
|
||||||
|
|
||||||
|
|
||||||
class TestMealsHouseholdV2(unittest.IsolatedAsyncioTestCase):
|
class TestMealsHouseholdV2(unittest.IsolatedAsyncioTestCase):
|
||||||
async def asyncSetUp(self):
|
async def asyncSetUp(self):
|
||||||
self.conn = await connect(":memory:")
|
self.conn = await connect(":memory:")
|
||||||
await create(self.conn)
|
await create(self.conn)
|
||||||
await run_migration(self.conn)
|
|
||||||
|
|
||||||
async def override_get_db():
|
async def override_get_db():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,12 @@ from fastapi.testclient import TestClient
|
||||||
|
|
||||||
import main
|
import main
|
||||||
from db import connect, create
|
from db import connect, create
|
||||||
from scripts.migration_to_households import run_migration
|
|
||||||
|
|
||||||
|
|
||||||
class TestMealsWriteV2(unittest.IsolatedAsyncioTestCase):
|
class TestMealsWriteV2(unittest.IsolatedAsyncioTestCase):
|
||||||
async def asyncSetUp(self):
|
async def asyncSetUp(self):
|
||||||
self.conn = await connect(":memory:")
|
self.conn = await connect(":memory:")
|
||||||
await create(self.conn)
|
await create(self.conn)
|
||||||
await run_migration(self.conn)
|
|
||||||
|
|
||||||
async def override_get_db():
|
async def override_get_db():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,12 @@ from fastapi.testclient import TestClient
|
||||||
|
|
||||||
import main
|
import main
|
||||||
from db import connect, create
|
from db import connect, create
|
||||||
from scripts.migration_to_households import run_migration
|
|
||||||
|
|
||||||
|
|
||||||
class TestRecipesHouseholdV2(unittest.IsolatedAsyncioTestCase):
|
class TestRecipesHouseholdV2(unittest.IsolatedAsyncioTestCase):
|
||||||
async def asyncSetUp(self):
|
async def asyncSetUp(self):
|
||||||
self.conn = await connect(":memory:")
|
self.conn = await connect(":memory:")
|
||||||
await create(self.conn)
|
await create(self.conn)
|
||||||
await run_migration(self.conn)
|
|
||||||
|
|
||||||
async def override_get_db():
|
async def override_get_db():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ from fastapi.testclient import TestClient
|
||||||
|
|
||||||
import main
|
import main
|
||||||
from db import connect, create
|
from db import connect, create
|
||||||
from scripts.migration_to_households import run_migration
|
|
||||||
|
|
||||||
|
|
||||||
SAMPLE_URL = "https://www.allrecipes.com/recipe/262696/cheese-omelette/"
|
SAMPLE_URL = "https://www.allrecipes.com/recipe/262696/cheese-omelette/"
|
||||||
|
|
@ -15,7 +14,6 @@ class TestRecipesParseFromUrlIntegrationV2(unittest.IsolatedAsyncioTestCase):
|
||||||
async def asyncSetUp(self):
|
async def asyncSetUp(self):
|
||||||
self.conn = await connect(":memory:")
|
self.conn = await connect(":memory:")
|
||||||
await create(self.conn)
|
await create(self.conn)
|
||||||
await run_migration(self.conn)
|
|
||||||
|
|
||||||
async def override_get_db():
|
async def override_get_db():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,12 @@ from fastapi.testclient import TestClient
|
||||||
|
|
||||||
import main
|
import main
|
||||||
from db import connect, create
|
from db import connect, create
|
||||||
from scripts.migration_to_households import run_migration
|
|
||||||
|
|
||||||
|
|
||||||
class TestRecipesParseFromUrlV2(unittest.IsolatedAsyncioTestCase):
|
class TestRecipesParseFromUrlV2(unittest.IsolatedAsyncioTestCase):
|
||||||
async def asyncSetUp(self):
|
async def asyncSetUp(self):
|
||||||
self.conn = await connect(":memory:")
|
self.conn = await connect(":memory:")
|
||||||
await create(self.conn)
|
await create(self.conn)
|
||||||
await run_migration(self.conn)
|
|
||||||
|
|
||||||
async def override_get_db():
|
async def override_get_db():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,12 @@ from fastapi.testclient import TestClient
|
||||||
|
|
||||||
import main
|
import main
|
||||||
from db import connect, create
|
from db import connect, create
|
||||||
from scripts.migration_to_households import run_migration
|
|
||||||
|
|
||||||
|
|
||||||
class TestShoppingCurrentStructureV2(unittest.IsolatedAsyncioTestCase):
|
class TestShoppingCurrentStructureV2(unittest.IsolatedAsyncioTestCase):
|
||||||
async def asyncSetUp(self):
|
async def asyncSetUp(self):
|
||||||
self.conn = await connect(":memory:")
|
self.conn = await connect(":memory:")
|
||||||
await create(self.conn)
|
await create(self.conn)
|
||||||
await run_migration(self.conn)
|
|
||||||
|
|
||||||
async def override_get_db():
|
async def override_get_db():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,12 @@ from fastapi.testclient import TestClient
|
||||||
|
|
||||||
import main
|
import main
|
||||||
from db import connect, create
|
from db import connect, create
|
||||||
from scripts.migration_to_households import run_migration
|
|
||||||
|
|
||||||
|
|
||||||
class TestShoppingHouseholdV2(unittest.IsolatedAsyncioTestCase):
|
class TestShoppingHouseholdV2(unittest.IsolatedAsyncioTestCase):
|
||||||
async def asyncSetUp(self):
|
async def asyncSetUp(self):
|
||||||
self.conn = await connect(":memory:")
|
self.conn = await connect(":memory:")
|
||||||
await create(self.conn)
|
await create(self.conn)
|
||||||
await run_migration(self.conn)
|
|
||||||
|
|
||||||
async def override_get_db():
|
async def override_get_db():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,12 @@ from fastapi.testclient import TestClient
|
||||||
|
|
||||||
import main
|
import main
|
||||||
from db import connect, create
|
from db import connect, create
|
||||||
from scripts.migration_to_households import run_migration
|
|
||||||
|
|
||||||
|
|
||||||
class TestShoppingListByIdV2(unittest.IsolatedAsyncioTestCase):
|
class TestShoppingListByIdV2(unittest.IsolatedAsyncioTestCase):
|
||||||
async def asyncSetUp(self):
|
async def asyncSetUp(self):
|
||||||
self.conn = await connect(":memory:")
|
self.conn = await connect(":memory:")
|
||||||
await create(self.conn)
|
await create(self.conn)
|
||||||
await run_migration(self.conn)
|
|
||||||
|
|
||||||
async def override_get_db():
|
async def override_get_db():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,12 @@ from fastapi.testclient import TestClient
|
||||||
|
|
||||||
import main
|
import main
|
||||||
from db import connect, create
|
from db import connect, create
|
||||||
from scripts.migration_to_households import run_migration
|
|
||||||
|
|
||||||
|
|
||||||
class TestShoppingPurchaseV2(unittest.IsolatedAsyncioTestCase):
|
class TestShoppingPurchaseV2(unittest.IsolatedAsyncioTestCase):
|
||||||
async def asyncSetUp(self):
|
async def asyncSetUp(self):
|
||||||
self.conn = await connect(":memory:")
|
self.conn = await connect(":memory:")
|
||||||
await create(self.conn)
|
await create(self.conn)
|
||||||
await run_migration(self.conn)
|
|
||||||
|
|
||||||
async def override_get_db():
|
async def override_get_db():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,12 @@ from fastapi.testclient import TestClient
|
||||||
|
|
||||||
import main
|
import main
|
||||||
from db import connect, create
|
from db import connect, create
|
||||||
from scripts.migration_to_households import run_migration
|
|
||||||
|
|
||||||
|
|
||||||
class TestShoppingRequestIngredientDedupeV2(unittest.IsolatedAsyncioTestCase):
|
class TestShoppingRequestIngredientDedupeV2(unittest.IsolatedAsyncioTestCase):
|
||||||
async def asyncSetUp(self):
|
async def asyncSetUp(self):
|
||||||
self.conn = await connect(":memory:")
|
self.conn = await connect(":memory:")
|
||||||
await create(self.conn)
|
await create(self.conn)
|
||||||
await run_migration(self.conn)
|
|
||||||
|
|
||||||
async def override_get_db():
|
async def override_get_db():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,12 @@ from fastapi.testclient import TestClient
|
||||||
|
|
||||||
import main
|
import main
|
||||||
from db import connect, create
|
from db import connect, create
|
||||||
from scripts.migration_to_households import run_migration
|
|
||||||
|
|
||||||
|
|
||||||
class TestShoppingRequestIngredientV2(unittest.IsolatedAsyncioTestCase):
|
class TestShoppingRequestIngredientV2(unittest.IsolatedAsyncioTestCase):
|
||||||
async def asyncSetUp(self):
|
async def asyncSetUp(self):
|
||||||
self.conn = await connect(":memory:")
|
self.conn = await connect(":memory:")
|
||||||
await create(self.conn)
|
await create(self.conn)
|
||||||
await run_migration(self.conn)
|
|
||||||
|
|
||||||
async def override_get_db():
|
async def override_get_db():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,12 @@ from fastapi.testclient import TestClient
|
||||||
|
|
||||||
import main
|
import main
|
||||||
from db import connect, create
|
from db import connect, create
|
||||||
from scripts.migration_to_households import run_migration
|
|
||||||
|
|
||||||
|
|
||||||
class TestShoppingRequestMealV2(unittest.IsolatedAsyncioTestCase):
|
class TestShoppingRequestMealV2(unittest.IsolatedAsyncioTestCase):
|
||||||
async def asyncSetUp(self):
|
async def asyncSetUp(self):
|
||||||
self.conn = await connect(":memory:")
|
self.conn = await connect(":memory:")
|
||||||
await create(self.conn)
|
await create(self.conn)
|
||||||
await run_migration(self.conn)
|
|
||||||
|
|
||||||
async def override_get_db():
|
async def override_get_db():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,12 @@ from fastapi.testclient import TestClient
|
||||||
|
|
||||||
import main
|
import main
|
||||||
from db import connect, create
|
from db import connect, create
|
||||||
from scripts.migration_to_households import run_migration
|
|
||||||
|
|
||||||
|
|
||||||
class TestShoppingUnrequestIngredientV2(unittest.IsolatedAsyncioTestCase):
|
class TestShoppingUnrequestIngredientV2(unittest.IsolatedAsyncioTestCase):
|
||||||
async def asyncSetUp(self):
|
async def asyncSetUp(self):
|
||||||
self.conn = await connect(":memory:")
|
self.conn = await connect(":memory:")
|
||||||
await create(self.conn)
|
await create(self.conn)
|
||||||
await run_migration(self.conn)
|
|
||||||
|
|
||||||
async def override_get_db():
|
async def override_get_db():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ Date: 2025-10-21
|
||||||
- Update `api/shopping.py` response models (CurrentShoppingList and PurchasedShoppingList) to use `Union[ListIngredientItem, RequestedMealItem]` for item arrays.
|
- Update `api/shopping.py` response models (CurrentShoppingList and PurchasedShoppingList) to use `Union[ListIngredientItem, RequestedMealItem]` for item arrays.
|
||||||
- Conversion helpers in `shopping` module to map from `ShoppingListItem` DB model to the outward union.
|
- Conversion helpers in `shopping` module to map from `ShoppingListItem` DB model to the outward union.
|
||||||
- Verify:
|
- Verify:
|
||||||
- [ ] Update tests in `tests/test_shopping_api.py` to accept the new shape while preserving field meanings.
|
- [ ] Update v2 tests to accept the new shape while preserving field meanings.
|
||||||
- [ ] OpenAPI shows `oneOf` for shopping list items.
|
- [ ] OpenAPI shows `oneOf` for shopping list items.
|
||||||
|
|
||||||
- [ ] Tighten invariants without breaking shape (keep for now)
|
- [ ] Tighten invariants without breaking shape (keep for now)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue