commit 240e54eec48a27f9b1e4d366fae05d254ffda648 Author: mohamad Date: Thu Mar 27 08:13:54 2025 +0100 weeee💃 diff --git a/app/.env b/app/.env new file mode 100644 index 0000000..9eef43b --- /dev/null +++ b/app/.env @@ -0,0 +1,8 @@ +POSTGRES_SERVER=ep-little-term-a2a5cvsf-pooler.eu-central-1.aws.neon.tech +POSTGRES_USER=neondb_owner +POSTGRES_PASSWORD=npg_gB8ivHCr7SeR +POSTGRES_DB=dooey +SQLALCHEMY_DATABASE_URI=postgresql+asyncpg://neondb_owner:npg_gB8ivHCr7SeR@ep-little-term-a2a5cvsf-pooler.eu-central-1.aws.neon.tech/dooey?sslmode=require +SECRET_KEY=your-secret-key-here +ACCESS_TOKEN_EXPIRE_MINUTES=1440 +BACKEND_CORS_ORIGINS=["http://localhost:5174", "http://localhost:5175", "http://localhost:5173"] diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..78cfa57 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,22 @@ +# Dockerfile +FROM python:3.11-slim + +WORKDIR /app/ + +# Install Poetry +RUN pip install poetry + +# Copy poetry configuration files +COPY pyproject.toml poetry.lock* /app/ + +# Configure poetry to not use a virtual environment +RUN poetry config virtualenvs.create false + +# Install dependencies +RUN poetry install --no-dev + +# Copy application code +COPY . /app/ + +# Run the application +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..e69de29 diff --git a/app/alembic.ini b/app/alembic.ini new file mode 100644 index 0000000..5bd6dc9 --- /dev/null +++ b/app/alembic.ini @@ -0,0 +1,38 @@ +[alembic] +script_location = alembic +prepend_sys_path = . +sqlalchemy.url = postgresql+asyncpg://postgres:postgres@localhost/household + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/app/alembic/__pycache__/env.cpython-312.pyc b/app/alembic/__pycache__/env.cpython-312.pyc new file mode 100644 index 0000000..28fb267 Binary files /dev/null and b/app/alembic/__pycache__/env.cpython-312.pyc differ diff --git a/app/alembic/env.py b/app/alembic/env.py new file mode 100644 index 0000000..d0a4804 --- /dev/null +++ b/app/alembic/env.py @@ -0,0 +1,85 @@ +# alembic/env.py +import asyncio +from logging.config import fileConfig + +from sqlalchemy import pool +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import async_engine_from_config + +from alembic import context +from app.db.base import Base +from app.models import user, house, shopping_list, expense, chore + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = async_engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + await connectable.dispose() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + asyncio.run(run_migrations_online()) diff --git a/app/app/__init__.py b/app/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/__pycache__/__init__.cpython-312.pyc b/app/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..d7bd02f Binary files /dev/null and b/app/app/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/app/__pycache__/main.cpython-312.pyc b/app/app/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..f7122bf Binary files /dev/null and b/app/app/__pycache__/main.cpython-312.pyc differ diff --git a/app/app/api/auth/__init__.py b/app/app/api/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/api/auth/__pycache__/__init__.cpython-312.pyc b/app/app/api/auth/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..fd123d1 Binary files /dev/null and b/app/app/api/auth/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/app/api/auth/__pycache__/router.cpython-312.pyc b/app/app/api/auth/__pycache__/router.cpython-312.pyc new file mode 100644 index 0000000..df2ca37 Binary files /dev/null and b/app/app/api/auth/__pycache__/router.cpython-312.pyc differ diff --git a/app/app/api/auth/router.py b/app/app/api/auth/router.py new file mode 100644 index 0000000..b1c3bbf --- /dev/null +++ b/app/app/api/auth/router.py @@ -0,0 +1,89 @@ +# app/api/auth/router.py +from datetime import timedelta +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordRequestForm +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + +from app.core.config import settings +from app.core.security import create_access_token, get_password_hash, verify_password +from app.db.session import get_db +from app.models.user import User +from app.schemas.user import User as UserSchema +from app.schemas.user import UserCreate +from app.core.logger import logger # Import the logger + +router = APIRouter() + + +@router.post("/register", response_model=UserSchema) +async def register( + user_in: UserCreate, + db: Annotated[AsyncSession, Depends(get_db)], +): + # Check if user exists + result = await db.execute(select(User).where(User.email == user_in.email)) + user = result.scalars().first() + if user: + logger.warning(f"Registration attempt with existing email: {user_in.email}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Email already registered", + ) + + # Create new user + db_user = User( + email=user_in.email, + password_hash=get_password_hash(user_in.password), + full_name=user_in.full_name, + ) + db.add(db_user) + await db.commit() + await db.refresh(db_user) + logger.info(f"New user registered: {db_user.email} (ID: {db_user.id})") + return db_user + + +@router.post("/login") +async def login( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()], + db: Annotated[AsyncSession, Depends(get_db)], +): + # Authenticate user + result = await db.execute(select(User).where(User.email == form_data.username)) + user = result.scalars().first() + + if not user or not verify_password(form_data.password, user.password_hash): + logger.warning(f"Failed login attempt for user: {form_data.username}") + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Create access token + access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + subject=str(user.id), expires_delta=access_token_expires + ) + + logger.info(f"User logged in: {user.email} (ID: {user.id})") + return { + "access_token": access_token, + "token_type": "bearer", + } + +@router.post("/refresh") +async def refresh_token(): + # This would be implemented with refresh tokens + # For simplicity, we're not implementing this now + pass + + +@router.post("/logout") +async def logout(): + # This would be implemented with token blacklisting + # For simplicity, we're not implementing this now + return {"detail": "Successfully logged out"} diff --git a/app/app/api/chores/__init__.py b/app/app/api/chores/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/api/chores/__pycache__/__init__.cpython-312.pyc b/app/app/api/chores/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..fc31d3c Binary files /dev/null and b/app/app/api/chores/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/app/api/chores/__pycache__/router.cpython-312.pyc b/app/app/api/chores/__pycache__/router.cpython-312.pyc new file mode 100644 index 0000000..f1e5b9e Binary files /dev/null and b/app/app/api/chores/__pycache__/router.cpython-312.pyc differ diff --git a/app/app/api/chores/router.py b/app/app/api/chores/router.py new file mode 100644 index 0000000..7bfab75 --- /dev/null +++ b/app/app/api/chores/router.py @@ -0,0 +1,260 @@ +# app/api/chores/router.py +from typing import Annotated, List, Optional +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.dependencies import check_house_membership, get_current_user +from app.db.session import get_db +from app.models.chore import Chore +from app.models.user import User +from app.schemas.chore import ( + Chore as ChoreSchema, + ChoreAssign, + ChoreComplete, + ChoreCreate, + ChoreUpdate, +) + +router = APIRouter() + + +@router.get("/{house_id}/chores", response_model=List[ChoreSchema]) +async def get_chores( + house_id: UUID, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Get all chores for a house.""" + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Get all chores for the house + result = await db.execute(select(Chore).where(Chore.house_id == house_id)) + chores = result.scalars().all() + return chores + + +@router.post( + "/{house_id}/chores", response_model=ChoreSchema, status_code=status.HTTP_201_CREATED +) +async def create_chore( + house_id: UUID, + chore_in: ChoreCreate, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Create new chore for a house.""" + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # If assigned_to is provided, check if the user is a member of the house + if chore_in.assigned_to: + is_assignee_member = await check_house_membership( + db, str(chore_in.assigned_to), str(house_id) + ) + if not is_assignee_member: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Assigned user is not a member of this house", + ) + + # Create the chore + db_chore = Chore( + house_id=house_id, + **chore_in.model_dump(), + ) + db.add(db_chore) + await db.commit() + await db.refresh(db_chore) + return db_chore + + +@router.put("/{house_id}/chores/{chore_id}", response_model=ChoreSchema) +async def update_chore( + house_id: UUID, + chore_id: UUID, + chore_in: ChoreUpdate, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Update chore.""" + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Get the chore + result = await db.execute( + select(Chore).where( + Chore.id == chore_id, + Chore.house_id == house_id, + ) + ) + chore = result.scalars().first() + + if not chore: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Chore not found", + ) + + # If assigned_to is provided, check if the user is a member of the house + if chore_in.assigned_to: + is_assignee_member = await check_house_membership( + db, str(chore_in.assigned_to), str(house_id) + ) + if not is_assignee_member: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Assigned user is not a member of this house", + ) + + # Update chore fields + update_data = chore_in.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(chore, field, value) + + await db.commit() + await db.refresh(chore) + return chore + + +@router.delete("/{house_id}/chores/{chore_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_chore( + house_id: UUID, + chore_id: UUID, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Delete chore.""" + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Get the chore + result = await db.execute( + select(Chore).where( + Chore.id == chore_id, + Chore.house_id == house_id, + ) + ) + chore = result.scalars().first() + + if not chore: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Chore not found", + ) + + # Delete the chore + await db.delete(chore) + await db.commit() + return None + + +@router.patch("/{house_id}/chores/{chore_id}/assign", response_model=ChoreSchema) +async def assign_chore( + house_id: UUID, + chore_id: UUID, + assign_data: ChoreAssign, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Assign chore to user.""" + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Get the chore + result = await db.execute( + select(Chore).where( + Chore.id == chore_id, + Chore.house_id == house_id, + ) + ) + chore = result.scalars().first() + + if not chore: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Chore not found", + ) + + # Check if the assigned user is a member of the house + is_assignee_member = await check_house_membership( + db, str(assign_data.assigned_to), str(house_id) + ) + if not is_assignee_member: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Assigned user is not a member of this house", + ) + + # Assign the chore + chore.assigned_to = assign_data.assigned_to + await db.commit() + await db.refresh(chore) + return chore + + +@router.patch("/{house_id}/chores/{chore_id}/complete", response_model=ChoreSchema) +async def complete_chore( + house_id: UUID, + chore_id: UUID, + complete_data: ChoreComplete, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Mark chore as complete/incomplete.""" + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Get the chore + result = await db.execute( + select(Chore).where( + Chore.id == chore_id, + Chore.house_id == house_id, + ) + ) + chore = result.scalars().first() + + if not chore: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Chore not found", + ) + + # Update completion status + chore.is_completed = complete_data.is_completed + await db.commit() + await db.refresh(chore) + return chore diff --git a/app/app/api/expenses/__init__.py b/app/app/api/expenses/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/api/expenses/__pycache__/__init__.cpython-312.pyc b/app/app/api/expenses/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..49ad10b Binary files /dev/null and b/app/app/api/expenses/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/app/api/expenses/__pycache__/router.cpython-312.pyc b/app/app/api/expenses/__pycache__/router.cpython-312.pyc new file mode 100644 index 0000000..22ee867 Binary files /dev/null and b/app/app/api/expenses/__pycache__/router.cpython-312.pyc differ diff --git a/app/app/api/expenses/router.py b/app/app/api/expenses/router.py new file mode 100644 index 0000000..433d319 --- /dev/null +++ b/app/app/api/expenses/router.py @@ -0,0 +1,320 @@ +# app/api/expenses/router.py +from decimal import Decimal +from typing import Annotated, Dict, List +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.dependencies import check_house_membership, get_current_user +from app.db.session import get_db +from app.models.expense import Expense, ExpenseSplit +from app.models.house import HouseMember +from app.models.shopping_list import ShoppingList +from app.models.user import User +from app.schemas.expense import ( + Expense as ExpenseSchema, + ExpenseCreate, + ExpenseSummary, + ExpenseUpdate, +) + +router = APIRouter() + + +@router.get("/{house_id}/lists/{list_id}/expenses", response_model=List[ExpenseSchema]) +async def get_expenses( + house_id: UUID, + list_id: UUID, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Get all expenses for a list.""" + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Check if the list belongs to the house + result = await db.execute( + select(ShoppingList).where( + ShoppingList.id == list_id, + ShoppingList.house_id == house_id, + ) + ) + shopping_list = result.scalars().first() + + if not shopping_list: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Shopping list not found", + ) + + # Get all expenses for the list + result = await db.execute(select(Expense).where(Expense.list_id == list_id)) + expenses = result.scalars().all() + return expenses + + +@router.post( + "/{house_id}/lists/{list_id}/expenses", + response_model=ExpenseSchema, + status_code=status.HTTP_201_CREATED, +) +async def create_expense( + house_id: UUID, + list_id: UUID, + expense_in: ExpenseCreate, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Create expense.""" + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Check if the list belongs to the house + result = await db.execute( + select(ShoppingList).where( + ShoppingList.id == list_id, + ShoppingList.house_id == house_id, + ) + ) + shopping_list = result.scalars().first() + + if not shopping_list: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Shopping list not found", + ) + + # Check if payer is a member of the house + is_payer_member = await check_house_membership( + db, str(expense_in.payer_id), str(house_id) + ) + if not is_payer_member: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Payer is not a member of this house", + ) + + # Validate that the sum of splits equals the total amount + splits_total = sum(split.amount for split in expense_in.splits) + if splits_total != expense_in.amount: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Sum of splits must equal the total expense amount", + ) + + # Create the expense + db_expense = Expense( + list_id=list_id, + payer_id=expense_in.payer_id, + amount=expense_in.amount, + description=expense_in.description, + date=expense_in.date, + ) + db.add(db_expense) + await db.flush() + + # Create the splits + for split in expense_in.splits: + # Check if user is a member of the house + is_user_member = await check_house_membership( + db, str(split.user_id), str(house_id) + ) + if not is_user_member: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"User {split.user_id} is not a member of this house", + ) + + db_split = ExpenseSplit( + expense_id=db_expense.id, + user_id=split.user_id, + amount=split.amount, + ) + db.add(db_split) + + await db.commit() + await db.refresh(db_expense) + return db_expense + + +@router.put("/{house_id}/lists/{list_id}/expenses/{expense_id}", response_model=ExpenseSchema) +async def update_expense( + house_id: UUID, + list_id: UUID, + expense_id: UUID, + expense_in: ExpenseUpdate, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Update expense.""" + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Check if the list belongs to the house + result = await db.execute( + select(ShoppingList).where( + ShoppingList.id == list_id, + ShoppingList.house_id == house_id, + ) + ) + shopping_list = result.scalars().first() + + if not shopping_list: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Shopping list not found", + ) + + # Get the expense + result = await db.execute( + select(Expense).where( + Expense.id == expense_id, + Expense.list_id == list_id, + ) + ) + expense = result.scalars().first() + + if not expense: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Expense not found", + ) + + # Update expense fields + update_data = expense_in.model_dump(exclude_unset=True) + + # Handle splits separately + splits = update_data.pop("splits", None) + + # Update basic expense fields + for field, value in update_data.items(): + if field == "payer_id" and value: + # Check if new payer is a member of the house + is_payer_member = await check_house_membership( + db, str(value), str(house_id) + ) + if not is_payer_member: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Payer is not a member of this house", + ) + setattr(expense, field, value) + + # Handle splits if provided + if splits is not None: + # Validate that the sum of splits equals the total amount + splits_total = sum(split.amount for split in splits) + if splits_total != expense.amount: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Sum of splits must equal the total expense amount", + ) + + # Delete existing splits + await db.execute( + "DELETE FROM expense_splits WHERE expense_id = :expense_id", + {"expense_id": expense_id}, + ) + + # Create new splits + for split in splits: + # Check if user is a member of the house + is_user_member = await check_house_membership( + db, str(split.user_id), str(house_id) + ) + if not is_user_member: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"User {split.user_id} is not a member of this house", + ) + + db_split = ExpenseSplit( + expense_id=expense_id, + user_id=split.user_id, + amount=split.amount, + ) + db.add(db_split) + + await db.commit() + await db.refresh(expense) + return expense + + +@router.get("/{house_id}/lists/{list_id}/expenses/summary", response_model=ExpenseSummary) +async def get_expense_summary( + house_id: UUID, + list_id: UUID, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Get expense summary and splits.""" + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Check if the list belongs to the house + result = await db.execute( + select(ShoppingList).where( + ShoppingList.id == list_id, + ShoppingList.house_id == house_id, + ) + ) + shopping_list = result.scalars().first() + + if not shopping_list: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Shopping list not found", + ) + + # Get all expenses for the list + result = await db.execute(select(Expense).where(Expense.list_id == list_id)) + expenses = result.scalars().all() + + # Get all house members + result = await db.execute( + select(HouseMember).where(HouseMember.house_id == house_id) + ) + members = result.scalars().all() + + # Calculate total amount + total_amount = sum(expense.amount for expense in expenses) + + # Calculate user balances + user_balances: Dict[UUID, Decimal] = {member.user_id: Decimal("0") for member in members} + + # For each expense, add the amount paid to the payer's balance + # and subtract the split amounts from each user's balance + for expense in expenses: + # Add the full amount to the payer's balance (they paid this amount) + user_balances[expense.payer_id] += expense.amount + + # Subtract each user's split from their balance (they owe this amount) + for split in expense.splits: + user_balances[split.user_id] -= split.amount + + return ExpenseSummary( + total_amount=total_amount, + user_balances={user_id: balance for user_id, balance in user_balances.items()}, + ) diff --git a/app/app/api/houses/__init__.py b/app/app/api/houses/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/api/houses/__pycache__/__init__.cpython-312.pyc b/app/app/api/houses/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..697d64c Binary files /dev/null and b/app/app/api/houses/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/app/api/houses/__pycache__/router.cpython-312.pyc b/app/app/api/houses/__pycache__/router.cpython-312.pyc new file mode 100644 index 0000000..c8ad1d2 Binary files /dev/null and b/app/app/api/houses/__pycache__/router.cpython-312.pyc differ diff --git a/app/app/api/houses/router.py b/app/app/api/houses/router.py new file mode 100644 index 0000000..9d1c1a6 --- /dev/null +++ b/app/app/api/houses/router.py @@ -0,0 +1,478 @@ +from datetime import datetime, timedelta +from sqlalchemy.future import select +from app.models.invite import HouseInvite +from app.schemas.invite import HouseInvite as HouseInviteSchema, HouseInviteCreate +from app.services.invite_service import generate_invite_code + +from typing import Annotated, List +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.dependencies import check_house_membership, get_current_user +from app.db.session import get_db +from app.models.house import House, HouseMember +from app.models.user import User +from app.schemas.house import ( + House as HouseSchema, + HouseCreate, + HouseMember as HouseMemberSchema, + HouseMemberCreate, + HouseMemberUpdate, + HouseUpdate, + HouseMemberResponse, +) + +router = APIRouter() + + +@router.get("/", response_model=List[HouseSchema]) +async def get_houses( + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Get all houses for the authenticated user.""" + query = ( + select(House) + .join(HouseMember, House.id == HouseMember.house_id) + .where(HouseMember.user_id == current_user.id) + ) + result = await db.execute(query) + houses = result.scalars().all() + return houses + + +@router.post("/", response_model=HouseSchema, status_code=status.HTTP_201_CREATED) +async def create_house( + house_in: HouseCreate, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Create a new house.""" + # Create the house + db_house = House(**house_in.model_dump()) + db.add(db_house) + await db.flush() + + # Add the current user as an admin + db_member = HouseMember( + house_id=db_house.id, + user_id=current_user.id, + role="admin", + ) + db.add(db_member) + + await db.commit() + await db.refresh(db_house) + return db_house + + +@router.get("/{house_id}", response_model=HouseSchema) +async def get_house( + house_id: UUID, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Get house details.""" + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Get the house + result = await db.execute(select(House).where(House.id == house_id)) + house = result.scalars().first() + + if not house: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="House not found", + ) + + return house + + +@router.put("/{house_id}", response_model=HouseSchema) +async def update_house( + house_id: UUID, + house_in: HouseUpdate, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Update house details.""" + # Check if user is an admin of the house + is_admin = await check_house_membership( + db, str(current_user.id), str(house_id), required_role="admin" + ) + if not is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Must be an admin to update house details", + ) + + # Get the house + result = await db.execute(select(House).where(House.id == house_id)) + house = result.scalars().first() + + if not house: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="House not found", + ) + + # Update house fields + update_data = house_in.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(house, field, value) + + await db.commit() + await db.refresh(house) + return house + + +@router.delete("/{house_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_house( + house_id: UUID, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Delete house.""" + # Check if user is an admin of the house + is_admin = await check_house_membership( + db, str(current_user.id), str(house_id), required_role="admin" + ) + if not is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Must be an admin to delete house", + ) + + # Get the house + result = await db.execute(select(House).where(House.id == house_id)) + house = result.scalars().first() + + if not house: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="House not found", + ) + + # Delete the house + await db.delete(house) + await db.commit() + return None + + +# House Members endpoints +@router.get("/{house_id}/members", response_model=List[HouseMemberSchema]) +async def get_house_members( + house_id: UUID, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Get all members of a house.""" + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Get all members + result = await db.execute( + select(HouseMember).where(HouseMember.house_id == house_id) + ) + members = result.scalars().all() + return members + + +@router.post( + "/{house_id}/members", response_model=HouseMemberResponse, status_code=status.HTTP_201_CREATED +) +async def add_house_member( + house_id: UUID, + member_in: HouseMemberCreate, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Add a member to the house.""" + # Check if user is an admin of the house + is_admin = await check_house_membership( + db, str(current_user.id), str(house_id), required_role="admin" + ) + if not is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Must be an admin to add members", + ) + + # Check if user exists + result = await db.execute(select(User).where(User.id == member_in.user_id)) + user = result.scalars().first() + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found", + ) + + # Check if user is already a member + result = await db.execute( + select(HouseMember).where( + HouseMember.house_id == house_id, + HouseMember.user_id == member_in.user_id, + ) + ) + existing_member = result.scalars().first() + if existing_member: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User is already a member of this house", + ) + + # Add the member + db_member = HouseMember( + house_id=house_id, + user_id=member_in.user_id, + role=member_in.role, + ) + db.add(db_member) + await db.commit() + await db.refresh(db_member) + return db_member + + +@router.put("/{house_id}/members/{user_id}", response_model=HouseMemberSchema) +async def update_house_member( + house_id: UUID, + user_id: UUID, + member_in: HouseMemberUpdate, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Update member role.""" + # Check if user is an admin of the house + is_admin = await check_house_membership( + db, str(current_user.id), str(house_id), required_role="admin" + ) + if not is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Must be an admin to update member roles", + ) + + # Get the member + result = await db.execute( + select(HouseMember).where( + HouseMember.house_id == house_id, + HouseMember.user_id == user_id, + ) + ) + member = result.scalars().first() + + if not member: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Member not found", + ) + + # Update member fields + update_data = member_in.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(member, field, value) + + await db.commit() + await db.refresh(member) + return member + + +@router.delete("/{house_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT) +async def remove_house_member( + house_id: UUID, + user_id: UUID, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Remove a member from the house.""" + # Check if user is an admin of the house + is_admin = await check_house_membership( + db, str(current_user.id), str(house_id), required_role="admin" + ) + if not is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Must be an admin to remove members", + ) + + # Get the member + result = await db.execute( + select(HouseMember).where( + HouseMember.house_id == house_id, + HouseMember.user_id == user_id, + ) + ) + member = result.scalars().first() + + if not member: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Member not found", + ) + + # Prevent removing the last admin + if member.role == "admin": + # Count admins + result = await db.execute( + select(HouseMember).where( + HouseMember.house_id == house_id, + HouseMember.role == "admin", + ) + ) + admins = result.scalars().all() + if len(admins) <= 1: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Cannot remove the last admin of the house", + ) + + # Remove the member + await db.delete(member) + await db.commit() + return None + +# Create a new invite (only admins can create invites) +@router.post( + "/{house_id}/invites", + response_model=HouseInviteSchema, + status_code=status.HTTP_201_CREATED, +) +async def create_house_invite( + house_id: UUID, + invite_in: HouseInviteCreate, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """ + Create an invitation code for a house with a set expiration. + Only an admin can create an invitation. + """ + is_admin = await check_house_membership( + db, str(current_user.id), str(house_id), required_role="admin" + ) + if not is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Only admins can create invitation codes", + ) + + # Generate a unique code + code = generate_invite_code() + # Check for uniqueness (in case of a rare collision) + while True: + result = await db.execute(select(HouseInvite).where(HouseInvite.code == code)) + existing_invite = result.scalars().first() + if existing_invite: + code = generate_invite_code() + else: + break + + expires_at = datetime.utcnow() + timedelta(minutes=invite_in.expires_in_minutes) + db_invite = HouseInvite( + code=code, + house_id=house_id, + inviter_id=current_user.id, + expires_at=expires_at, + ) + db.add(db_invite) + await db.commit() + await db.refresh(db_invite) + return db_invite + + +# List active (nonexpired) invites for a house (accessible to house members) +@router.get( + "/{house_id}/invites", + response_model=list[HouseInviteSchema], +) +async def list_active_invites( + house_id: UUID, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """ + Get all nonexpired invitation codes for a given house. + """ + is_member = await check_house_membership( + db, str(current_user.id), str(house_id) + ) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + invites_query = select(HouseInvite).where( + HouseInvite.house_id == house_id, + HouseInvite.expires_at > datetime.utcnow(), + ) + result = await db.execute(invites_query) + invites = result.scalars().all() + return invites + + +# Accept an invite code (allows a user to join a house) +@router.post( + "/invites/accept", + response_model=HouseMemberResponse, + status_code=status.HTTP_201_CREATED, +) +async def accept_invite( + code: str, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """ + Accept an invitation code to join a house. + If the invite has expired or is invalid, an error is raised. + """ + result = await db.execute(select(HouseInvite).where(HouseInvite.code == code)) + invite = result.scalars().first() + if not invite: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Invalid invitation code", + ) + if invite.expires_at < datetime.utcnow(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invitation code has expired", + ) + + # Check if the user is already a member + result = await db.execute( + select(HouseMember).where( + HouseMember.house_id == invite.house_id, + HouseMember.user_id == current_user.id, + ) + ) + existing_member = result.scalars().first() + if existing_member: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User is already a member of this house", + ) + + db_member = HouseMember( + house_id=invite.house_id, + user_id=current_user.id, + role="member", # default role on joining + ) + db.add(db_member) + await db.commit() + await db.refresh(db_member) + return db_member \ No newline at end of file diff --git a/app/app/api/ocr/__init__.py b/app/app/api/ocr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/api/ocr/__pycache__/__init__.cpython-312.pyc b/app/app/api/ocr/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..b15b5bb Binary files /dev/null and b/app/app/api/ocr/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/app/api/ocr/__pycache__/router.cpython-312.pyc b/app/app/api/ocr/__pycache__/router.cpython-312.pyc new file mode 100644 index 0000000..ad08c96 Binary files /dev/null and b/app/app/api/ocr/__pycache__/router.cpython-312.pyc differ diff --git a/app/app/api/ocr/router.py b/app/app/api/ocr/router.py new file mode 100644 index 0000000..bed35a4 --- /dev/null +++ b/app/app/api/ocr/router.py @@ -0,0 +1,109 @@ +# app/api/ocr/router.py +from typing import Annotated, List +from uuid import UUID + +from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.dependencies import check_house_membership, get_current_user +from app.db.session import get_db +from app.models.shopping_list import ListItem, ShoppingList +from app.models.user import User +from app.schemas.shopping_list import ListItemCreate +from app.services.ocr_service import ocr_service + +router = APIRouter() + + +@router.post("/process", response_model=List[dict]) +async def process_image( + current_user: Annotated[User, Depends(get_current_user)], + image: UploadFile = File(...), +): + """Process image and extract items.""" + if not image.content_type.startswith("image/"): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="File must be an image", + ) + + # Read the image file + image_bytes = await image.read() + + # Process the image with OCR + items = await ocr_service.process_image(image_bytes) + + return items + + +@router.post("/{house_id}/lists/{list_id}/ocr/apply") +async def apply_ocr_results( + house_id: UUID, + list_id: UUID, + items: List[dict], + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Add OCR results to shopping list.""" + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Check if the list belongs to the house + result = await db.execute( + select(ShoppingList).where( + ShoppingList.id == list_id, + ShoppingList.house_id == house_id, + ) + ) + shopping_list = result.scalars().first() + + if not shopping_list: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Shopping list not found", + ) + + # Get the highest position to place new items at the end + result = await db.execute( + select(ListItem).where(ListItem.list_id == list_id).order_by(ListItem.position.desc()) + ) + last_item = result.scalars().first() + position = 1 if not last_item else (last_item.position or 0) + 1 + + # Add items to the list + added_items = [] + for item_data in items: + # Create item schema for validation + try: + item_create = ListItemCreate( + name=item_data.get("name", "Unknown Item"), + quantity=item_data.get("quantity", 1), + price=item_data.get("price"), + unit=item_data.get("unit"), + ) + + # Create the item + db_item = ListItem( + list_id=list_id, + position=position, + **item_create.model_dump(), + ) + db.add(db_item) + position += 1 + added_items.append(item_data) + except Exception as e: + # Log the error but continue with other items + continue + + await db.commit() + + return { + "detail": f"Added {len(added_items)} items to the shopping list", + "items": added_items, + } diff --git a/app/app/api/shopping_lists/__init__.py b/app/app/api/shopping_lists/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/api/shopping_lists/__pycache__/__init__.cpython-312.pyc b/app/app/api/shopping_lists/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..9ea3b98 Binary files /dev/null and b/app/app/api/shopping_lists/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/app/api/shopping_lists/__pycache__/router.cpython-312.pyc b/app/app/api/shopping_lists/__pycache__/router.cpython-312.pyc new file mode 100644 index 0000000..51edce8 Binary files /dev/null and b/app/app/api/shopping_lists/__pycache__/router.cpython-312.pyc differ diff --git a/app/app/api/shopping_lists/router.py b/app/app/api/shopping_lists/router.py new file mode 100644 index 0000000..c76af64 --- /dev/null +++ b/app/app/api/shopping_lists/router.py @@ -0,0 +1,512 @@ +# app/api/shopping_lists/router.py +from typing import Annotated, List +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.dependencies import check_house_membership, get_current_user +from app.db.session import get_db +from app.models.shopping_list import ListItem, ShoppingList +from app.models.user import User +from app.schemas.shopping_list import ( + ItemReorder, + ListItem as ListItemSchema, + ListItemCreate, + ListItemUpdate, + ShoppingList as ShoppingListSchema, + ShoppingListCreate, + ShoppingListUpdate, +) +from app.core.logger import shopping_lists_logger + +router = APIRouter() + + +@router.get("/{house_id}/lists", response_model=List[ShoppingListSchema]) +async def get_shopping_lists( + house_id: UUID, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Get all shopping lists for a house.""" + shopping_lists_logger.debug(f"User {current_user.id} requested shopping lists for house {house_id}") + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Get all shopping lists for the house + result = await db.execute( + select(ShoppingList).where(ShoppingList.house_id == house_id) + ) + lists = result.scalars().all() + return lists + + +@router.post( + "/{house_id}/lists", response_model=ShoppingListSchema, status_code=status.HTTP_201_CREATED +) +async def create_shopping_list( + house_id: UUID, + list_in: ShoppingListCreate, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Create new shopping list for a house.""" + shopping_lists_logger.debug(f"User {current_user.id} is creating a new shopping list for house {house_id}") + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Create the shopping list + db_list = ShoppingList( + house_id=house_id, + **list_in.model_dump(), + ) + db.add(db_list) + await db.commit() + await db.refresh(db_list) + return db_list + + +@router.get("/{house_id}/lists/{list_id}", response_model=ShoppingListSchema) +async def get_shopping_list( + house_id: UUID, + list_id: UUID, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Get single list details.""" + shopping_lists_logger.debug(f"User {current_user.id} requested details for list {list_id} in house {house_id}") + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Get the shopping list + result = await db.execute( + select(ShoppingList).where( + ShoppingList.id == list_id, + ShoppingList.house_id == house_id, + ) + ) + shopping_list = result.scalars().first() + + if not shopping_list: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Shopping list not found", + ) + + return shopping_list + + +@router.put("/{house_id}/lists/{list_id}", response_model=ShoppingListSchema) +async def update_shopping_list( + house_id: UUID, + list_id: UUID, + list_in: ShoppingListUpdate, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Update list details.""" + shopping_lists_logger.debug(f"User {current_user.id} is updating list {list_id} in house {house_id}") + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Get the shopping list + result = await db.execute( + select(ShoppingList).where( + ShoppingList.id == list_id, + ShoppingList.house_id == house_id, + ) + ) + shopping_list = result.scalars().first() + + if not shopping_list: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Shopping list not found", + ) + + # Update shopping list fields + update_data = list_in.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(shopping_list, field, value) + + await db.commit() + await db.refresh(shopping_list) + return shopping_list + + +@router.delete("/{house_id}/lists/{list_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_shopping_list( + house_id: UUID, + list_id: UUID, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Delete/archive list.""" + shopping_lists_logger.debug(f"User {current_user.id} is deleting list {list_id} in house {house_id}") + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Get the shopping list + result = await db.execute( + select(ShoppingList).where( + ShoppingList.id == list_id, + ShoppingList.house_id == house_id, + ) + ) + shopping_list = result.scalars().first() + + if not shopping_list: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Shopping list not found", + ) + + # Archive the list instead of deleting + shopping_list.is_archived = True + await db.commit() + return None + + +# List Items endpoints +@router.get("/{house_id}/lists/{list_id}/items", response_model=List[ListItemSchema]) +async def get_list_items( + house_id: UUID, + list_id: UUID, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Get all items in a list.""" + shopping_lists_logger.debug(f"User {current_user.id} requested items for list {list_id} in house {house_id}") + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Check if the list belongs to the house + result = await db.execute( + select(ShoppingList).where( + ShoppingList.id == list_id, + ShoppingList.house_id == house_id, + ) + ) + shopping_list = result.scalars().first() + + if not shopping_list: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Shopping list not found", + ) + + # Get all items in the list + result = await db.execute(select(ListItem).where(ListItem.list_id == list_id)) + items = result.scalars().all() + return items + + +@router.post( + "/{house_id}/lists/{list_id}/items", + response_model=ListItemSchema, + status_code=status.HTTP_201_CREATED, +) +async def add_list_item( + house_id: UUID, + list_id: UUID, + item_in: ListItemCreate, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Add item to list.""" + shopping_lists_logger.debug(f"User {current_user.id} is adding an item to list {list_id} in house {house_id}") + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Check if the list belongs to the house + result = await db.execute( + select(ShoppingList).where( + ShoppingList.id == list_id, + ShoppingList.house_id == house_id, + ) + ) + shopping_list = result.scalars().first() + + if not shopping_list: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Shopping list not found", + ) + + # Get the highest position to place the new item at the end + result = await db.execute( + select(ListItem).where(ListItem.list_id == list_id).order_by(ListItem.position.desc()) + ) + last_item = result.scalars().first() + new_position = 1 if not last_item else (last_item.position or 0) + 1 + + # Create the item + db_item = ListItem( + list_id=list_id, + position=new_position, + **item_in.model_dump(), + ) + db.add(db_item) + await db.commit() + await db.refresh(db_item) + return db_item + + +@router.put("/{house_id}/lists/{list_id}/items/{item_id}", response_model=ListItemSchema) +async def update_list_item( + house_id: UUID, + list_id: UUID, + item_id: UUID, + item_in: ListItemUpdate, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Update item.""" + shopping_lists_logger.debug(f"User {current_user.id} is updating item {item_id} in list {list_id} in house {house_id}") + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Check if the list belongs to the house + result = await db.execute( + select(ShoppingList).where( + ShoppingList.id == list_id, + ShoppingList.house_id == house_id, + ) + ) + shopping_list = result.scalars().first() + + if not shopping_list: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Shopping list not found", + ) + + # Get the item + result = await db.execute( + select(ListItem).where( + ListItem.id == item_id, + ListItem.list_id == list_id, + ) + ) + item = result.scalars().first() + + if not item: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Item not found", + ) + + # Update item fields + update_data = item_in.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(item, field, value) + + await db.commit() + await db.refresh(item) + return item + + +@router.delete( + "/{house_id}/lists/{list_id}/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT +) +async def delete_list_item( + house_id: UUID, + list_id: UUID, + item_id: UUID, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Delete item.""" + shopping_lists_logger.debug(f"User {current_user.id} is deleting item {item_id} in list {list_id} in house {house_id}") + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Check if the list belongs to the house + result = await db.execute( + select(ShoppingList).where( + ShoppingList.id == list_id, + ShoppingList.house_id == house_id, + ) + ) + shopping_list = result.scalars().first() + + if not shopping_list: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Shopping list not found", + ) + + # Get the item + result = await db.execute( + select(ListItem).where( + ListItem.id == item_id, + ListItem.list_id == list_id, + ) + ) + item = result.scalars().first() + + if not item: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Item not found", + ) + + # Delete the item + await db.delete(item) + await db.commit() + return None + + +@router.patch( + "/{house_id}/lists/{list_id}/items/{item_id}/complete", response_model=ListItemSchema +) +async def mark_item_complete( + house_id: UUID, + list_id: UUID, + item_id: UUID, + is_completed: bool, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Mark item as complete/incomplete.""" + shopping_lists_logger.debug(f"User {current_user.id} is marking item {item_id} as {'complete' if is_completed else 'incomplete'} in list {list_id} in house {house_id}") + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Check if the list belongs to the house + result = await db.execute( + select(ShoppingList).where( + ShoppingList.id == list_id, + ShoppingList.house_id == house_id, + ) + ) + shopping_list = result.scalars().first() + + if not shopping_list: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Shopping list not found", + ) + + # Get the item + result = await db.execute( + select(ListItem).where( + ListItem.id == item_id, + ListItem.list_id == list_id, + ) + ) + item = result.scalars().first() + + if not item: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Item not found", + ) + + # Update completion status + item.is_completed = is_completed + await db.commit() + await db.refresh(item) + return item + + +@router.post("/{house_id}/lists/{list_id}/items/reorder") +async def reorder_items( + house_id: UUID, + list_id: UUID, + reorder_data: List[ItemReorder], + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +): + """Reorder items in list.""" + shopping_lists_logger.debug(f"User {current_user.id} is reordering items in list {list_id} in house {house_id}") + # Check if user is a member of the house + is_member = await check_house_membership(db, str(current_user.id), str(house_id)) + if not is_member: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a member of this house", + ) + + # Check if the list belongs to the house + result = await db.execute( + select(ShoppingList).where( + ShoppingList.id == list_id, + ShoppingList.house_id == house_id, + ) + ) + shopping_list = result.scalars().first() + + if not shopping_list: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Shopping list not found", + ) + + # Update positions for each item + for item_order in reorder_data: + result = await db.execute( + select(ListItem).where( + ListItem.id == item_order.item_id, + ListItem.list_id == list_id, + ) + ) + item = result.scalars().first() + + if item: + item.position = item_order.new_position + + await db.commit() + return {"detail": "Items reordered successfully"} diff --git a/app/app/api/users/__init__.py b/app/app/api/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/api/users/__pycache__/__init__.cpython-312.pyc b/app/app/api/users/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..7608a02 Binary files /dev/null and b/app/app/api/users/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/app/api/users/__pycache__/router.cpython-312.pyc b/app/app/api/users/__pycache__/router.cpython-312.pyc new file mode 100644 index 0000000..d1f7088 Binary files /dev/null and b/app/app/api/users/__pycache__/router.cpython-312.pyc differ diff --git a/app/app/api/users/router.py b/app/app/api/users/router.py new file mode 100644 index 0000000..ec7879e --- /dev/null +++ b/app/app/api/users/router.py @@ -0,0 +1,52 @@ +# app/api/users/router.py +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.db.session import get_db +from app.core.dependencies import get_current_user +from app.models.user import User as UserModel +from app.schemas.user import User, UserUpdate +from app.core.security import get_password_hash +from app.core.logger import logger + +router = APIRouter() + + +@router.get("/me", response_model=User) +async def read_current_user( + current_user: Annotated[UserModel, Depends(get_current_user)] +): + """ + Retrieve the current user's profile. + """ + logger.info(f"User {current_user.id} profile retrieved") + return current_user + + +@router.put("/me", response_model=User) +async def update_current_user( + user_in: UserUpdate, + current_user: Annotated[UserModel, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(get_db)], +): + """ + Update the current user’s profile. + If a password is provided, it will be hashed before also updating. + """ + update_data = user_in.model_dump(exclude_unset=True) + + # If password is provided in the update, hash it first. + if "password" in update_data: + update_data["password_hash"] = get_password_hash(update_data.pop("password")) + + # Update each field on the current user + for field, value in update_data.items(): + setattr(current_user, field, value) + + db.add(current_user) + await db.commit() + await db.refresh(current_user) + logger.info(f"User {current_user.id} profile updated") + return current_user diff --git a/app/app/core/__init__.py b/app/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/core/__pycache__/__init__.cpython-312.pyc b/app/app/core/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..9b8c077 Binary files /dev/null and b/app/app/core/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/app/core/__pycache__/config.cpython-312.pyc b/app/app/core/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..b226740 Binary files /dev/null and b/app/app/core/__pycache__/config.cpython-312.pyc differ diff --git a/app/app/core/__pycache__/dependencies.cpython-312.pyc b/app/app/core/__pycache__/dependencies.cpython-312.pyc new file mode 100644 index 0000000..526a8fe Binary files /dev/null and b/app/app/core/__pycache__/dependencies.cpython-312.pyc differ diff --git a/app/app/core/__pycache__/logger.cpython-312.pyc b/app/app/core/__pycache__/logger.cpython-312.pyc new file mode 100644 index 0000000..942658e Binary files /dev/null and b/app/app/core/__pycache__/logger.cpython-312.pyc differ diff --git a/app/app/core/__pycache__/security.cpython-312.pyc b/app/app/core/__pycache__/security.cpython-312.pyc new file mode 100644 index 0000000..0b09d39 Binary files /dev/null and b/app/app/core/__pycache__/security.cpython-312.pyc differ diff --git a/app/app/core/config.py b/app/app/core/config.py new file mode 100644 index 0000000..0ff4454 --- /dev/null +++ b/app/app/core/config.py @@ -0,0 +1,57 @@ +# app/core/config.py +import secrets +from typing import Any, Dict, List, Optional, Union + +from pydantic import AnyHttpUrl, PostgresDsn, validator +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + API_V1_STR: str = "/api" + SECRET_KEY: str = secrets.token_urlsafe(32) + # 60 minutes * 24 hours * 8 days = 8 days + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 + SERVER_NAME: str = "HouseHold API" + SERVER_HOST: AnyHttpUrl = "http://localhost:8000" + # BACKEND_CORS_ORIGINS is a JSON-formatted list of origins + # e.g: '' + BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [] + + @validator("BACKEND_CORS_ORIGINS", pre=True) + def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]: + if isinstance(v, str) and not v.startswith("["): + return [i.strip() for i in v.split(",")] + elif isinstance(v, (list, str)): + return v + raise ValueError(v) + + PROJECT_NAME: str = "HouseHold API" + + POSTGRES_SERVER: str = "localhost" + POSTGRES_USER: str = "postgres" + POSTGRES_PASSWORD: str = "postgres" + POSTGRES_DB: str = "household" + SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None + + @validator("SQLALCHEMY_DATABASE_URI", pre=True) + def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any: + if isinstance(v, str): + return v + return PostgresDsn.build( + scheme="postgresql+asyncpg", + username=values.get("POSTGRES_USER"), + password=values.get("POSTGRES_PASSWORD"), + host=values.get("POSTGRES_SERVER"), + path=f"/{values.get('POSTGRES_DB') or ''}", + ) + + # OCR Service + OCR_API_KEY: Optional[str] = None + OCR_SERVICE_URL: Optional[str] = None + + class Config: + case_sensitive = True + env_file = ".env" + + +settings = Settings() diff --git a/app/app/core/dependencies.py b/app/app/core/dependencies.py new file mode 100644 index 0000000..726a5c6 --- /dev/null +++ b/app/app/core/dependencies.py @@ -0,0 +1,63 @@ +# app/core/dependencies.py +from typing import Annotated, Optional + +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from jose import JWTError, jwt +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + +from app.core.config import settings +from app.core.security import ALGORITHM +from app.db.session import get_db +from app.models.user import User + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login") + + +async def get_current_user( + db: Annotated[AsyncSession, Depends(get_db)], + token: Annotated[str, Depends(oauth2_scheme)], +) -> User: + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM]) + user_id: str = payload.get("sub") + if user_id is None: + raise credentials_exception + except JWTError: + raise credentials_exception + + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalars().first() + + if user is None: + raise credentials_exception + return user + + +async def check_house_membership( + db: AsyncSession, user_id: str, house_id: str, required_role: Optional[str] = None +) -> bool: + """Check if a user is a member of a house with optional role check.""" + from app.models.house import HouseMember + + query = select(HouseMember).where( + HouseMember.user_id == user_id, + HouseMember.house_id == house_id + ) + + result = await db.execute(query) + membership = result.scalars().first() + + if not membership: + return False + + if required_role and membership.role != required_role: + return False + + return True diff --git a/app/app/core/logger.py b/app/app/core/logger.py new file mode 100644 index 0000000..68243ce --- /dev/null +++ b/app/app/core/logger.py @@ -0,0 +1,64 @@ +# app/core/logger.py +import logging +import logging.config + +def configure_logging(): + """Configure basic logging.""" + logging_config = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "simple": { + "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + } + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "simple", + "stream": "ext://sys.stdout", + }, + "file": { + "class": "logging.FileHandler", + "level": "INFO", + "formatter": "simple", + "filename": "household_api.log", + "mode": "a", # Append to the log file + }, + }, + "loggers": { + "api": { + "handlers": ["console", "file"], + "level": "DEBUG", + "propagate": True, + }, + "services": { + "handlers": ["console", "file"], + "level": "INFO", + "propagate": True, + }, + "db": { + "handlers": ["console", "file"], + "level": "WARN", + "propagate": True, + }, + "shopping_lists": { + "handlers": ["console", "file"], + "level": "INFO", + "propagate": True, + }, + }, + "root": { + "level": "WARNING", + "handlers": ["console", "file"], + }, + } + logging.config.dictConfig(logging_config) + +# Initialize logging configuration +configure_logging() + +# Get logger instance for use in other modules +logger = logging.getLogger("api") +shopping_lists_logger = logging.getLogger("shopping_lists") diff --git a/app/app/core/security.py b/app/app/core/security.py new file mode 100644 index 0000000..2cf5437 --- /dev/null +++ b/app/app/core/security.py @@ -0,0 +1,34 @@ +# app/core/security.py +from datetime import datetime, timedelta +from typing import Any, Optional, Union + +from jose import jwt +from passlib.context import CryptContext + +from app.core.config import settings + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +ALGORITHM = "HS256" + + +def create_access_token( + subject: Union[str, Any], expires_delta: Optional[timedelta] = None +) -> str: + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta( + minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES + ) + to_encode = {"exp": expire, "sub": str(subject)} + encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) diff --git a/app/app/db/__init__.py b/app/app/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/db/__pycache__/__init__.cpython-312.pyc b/app/app/db/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..e79a763 Binary files /dev/null and b/app/app/db/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/app/db/__pycache__/base.cpython-312.pyc b/app/app/db/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000..f1320af Binary files /dev/null and b/app/app/db/__pycache__/base.cpython-312.pyc differ diff --git a/app/app/db/__pycache__/session.cpython-312.pyc b/app/app/db/__pycache__/session.cpython-312.pyc new file mode 100644 index 0000000..d74a059 Binary files /dev/null and b/app/app/db/__pycache__/session.cpython-312.pyc differ diff --git a/app/app/db/base.py b/app/app/db/base.py new file mode 100644 index 0000000..6568d72 --- /dev/null +++ b/app/app/db/base.py @@ -0,0 +1,17 @@ +# app/db/base.py +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +from app.core.config import settings + +engine = create_async_engine( + str(settings.SQLALCHEMY_DATABASE_URI), + echo=True, + future=True, +) +AsyncSessionLocal = sessionmaker( + engine, class_=AsyncSession, expire_on_commit=False +) + +Base = declarative_base() diff --git a/app/app/db/init_db.py b/app/app/db/init_db.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/db/session.py b/app/app/db/session.py new file mode 100644 index 0000000..46311cb --- /dev/null +++ b/app/app/db/session.py @@ -0,0 +1,18 @@ +# app/db/session.py +from typing import AsyncGenerator + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.db.base import AsyncSessionLocal + + +async def get_db() -> AsyncGenerator[AsyncSession, None]: + async with AsyncSessionLocal() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise + finally: + await session.close() diff --git a/app/app/main.py b/app/app/main.py new file mode 100644 index 0000000..ea25177 --- /dev/null +++ b/app/app/main.py @@ -0,0 +1,66 @@ +# app/main.py +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from app.core.logger import logger + +from app.api.auth.router import router as auth_router +from app.api.users.router import router as users_router +from app.api.chores.router import router as chores_router +from app.api.expenses.router import router as expenses_router +from app.api.houses.router import router as houses_router +from app.api.ocr.router import router as ocr_router +from app.api.shopping_lists.router import router as shopping_lists_router +from app.core.config import settings + +app = FastAPI( + title=settings.PROJECT_NAME, + openapi_url=f"{settings.API_V1_STR}/openapi.json", +) + +# Set all CORS enabled origins +if settings.BACKEND_CORS_ORIGINS: + app.add_middleware( + CORSMiddleware, + allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + +@app.middleware("http") +async def log_requests(request: Request, call_next): + logger.info(f"Request: {request.method} {request.url}") + response = await call_next(request) + logger.info(f"Response: {response.status_code}") + return response + +@app.on_event("startup") +async def startup_event(): + logger.info("Starting up...") + +@app.on_event("shutdown") +async def shutdown_event(): + logger.info("Shutting down...") + +# Include routers +app.include_router(auth_router, prefix=f"{settings.API_V1_STR}/auth", tags=["auth"]) +app.include_router(houses_router, prefix=f"{settings.API_V1_STR}/houses", tags=["houses"]) +app.include_router( + shopping_lists_router, prefix=f"{settings.API_V1_STR}/houses", tags=["shopping_lists"] +) +app.include_router( + expenses_router, prefix=f"{settings.API_V1_STR}/houses", tags=["expenses"] +) +app.include_router( + chores_router, prefix=f"{settings.API_V1_STR}/houses", tags=["chores"] +) +app.include_router(ocr_router, prefix=f"{settings.API_V1_STR}/ocr", tags=["ocr"]) +app.include_router( + users_router, + prefix=f"{settings.API_V1_STR}/users", + tags=["users"] +) + +@app.get("/") +async def root(): + return {"message": "Welcome to the Household API"} diff --git a/app/app/models/__init__.py b/app/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/models/__pycache__/__init__.cpython-312.pyc b/app/app/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..253efdc Binary files /dev/null and b/app/app/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/app/models/__pycache__/chore.cpython-312.pyc b/app/app/models/__pycache__/chore.cpython-312.pyc new file mode 100644 index 0000000..b56099a Binary files /dev/null and b/app/app/models/__pycache__/chore.cpython-312.pyc differ diff --git a/app/app/models/__pycache__/expense.cpython-312.pyc b/app/app/models/__pycache__/expense.cpython-312.pyc new file mode 100644 index 0000000..e265c67 Binary files /dev/null and b/app/app/models/__pycache__/expense.cpython-312.pyc differ diff --git a/app/app/models/__pycache__/house.cpython-312.pyc b/app/app/models/__pycache__/house.cpython-312.pyc new file mode 100644 index 0000000..2381d5a Binary files /dev/null and b/app/app/models/__pycache__/house.cpython-312.pyc differ diff --git a/app/app/models/__pycache__/invite.cpython-312.pyc b/app/app/models/__pycache__/invite.cpython-312.pyc new file mode 100644 index 0000000..a92ae48 Binary files /dev/null and b/app/app/models/__pycache__/invite.cpython-312.pyc differ diff --git a/app/app/models/__pycache__/shopping_list.cpython-312.pyc b/app/app/models/__pycache__/shopping_list.cpython-312.pyc new file mode 100644 index 0000000..8806ef6 Binary files /dev/null and b/app/app/models/__pycache__/shopping_list.cpython-312.pyc differ diff --git a/app/app/models/__pycache__/user.cpython-312.pyc b/app/app/models/__pycache__/user.cpython-312.pyc new file mode 100644 index 0000000..c8a1292 Binary files /dev/null and b/app/app/models/__pycache__/user.cpython-312.pyc differ diff --git a/app/app/models/chore.py b/app/app/models/chore.py new file mode 100644 index 0000000..ac7ec17 --- /dev/null +++ b/app/app/models/chore.py @@ -0,0 +1,27 @@ +# app/models/chore.py +import uuid +from datetime import datetime + +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, String, Text +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship + +from app.db.base import Base + + +class Chore(Base): + __tablename__ = "chores" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + house_id = Column(UUID(as_uuid=True), ForeignKey("houses.id"), nullable=False) + title = Column(String(255), nullable=False) + description = Column(Text) + assigned_to = Column(UUID(as_uuid=True), ForeignKey("users.id")) + due_date = Column(DateTime) + is_completed = Column(Boolean, default=False) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # Relationships + house = relationship("House", back_populates="chores") + assignee = relationship("User") diff --git a/app/app/models/expense.py b/app/app/models/expense.py new file mode 100644 index 0000000..b18b6d2 --- /dev/null +++ b/app/app/models/expense.py @@ -0,0 +1,40 @@ +# app/models/expense.py +import uuid +from datetime import datetime + +from sqlalchemy import Column, DateTime, ForeignKey, Numeric, Text +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship + +from app.db.base import Base + + +class Expense(Base): + __tablename__ = "expenses" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + list_id = Column(UUID(as_uuid=True), ForeignKey("shopping_lists.id"), nullable=False) + payer_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + amount = Column(Numeric(10, 2), nullable=False) + description = Column(Text) + date = Column(DateTime, default=datetime.utcnow) + created_at = Column(DateTime, default=datetime.utcnow) + + # Relationships + shopping_list = relationship("ShoppingList", back_populates="expenses") + payer = relationship("User") + splits = relationship("ExpenseSplit", back_populates="expense", cascade="all, delete-orphan") + + +class ExpenseSplit(Base): + __tablename__ = "expense_splits" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + expense_id = Column(UUID(as_uuid=True), ForeignKey("expenses.id"), nullable=False) + user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + amount = Column(Numeric(10, 2), nullable=False) + created_at = Column(DateTime, default=datetime.utcnow) + + # Relationships + expense = relationship("Expense", back_populates="splits") + user = relationship("User") diff --git a/app/app/models/house.py b/app/app/models/house.py new file mode 100644 index 0000000..a849355 --- /dev/null +++ b/app/app/models/house.py @@ -0,0 +1,55 @@ +# app/models/house.py +import uuid +from datetime import datetime + +from sqlalchemy import Column, DateTime, ForeignKey, String, Text +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship + +from app.db.base import Base + + +class House(Base): + __tablename__ = "houses" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + name = Column(String(255), nullable=False) + description = Column(Text) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # Relationships + members = relationship( + "HouseMember", + back_populates="house", + cascade="all, delete-orphan" + ) + shopping_lists = relationship( + "ShoppingList", + back_populates="house", + cascade="all, delete-orphan" + ) + chores = relationship( + "Chore", + back_populates="house", + cascade="all, delete-orphan" + ) + invites = relationship( + "HouseInvite", + back_populates="house", + cascade="all, delete-orphan" + ) + + +class HouseMember(Base): + __tablename__ = "house_members" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + house_id = Column(UUID(as_uuid=True), ForeignKey("houses.id"), nullable=False) + user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + role = Column(String(50), default="member") + created_at = Column(DateTime, default=datetime.utcnow) + + # Relationships + house = relationship("House", back_populates="members") + user = relationship("User") diff --git a/app/app/models/invite.py b/app/app/models/invite.py new file mode 100644 index 0000000..9c687be --- /dev/null +++ b/app/app/models/invite.py @@ -0,0 +1,25 @@ +# app/models/invite.py +import uuid +from datetime import datetime + +from sqlalchemy import Column, DateTime, ForeignKey, String +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship + +from app.db.base import Base + + +class HouseInvite(Base): + __tablename__ = "house_invites" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + code = Column(String(255), unique=True, nullable=False, index=True) + house_id = Column(UUID(as_uuid=True), ForeignKey("houses.id"), nullable=False) + inviter_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + created_at = Column(DateTime, default=datetime.utcnow) + expires_at = Column(DateTime, nullable=False) + + # Relationships + # (The host-side as a “child” of House; see also the update below in app/models/house.py) + house = relationship("House", back_populates="invites") + inviter = relationship("User") diff --git a/app/app/models/shopping_list.py b/app/app/models/shopping_list.py new file mode 100644 index 0000000..f969933 --- /dev/null +++ b/app/app/models/shopping_list.py @@ -0,0 +1,53 @@ +# app/models/shopping_list.py +import uuid +from datetime import datetime + +from sqlalchemy import ( + Boolean, + Column, + DateTime, + ForeignKey, + Integer, + Numeric, + String, + Text, +) +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship + +from app.db.base import Base + + +class ShoppingList(Base): + __tablename__ = "shopping_lists" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + house_id = Column(UUID(as_uuid=True), ForeignKey("houses.id"), nullable=False) + title = Column(String(255), nullable=False) + description = Column(Text) + is_archived = Column(Boolean, default=False) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # Relationships + house = relationship("House", back_populates="shopping_lists") + items = relationship("ListItem", back_populates="shopping_list", cascade="all, delete-orphan") + expenses = relationship("Expense", back_populates="shopping_list", cascade="all, delete-orphan") + + +class ListItem(Base): + __tablename__ = "list_items" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + list_id = Column(UUID(as_uuid=True), ForeignKey("shopping_lists.id"), nullable=False) + name = Column(String(255), nullable=False) + quantity = Column(Numeric(10, 2), default=1) + unit = Column(String(50)) + price = Column(Numeric(10, 2)) + is_completed = Column(Boolean, default=False) + position = Column(Integer) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # Relationships + shopping_list = relationship("ShoppingList", back_populates="items") diff --git a/app/app/models/user.py b/app/app/models/user.py new file mode 100644 index 0000000..607c5b1 --- /dev/null +++ b/app/app/models/user.py @@ -0,0 +1,19 @@ +# app/models/user.py +import uuid +from datetime import datetime + +from sqlalchemy import Column, DateTime, String +from sqlalchemy.dialects.postgresql import UUID + +from app.db.base import Base + + +class User(Base): + __tablename__ = "users" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + email = Column(String(255), unique=True, nullable=False, index=True) + password_hash = Column(String(255), nullable=False) + full_name = Column(String(255)) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) diff --git a/app/app/schemas/__init__.py b/app/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/schemas/__pycache__/__init__.cpython-312.pyc b/app/app/schemas/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..955fa7e Binary files /dev/null and b/app/app/schemas/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/app/schemas/__pycache__/chore.cpython-312.pyc b/app/app/schemas/__pycache__/chore.cpython-312.pyc new file mode 100644 index 0000000..7b06818 Binary files /dev/null and b/app/app/schemas/__pycache__/chore.cpython-312.pyc differ diff --git a/app/app/schemas/__pycache__/expense.cpython-312.pyc b/app/app/schemas/__pycache__/expense.cpython-312.pyc new file mode 100644 index 0000000..a0574cc Binary files /dev/null and b/app/app/schemas/__pycache__/expense.cpython-312.pyc differ diff --git a/app/app/schemas/__pycache__/house.cpython-312.pyc b/app/app/schemas/__pycache__/house.cpython-312.pyc new file mode 100644 index 0000000..d5b0e92 Binary files /dev/null and b/app/app/schemas/__pycache__/house.cpython-312.pyc differ diff --git a/app/app/schemas/__pycache__/invite.cpython-312.pyc b/app/app/schemas/__pycache__/invite.cpython-312.pyc new file mode 100644 index 0000000..61febb0 Binary files /dev/null and b/app/app/schemas/__pycache__/invite.cpython-312.pyc differ diff --git a/app/app/schemas/__pycache__/shopping_list.cpython-312.pyc b/app/app/schemas/__pycache__/shopping_list.cpython-312.pyc new file mode 100644 index 0000000..5ae586e Binary files /dev/null and b/app/app/schemas/__pycache__/shopping_list.cpython-312.pyc differ diff --git a/app/app/schemas/__pycache__/user.cpython-312.pyc b/app/app/schemas/__pycache__/user.cpython-312.pyc new file mode 100644 index 0000000..51eb655 Binary files /dev/null and b/app/app/schemas/__pycache__/user.cpython-312.pyc differ diff --git a/app/app/schemas/chore.py b/app/app/schemas/chore.py new file mode 100644 index 0000000..e50a457 --- /dev/null +++ b/app/app/schemas/chore.py @@ -0,0 +1,42 @@ +# app/schemas/chore.py +from datetime import datetime +from typing import Optional +from uuid import UUID + +from pydantic import BaseModel + + +class ChoreBase(BaseModel): + title: str + description: Optional[str] = None + due_date: Optional[datetime] = None + + +class ChoreCreate(ChoreBase): + assigned_to: Optional[UUID] = None + + +class ChoreUpdate(ChoreBase): + title: Optional[str] = None + assigned_to: Optional[UUID] = None + is_completed: Optional[bool] = None + + +class ChoreAssign(BaseModel): + assigned_to: UUID + + +class ChoreComplete(BaseModel): + is_completed: bool + + +class Chore(ChoreBase): + id: UUID + house_id: UUID + assigned_to: Optional[UUID] = None + is_completed: bool + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True diff --git a/app/app/schemas/expense.py b/app/app/schemas/expense.py new file mode 100644 index 0000000..41042a1 --- /dev/null +++ b/app/app/schemas/expense.py @@ -0,0 +1,57 @@ +# app/schemas/expense.py +from datetime import datetime +from decimal import Decimal +from typing import List, Optional +from uuid import UUID + +from pydantic import BaseModel + + +class ExpenseSplitBase(BaseModel): + user_id: UUID + amount: Decimal + + +class ExpenseSplitCreate(ExpenseSplitBase): + pass + + +class ExpenseSplit(ExpenseSplitBase): + id: UUID + expense_id: UUID + created_at: datetime + + class Config: + from_attributes = True + + +class ExpenseBase(BaseModel): + amount: Decimal + description: Optional[str] = None + date: Optional[datetime] = None + + +class ExpenseCreate(ExpenseBase): + payer_id: UUID + splits: List[ExpenseSplitCreate] + + +class ExpenseUpdate(ExpenseBase): + payer_id: Optional[UUID] = None + splits: Optional[List[ExpenseSplitCreate]] = None + + +class Expense(ExpenseBase): + id: UUID + list_id: UUID + payer_id: UUID + created_at: datetime + splits: List[ExpenseSplit] = [] + + class Config: + from_attributes = True + + +class ExpenseSummary(BaseModel): + total_amount: Decimal + user_balances: dict[UUID, Decimal] diff --git a/app/app/schemas/house.py b/app/app/schemas/house.py new file mode 100644 index 0000000..bec52cc --- /dev/null +++ b/app/app/schemas/house.py @@ -0,0 +1,62 @@ +# app/schemas/house.py +from datetime import datetime +from typing import List, Optional +from uuid import UUID + +from pydantic import BaseModel + + +class HouseBase(BaseModel): + name: str + description: Optional[str] = None + + +class HouseCreate(HouseBase): + pass + + +class HouseUpdate(HouseBase): + name: Optional[str] = None + + +class HouseMemberBase(BaseModel): + role: str = "member" + + +class HouseMemberCreate(HouseMemberBase): + user_id: UUID + + +class HouseMemberUpdate(HouseMemberBase): + pass + + +class HouseMember(HouseMemberBase): + id: UUID + house_id: UUID + user_id: UUID + created_at: datetime + + class Config: + from_attributes = True + + +class HouseMemberResponse(BaseModel): + id: UUID + house_id: UUID + user_id: UUID + role: str + created_at: datetime + + class Config: + orm_mode = True + + +class House(HouseBase): + id: UUID + created_at: datetime + updated_at: datetime + members: List[HouseMember] = [] + + class Config: + from_attributes = True diff --git a/app/app/schemas/invite.py b/app/app/schemas/invite.py new file mode 100644 index 0000000..803b945 --- /dev/null +++ b/app/app/schemas/invite.py @@ -0,0 +1,26 @@ +# app/schemas/invite.py +from datetime import datetime +from uuid import UUID + +from pydantic import BaseModel, Field + + +class HouseInviteBase(BaseModel): + pass + + +class HouseInviteCreate(HouseInviteBase): + # Number of minutes until this invite expires (default: 60 minutes) + expires_in_minutes: int = Field(default=60) + + +class HouseInvite(HouseInviteBase): + id: UUID + code: str + house_id: UUID + inviter_id: UUID + created_at: datetime + expires_at: datetime + + class Config: + from_attributes = True diff --git a/app/app/schemas/shopping_list.py b/app/app/schemas/shopping_list.py new file mode 100644 index 0000000..f346aed --- /dev/null +++ b/app/app/schemas/shopping_list.py @@ -0,0 +1,66 @@ +# app/schemas/shopping_list.py +from datetime import datetime +from decimal import Decimal +from typing import List, Optional +from uuid import UUID + +from pydantic import BaseModel, Field + + +class ListItemBase(BaseModel): + name: str + quantity: Optional[Decimal] = Field(1, ge=0) + unit: Optional[str] = None + price: Optional[Decimal] = None + position: Optional[int] = None + + +class ListItemCreate(ListItemBase): + pass + + +class ListItemUpdate(ListItemBase): + name: Optional[str] = None + is_completed: Optional[bool] = None + + +class ListItem(ListItemBase): + id: UUID + list_id: UUID + is_completed: bool + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + + +class ShoppingListBase(BaseModel): + title: str + description: Optional[str] = None + + +class ShoppingListCreate(ShoppingListBase): + pass + + +class ShoppingListUpdate(ShoppingListBase): + title: Optional[str] = None + is_archived: Optional[bool] = None + + +class ShoppingList(ShoppingListBase): + id: UUID + house_id: UUID + is_archived: bool + created_at: datetime + updated_at: datetime + items: List[ListItem] = [] + + class Config: + from_attributes = True + + +class ItemReorder(BaseModel): + item_id: UUID + new_position: int diff --git a/app/app/schemas/user.py b/app/app/schemas/user.py new file mode 100644 index 0000000..f43bc42 --- /dev/null +++ b/app/app/schemas/user.py @@ -0,0 +1,37 @@ +# app/schemas/user.py +from datetime import datetime +from typing import Optional +from uuid import UUID + +from pydantic import BaseModel, EmailStr, Field + + +class UserBase(BaseModel): + email: Optional[EmailStr] = None + full_name: Optional[str] = None + + +class UserCreate(UserBase): + email: EmailStr + password: str = Field(..., min_length=8) + + +class UserUpdate(UserBase): + password: Optional[str] = Field(None, min_length=8) + + +class UserInDBBase(UserBase): + id: UUID + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + + +class User(UserInDBBase): + pass + + +class UserInDB(UserInDBBase): + password_hash: str diff --git a/app/app/services/__init__.py b/app/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/services/__pycache__/__init__.cpython-312.pyc b/app/app/services/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..458701d Binary files /dev/null and b/app/app/services/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/app/services/__pycache__/invite_service.cpython-312.pyc b/app/app/services/__pycache__/invite_service.cpython-312.pyc new file mode 100644 index 0000000..eb6af0f Binary files /dev/null and b/app/app/services/__pycache__/invite_service.cpython-312.pyc differ diff --git a/app/app/services/__pycache__/ocr_service.cpython-312.pyc b/app/app/services/__pycache__/ocr_service.cpython-312.pyc new file mode 100644 index 0000000..a85d673 Binary files /dev/null and b/app/app/services/__pycache__/ocr_service.cpython-312.pyc differ diff --git a/app/app/services/auth_service.py b/app/app/services/auth_service.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/services/chore_service.py b/app/app/services/chore_service.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/services/db_service.py b/app/app/services/db_service.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/services/expense_service.py b/app/app/services/expense_service.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app/services/invite_service.py b/app/app/services/invite_service.py new file mode 100644 index 0000000..f9d46ca --- /dev/null +++ b/app/app/services/invite_service.py @@ -0,0 +1,16 @@ +# app/services/invite_service.py +import random + +ADJECTIVES = [ + "happy", "bright", "blue", "swift", "gentle", "fancy", "warm", "lucky", + "brave", "calm", "eager", "jolly" +] +NOUNS = [ + "panda", "tiger", "river", "forest", "sky", "mountain", "ocean", "falcon", + "eagle", "lion", "wolf", "bear" +] + + +def generate_invite_code() -> str: + """Return a random code made up of an adjective and a noun.""" + return f"{random.choice(ADJECTIVES)}-{random.choice(NOUNS)}" diff --git a/app/app/services/ocr_service.py b/app/app/services/ocr_service.py new file mode 100644 index 0000000..d4bcc8f --- /dev/null +++ b/app/app/services/ocr_service.py @@ -0,0 +1,89 @@ +# app/services/ocr_service.py +import io +import logging +from typing import List, Optional + +# from google.cloud import vision +# from google.cloud.vision_v1 import types + +from app.core.config import settings +from app.schemas.shopping_list import ListItemCreate + +logger = logging.getLogger(__name__) + + +class OCRService: + def __init__(self): + self.client = None + if settings.OCR_API_KEY: + try: + self.client = vision.ImageAnnotatorClient.from_service_account_json( + settings.OCR_API_KEY + ) + except Exception as e: + logger.error(f"Failed to initialize OCR client: {e}") + + async def process_image(self, image_bytes: bytes) -> List[dict]: + """Process an image and extract text.""" + if not self.client: + logger.error("OCR client not initialized") + return [] + + try: + image = types.Image(content=image_bytes) + response = self.client.text_detection(image=image) + texts = response.text_annotations + + # The first text annotation contains the entire detected text + if not texts: + return [] + + # Extract potential shopping items from the text + full_text = texts[0].description + return self._extract_items_from_text(full_text) + except Exception as e: + logger.error(f"Error processing image: {e}") + return [] + + def _extract_items_from_text(self, text: str) -> List[dict]: + """Extract potential shopping items from the OCR text.""" + # This is a simplified implementation + # In a real-world scenario, you would use more sophisticated NLP techniques + + lines = text.split("\n") + items = [] + + for line in lines: + # Skip very short lines or lines that are likely headers + if len(line) < 3 or line.isupper() or "TOTAL" in line.upper(): + continue + + # Try to extract item name and potentially price + parts = line.split() + if not parts: + continue + + # Simple heuristic: look for price patterns + item_name = " ".join(parts[:-1]) if len(parts) > 1 else line + price = None + + # Check if the last part looks like a price + if len(parts) > 1 and parts[-1].replace(".", "").replace(",", "").isdigit(): + try: + price = float(parts[-1].replace(",", ".")) + item_name = " ".join(parts[:-1]) + except ValueError: + pass + + # Add the item if we have a name + if item_name.strip(): + items.append({ + "name": item_name.strip(), + "price": price, + "quantity": 1, + }) + + return items + + +ocr_service = OCRService() diff --git a/app/docker-compose.yml b/app/docker-compose.yml new file mode 100644 index 0000000..7bc79aa --- /dev/null +++ b/app/docker-compose.yml @@ -0,0 +1,31 @@ +# docker-compose.yml +version: '3' + +services: + api: + build: . + ports: + - "8000:8000" + depends_on: + - db + environment: + - POSTGRES_SERVER=db + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=household + volumes: + - ./:/app/ + + db: + image: postgres:15 + volumes: + - postgres_data:/var/lib/postgresql/data/ + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=household + ports: + - "5432:5432" + +volumes: + postgres_data: diff --git a/app/household_api.log b/app/household_api.log new file mode 100644 index 0000000..97c21e4 --- /dev/null +++ b/app/household_api.log @@ -0,0 +1,38 @@ +2025-03-25 14:29:37,555 - api - INFO - Starting up... +2025-03-25 14:29:37,555 - api - INFO - Starting up... +2025-03-25 14:34:36,079 - api - INFO - Shutting down... +2025-03-25 14:34:36,079 - api - INFO - Shutting down... +2025-03-25 14:34:37,899 - api - INFO - Starting up... +2025-03-25 14:34:37,899 - api - INFO - Starting up... +2025-03-25 15:36:53,193 - api - INFO - Shutting down... +2025-03-25 15:36:53,193 - api - INFO - Shutting down... +2025-03-25 15:36:55,243 - api - INFO - Starting up... +2025-03-25 15:36:55,243 - api - INFO - Starting up... +2025-03-25 16:04:47,516 - api - INFO - Starting up... +2025-03-25 16:04:47,516 - api - INFO - Starting up... +2025-03-25 16:07:04,886 - api - INFO - Request: OPTIONS http://localhost:8000/api/auth/register +2025-03-25 16:07:04,886 - api - INFO - Request: OPTIONS http://localhost:8000/api/auth/register +2025-03-25 16:07:04,886 - api - INFO - Response: 400 +2025-03-25 16:07:04,886 - api - INFO - Response: 400 +2025-03-25 16:10:14,792 - api - INFO - Request: POST http://localhost:8000/api/auth/login +2025-03-25 16:10:14,792 - api - INFO - Request: POST http://localhost:8000/api/auth/login +2025-03-25 16:10:17,590 - api - INFO - Request: POST http://localhost:8000/api/auth/login +2025-03-25 16:10:17,590 - api - INFO - Request: POST http://localhost:8000/api/auth/login +2025-03-25 20:57:50,843 - api - INFO - Shutting down... +2025-03-25 20:57:50,843 - api - INFO - Shutting down... +2025-03-25 20:57:54,809 - api - INFO - Starting up... +2025-03-25 20:57:54,809 - api - INFO - Starting up... +2025-03-25 20:58:16,244 - api - INFO - Request: POST http://localhost:8000/api/auth/login +2025-03-25 20:58:16,244 - api - INFO - Request: POST http://localhost:8000/api/auth/login +2025-03-25 20:58:17,139 - sqlalchemy.engine.Engine - INFO - select pg_catalog.version() +2025-03-25 20:58:17,139 - sqlalchemy.engine.Engine - INFO - [raw sql] () +2025-03-25 20:58:17,197 - sqlalchemy.engine.Engine - INFO - select current_schema() +2025-03-25 20:58:17,197 - sqlalchemy.engine.Engine - INFO - [raw sql] () +2025-03-25 20:58:17,275 - sqlalchemy.engine.Engine - INFO - show standard_conforming_strings +2025-03-25 20:58:17,276 - sqlalchemy.engine.Engine - INFO - [raw sql] () +2025-03-25 20:58:17,319 - sqlalchemy.engine.Engine - INFO - BEGIN (implicit) +2025-03-25 20:58:17,348 - sqlalchemy.engine.Engine - INFO - SELECT users.id, users.email, users.password_hash, users.full_name, users.created_at, users.updated_at +FROM users +WHERE users.email = $1::VARCHAR +2025-03-25 20:58:17,348 - sqlalchemy.engine.Engine - INFO - [generated in 0.00074s] ('mo@mo.mo',) +2025-03-25 20:58:17,401 - sqlalchemy.engine.Engine - INFO - ROLLBACK diff --git a/app/pyproject.toml b/app/pyproject.toml new file mode 100644 index 0000000..3d02710 --- /dev/null +++ b/app/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "dooey" +version = "0.1.0" +description = "Household management API" +dependencies = [ + "fastapi>=0.104.0", + "uvicorn>=0.23.2", + "sqlalchemy>=2.0.22", + "alembic>=1.12.0", + "asyncpg>=0.28.0", + "pydantic>=2.4.2", + "pydantic-settings>=2.0.3", + "python-jose>=3.3.0", + "passlib>=1.7.4", + "python-multipart>=0.0.6", + "bcrypt>=4.0.1" +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.4.2", + "pytest-asyncio>=0.21.1", + "httpx>=0.25.0", + "black>=23.10.0", + "isort>=5.12.0", + "mypy>=1.6.1" +] diff --git a/app/tests/api/auth.py b/app/tests/api/auth.py new file mode 100644 index 0000000..76d7477 --- /dev/null +++ b/app/tests/api/auth.py @@ -0,0 +1,50 @@ +import pytest +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession + +from app.main import app +from app.db.session import get_db +from app.models.user import User + +@pytest.fixture +async def client(): + async with AsyncClient(app=app, base_url="http://test") as ac: + yield ac + +@pytest.fixture +async def db_session(): + async with AsyncSession() as session: + yield session + +@pytest.mark.asyncio +async def test_register(client: AsyncClient, db_session: AsyncSession): + response = await client.post("/register", json={ + "email": "test@example.com", + "password": "password123", + "full_name": "Test User" + }) + assert response.status_code == 200 + data = response.json() + assert data["email"] == "test@example.com" + assert "id" in data + +@pytest.mark.asyncio +async def test_login(client: AsyncClient, db_session: AsyncSession): + # Create a user first + user = User( + email="test@example.com", + password_hash=get_password_hash("password123"), + full_name="Test User" + ) + db_session.add(user) + await db_session.commit() + await db_session.refresh(user) + + response = await client.post("/login", data={ + "username": "test@example.com", + "password": "password123" + }) + assert response.status_code == 200 + data = response.json() + assert "access_token" in data + assert data["token_type"] == "bearer" diff --git a/app/tests/api/shopping_lists.py b/app/tests/api/shopping_lists.py new file mode 100644 index 0000000..02c1f1c --- /dev/null +++ b/app/tests/api/shopping_lists.py @@ -0,0 +1,109 @@ +import pytest +from httpx import AsyncClient +from fastapi import status +from sqlalchemy.ext.asyncio import AsyncSession +from app.models.shopping_list import ShoppingList, ListItem +from app.schemas.shopping_list import ShoppingListCreate, ListItemCreate + +@pytest.mark.asyncio +async def test_get_shopping_lists(client: AsyncClient, db_session: AsyncSession, test_user, test_house): + response = await client.get(f"/{test_house.id}/lists", headers={"Authorization": f"Bearer {test_user.token}"}) + assert response.status_code == status.HTTP_200_OK + +@pytest.mark.asyncio +async def test_create_shopping_list(client: AsyncClient, db_session: AsyncSession, test_user, test_house): + shopping_list_data = ShoppingListCreate(name="Groceries") + response = await client.post( + f"/{test_house.id}/lists", + json=shopping_list_data.dict(), + headers={"Authorization": f"Bearer {test_user.token}"} + ) + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["name"] == shopping_list_data.name + +@pytest.mark.asyncio +async def test_get_shopping_list(client: AsyncClient, db_session: AsyncSession, test_user, test_house, test_shopping_list): + response = await client.get( + f"/{test_house.id}/lists/{test_shopping_list.id}", + headers={"Authorization": f"Bearer {test_user.token}"} + ) + assert response.status_code == status.HTTP_200_OK + assert response.json()["id"] == str(test_shopping_list.id) + +@pytest.mark.asyncio +async def test_update_shopping_list(client: AsyncClient, db_session: AsyncSession, test_user, test_house, test_shopping_list): + update_data = {"name": "Updated List"} + response = await client.put( + f"/{test_house.id}/lists/{test_shopping_list.id}", + json=update_data, + headers={"Authorization": f"Bearer {test_user.token}"} + ) + assert response.status_code == status.HTTP_200_OK + assert response.json()["name"] == update_data["name"] + +@pytest.mark.asyncio +async def test_delete_shopping_list(client: AsyncClient, db_session: AsyncSession, test_user, test_house, test_shopping_list): + response = await client.delete( + f"/{test_house.id}/lists/{test_shopping_list.id}", + headers={"Authorization": f"Bearer {test_user.token}"} + ) + assert response.status_code == status.HTTP_204_NO_CONTENT + +@pytest.mark.asyncio +async def test_get_list_items(client: AsyncClient, db_session: AsyncSession, test_user, test_house, test_shopping_list): + response = await client.get( + f"/{test_house.id}/lists/{test_shopping_list.id}/items", + headers={"Authorization": f"Bearer {test_user.token}"} + ) + assert response.status_code == status.HTTP_200_OK + +@pytest.mark.asyncio +async def test_add_list_item(client: AsyncClient, db_session: AsyncSession, test_user, test_house, test_shopping_list): + item_data = ListItemCreate(name="Milk", quantity=2) + response = await client.post( + f"/{test_house.id}/lists/{test_shopping_list.id}/items", + json=item_data.dict(), + headers={"Authorization": f"Bearer {test_user.token}"} + ) + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["name"] == item_data.name + +@pytest.mark.asyncio +async def test_update_list_item(client: AsyncClient, db_session: AsyncSession, test_user, test_house, test_shopping_list, test_list_item): + update_data = {"name": "Updated Item"} + response = await client.put( + f"/{test_house.id}/lists/{test_shopping_list.id}/items/{test_list_item.id}", + json=update_data, + headers={"Authorization": f"Bearer {test_user.token}"} + ) + assert response.status_code == status.HTTP_200_OK + assert response.json()["name"] == update_data["name"] + +@pytest.mark.asyncio +async def test_delete_list_item(client: AsyncClient, db_session: AsyncSession, test_user, test_house, test_shopping_list, test_list_item): + response = await client.delete( + f"/{test_house.id}/lists/{test_shopping_list.id}/items/{test_list_item.id}", + headers={"Authorization": f"Bearer {test_user.token}"} + ) + assert response.status_code == status.HTTP_204_NO_CONTENT + +@pytest.mark.asyncio +async def test_mark_item_complete(client: AsyncClient, db_session: AsyncSession, test_user, test_house, test_shopping_list, test_list_item): + response = await client.patch( + f"/{test_house.id}/lists/{test_shopping_list.id}/items/{test_list_item.id}/complete", + json={"is_completed": True}, + headers={"Authorization": f"Bearer {test_user.token}"} + ) + assert response.status_code == status.HTTP_200_OK + assert response.json()["is_completed"] is True + +@pytest.mark.asyncio +async def test_reorder_items(client: AsyncClient, db_session: AsyncSession, test_user, test_house, test_shopping_list, test_list_item): + reorder_data = [{"item_id": str(test_list_item.id), "new_position": 1}] + response = await client.post( + f"/{test_house.id}/lists/{test_shopping_list.id}/items/reorder", + json=reorder_data, + headers={"Authorization": f"Bearer {test_user.token}"} + ) + assert response.status_code == status.HTTP_200_OK + assert response.json()["detail"] == "Items reordered successfully" diff --git a/app/tests/conftest.py b/app/tests/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/app/uv.lock b/app/uv.lock new file mode 100644 index 0000000..8db57de --- /dev/null +++ b/app/uv.lock @@ -0,0 +1,697 @@ +version = 1 +revision = 1 +requires-python = ">=3.12" + +[[package]] +name = "alembic" +version = "1.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/ed/901044acb892caa5604bf818d2da9ab0df94ef606c6059fdf367894ebf60/alembic-1.15.1.tar.gz", hash = "sha256:e1a1c738577bca1f27e68728c910cd389b9a92152ff91d902da649c192e30c49", size = 1924789 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/f7/d398fae160568472ddce0b3fde9c4581afc593019a6adc91006a66406991/alembic-1.15.1-py3-none-any.whl", hash = "sha256:197de710da4b3e91cf66a826a5b31b5d59a127ab41bd0fc42863e2902ce2bbbe", size = 231753 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, +] + +[[package]] +name = "asyncpg" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162 }, + { url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025 }, + { url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243 }, + { url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059 }, + { url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596 }, + { url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632 }, + { url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186 }, + { url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064 }, + { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373 }, + { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745 }, + { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103 }, + { url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471 }, + { url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253 }, + { url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720 }, + { url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404 }, + { url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623 }, +] + +[[package]] +name = "bcrypt" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719 }, + { url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001 }, + { url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451 }, + { url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792 }, + { url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752 }, + { url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762 }, + { url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384 }, + { url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329 }, + { url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241 }, + { url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617 }, + { url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751 }, + { url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965 }, + { url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316 }, + { url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752 }, + { url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019 }, + { url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174 }, + { url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870 }, + { url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601 }, + { url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660 }, + { url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083 }, + { url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237 }, + { url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737 }, + { url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741 }, + { url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472 }, + { url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606 }, + { url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867 }, + { url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589 }, + { url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794 }, + { url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969 }, + { url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158 }, + { url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285 }, + { url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583 }, + { url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896 }, + { url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492 }, + { url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213 }, + { url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162 }, + { url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856 }, + { url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726 }, + { url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664 }, + { url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128 }, + { url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598 }, + { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799 }, +] + +[[package]] +name = "black" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 }, + { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 }, + { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 }, + { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 }, + { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "dooey" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "alembic" }, + { name = "asyncpg" }, + { name = "bcrypt" }, + { name = "fastapi" }, + { name = "passlib" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-jose" }, + { name = "python-multipart" }, + { name = "sqlalchemy" }, + { name = "uvicorn" }, +] + +[package.optional-dependencies] +dev = [ + { name = "black" }, + { name = "httpx" }, + { name = "isort" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, +] + +[package.metadata] +requires-dist = [ + { name = "alembic", specifier = ">=1.12.0" }, + { name = "asyncpg", specifier = ">=0.28.0" }, + { name = "bcrypt", specifier = ">=4.0.1" }, + { name = "black", marker = "extra == 'dev'", specifier = ">=23.10.0" }, + { name = "fastapi", specifier = ">=0.104.0" }, + { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.25.0" }, + { name = "isort", marker = "extra == 'dev'", specifier = ">=5.12.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.6.1" }, + { name = "passlib", specifier = ">=1.7.4" }, + { name = "pydantic", specifier = ">=2.4.2" }, + { name = "pydantic-settings", specifier = ">=2.0.3" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.2" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.1" }, + { name = "python-jose", specifier = ">=3.3.0" }, + { name = "python-multipart", specifier = ">=0.0.6" }, + { name = "sqlalchemy", specifier = ">=2.0.22" }, + { name = "uvicorn", specifier = ">=0.23.2" }, +] +provides-extras = ["dev"] + +[[package]] +name = "ecdsa" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/1f/924e3caae75f471eae4b26bd13b698f6af2c44279f67af317439c2f4c46a/ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61", size = 201793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607 }, +] + +[[package]] +name = "fastapi" +version = "0.115.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 }, +] + +[[package]] +name = "greenlet" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 }, + { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 }, + { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 }, + { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 }, + { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 }, + { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 }, + { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 }, + { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 }, + { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 }, + { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 }, + { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 }, + { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 }, + { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 }, + { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 }, + { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 }, + { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 }, + { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 }, + { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 }, + { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 }, + { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 }, + { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 }, + { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 }, + { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 }, + { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 }, + { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + +[[package]] +name = "isort" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186 }, +] + +[[package]] +name = "mako" +version = "1.3.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/4f/ddb1965901bc388958db9f0c991255b2c469349a741ae8c9cd8a562d70a6/mako-1.3.9.tar.gz", hash = "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac", size = 392195 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/83/de0a49e7de540513f53ab5d2e105321dedeb08a8f5850f0208decf4390ec/Mako-1.3.9-py3-none-any.whl", hash = "sha256:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1", size = 78456 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "mypy" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, + { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, + { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, + { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, + { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, + { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, + { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, + { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, + { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, + { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, + { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, + { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, + { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "passlib" +version = "1.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554 }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pyasn1" +version = "0.4.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/db/fffec68299e6d7bad3d504147f9094830b704527a7fc098b721d38cc7fa7/pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", size = 146820 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/1e/a94a8d635fa3ce4cfc7f506003548d0a2447ae76fd5ca53932970fe3053f/pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", size = 77145 }, +] + +[[package]] +name = "pydantic" +version = "2.10.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, + { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, + { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, + { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, + { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, + { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, + { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, + { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, + { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, + { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, + { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, + { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, + { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, + { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.25.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/a8/ecbc8ede70921dd2f544ab1cadd3ff3bf842af27f87bbdea774c7baa1d38/pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a", size = 54239 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/17/3493c5624e48fd97156ebaec380dcaafee9506d7e2c46218ceebbb57d7de/pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3", size = 19467 }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + +[[package]] +name = "python-jose" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ecdsa" }, + { name = "pyasn1" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/a0/c49687cf40cb6128ea4e0559855aff92cd5ebd1a60a31c08526818c0e51e/python-jose-3.4.0.tar.gz", hash = "sha256:9a9a40f418ced8ecaf7e3b28d69887ceaa76adad3bcaa6dae0d9e596fec1d680", size = 92145 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/b0/2586ea6b6fd57a994ece0b56418cbe93fff0efb85e2c9eb6b0caf24a4e37/python_jose-3.4.0-py2.py3-none-any.whl", hash = "sha256:9c9f616819652d109bd889ecd1e15e9a162b9b94d682534c9c2146092945b78f", size = 34616 }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, +] + +[[package]] +name = "rsa" +version = "4.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/65/7d973b89c4d2351d7fb232c2e452547ddfa243e93131e7cfa766da627b52/rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21", size = 29711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.39" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/8e/e77fcaa67f8b9f504b4764570191e291524575ddbfe78a90fc656d671fdc/sqlalchemy-2.0.39.tar.gz", hash = "sha256:5d2d1fe548def3267b4c70a8568f108d1fed7cbbeccb9cc166e05af2abc25c22", size = 9644602 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/86/b2cb432aeb00a1eda7ed33ce86d943c2452dc1642f3ec51bfe9eaae9604b/sqlalchemy-2.0.39-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c457a38351fb6234781d054260c60e531047e4d07beca1889b558ff73dc2014b", size = 2107210 }, + { url = "https://files.pythonhosted.org/packages/bf/b0/b2479edb3419ca763ba1b587161c292d181351a33642985506a530f9162b/sqlalchemy-2.0.39-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:018ee97c558b499b58935c5a152aeabf6d36b3d55d91656abeb6d93d663c0c4c", size = 2097599 }, + { url = "https://files.pythonhosted.org/packages/58/5e/c5b792a4abcc71e68d44cb531c4845ac539d558975cc61db1afbc8a73c96/sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5493a8120d6fc185f60e7254fc056a6742f1db68c0f849cfc9ab46163c21df47", size = 3247012 }, + { url = "https://files.pythonhosted.org/packages/e0/a8/055fa8a7c5f85e6123b7e40ec2e9e87d63c566011d599b4a5ab75e033017/sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2cf5b5ddb69142511d5559c427ff00ec8c0919a1e6c09486e9c32636ea2b9dd", size = 3257851 }, + { url = "https://files.pythonhosted.org/packages/f6/40/aec16681e91a22ddf03dbaeb3c659bce96107c5f47d2a7c665eb7f24a014/sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f03143f8f851dd8de6b0c10784363712058f38209e926723c80654c1b40327a", size = 3193155 }, + { url = "https://files.pythonhosted.org/packages/21/9d/cef697b137b9eb0b66ab8e9cf193a7c7c048da3b4bb667e5fcea4d90c7a2/sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06205eb98cb3dd52133ca6818bf5542397f1dd1b69f7ea28aa84413897380b06", size = 3219770 }, + { url = "https://files.pythonhosted.org/packages/57/05/e109ca7dde837d8f2f1b235357e4e607f8af81ad8bc29c230fed8245687d/sqlalchemy-2.0.39-cp312-cp312-win32.whl", hash = "sha256:7f5243357e6da9a90c56282f64b50d29cba2ee1f745381174caacc50d501b109", size = 2077567 }, + { url = "https://files.pythonhosted.org/packages/97/c6/25ca068e38c29ed6be0fde2521888f19da923dbd58f5ff16af1b73ec9b58/sqlalchemy-2.0.39-cp312-cp312-win_amd64.whl", hash = "sha256:2ed107331d188a286611cea9022de0afc437dd2d3c168e368169f27aa0f61338", size = 2103136 }, + { url = "https://files.pythonhosted.org/packages/32/47/55778362642344324a900b6b2b1b26f7f02225b374eb93adc4a363a2d8ae/sqlalchemy-2.0.39-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fe193d3ae297c423e0e567e240b4324d6b6c280a048e64c77a3ea6886cc2aa87", size = 2102484 }, + { url = "https://files.pythonhosted.org/packages/1b/e1/f5f26f67d095f408138f0fb2c37f827f3d458f2ae51881546045e7e55566/sqlalchemy-2.0.39-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:79f4f502125a41b1b3b34449e747a6abfd52a709d539ea7769101696bdca6716", size = 2092955 }, + { url = "https://files.pythonhosted.org/packages/c5/c2/0db0022fc729a54fc7aef90a3457bf20144a681baef82f7357832b44c566/sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a10ca7f8a1ea0fd5630f02feb055b0f5cdfcd07bb3715fc1b6f8cb72bf114e4", size = 3179367 }, + { url = "https://files.pythonhosted.org/packages/33/b7/f33743d87d0b4e7a1f12e1631a4b9a29a8d0d7c0ff9b8c896d0bf897fb60/sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6b0a1c7ed54a5361aaebb910c1fa864bae34273662bb4ff788a527eafd6e14d", size = 3192705 }, + { url = "https://files.pythonhosted.org/packages/c9/74/6814f31719109c973ddccc87bdfc2c2a9bc013bec64a375599dc5269a310/sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52607d0ebea43cf214e2ee84a6a76bc774176f97c5a774ce33277514875a718e", size = 3125927 }, + { url = "https://files.pythonhosted.org/packages/e8/6b/18f476f4baaa9a0e2fbc6808d8f958a5268b637c8eccff497bf96908d528/sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c08a972cbac2a14810463aec3a47ff218bb00c1a607e6689b531a7c589c50723", size = 3154055 }, + { url = "https://files.pythonhosted.org/packages/b4/60/76714cecb528da46bc53a0dd36d1ccef2f74ef25448b630a0a760ad07bdb/sqlalchemy-2.0.39-cp313-cp313-win32.whl", hash = "sha256:23c5aa33c01bd898f879db158537d7e7568b503b15aad60ea0c8da8109adf3e7", size = 2075315 }, + { url = "https://files.pythonhosted.org/packages/5b/7c/76828886d913700548bac5851eefa5b2c0251ebc37921fe476b93ce81b50/sqlalchemy-2.0.39-cp313-cp313-win_amd64.whl", hash = "sha256:4dabd775fd66cf17f31f8625fc0e4cfc5765f7982f94dc09b9e5868182cb71c0", size = 2099175 }, + { url = "https://files.pythonhosted.org/packages/7b/0f/d69904cb7d17e65c65713303a244ec91fd3c96677baf1d6331457fd47e16/sqlalchemy-2.0.39-py3-none-any.whl", hash = "sha256:a1c6b0a5e3e326a466d809b651c63f278b1256146a377a528b6938a279da334f", size = 1898621 }, +] + +[[package]] +name = "starlette" +version = "0.46.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "uvicorn" +version = "0.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, +] diff --git a/dooey/.gitignore b/dooey/.gitignore new file mode 100644 index 0000000..bff793d --- /dev/null +++ b/dooey/.gitignore @@ -0,0 +1,24 @@ +test-results +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/dooey/.npmrc b/dooey/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/dooey/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/dooey/.prettierignore b/dooey/.prettierignore new file mode 100644 index 0000000..6562bcb --- /dev/null +++ b/dooey/.prettierignore @@ -0,0 +1,6 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb diff --git a/dooey/.prettierrc b/dooey/.prettierrc new file mode 100644 index 0000000..3f7802c --- /dev/null +++ b/dooey/.prettierrc @@ -0,0 +1,15 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/dooey/README.md b/dooey/README.md new file mode 100644 index 0000000..b5b2950 --- /dev/null +++ b/dooey/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```bash +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/dooey/e2e/demo.test.ts b/dooey/e2e/demo.test.ts new file mode 100644 index 0000000..9985ce1 --- /dev/null +++ b/dooey/e2e/demo.test.ts @@ -0,0 +1,6 @@ +import { expect, test } from '@playwright/test'; + +test('home page has expected h1', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('h1')).toBeVisible(); +}); diff --git a/dooey/eslint.config.js b/dooey/eslint.config.js new file mode 100644 index 0000000..9b34ef5 --- /dev/null +++ b/dooey/eslint.config.js @@ -0,0 +1,37 @@ +import prettier from 'eslint-config-prettier'; +import js from '@eslint/js'; +import { includeIgnoreFile } from '@eslint/compat'; +import svelte from 'eslint-plugin-svelte'; +import globals from 'globals'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript-eslint'; +import svelteConfig from './svelte.config.js'; + +const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); + +export default ts.config( + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs.recommended, + prettier, + ...svelte.configs.prettier, + { + languageOptions: { + globals: { ...globals.browser, ...globals.node } + }, + rules: { 'no-undef': 'off' } + }, + { + files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], + ignores: ['eslint.config.js', 'svelte.config.js'], + languageOptions: { + parserOptions: { + projectService: true, + extraFileExtensions: ['.svelte'], + parser: ts.parser, + svelteConfig + } + } + } +); diff --git a/dooey/package-lock.json b/dooey/package-lock.json new file mode 100644 index 0000000..a2d92bd --- /dev/null +++ b/dooey/package-lock.json @@ -0,0 +1,4103 @@ +{ + "name": "dooey", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dooey", + "version": "0.0.1", + "devDependencies": { + "@eslint/compat": "^1.2.5", + "@eslint/js": "^9.18.0", + "@playwright/test": "^1.49.1", + "@sveltejs/adapter-node": "^5.2.11", + "@sveltejs/kit": "^2.16.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-svelte": "^3.0.0", + "globals": "^16.0.0", + "prettier": "^3.4.2", + "prettier-plugin-svelte": "^3.3.3", + "sass-embedded": "^1.86.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "typescript": "^5.0.0", + "typescript-eslint": "^8.20.0", + "vite": "^6.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.5.tgz", + "integrity": "sha512-/g5EzJifw5GF8aren8wZ/G5oMuPoGeS6MQD3ca8ddcvdXR5UELUfdTZITCGNhNXynY/AYl3Z4plmxdj/tRl/hQ==", + "dev": true, + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", + "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/compat": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.7.tgz", + "integrity": "sha512-xvv7hJE32yhegJ8xNAnb62ggiAwTYHBpUCWhRxEj/ksvgDJuSXfoDkBcRYaYNFiJ+jH0IE3K16hd+xXzhBgNbg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.10.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.0.tgz", + "integrity": "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz", + "integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.12.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@playwright/test": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.1.tgz", + "integrity": "sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.51.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz", + "integrity": "sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz", + "integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.37.0.tgz", + "integrity": "sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.37.0.tgz", + "integrity": "sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.37.0.tgz", + "integrity": "sha512-+iTQ5YHuGmPt10NTzEyMPbayiNTcOZDWsbxZYR1ZnmLnZxG17ivrPSWFO9j6GalY0+gV3Jtwrrs12DBscxnlYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.37.0.tgz", + "integrity": "sha512-m8W2UbxLDcmRKVjgl5J/k4B8d7qX2EcJve3Sut7YGrQoPtCIQGPH5AMzuFvYRWZi0FVS0zEY4c8uttPfX6bwYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.37.0.tgz", + "integrity": "sha512-FOMXGmH15OmtQWEt174v9P1JqqhlgYge/bUjIbiVD1nI1NeJ30HYT9SJlZMqdo1uQFyt9cz748F1BHghWaDnVA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.37.0.tgz", + "integrity": "sha512-SZMxNttjPKvV14Hjck5t70xS3l63sbVwl98g3FlVVx2YIDmfUIy29jQrsw06ewEYQ8lQSuY9mpAPlmgRD2iSsA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.37.0.tgz", + "integrity": "sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.37.0.tgz", + "integrity": "sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.37.0.tgz", + "integrity": "sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.37.0.tgz", + "integrity": "sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.37.0.tgz", + "integrity": "sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.37.0.tgz", + "integrity": "sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.37.0.tgz", + "integrity": "sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.37.0.tgz", + "integrity": "sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.37.0.tgz", + "integrity": "sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.37.0.tgz", + "integrity": "sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.37.0.tgz", + "integrity": "sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.37.0.tgz", + "integrity": "sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.37.0.tgz", + "integrity": "sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.37.0.tgz", + "integrity": "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", + "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-node": { + "version": "5.2.12", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz", + "integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-commonjs": "^28.0.1", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "rollup": "^4.9.5" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.4.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.20.2", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.2.tgz", + "integrity": "sha512-Dv8TOAZC9vyfcAB9TMsvUEJsRbklRTeNfcYBPaeH6KnABJ99i3CvCB2eNx8fiiliIqe+9GIchBg4RodRH5p1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.6.0", + "devalue": "^5.1.0", + "esm-env": "^1.2.2", + "import-meta-resolve": "^4.1.0", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.3 || ^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.0.3.tgz", + "integrity": "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.0", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.15", + "vitefu": "^1.0.4" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.27.0.tgz", + "integrity": "sha512-4henw4zkePi5p252c8ncBLzLce52SEUz2Ebj8faDnuUXz2UuHEONYcJ+G0oaCF+bYCWVZtrGzq3FD7YXetmnSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.27.0", + "@typescript-eslint/type-utils": "8.27.0", + "@typescript-eslint/utils": "8.27.0", + "@typescript-eslint/visitor-keys": "8.27.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.27.0.tgz", + "integrity": "sha512-XGwIabPallYipmcOk45DpsBSgLC64A0yvdAkrwEzwZ2viqGqRUJ8eEYoPz0CWnutgAFbNMPdsGGvzjSmcWVlEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.27.0", + "@typescript-eslint/types": "8.27.0", + "@typescript-eslint/typescript-estree": "8.27.0", + "@typescript-eslint/visitor-keys": "8.27.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.27.0.tgz", + "integrity": "sha512-8oI9GwPMQmBryaaxG1tOZdxXVeMDte6NyJA4i7/TWa4fBwgnAXYlIQP+uYOeqAaLJ2JRxlG9CAyL+C+YE9Xknw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.27.0", + "@typescript-eslint/visitor-keys": "8.27.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.27.0.tgz", + "integrity": "sha512-wVArTVcz1oJOIEJxui/nRhV0TXzD/zMSOYi/ggCfNq78EIszddXcJb7r4RCp/oBrjt8n9A0BSxRMKxHftpDxDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.27.0", + "@typescript-eslint/utils": "8.27.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.27.0.tgz", + "integrity": "sha512-/6cp9yL72yUHAYq9g6DsAU+vVfvQmd1a8KyA81uvfDE21O2DwQ/qxlM4AR8TSdAu+kJLBDrEHKC5/W2/nxsY0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.27.0.tgz", + "integrity": "sha512-BnKq8cqPVoMw71O38a1tEb6iebEgGA80icSxW7g+kndx0o6ot6696HjG7NdgfuAVmVEtwXUr3L8R9ZuVjoQL6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.27.0", + "@typescript-eslint/visitor-keys": "8.27.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.27.0.tgz", + "integrity": "sha512-njkodcwH1yvmo31YWgRHNb/x1Xhhq4/m81PhtvmRngD8iHPehxffz1SNCO+kwaePhATC+kOa/ggmvPoPza5i0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.27.0", + "@typescript-eslint/types": "8.27.0", + "@typescript-eslint/typescript-estree": "8.27.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.27.0.tgz", + "integrity": "sha512-WsXQwMkILJvffP6z4U3FYJPlbf/j07HIxmDjZpbNvBJkMfvwXj5ACRkkHwBDvLBbDbtX5TdU64/rcvKJ/vuInQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.27.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-builder": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz", + "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==", + "dev": true, + "license": "MIT/X11" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorjs.io": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", + "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/devalue": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", + "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", + "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.1", + "@esbuild/android-arm": "0.25.1", + "@esbuild/android-arm64": "0.25.1", + "@esbuild/android-x64": "0.25.1", + "@esbuild/darwin-arm64": "0.25.1", + "@esbuild/darwin-x64": "0.25.1", + "@esbuild/freebsd-arm64": "0.25.1", + "@esbuild/freebsd-x64": "0.25.1", + "@esbuild/linux-arm": "0.25.1", + "@esbuild/linux-arm64": "0.25.1", + "@esbuild/linux-ia32": "0.25.1", + "@esbuild/linux-loong64": "0.25.1", + "@esbuild/linux-mips64el": "0.25.1", + "@esbuild/linux-ppc64": "0.25.1", + "@esbuild/linux-riscv64": "0.25.1", + "@esbuild/linux-s390x": "0.25.1", + "@esbuild/linux-x64": "0.25.1", + "@esbuild/netbsd-arm64": "0.25.1", + "@esbuild/netbsd-x64": "0.25.1", + "@esbuild/openbsd-arm64": "0.25.1", + "@esbuild/openbsd-x64": "0.25.1", + "@esbuild/sunos-x64": "0.25.1", + "@esbuild/win32-arm64": "0.25.1", + "@esbuild/win32-ia32": "0.25.1", + "@esbuild/win32-x64": "0.25.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz", + "integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.2", + "@eslint/config-helpers": "^0.2.0", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.23.0", + "@eslint/plugin-kit": "^0.2.7", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.6.4.tgz", + "integrity": "sha512-/u+GQt8NMfXO8w17QendT4gvO5acfxQsAKirAt0LVxDnr2N8YLCVbregaNc/Yhp7NM128DwCaRvr8PLDfeNkQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz", + "integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.3.3.tgz", + "integrity": "sha512-imzGqIgWbfsb/CR14d3k3M8MiVNGet+l9mjPhvo1Rm0Nxi0rNn4/eELqyR8FWlgKBMlGkOp2kshRJm0xpxNfHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.1", + "@jridgewell/sourcemap-codec": "^1.5.0", + "eslint-compat-utils": "^0.6.4", + "esutils": "^2.0.3", + "known-css-properties": "^0.35.0", + "postcss": "^8.4.49", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^7.0.0", + "semver": "^7.6.3", + "svelte-eslint-parser": "^1.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^8.57.1 || ^9.0.0", + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrap": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.5.tgz", + "integrity": "sha512-CjNMjkBWWZeHn+VX+gS8YvFwJ5+NDhg8aWZBSFJPR8qQduDNjbJodA2WcwCm7uQa5Rjqj+nZvVmceg1RbHFB9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", + "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", + "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", + "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", + "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz", + "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.51.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz", + "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.3.3.tgz", + "integrity": "sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.37.0.tgz", + "integrity": "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.37.0", + "@rollup/rollup-android-arm64": "4.37.0", + "@rollup/rollup-darwin-arm64": "4.37.0", + "@rollup/rollup-darwin-x64": "4.37.0", + "@rollup/rollup-freebsd-arm64": "4.37.0", + "@rollup/rollup-freebsd-x64": "4.37.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.37.0", + "@rollup/rollup-linux-arm-musleabihf": "4.37.0", + "@rollup/rollup-linux-arm64-gnu": "4.37.0", + "@rollup/rollup-linux-arm64-musl": "4.37.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.37.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.37.0", + "@rollup/rollup-linux-riscv64-gnu": "4.37.0", + "@rollup/rollup-linux-riscv64-musl": "4.37.0", + "@rollup/rollup-linux-s390x-gnu": "4.37.0", + "@rollup/rollup-linux-x64-gnu": "4.37.0", + "@rollup/rollup-linux-x64-musl": "4.37.0", + "@rollup/rollup-win32-arm64-msvc": "4.37.0", + "@rollup/rollup-win32-ia32-msvc": "4.37.0", + "@rollup/rollup-win32-x64-msvc": "4.37.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/sass-embedded": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.86.0.tgz", + "integrity": "sha512-Ibq5DzxjSf9f/IJmKeHVeXlVqiZWdRJF+RXy6v6UupvMYVMU5Ei+teSFBvvpPD5bB2QhhnU/OJlSM0EBCtfr9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^2.0.0", + "buffer-builder": "^0.2.0", + "colorjs.io": "^0.5.0", + "immutable": "^5.0.2", + "rxjs": "^7.4.0", + "supports-color": "^8.1.1", + "sync-child-process": "^1.0.2", + "varint": "^6.0.0" + }, + "bin": { + "sass": "dist/bin/sass.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "optionalDependencies": { + "sass-embedded-android-arm": "1.86.0", + "sass-embedded-android-arm64": "1.86.0", + "sass-embedded-android-ia32": "1.86.0", + "sass-embedded-android-riscv64": "1.86.0", + "sass-embedded-android-x64": "1.86.0", + "sass-embedded-darwin-arm64": "1.86.0", + "sass-embedded-darwin-x64": "1.86.0", + "sass-embedded-linux-arm": "1.86.0", + "sass-embedded-linux-arm64": "1.86.0", + "sass-embedded-linux-ia32": "1.86.0", + "sass-embedded-linux-musl-arm": "1.86.0", + "sass-embedded-linux-musl-arm64": "1.86.0", + "sass-embedded-linux-musl-ia32": "1.86.0", + "sass-embedded-linux-musl-riscv64": "1.86.0", + "sass-embedded-linux-musl-x64": "1.86.0", + "sass-embedded-linux-riscv64": "1.86.0", + "sass-embedded-linux-x64": "1.86.0", + "sass-embedded-win32-arm64": "1.86.0", + "sass-embedded-win32-ia32": "1.86.0", + "sass-embedded-win32-x64": "1.86.0" + } + }, + "node_modules/sass-embedded-android-arm": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.86.0.tgz", + "integrity": "sha512-NS8v6BCbzskXUMBtzfuB+j2yQMgiwg5edKHTYfQU7gAWai2hkRhS06YNEMff3aRxV0IFInxPRHOobd8xWPHqeA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-arm64": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.86.0.tgz", + "integrity": "sha512-r7MZtlAI2VFUnKE8B5UOrpoE6OGpdf1dIB6ndoxb3oiURgMyfTVU7yvJcL12GGvtVwQ2boCj6dq//Lqq9CXPlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-ia32": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.86.0.tgz", + "integrity": "sha512-UjfElrGaOTNOnxLZLxf6MFndFIe7zyK+81f83BioZ7/jcoAd6iCHZT8yQMvu8wINyVodPcaXZl8KxlKcl62VAA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-riscv64": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.86.0.tgz", + "integrity": "sha512-TsqCLxHWLFS2mbpUkL/nge3jSkaPK2VmLkkoi5iO/EQT4SFvm1lNUgPwlLXu9DplZ+aqGVzRS9Y6Psjv+qW7kw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-x64": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.86.0.tgz", + "integrity": "sha512-8Q263GgwGjz7Jkf7Eghp7NrwqskDL95WO9sKrNm9iOd2re/M48W7RN/lpdcZwrUnEOhueks0RRyYyZYBNRz8Tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-arm64": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.86.0.tgz", + "integrity": "sha512-d8oMEaIweq1tjrb/BT43igDviOMS1TeDpc51QF7vAHkt9drSjPmqEmbqStdFYPAGZj1j0RA4WCRoVl6jVixi/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-x64": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.86.0.tgz", + "integrity": "sha512-5NLRtn0ZUDBkfpKOsgLGl9B34po4Qui8Nff/lXTO+YkxBQFX4GoMkYNk9EJqHwoLLzICsxIhNDMMDiPGz7Fdrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.86.0.tgz", + "integrity": "sha512-b6wm0+Il+blJDleRXAqA6JISGMjRb0/thTEg4NWgmiJwUoZjDycj5FTbfYPnLXjCEIMGaYmW3patrJ3JMJcT3Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm64": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.86.0.tgz", + "integrity": "sha512-50A+0rhahRDRkKkv+qS7GDAAkW1VPm2RCX4zY4JWydhV4NwMXr6HbkLnsJ2MGixCyibPh59iflMpNBhe7SEMNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-ia32": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.86.0.tgz", + "integrity": "sha512-h0mr9w71TV3BRPk9JHr0flnRCznhkraY14gaj5T+t78vUFByOUMxp4hTr+JpZAR5mv0mIeoMwrQYwWJoqKI0mw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.86.0.tgz", + "integrity": "sha512-KZU70jBMVykC9HzS+o2FhrJaprFLDk3LWXVPtBFxgLlkcQ/apCkUCh2WVNViLhI2U4NrMSnTvd4kDnC/0m8qIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm64": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.86.0.tgz", + "integrity": "sha512-5OZjiJIUyhvKJIGNDEjyRUWDe+W91hq4Bji27sy8gdEuDzPWLx4NzwpKwsBUALUfyW/J5dxgi0ZAQnI3HieyQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-ia32": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.86.0.tgz", + "integrity": "sha512-vq9wJ7kaELrsNU6Ld6kvrIHxoIUWaD+5T6TQVj4SJP/iw1NjonyCDMQGGs6UgsIEzvaIwtlSlDbRewAq+4PchA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-riscv64": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.86.0.tgz", + "integrity": "sha512-UZJPu4zKe3phEzoSVRh5jcSicBBPe+jEbVNALHSSz881iOAYnDQXHITGeQ4mM1/7e/LTyryHk6EPBoaLOv6JrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-x64": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.86.0.tgz", + "integrity": "sha512-8taAgbWMk4QHneJcouWmWZJlmKa2O03g4I/CFo4bfMPL87bibY90pAsSDd+C+t81g0+2aK0/lY/BoB0r3qXLiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-riscv64": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.86.0.tgz", + "integrity": "sha512-yREY6o2sLwiiA03MWHVpnUliLscz0flEmFW/wzxYZJDqg9eZteB3hUWgZD63eLm2PTZsYxDQpjAHpa48nnIEmA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-x64": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.86.0.tgz", + "integrity": "sha512-sH0F8np9PTgTbFcJWxfr1NzPkL5ID2NcpMtZyKPTdnn9NkE/L2UwXSo6xOvY0Duc4Hg+58wSrDnj6KbvdeHCPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-arm64": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.86.0.tgz", + "integrity": "sha512-4O1XVUxLTIjMOvrziYwEZgvFqC5sF6t0hTAPJ+h2uiAUZg9Joo0PvuEedXurjISgDBsb5W5DTL9hH9q1BbP4cQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-ia32": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.86.0.tgz", + "integrity": "sha512-zuSP2axkGm4VaJWt38P464H+4424Swr9bzFNfbbznxe3Ue4RuqSBqwiLiYdg9Q1cecTQ2WGH7G7WO56KK7WLwg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-x64": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.86.0.tgz", + "integrity": "sha512-GVX0CHtukr3kjqfqretSlPiJzV7V4JxUjpRZV+yC9gUMTiDErilJh2Chw1r0+MYiYvumCDUSDlticmvJs7v0tA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sirv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", + "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.25.3.tgz", + "integrity": "sha512-J9rcZ/xVJonAoESqVGHHZhrNdVbrCfkdB41BP6eiwHMoFShD9it3yZXApVYMHdGfCshBsZCKsajwJeBbS/M1zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^1.4.3", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.5.tgz", + "integrity": "sha512-Gb0T2IqBNe1tLB9EB1Qh+LOe+JB8wt2/rNBDGvkxQVvk8vNeAoG+vZgFB/3P5+zC7RWlyBlzm9dVjZFph/maIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-eslint-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.1.0.tgz", + "integrity": "sha512-JP0v/wzDXWxza6c8K9ZjKKHYfgt0KidlbWx1e9n9UV4q+o28GTkk71fR0IDZDmLUDYs3vSq0+Tm9fofDqzGe1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.0", + "postcss": "^8.4.49", + "postcss-scss": "^4.0.9", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/svelte/node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/sync-child-process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", + "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sync-message-port": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/sync-message-port": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz", + "integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.27.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.27.0.tgz", + "integrity": "sha512-ZZ/8+Y0rRUMuW1gJaPtLWe4ryHbsPLzzibk5Sq+IFa2aOH1Vo0gPr1fbA6pOnzBke7zC2Da4w8AyCgxKXo3lqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.27.0", + "@typescript-eslint/parser": "8.27.0", + "@typescript-eslint/utils": "8.27.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz", + "integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitefu": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz", + "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/dooey/package.json b/dooey/package.json new file mode 100644 index 0000000..70c76a2 --- /dev/null +++ b/dooey/package.json @@ -0,0 +1,38 @@ +{ + "name": "dooey", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check . && eslint .", + "test:e2e": "playwright test", + "test": "npm run test:e2e" + }, + "devDependencies": { + "@eslint/compat": "^1.2.5", + "@eslint/js": "^9.18.0", + "@playwright/test": "^1.49.1", + "@sveltejs/adapter-node": "^5.2.11", + "@sveltejs/kit": "^2.16.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-svelte": "^3.0.0", + "globals": "^16.0.0", + "prettier": "^3.4.2", + "prettier-plugin-svelte": "^3.3.3", + "sass-embedded": "^1.86.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "typescript": "^5.0.0", + "typescript-eslint": "^8.20.0", + "vite": "^6.0.0" + } +} diff --git a/dooey/playwright.config.ts b/dooey/playwright.config.ts new file mode 100644 index 0000000..f6c81af --- /dev/null +++ b/dooey/playwright.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + webServer: { + command: 'npm run build && npm run preview', + port: 4173 + }, + testDir: 'e2e' +}); diff --git a/dooey/src/app.css b/dooey/src/app.css new file mode 100644 index 0000000..15f3bcc --- /dev/null +++ b/dooey/src/app.css @@ -0,0 +1,126 @@ +/* src/app.css */ +:root { + --primary-color: rgb(192, 55, 123); + --primary-light: #a0c4f1; + --primary-dark: #2c5aa0; + --success-color: #10b981; + --error-color: #ef4444; + --warning-color: #f59e0b; + --text-primary: #333333; + --text-secondary: #666666; + --text-tertiary: #888888; + --bg-light: #f9f9f9; + --bg-white: #ffffff; + --border-color: rgb(192, 55, 123); + --border-light: #eeeeee; + --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + font-size: 16px; + line-height: 1.5; + color: var(--text-primary); + background-color: var(--bg-light); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + height: 100%; +} + +a { + color: var(--primary-color); + text-decoration: none; +} + +button { + cursor: pointer; + font-family: inherit; +} + +input, +textarea, +select { + font-family: inherit; +} + +/* Utility classes */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +/* Animations */ +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Responsive breakpoints */ +@media (min-width: 480px) { + /* Small devices */ +} + +@media (min-width: 768px) { + /* Medium devices */ +} + +@media (min-width: 1024px) { + /* Large devices */ +} + +@media (min-width: 1280px) { + /* Extra large devices */ +} + +/* Reduced motion preferences */ +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} \ No newline at end of file diff --git a/dooey/src/app.d.ts b/dooey/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/dooey/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/dooey/src/app.html b/dooey/src/app.html new file mode 100644 index 0000000..77a5ff5 --- /dev/null +++ b/dooey/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/dooey/src/hooks.server.ts b/dooey/src/hooks.server.ts new file mode 100644 index 0000000..f765737 --- /dev/null +++ b/dooey/src/hooks.server.ts @@ -0,0 +1,22 @@ +import { dev } from '$app/environment'; + + +if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/service-worker.js'); +} + +/** @type {import('@sveltejs/kit').Handle} */ +export async function handle({ event, resolve }) { + // Add custom headers for security + const response = await resolve(event); + + // Security headers + if (!dev) { + response.headers.set('X-Frame-Options', 'SAMEORIGIN'); + response.headers.set('X-Content-Type-Options', 'nosniff'); + response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); + response.headers.set('Permissions-Policy', 'camera=self'); + } + + return response; +} diff --git a/dooey/src/lib/api.ts b/dooey/src/lib/api.ts new file mode 100644 index 0000000..b1e6783 --- /dev/null +++ b/dooey/src/lib/api.ts @@ -0,0 +1,630 @@ +import type { + User, + House, + HouseMember, + ShoppingList, + ListItem, + Expense, + ExpenseSummary, + Chore, + HouseInvite +} from './types'; + +const API_URL = 'http://localhost:8000/api'; +// import.meta.env.VITE_API_URL || +async function handleResponse(response: Response): Promise { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || `API error: ${response.status}`); + } + return response.json() as Promise; +} + +export const authApi = { + async register(email: string, password: string, fullName?: string): Promise { + const response = await fetch(`${API_URL}/auth/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email, + password, + full_name: fullName, + }), + }); + return handleResponse(response); + }, + + async login(email: string, password: string): Promise<{ access_token: string; token_type: string }> { + const formData = new FormData(); + formData.append('username', email); + formData.append('password', password); + + const response = await fetch(`${API_URL}/auth/login`, { + method: 'POST', + body: formData, + }); + return handleResponse<{ access_token: string; token_type: string }>(response); + }, + + async logout(): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/auth/logout`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return handleResponse(response); + }, + + isAuthenticated(): boolean { + const token = localStorage.getItem('token'); + if (token) { + return true + } + return false + } +}; + +export const usersApi = { + /** + * Retrieves the profile information for the current user. + * Requires a valid JWT stored in localStorage as "token". + */ + async getProfile(): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/users/me`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return handleResponse(response); + }, + + /** + * Updates the current user's profile. + * Accepts an object with fields such as full_name, email, or password. + * Note: When updating the password, it will be hashed on the server side. + */ + async updateProfile(data: { + full_name?: string; + email?: string; + password?: string; + }): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/users/me`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(data), + }); + return handleResponse(response); + }, +}; + +export const invitesApi = { + /** + * Creates a new invite for a house. + * Only admins can create invites. + */ + async createInvite(houseId: string, expiresInMinutes: number = 60): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/invites`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ expires_in_minutes: expiresInMinutes }), + }); + return handleResponse(response); + }, + + /** + * Lists all active (nonexpired) invites for a house. + * Only house members can view invites. + */ + async listActiveInvites(houseId: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/invites`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return handleResponse(response); + }, + + /** + * Accepts an invite code to join a house. + * Any authenticated user can accept an invite (if it’s valid and not expired). + */ + async acceptInvite(code: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/invites/accept?code=${code}`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return handleResponse(response); + }, +}; + +export const housesApi = { + async getHouses(): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return handleResponse(response); + }, + + async createHouse(name: string, description?: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + name, + description, + }), + }); + return handleResponse(response); + }, + + async getHouse(houseId: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return handleResponse(response); + }, + + async updateHouse(houseId: string, data: { name?: string; description?: string }): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(data), + }); + return handleResponse(response); + }, + + async deleteHouse(houseId: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return handleResponse(response); + }, + + // House Members + async getHouseMembers(houseId: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/members`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return handleResponse(response); + }, + + async addHouseMember(houseId: string, userId: string, role: string = 'member'): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/members`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + user_id: userId, + role, + }), + }); + return handleResponse(response); + }, + + async updateHouseMember(houseId: string, userId: string, role: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/members/${userId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + role, + }), + }); + return handleResponse(response); + }, + + async removeHouseMember(houseId: string, userId: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/members/${userId}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return handleResponse(response); + }, +}; + +export const shoppingListsApi = { + async getLists(houseId: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/lists`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return handleResponse(response); + }, + + async createList(houseId: string, title: string, description?: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/lists`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + title, + description, + }), + }); + return handleResponse(response); + }, + + async getList(houseId: string, listId: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/lists/${listId}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return handleResponse(response); + }, + + async updateList( + houseId: string, + listId: string, + data: { title?: string; description?: string; is_archived?: boolean } + ): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/lists/${listId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(data), + }); + return handleResponse(response); + }, + + async deleteList(houseId: string, listId: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/lists/${listId}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return handleResponse(response); + }, + + // List Items + async getListItems(houseId: string, listId: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/lists/${listId}/items`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return handleResponse(response); + }, + + async addListItem( + houseId: string, + listId: string, + item: { name: string; quantity?: number; unit?: string; price?: number } + ): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/lists/${listId}/items`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(item), + }); + return handleResponse(response); + }, + + async updateListItem( + houseId: string, + listId: string, + itemId: string, + data: { name?: string; quantity?: number; unit?: string; price?: number; is_completed?: boolean } + ): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/lists/${listId}/items/${itemId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(data), + }); + return handleResponse(response); + }, + + async deleteListItem(houseId: string, listId: string, itemId: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/lists/${listId}/items/${itemId}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return handleResponse(response); + }, + + async markItemComplete( + houseId: string, + listId: string, + itemId: string, + isCompleted: boolean + ): Promise { + const token = localStorage.getItem('token'); + const response = await fetch( + `${API_URL}/houses/${houseId}/lists/${listId}/items/${itemId}/complete?is_completed=${isCompleted}`, + { + method: 'PATCH', + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + return handleResponse(response); + }, + + async reorderItems( + houseId: string, + listId: string, + reorderData: Array<{ item_id: string; new_position: number }> + ): Promise<{ detail: string }> { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/lists/${listId}/items/reorder`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(reorderData), + }); + return handleResponse<{ detail: string }>(response); + }, +}; + +export const expensesApi = { + async getExpenses(houseId: string, listId: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/lists/${listId}/expenses`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return handleResponse(response); + }, + + async createExpense( + houseId: string, + listId: string, + expense: { + amount: number; + payer_id: string; + description?: string; + date?: string; + splits: Array<{ user_id: string; amount: number }>; + } + ): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/lists/${listId}/expenses`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(expense), + }); + return handleResponse(response); + }, + + async updateExpense( + houseId: string, + listId: string, + expenseId: string, + data: { + amount?: number; + payer_id?: string; + description?: string; + date?: string; + splits?: Array<{ user_id: string; amount: number }>; + } + ): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/lists/${listId}/expenses/${expenseId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(data), + }); + return handleResponse(response); + }, + + async getExpenseSummary(houseId: string, listId: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/lists/${listId}/expenses/summary`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return handleResponse(response); + }, +}; + +export const choresApi = { + async getChores(houseId: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/chores`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return handleResponse(response); + }, + + async createChore( + houseId: string, + chore: { + title: string; + frequency: string; + description?: string; + assigned_to?: string; + due_date?: string; + } + ): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses${houseId}/chores`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(chore), + }); + return handleResponse(response); + }, + + async updateChore( + houseId: string, + choreId: string, + data: { + title?: string; + description?: string; + assigned_to?: string; + due_date?: string; + is_completed?: boolean; + } + ): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/chores/${choreId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(data), + }); + return handleResponse(response); + }, + + async deleteChore(houseId: string, choreId: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/chores/${choreId}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return handleResponse(response); + }, + + async assignChore(houseId: string, choreId: string, userId: string): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/chores/${choreId}/assign`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + assigned_to: userId, + }), + }); + return handleResponse(response); + }, + + async completeChore(houseId: string, choreId: string, isCompleted: boolean): Promise { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/chores/${choreId}/complete`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + is_completed: isCompleted, + }), + }); + return handleResponse(response); + }, +}; + +export const ocrApi = { + async processImage(imageFile: File): Promise> { + const token = localStorage.getItem('token'); + const formData = new FormData(); + formData.append('image', imageFile); + + const response = await fetch(`${API_URL}/ocr/process`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + }, + body: formData, + }); + return handleResponse>(response); + }, + + async applyOcrResults( + houseId: string, + listId: string, + items: Array<{ name: string; price?: number; quantity?: number; unit?: string }> + ): Promise<{ detail: string; items: Array }> { + const token = localStorage.getItem('token'); + const response = await fetch(`${API_URL}/houses/${houseId}/lists/${listId}/ocr/apply`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(items), + }); + return handleResponse<{ detail: string; items: Array }>(response); + }, +}; diff --git a/dooey/src/lib/components/BottomNavigation.svelte b/dooey/src/lib/components/BottomNavigation.svelte new file mode 100644 index 0000000..b3c7a3d --- /dev/null +++ b/dooey/src/lib/components/BottomNavigation.svelte @@ -0,0 +1,80 @@ + + + + + diff --git a/dooey/src/lib/components/Toast.svelte b/dooey/src/lib/components/Toast.svelte new file mode 100644 index 0000000..93ea782 --- /dev/null +++ b/dooey/src/lib/components/Toast.svelte @@ -0,0 +1,235 @@ + + +{#if message} + +{/if} + + diff --git a/dooey/src/lib/stores/chores.ts b/dooey/src/lib/stores/chores.ts new file mode 100644 index 0000000..e42f616 --- /dev/null +++ b/dooey/src/lib/stores/chores.ts @@ -0,0 +1,260 @@ +import { writable } from 'svelte/store'; +import type { Chore } from '$lib/types'; + +interface ChoresState { + chores: Chore[]; + currentChore: Chore | null; + isLoading: boolean; + currentUser: string | null; // User ID for filtering "my chores" +} + +function createChoresStore() { + const { subscribe, set, update } = writable({ + chores: [], + currentChore: null, + isLoading: false, + currentUser: null + }); + + return { + subscribe, + + // Set current user ID (for filtering "my chores") + setCurrentUser: (userId: string) => { + update(state => ({ ...state, currentUser: userId })); + }, + + // Fetch all chores + fetchChores: async () => { + update(state => ({ ...state, isLoading: true })); + + try { + const response = await fetch('/api/chores'); + if (!response.ok) { + throw new Error('Failed to fetch chores'); + } + + const chores = await response.json(); + update(state => ({ ...state, chores, isLoading: false })); + return chores; + } catch (error) { + update(state => ({ ...state, isLoading: false })); + throw error; + } + }, + + // Fetch a single chore by ID + fetchChoreDetails: async (choreId: string) => { + update(state => ({ ...state, isLoading: true })); + + try { + const response = await fetch(`/api/chores/${choreId}`); + if (!response.ok) { + throw new Error('Failed to fetch chore details'); + } + + const chore = await response.json(); + update(state => ({ ...state, currentChore: chore, isLoading: false })); + return chore; + } catch (error) { + update(state => ({ ...state, isLoading: false })); + throw error; + } + }, + + // Create a new chore + createChore: async (choreData: Partial) => { + update(state => ({ ...state, isLoading: true })); + + try { + const response = await fetch('/api/chores', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(choreData) + }); + + if (!response.ok) { + throw new Error('Failed to create chore'); + } + + const newChore = await response.json(); + update(state => ({ + ...state, + chores: [...state.chores, newChore], + currentChore: newChore, + isLoading: false + })); + + return newChore; + } catch (error) { + update(state => ({ ...state, isLoading: false })); + throw error; + } + }, + + // Update a chore + updateChore: async (choreId: string, choreData: Partial) => { + update(state => ({ ...state, isLoading: true })); + + try { + const response = await fetch(`/api/chores/${choreId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(choreData) + }); + + if (!response.ok) { + throw new Error('Failed to update chore'); + } + + const updatedChore = await response.json(); + update(state => ({ + ...state, + chores: state.chores.map(chore => + chore.id === choreId ? updatedChore : chore + ), + currentChore: state.currentChore?.id === choreId + ? updatedChore + : state.currentChore, + isLoading: false + })); + + return updatedChore; + } catch (error) { + update(state => ({ ...state, isLoading: false })); + throw error; + } + }, + + // Delete a chore + deleteChore: async (choreId: string) => { + update(state => ({ ...state, isLoading: true })); + + try { + const response = await fetch(`/api/chores/${choreId}`, { + method: 'DELETE' + }); + + if (!response.ok) { + throw new Error('Failed to delete chore'); + } + + update(state => ({ + ...state, + chores: state.chores.filter(chore => chore.id !== choreId), + currentChore: state.currentChore?.id === choreId + ? null + : state.currentChore, + isLoading: false + })); + } catch (error) { + update(state => ({ ...state, isLoading: false })); + throw error; + } + }, + + // Toggle chore completion status + toggleChoreCompletion: async (choreId: string) => { + try { + let currentChore: Chore | null = null as Chore | null; + + // Find the current state of the chore + update(state => { + const chore = state.chores.find(c => c.id === choreId); + if (chore) { + currentChore = chore; + + // Optimistically update the UI + return { + ...state, + chores: state.chores.map(c => + c.id === choreId + ? { + ...c, + completed: !c.completed, + completedAt: !c.completed ? new Date().toISOString() : undefined + } + : c + ), + currentChore: state.currentChore?.id === choreId + ? { + ...state.currentChore, + completed: !state.currentChore.completed, + completedAt: !state.currentChore.completed ? new Date().toISOString() : undefined + } + : state.currentChore + }; + } + return state; + }); + + if (!currentChore) { + throw new Error('Chore not found'); + } + + // Send the update to the server + const response = await fetch(`/api/chores/${choreId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + completed: !currentChore.completed, + completedAt: !currentChore.completed ? new Date().toISOString() : null + }) + }); + + if (!response.ok) { + // Revert the optimistic update if the server request fails + update(state => ({ + ...state, + chores: state.chores.map(c => + c.id === choreId ? currentChore : c + ), + currentChore: state.currentChore?.id === choreId + ? currentChore + : state.currentChore + })); + + throw new Error('Failed to update chore'); + } + + const updatedChore = await response.json(); + return updatedChore; + } catch (error) { + throw error; + } + }, + + // Reassign a chore to a different user + reassignChore: async (choreId: string, userId: string) => { + try { + const response = await fetch(`/api/chores/${choreId}/assign`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ userId }) + }); + + if (!response.ok) { + throw new Error('Failed to reassign chore'); + } + + const updatedChore = await response.json(); + + update(state => ({ + ...state, + chores: state.chores.filter(c => c !== null).map(c => + c.id === choreId ? updatedChore : c + ) as Chore[], + currentChore: state.currentChore?.id === choreId + ? updatedChore + : state.currentChore + })); + + return updatedChore; + } catch (error) { + throw error; + } + } + }; +} + +export const choresStore = createChoresStore(); diff --git a/dooey/src/lib/stores/lists.ts b/dooey/src/lib/stores/lists.ts new file mode 100644 index 0000000..52c7912 --- /dev/null +++ b/dooey/src/lib/stores/lists.ts @@ -0,0 +1,364 @@ +// src/lib/stores/lists.ts +import { writable } from 'svelte/store'; +import type { ShoppingList, ShoppingItem } from '$lib/types'; + +interface ListsState { + lists: ShoppingList[]; + currentList: ShoppingList | null; + isLoading: boolean; +} + +function createListsStore() { + const { subscribe, set, update } = writable({ + lists: [], + currentList: null, + isLoading: false + }); + + return { + subscribe, + + // Fetch all lists + fetchLists: async () => { + update(state => ({ ...state, isLoading: true })); + + try { + const response = await fetch('/api/lists'); + if (!response.ok) { + throw new Error('Failed to fetch lists'); + } + + const lists = await response.json(); + update(state => ({ ...state, lists, isLoading: false })); + return lists; + } catch (error) { + update(state => ({ ...state, isLoading: false })); + throw error; + } + }, + + // Fetch a single list by ID + fetchListDetails: async (listId: string) => { + update(state => ({ ...state, isLoading: true })); + + try { + const response = await fetch(`/api/lists/${listId}`); + if (!response.ok) { + throw new Error('Failed to fetch list details'); + } + + const list = await response.json(); + update(state => ({ ...state, currentList: list, isLoading: false })); + return list; + } catch (error) { + update(state => ({ ...state, isLoading: false })); + throw error; + } + }, + + // Create a new list + createList: async (listData: Partial) => { + update(state => ({ ...state, isLoading: true })); + + try { + const response = await fetch('/api/lists', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(listData) + }); + + if (!response.ok) { + throw new Error('Failed to create list'); + } + + const newList = await response.json(); + update(state => ({ + ...state, + lists: [...state.lists, newList], + currentList: newList, + isLoading: false + })); + + return newList; + } catch (error) { + update(state => ({ ...state, isLoading: false })); + throw error; + } + }, + + // Update a list + updateList: async (listId: string, listData: Partial) => { + update(state => ({ ...state, isLoading: true })); + + try { + const response = await fetch(`/api/lists/${listId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(listData) + }); + + if (!response.ok) { + throw new Error('Failed to update list'); + } + + const updatedList = await response.json(); + update(state => ({ + ...state, + lists: state.lists.map(list => + list.id === listId ? updatedList : list + ), + currentList: state.currentList?.id === listId + ? updatedList + : state.currentList, + isLoading: false + })); + + return updatedList; + } catch (error) { + update(state => ({ ...state, isLoading: false })); + throw error; + } + }, + + // Delete a list + deleteList: async (listId: string) => { + update(state => ({ ...state, isLoading: true })); + + try { + const response = await fetch(`/api/lists/${listId}`, { + method: 'DELETE' + }); + + if (!response.ok) { + throw new Error('Failed to delete list'); + } + + update(state => ({ + ...state, + lists: state.lists.filter(list => list.id !== listId), + currentList: state.currentList?.id === listId + ? null + : state.currentList, + isLoading: false + })); + } catch (error) { + update(state => ({ ...state, isLoading: false })); + throw error; + } + }, + + // Add an item to a list + addItem: async (listId: string, item: Partial) => { + try { + const response = await fetch(`/api/lists/${listId}/items`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(item) + }); + + if (!response.ok) { + throw new Error('Failed to add item'); + } + + const newItem = await response.json(); + + update(state => { + // Update the current list if it's the one we're adding to + if (state.currentList && state.currentList.id === listId) { + return { + ...state, + currentList: { + ...state.currentList, + items: [...state.currentList.items, newItem], + updatedAt: new Date().toISOString() + } + }; + } + + // Update the list in the lists array + return { + ...state, + lists: state.lists.map(list => { + if (list.id === listId) { + return { + ...list, + items: [...(list.items || []), newItem], + updatedAt: new Date().toISOString() + }; + } + return list; + }) + }; + }); + + return newItem; + } catch (error) { + throw error; + } + }, + + // Add multiple items to a list + addMultipleItems: async (listId: string, items: Partial[]) => { + try { + const response = await fetch(`/api/lists/${listId}/items/batch`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ items }) + }); + + if (!response.ok) { + throw new Error('Failed to add items'); + } + + const newItems = await response.json(); + + update(state => { + // Update the current list if it's the one we're adding to + if (state.currentList && state.currentList.id === listId) { + return { + ...state, + currentList: { + ...state.currentList, + items: [...state.currentList.items, ...newItems], + updatedAt: new Date().toISOString() + } + }; + } + + // Update the list in the lists array + return { + ...state, + lists: state.lists.map(list => { + if (list.id === listId) { + return { + ...list, + items: [...(list.items || []), ...newItems], + updatedAt: new Date().toISOString() + }; + } + return list; + }) + }; + }); + + return newItems; + } catch (error) { + throw error; + } + }, + + // Toggle item completion status + toggleItemCompletion: async (listId: string, itemId: string) => { + try { + let currentItem; + + // Find the current state of the item + update(state => { + if (state.currentList && state.currentList.id === listId) { + const item = state.currentList.items.find(i => i.id === itemId); + if (item) { + currentItem = item; + + // Optimistically update the UI + return { + ...state, + currentList: { + ...state.currentList, + items: state.currentList.items.map(i => + i.id === itemId ? { ...i, completed: !i.completed } : i + ), + updatedAt: new Date().toISOString() + } + }; + } + } + return state; + }); + + if (!currentItem) { + throw new Error('Item not found'); + } + + // Send the update to the server + const response = await fetch(`/api/lists/${listId}/items/${itemId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ completed: !currentItem.completed }) + }); + + if (!response.ok) { + // Revert the optimistic update if the server request fails + update(state => { + if (state.currentList && state.currentList.id === listId) { + return { + ...state, + currentList: { + ...state.currentList, + items: state.currentList.items.map(i => + i.id === itemId ? { ...i, completed: currentItem.completed } : i + ) + } + }; + } + return state; + }); + + throw new Error('Failed to update item'); + } + + const updatedItem = await response.json(); + return updatedItem; + } catch (error) { + throw error; + } + }, + + // Delete an item from a list + deleteItem: async (listId: string, itemId: string) => { + try { + // Optimistically update the UI + update(state => { + if (state.currentList && state.currentList.id === listId) { + return { + ...state, + currentList: { + ...state.currentList, + items: state.currentList.items.filter(i => i.id !== itemId), + updatedAt: new Date().toISOString() + } + }; + } + + return { + ...state, + lists: state.lists.map(list => { + if (list.id === listId) { + return { + ...list, + items: (list.items || []).filter(i => i.id !== itemId), + updatedAt: new Date().toISOString() + }; + } + return list; + }) + }; + }); + + // Send the delete request to the server + const response = await fetch(`/api/lists/${listId}/items/${itemId}`, { + method: 'DELETE' + }); + + if (!response.ok) { + // If the server request fails, we should fetch the list again to get the correct state + await this.fetchListDetails(listId); + throw new Error('Failed to delete item'); + } + } catch (error) { + throw error; + } + } + }; +} + +export const listsStore = createListsStore(); diff --git a/dooey/src/lib/stores/toast.ts b/dooey/src/lib/stores/toast.ts new file mode 100644 index 0000000..da8600d --- /dev/null +++ b/dooey/src/lib/stores/toast.ts @@ -0,0 +1,50 @@ +// src/lib/stores/toast.ts +import { writable } from 'svelte/store'; + +type ToastType = 'success' | 'error' | 'info' | 'warning'; + +interface ToastState { + visible: boolean; + message: string; + type: ToastType; +} + +function createToastStore() { + const { subscribe, set, update } = writable({ + visible: false, + message: '', + type: 'info' + }); + + let timeout: NodeJS.Timeout; + + function show(message: string, type: ToastType = 'info', duration: number = 3000) { + // Clear any existing timeout + if (timeout) clearTimeout(timeout); + + // Show the toast + set({ visible: true, message, type }); + + // Hide after duration + timeout = setTimeout(() => { + update(state => ({ ...state, visible: false })); + }, duration); + } + + function hide() { + if (timeout) clearTimeout(timeout); + update(state => ({ ...state, visible: false })); + } + + return { + subscribe, + show, + hide, + success: (message: string, duration?: number) => show(message, 'success', duration), + error: (message: string, duration?: number) => show(message, 'error', duration), + warning: (message: string, duration?: number) => show(message, 'warning', duration), + info: (message: string, duration?: number) => show(message, 'info', duration) + }; +} + +export const toastStore = createToastStore(); diff --git a/dooey/src/lib/stores/user.ts b/dooey/src/lib/stores/user.ts new file mode 100644 index 0000000..8371757 --- /dev/null +++ b/dooey/src/lib/stores/user.ts @@ -0,0 +1,144 @@ +import type { User } from '$lib/types'; +import { writable } from 'svelte/store'; + +interface UserState { + user: User | null; + isLoggedIn: boolean; + isLoading: boolean; + avatar?: string; +} + +function createUserStore() { + const { subscribe, set, update } = writable({ + user: null, + isLoggedIn: false, + isLoading: true + }); + + return { + subscribe, + + // Initialize the store and check for existing session + init: async () => { + try { + const response = await fetch('/api/auth/me'); + if (response.ok) { + const user = await response.json(); + set({ user, isLoggedIn: true, isLoading: false }); + } else { + set({ user: null, isLoggedIn: false, isLoading: false }); + } + } catch (error) { + console.error('Failed to initialize user session:', error); + set({ user: null, isLoggedIn: false, isLoading: false }); + } + }, + + // Login user + login: async (email: string, password: string, rememberMe: boolean = false) => { + update(state => ({ ...state, isLoading: true })); + + try { + const response = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password, rememberMe }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Login failed'); + } + + const user = await response.json(); + set({ user, isLoggedIn: true, isLoading: false }); + return user; + } catch (error) { + update(state => ({ ...state, isLoading: false })); + throw error; + } + }, + + // Register new user + register: async (name: string, email: string, password: string) => { + update(state => ({ ...state, isLoading: true })); + + try { + const response = await fetch('/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, email, password }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Registration failed'); + } + + const user = await response.json(); + set({ user, isLoggedIn: true, isLoading: false }); + return user; + } catch (error) { + update(state => ({ ...state, isLoading: false })); + throw error; + } + }, + + // Logout user + logout: async () => { + try { + await fetch('/api/auth/logout', { method: 'POST' }); + } catch (error) { + console.error('Logout error:', error); + } finally { + set({ user: null, isLoggedIn: false, isLoading: false }); + } + }, + + // Update user profile + updateProfile: async (userData: Partial) => { + update(state => ({ ...state, isLoading: true })); + + try { + const response = await fetch('/api/auth/profile', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(userData) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Failed to update profile'); + } + + const updatedUser = await response.json(); + update(state => ({ + ...state, + user: { ...state.user, ...updatedUser }, + isLoading: false + })); + + return updatedUser; + } catch (error) { + update(state => ({ ...state, isLoading: false })); + throw error; + } + }, + + // Fetch users + getUsers: async (): Promise => { + try { + const response = await fetch('/api/users'); + if (!response.ok) { + throw new Error('Failed to fetch users'); + } + return await response.json(); + } catch (error) { + console.error('Failed to fetch users:', error); + throw error; + } + } + }; +} + +export const userStore = createUserStore(); diff --git a/dooey/src/lib/types.ts b/dooey/src/lib/types.ts new file mode 100644 index 0000000..c7fc2bc --- /dev/null +++ b/dooey/src/lib/types.ts @@ -0,0 +1,97 @@ +export interface User { + id: string; + email: string; + full_name?: string; + created_at: string; + updated_at: string; +} + +export interface House { + id: string; + name: string; + description?: string; + created_at: string; + updated_at: string; + members: HouseMember[]; +} + +export interface HouseInvite { + id: string; + code: string; + house_id: string; + inviter_id: string; + created_at: string; + expires_at: string; +} + +export interface HouseMember { + id: string; + house_id: string; + user_id: string; + role: string; + created_at: string; +} + +export interface ShoppingList { + id: string; + house_id: string; + title: string; + description?: string; + is_archived: boolean; + created_at: string; + updated_at: string; + items: ListItem[]; +} + +export interface ListItem { + id: string; + list_id: string; + name: string; + quantity: number; + unit?: string; + price?: number; + is_completed: boolean; + position: number; + created_at: string; + updated_at: string; +} + +export interface ExpenseSplit { + id: string; + expense_id: string; + user_id: string; + amount: number; + created_at: string; + user?: User; +} + +export interface Expense { + id: string; + list_id: string; + payer_id: string; + amount: number; + description?: string; + date: string; + created_at: string; + payer?: User; + splits: ExpenseSplit[]; +} + +export interface ExpenseSummary { + total_amount: number; + user_balances: Record; +} + +export interface Chore { + id: string; + house_id: string; + title: string; + description?: string; + assigned_to?: string; + due_date?: string; + is_completed: boolean; + created_at: string; + updated_at: string; + assignee?: User; + frequency?: string; +} diff --git a/dooey/src/lib/utils/date.ts b/dooey/src/lib/utils/date.ts new file mode 100644 index 0000000..d4d6fa3 --- /dev/null +++ b/dooey/src/lib/utils/date.ts @@ -0,0 +1,40 @@ +// src/lib/utils/date.ts +export function formatDate(dateString: string): string { + if (!dateString) return ''; + + const date = new Date(dateString); + const now = new Date(); + const yesterday = new Date(now); + yesterday.setDate(yesterday.getDate() - 1); + + // Check if the date is today + if (date.toDateString() === now.toDateString()) { + return `Today, ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; + } + + // Check if the date is yesterday + if (date.toDateString() === yesterday.toDateString()) { + return `Yesterday, ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; + } + + // Check if the date is within the current year + if (date.getFullYear() === now.getFullYear()) { + return date.toLocaleDateString([], { month: 'short', day: 'numeric' }); + } + + // Otherwise, show the full date + return date.toLocaleDateString([], { year: 'numeric', month: 'short', day: 'numeric' }); +} + +export function isOverdue(dateString: string): boolean { + if (!dateString) return false; + + const date = new Date(dateString); + const now = new Date(); + + // Set both dates to midnight for comparison + date.setHours(0, 0, 0, 0); + now.setHours(0, 0, 0, 0); + + return date < now; +} diff --git a/dooey/src/routes/(auth)/+layout.ts b/dooey/src/routes/(auth)/+layout.ts new file mode 100644 index 0000000..1e2c9dc --- /dev/null +++ b/dooey/src/routes/(auth)/+layout.ts @@ -0,0 +1,8 @@ +import { redirect } from '@sveltejs/kit'; +import { authApi } from '$lib/api'; + +export async function load() { + if (!authApi.isAuthenticated()) { + redirect(307, '/login'); + } +} \ No newline at end of file diff --git a/dooey/src/routes/(auth)/chores/+page.svelte b/dooey/src/routes/(auth)/chores/+page.svelte new file mode 100644 index 0000000..673e50e --- /dev/null +++ b/dooey/src/routes/(auth)/chores/+page.svelte @@ -0,0 +1,252 @@ + + + + + Chores | dooey + + +
+
+

Household Chores

+ +
+ +
+ + + + +
+ + {#if loading} +
+ +
+ {:else if filteredChores.length === 0} +
+ + + +

No chores found for the selected filter

+ +
+ {:else} +
+ {#each filteredChores as chore (chore.id)} +
+ +
+ {/each} +
+ {/if} +
+ + + + diff --git a/dooey/src/routes/(auth)/chores/as/+page.svelte b/dooey/src/routes/(auth)/chores/as/+page.svelte new file mode 100644 index 0000000..988eae2 --- /dev/null +++ b/dooey/src/routes/(auth)/chores/as/+page.svelte @@ -0,0 +1,319 @@ + + + + Auto-Schedule Chores | dooey + + +
+
+

Auto-Schedule Chores

+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + {#if loading} +
+ +
+ {:else if scheduledChores.length === 0} +
+ + + +

No schedule generated yet

+ +
+ {:else} +
+

Schedule Preview

+
+ {#each scheduledChores as chore (chore.id)} +
+

{chore.title}

+

Due: {new Date(chore.dueDate).toLocaleDateString()}

+

Assigned: {chore.assignedTo}

+
+ {/each} +
+ +
+ {/if} +
+ + diff --git a/dooey/src/routes/(auth)/chores/new/+page.svelte b/dooey/src/routes/(auth)/chores/new/+page.svelte new file mode 100644 index 0000000..a2c16bc --- /dev/null +++ b/dooey/src/routes/(auth)/chores/new/+page.svelte @@ -0,0 +1,424 @@ + + + + + Create New Chore | dooey + + +
+
+

Create New Chore

+ Cancel +
+ + {#if error} +
+ {error} +
+ {/if} + +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ {#if availableUsers.length > 0} + {#each availableUsers as user} + + {/each} + {:else} +

No users available. You'll need to create users first.

+ {/if} +
+
+
+ +
+ Back + +
+
+
+ + diff --git a/dooey/src/routes/(auth)/lists/+page.svelte b/dooey/src/routes/(auth)/lists/+page.svelte new file mode 100644 index 0000000..a386d64 --- /dev/null +++ b/dooey/src/routes/(auth)/lists/+page.svelte @@ -0,0 +1,184 @@ + + + + + Shopping Lists | dooey + + +
+
+

Your Shopping Lists

+ +
+ + {#if loading} +
+ +
+ {:else if $listsStore.lists.length === 0} +
+ + + +

You don't have any shopping lists yet

+ +
+ {:else} +
+ {#each $listsStore.lists as list (list.id)} + + {/each} +
+ {/if} +
+ + + + diff --git a/dooey/src/routes/(auth)/lists/[id]/+page.svelte b/dooey/src/routes/(auth)/lists/[id]/+page.svelte new file mode 100644 index 0000000..4062414 --- /dev/null +++ b/dooey/src/routes/(auth)/lists/[id]/+page.svelte @@ -0,0 +1,312 @@ + + + + + {currentList ? currentList.name : 'Shopping List'} | dooey + + +{#if loading} +
+ +
+{:else if !currentList} +
+

List not found or you don't have access to it.

+ Back to Lists +
+{:else} +
+
+

{currentList.name}

+ +
+ + + +
+
+ +
+ + +
+ + {#if activeTab === 'items'} +
+ {#if currentList.items.length === 0} +
+

This list is empty. Add items below or scan a receipt.

+
+ {:else} +
+ {#each currentList.items as item (item.id)} +
+ toggleItemCompletion(item.id)} + onDelete={() => deleteItem(item.id)} + /> +
+ {/each} +
+ {/if} + + +
+ {:else if activeTab === 'costs'} +
+ +
+

Cost splitting features coming soon!

+
+
+ {/if} +
+{/if} + + + + diff --git a/dooey/src/routes/(auth)/lists/new/+layout.ts b/dooey/src/routes/(auth)/lists/new/+layout.ts new file mode 100644 index 0000000..e69de29 diff --git a/dooey/src/routes/(auth)/lists/new/+page.svelte b/dooey/src/routes/(auth)/lists/new/+page.svelte new file mode 100644 index 0000000..b43c0d4 --- /dev/null +++ b/dooey/src/routes/(auth)/lists/new/+page.svelte @@ -0,0 +1,865 @@ + + + + Scan Shopping List | dooey + + +
+ {#if step === 'capture'} +
+
+

Scan Shopping List

+ + + + + + + +
+ +
+ + + + + + + + +
+
+
+
+
+
+
+ +
+

Position handwritten list within frame

+
+
+ + +
+ + + + + +
+ + + +
+ + {#if error} +
+ {error} +
+ {/if} +
+ {:else if step === 'processing'} +
+
+

Converting...

+
+ +
+
+
+
+
+
+
+
+
+ +

Reading your handwritten list...

+
+ {:else if step === 'review'} +
+
+

Review Items

+ + +
+ + {#if error} +
+ {error} +
+ {/if} + +
+ + +
+ +
+
+

Items ({scannedItems.length})

+ +
+ +
+ {#each scannedItems as item, index} +
+
+ +
+
+ +
+
+ +
+ + +
+ {/each} +
+
+ +
+ +
+
+ {/if} +
+ + + + diff --git a/dooey/src/routes/+error.svelte b/dooey/src/routes/+error.svelte new file mode 100644 index 0000000..99184c2 --- /dev/null +++ b/dooey/src/routes/+error.svelte @@ -0,0 +1,113 @@ + + + + + {$page.status}: {$page.error ? $page.error.message : 'Unknown error'} + + +
+
+

{$page.status}

+

{$page.error ? $page.error.message : 'An unknown error occurred.'}

+ + {#if $page.status === 404} +

We couldn't find the page you were looking for.

+ {:else} +

Something went wrong. Please try again later.

+ {/if} + +
+ Go to Home + +
+
+
+ + diff --git a/dooey/src/routes/+layout.svelte b/dooey/src/routes/+layout.svelte new file mode 100644 index 0000000..e84c5c5 --- /dev/null +++ b/dooey/src/routes/+layout.svelte @@ -0,0 +1,48 @@ + + +
+ + +
+ +
+ + {#if $page.url.pathname !== '/lists/new'} + + {/if} + + {#if $toastStore.visible} + + {/if} +
+ + diff --git a/dooey/src/routes/+layout.ts b/dooey/src/routes/+layout.ts new file mode 100644 index 0000000..a3d1578 --- /dev/null +++ b/dooey/src/routes/+layout.ts @@ -0,0 +1 @@ +export const ssr = false; diff --git a/dooey/src/routes/+page.svelte b/dooey/src/routes/+page.svelte new file mode 100644 index 0000000..c6d62a3 --- /dev/null +++ b/dooey/src/routes/+page.svelte @@ -0,0 +1,381 @@ + + + + dooey | Dashboard + + +
+ +
+
+

{greeting}, {$userStore.user?.name || 'Friend'}!

+ +
+
+
+ 🔥 + {streakDays} +
+ day streak +
+
+ + +
+
+

Shopping Lists

+ +
+ + {#if recentLists.length > 0} +
+ {#each recentLists as list, i} +
+ +
+ {/each} +
+ {:else} +
+ No shopping lists +

No shopping lists yet. Create your first list!

+ +
+ {/if} +
+ + +
+
+

Chores

+
+ + +
+
+ + {#if upcomingChores.length > 0} +
+ {#each upcomingChores as chore, i} +
+ +
+ {/each} +
+ {:else} +
+ No chores +

No chores assigned. Add some tasks to get started!

+ +
+ {/if} +
+ + +
+
+

Expenses

+ +
+ + {#if upcomingChores.length > 0} +
+ {#each upcomingChores as chore, i} +
+ +
+ {/each} +
+ {:else} +
+ No chores +

No expenses yet!

+ +
+ {/if} +
+
+ + diff --git a/dooey/src/routes/+page.ts b/dooey/src/routes/+page.ts new file mode 100644 index 0000000..762c916 --- /dev/null +++ b/dooey/src/routes/+page.ts @@ -0,0 +1,7 @@ +import { redirect } from '@sveltejs/kit'; +import { authApi } from '$lib/api'; + +export async function load() { + const page = authApi.isAuthenticated() ? '/' : '/login'; + redirect(307, page); +} \ No newline at end of file diff --git a/dooey/src/routes/login/+page.svelte b/dooey/src/routes/login/+page.svelte new file mode 100644 index 0000000..0d381d0 --- /dev/null +++ b/dooey/src/routes/login/+page.svelte @@ -0,0 +1,322 @@ + + + + + Login | dooey + + +
+
+
+

Welcome Back

+

Sign in to your account to continue

+
+ + {#if error} +
+ {error} +
+ {/if} + +
+
+ + +
+ +
+ + +
+ +
+ + + Forgot password? +
+ + +
+ + +
+
+ + + + diff --git a/dooey/src/routes/onboarding/+page.svelte b/dooey/src/routes/onboarding/+page.svelte new file mode 100644 index 0000000..353b7c9 --- /dev/null +++ b/dooey/src/routes/onboarding/+page.svelte @@ -0,0 +1,439 @@ + + +