From 240e54eec48a27f9b1e4d366fae05d254ffda648 Mon Sep 17 00:00:00 2001 From: mohamad Date: Thu, 27 Mar 2025 08:13:54 +0100 Subject: [PATCH] =?UTF-8?q?weeee=F0=9F=92=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/.env | 8 + app/.gitignore | 0 app/Dockerfile | 22 + app/README.md | 0 app/alembic.ini | 38 + app/alembic/__pycache__/env.cpython-312.pyc | Bin 0 -> 3356 bytes app/alembic/env.py | 85 + app/app/__init__.py | 0 app/app/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 172 bytes app/app/__pycache__/main.cpython-312.pyc | Bin 0 -> 3582 bytes app/app/api/auth/__init__.py | 0 .../auth/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 181 bytes .../auth/__pycache__/router.cpython-312.pyc | Bin 0 -> 4667 bytes app/app/api/auth/router.py | 89 + app/app/api/chores/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 183 bytes .../chores/__pycache__/router.cpython-312.pyc | Bin 0 -> 10198 bytes app/app/api/chores/router.py | 260 ++ app/app/api/expenses/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 185 bytes .../__pycache__/router.cpython-312.pyc | Bin 0 -> 12498 bytes app/app/api/expenses/router.py | 320 ++ app/app/api/houses/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 183 bytes .../houses/__pycache__/router.cpython-312.pyc | Bin 0 -> 19552 bytes app/app/api/houses/router.py | 478 ++ app/app/api/ocr/__init__.py | 0 .../ocr/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 180 bytes .../ocr/__pycache__/router.cpython-312.pyc | Bin 0 -> 4721 bytes app/app/api/ocr/router.py | 109 + app/app/api/shopping_lists/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 191 bytes .../__pycache__/router.cpython-312.pyc | Bin 0 -> 21138 bytes app/app/api/shopping_lists/router.py | 512 ++ app/app/api/users/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 182 bytes .../users/__pycache__/router.cpython-312.pyc | Bin 0 -> 2473 bytes app/app/api/users/router.py | 52 + app/app/core/__init__.py | 0 .../core/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 177 bytes .../core/__pycache__/config.cpython-312.pyc | Bin 0 -> 3176 bytes .../__pycache__/dependencies.cpython-312.pyc | Bin 0 -> 2967 bytes .../core/__pycache__/logger.cpython-312.pyc | Bin 0 -> 1405 bytes .../core/__pycache__/security.cpython-312.pyc | Bin 0 -> 1782 bytes app/app/core/config.py | 57 + app/app/core/dependencies.py | 63 + app/app/core/logger.py | 64 + app/app/core/security.py | 34 + app/app/db/__init__.py | 0 .../db/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 175 bytes app/app/db/__pycache__/base.cpython-312.pyc | Bin 0 -> 691 bytes .../db/__pycache__/session.cpython-312.pyc | Bin 0 -> 1211 bytes app/app/db/base.py | 17 + app/app/db/init_db.py | 0 app/app/db/session.py | 18 + app/app/main.py | 66 + app/app/models/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 179 bytes .../models/__pycache__/chore.cpython-312.pyc | Bin 0 -> 1573 bytes .../__pycache__/expense.cpython-312.pyc | Bin 0 -> 2301 bytes .../models/__pycache__/house.cpython-312.pyc | Bin 0 -> 2227 bytes .../models/__pycache__/invite.cpython-312.pyc | Bin 0 -> 1397 bytes .../__pycache__/shopping_list.cpython-312.pyc | Bin 0 -> 2620 bytes .../models/__pycache__/user.cpython-312.pyc | Bin 0 -> 1182 bytes app/app/models/chore.py | 27 + app/app/models/expense.py | 40 + app/app/models/house.py | 55 + app/app/models/invite.py | 25 + app/app/models/shopping_list.py | 53 + app/app/models/user.py | 19 + app/app/schemas/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 180 bytes .../schemas/__pycache__/chore.cpython-312.pyc | Bin 0 -> 2116 bytes .../__pycache__/expense.cpython-312.pyc | Bin 0 -> 2795 bytes .../schemas/__pycache__/house.cpython-312.pyc | Bin 0 -> 2924 bytes .../__pycache__/invite.cpython-312.pyc | Bin 0 -> 1281 bytes .../__pycache__/shopping_list.cpython-312.pyc | Bin 0 -> 3207 bytes .../schemas/__pycache__/user.cpython-312.pyc | Bin 0 -> 1990 bytes app/app/schemas/chore.py | 42 + app/app/schemas/expense.py | 57 + app/app/schemas/house.py | 62 + app/app/schemas/invite.py | 26 + app/app/schemas/shopping_list.py | 66 + app/app/schemas/user.py | 37 + app/app/services/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 181 bytes .../invite_service.cpython-312.pyc | Bin 0 -> 789 bytes .../__pycache__/ocr_service.cpython-312.pyc | Bin 0 -> 3768 bytes app/app/services/auth_service.py | 0 app/app/services/chore_service.py | 0 app/app/services/db_service.py | 0 app/app/services/expense_service.py | 0 app/app/services/invite_service.py | 16 + app/app/services/ocr_service.py | 89 + app/docker-compose.yml | 31 + app/household_api.log | 38 + app/pyproject.toml | 27 + app/tests/api/auth.py | 50 + app/tests/api/shopping_lists.py | 109 + app/tests/conftest.py | 0 app/uv.lock | 697 +++ dooey/.gitignore | 24 + dooey/.npmrc | 1 + dooey/.prettierignore | 6 + dooey/.prettierrc | 15 + dooey/README.md | 38 + dooey/e2e/demo.test.ts | 6 + dooey/eslint.config.js | 37 + dooey/package-lock.json | 4103 +++++++++++++++++ dooey/package.json | 38 + dooey/playwright.config.ts | 9 + dooey/src/app.css | 126 + dooey/src/app.d.ts | 13 + dooey/src/app.html | 12 + dooey/src/hooks.server.ts | 22 + dooey/src/lib/api.ts | 630 +++ .../lib/components/BottomNavigation.svelte | 80 + dooey/src/lib/components/Toast.svelte | 235 + dooey/src/lib/stores/chores.ts | 260 ++ dooey/src/lib/stores/lists.ts | 364 ++ dooey/src/lib/stores/toast.ts | 50 + dooey/src/lib/stores/user.ts | 144 + dooey/src/lib/types.ts | 97 + dooey/src/lib/utils/date.ts | 40 + dooey/src/routes/(auth)/+layout.ts | 8 + dooey/src/routes/(auth)/chores/+page.svelte | 252 + .../src/routes/(auth)/chores/as/+page.svelte | 319 ++ .../src/routes/(auth)/chores/new/+page.svelte | 424 ++ dooey/src/routes/(auth)/lists/+page.svelte | 184 + .../src/routes/(auth)/lists/[id]/+page.svelte | 312 ++ dooey/src/routes/(auth)/lists/new/+layout.ts | 0 .../src/routes/(auth)/lists/new/+page.svelte | 865 ++++ dooey/src/routes/+error.svelte | 113 + dooey/src/routes/+layout.svelte | 48 + dooey/src/routes/+layout.ts | 1 + dooey/src/routes/+page.svelte | 381 ++ dooey/src/routes/+page.ts | 7 + dooey/src/routes/login/+page.svelte | 322 ++ dooey/src/routes/onboarding/+page.svelte | 439 ++ dooey/src/service-worker.js | 17 + dooey/static/favicon.png | Bin 0 -> 1571 bytes dooey/static/manifest.json | 58 + dooey/svelte.config.js | 14 + dooey/tsconfig.json | 19 + dooey/vite.config.ts | 15 + 145 files changed, 14006 insertions(+) create mode 100644 app/.env create mode 100644 app/.gitignore create mode 100644 app/Dockerfile create mode 100644 app/README.md create mode 100644 app/alembic.ini create mode 100644 app/alembic/__pycache__/env.cpython-312.pyc create mode 100644 app/alembic/env.py create mode 100644 app/app/__init__.py create mode 100644 app/app/__pycache__/__init__.cpython-312.pyc create mode 100644 app/app/__pycache__/main.cpython-312.pyc create mode 100644 app/app/api/auth/__init__.py create mode 100644 app/app/api/auth/__pycache__/__init__.cpython-312.pyc create mode 100644 app/app/api/auth/__pycache__/router.cpython-312.pyc create mode 100644 app/app/api/auth/router.py create mode 100644 app/app/api/chores/__init__.py create mode 100644 app/app/api/chores/__pycache__/__init__.cpython-312.pyc create mode 100644 app/app/api/chores/__pycache__/router.cpython-312.pyc create mode 100644 app/app/api/chores/router.py create mode 100644 app/app/api/expenses/__init__.py create mode 100644 app/app/api/expenses/__pycache__/__init__.cpython-312.pyc create mode 100644 app/app/api/expenses/__pycache__/router.cpython-312.pyc create mode 100644 app/app/api/expenses/router.py create mode 100644 app/app/api/houses/__init__.py create mode 100644 app/app/api/houses/__pycache__/__init__.cpython-312.pyc create mode 100644 app/app/api/houses/__pycache__/router.cpython-312.pyc create mode 100644 app/app/api/houses/router.py create mode 100644 app/app/api/ocr/__init__.py create mode 100644 app/app/api/ocr/__pycache__/__init__.cpython-312.pyc create mode 100644 app/app/api/ocr/__pycache__/router.cpython-312.pyc create mode 100644 app/app/api/ocr/router.py create mode 100644 app/app/api/shopping_lists/__init__.py create mode 100644 app/app/api/shopping_lists/__pycache__/__init__.cpython-312.pyc create mode 100644 app/app/api/shopping_lists/__pycache__/router.cpython-312.pyc create mode 100644 app/app/api/shopping_lists/router.py create mode 100644 app/app/api/users/__init__.py create mode 100644 app/app/api/users/__pycache__/__init__.cpython-312.pyc create mode 100644 app/app/api/users/__pycache__/router.cpython-312.pyc create mode 100644 app/app/api/users/router.py create mode 100644 app/app/core/__init__.py create mode 100644 app/app/core/__pycache__/__init__.cpython-312.pyc create mode 100644 app/app/core/__pycache__/config.cpython-312.pyc create mode 100644 app/app/core/__pycache__/dependencies.cpython-312.pyc create mode 100644 app/app/core/__pycache__/logger.cpython-312.pyc create mode 100644 app/app/core/__pycache__/security.cpython-312.pyc create mode 100644 app/app/core/config.py create mode 100644 app/app/core/dependencies.py create mode 100644 app/app/core/logger.py create mode 100644 app/app/core/security.py create mode 100644 app/app/db/__init__.py create mode 100644 app/app/db/__pycache__/__init__.cpython-312.pyc create mode 100644 app/app/db/__pycache__/base.cpython-312.pyc create mode 100644 app/app/db/__pycache__/session.cpython-312.pyc create mode 100644 app/app/db/base.py create mode 100644 app/app/db/init_db.py create mode 100644 app/app/db/session.py create mode 100644 app/app/main.py create mode 100644 app/app/models/__init__.py create mode 100644 app/app/models/__pycache__/__init__.cpython-312.pyc create mode 100644 app/app/models/__pycache__/chore.cpython-312.pyc create mode 100644 app/app/models/__pycache__/expense.cpython-312.pyc create mode 100644 app/app/models/__pycache__/house.cpython-312.pyc create mode 100644 app/app/models/__pycache__/invite.cpython-312.pyc create mode 100644 app/app/models/__pycache__/shopping_list.cpython-312.pyc create mode 100644 app/app/models/__pycache__/user.cpython-312.pyc create mode 100644 app/app/models/chore.py create mode 100644 app/app/models/expense.py create mode 100644 app/app/models/house.py create mode 100644 app/app/models/invite.py create mode 100644 app/app/models/shopping_list.py create mode 100644 app/app/models/user.py create mode 100644 app/app/schemas/__init__.py create mode 100644 app/app/schemas/__pycache__/__init__.cpython-312.pyc create mode 100644 app/app/schemas/__pycache__/chore.cpython-312.pyc create mode 100644 app/app/schemas/__pycache__/expense.cpython-312.pyc create mode 100644 app/app/schemas/__pycache__/house.cpython-312.pyc create mode 100644 app/app/schemas/__pycache__/invite.cpython-312.pyc create mode 100644 app/app/schemas/__pycache__/shopping_list.cpython-312.pyc create mode 100644 app/app/schemas/__pycache__/user.cpython-312.pyc create mode 100644 app/app/schemas/chore.py create mode 100644 app/app/schemas/expense.py create mode 100644 app/app/schemas/house.py create mode 100644 app/app/schemas/invite.py create mode 100644 app/app/schemas/shopping_list.py create mode 100644 app/app/schemas/user.py create mode 100644 app/app/services/__init__.py create mode 100644 app/app/services/__pycache__/__init__.cpython-312.pyc create mode 100644 app/app/services/__pycache__/invite_service.cpython-312.pyc create mode 100644 app/app/services/__pycache__/ocr_service.cpython-312.pyc create mode 100644 app/app/services/auth_service.py create mode 100644 app/app/services/chore_service.py create mode 100644 app/app/services/db_service.py create mode 100644 app/app/services/expense_service.py create mode 100644 app/app/services/invite_service.py create mode 100644 app/app/services/ocr_service.py create mode 100644 app/docker-compose.yml create mode 100644 app/household_api.log create mode 100644 app/pyproject.toml create mode 100644 app/tests/api/auth.py create mode 100644 app/tests/api/shopping_lists.py create mode 100644 app/tests/conftest.py create mode 100644 app/uv.lock create mode 100644 dooey/.gitignore create mode 100644 dooey/.npmrc create mode 100644 dooey/.prettierignore create mode 100644 dooey/.prettierrc create mode 100644 dooey/README.md create mode 100644 dooey/e2e/demo.test.ts create mode 100644 dooey/eslint.config.js create mode 100644 dooey/package-lock.json create mode 100644 dooey/package.json create mode 100644 dooey/playwright.config.ts create mode 100644 dooey/src/app.css create mode 100644 dooey/src/app.d.ts create mode 100644 dooey/src/app.html create mode 100644 dooey/src/hooks.server.ts create mode 100644 dooey/src/lib/api.ts create mode 100644 dooey/src/lib/components/BottomNavigation.svelte create mode 100644 dooey/src/lib/components/Toast.svelte create mode 100644 dooey/src/lib/stores/chores.ts create mode 100644 dooey/src/lib/stores/lists.ts create mode 100644 dooey/src/lib/stores/toast.ts create mode 100644 dooey/src/lib/stores/user.ts create mode 100644 dooey/src/lib/types.ts create mode 100644 dooey/src/lib/utils/date.ts create mode 100644 dooey/src/routes/(auth)/+layout.ts create mode 100644 dooey/src/routes/(auth)/chores/+page.svelte create mode 100644 dooey/src/routes/(auth)/chores/as/+page.svelte create mode 100644 dooey/src/routes/(auth)/chores/new/+page.svelte create mode 100644 dooey/src/routes/(auth)/lists/+page.svelte create mode 100644 dooey/src/routes/(auth)/lists/[id]/+page.svelte create mode 100644 dooey/src/routes/(auth)/lists/new/+layout.ts create mode 100644 dooey/src/routes/(auth)/lists/new/+page.svelte create mode 100644 dooey/src/routes/+error.svelte create mode 100644 dooey/src/routes/+layout.svelte create mode 100644 dooey/src/routes/+layout.ts create mode 100644 dooey/src/routes/+page.svelte create mode 100644 dooey/src/routes/+page.ts create mode 100644 dooey/src/routes/login/+page.svelte create mode 100644 dooey/src/routes/onboarding/+page.svelte create mode 100644 dooey/src/service-worker.js create mode 100644 dooey/static/favicon.png create mode 100644 dooey/static/manifest.json create mode 100644 dooey/svelte.config.js create mode 100644 dooey/tsconfig.json create mode 100644 dooey/vite.config.ts 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 0000000000000000000000000000000000000000..28fb2675dec77459179800fe2af55c465e8724ed GIT binary patch literal 3356 zcmai0U2Gf25#Hk+$>UMFqon9xwobBs%*0{iM6ug6Xdyankhm3sASn<+0CD22sAKMj z-8)KRY6D7Lv<(oT4GKgreeh!oxj-KJm>20aFD4{`%0WgOBrV!E0di48Po25rkyI?8 zE8Xtw?Ck7tXXcyzqPJHf(0;%1Rr!+?Az$MpYC?-RUFHbcCOXkMlN4}_nVc0X#5kmR zledI|5YU1dw-SX!K*!CbB^JbhPM9f6Do6pHG}Bg3p(mh4v)7UfvZWLhD^ti=eT6=& zztGPSDplqpGdg2ofDTlycIFZFVSkL!*u`r*U^_O@*;9Eoq`UjVkS^=euY|&|o~8*s zrE{x--UIUy;P=8f3c97rLie7K-f6Lz2ol55j3adp=A?4t=^elyjpRCZ)w!(lZxmk2 za=x@`nDmxouNq~T30222kqaD~mOR6;VK$<#vE zmuBO>P;)8s<7*C#ey_XcRI7$vE}Dkx`ANE2r8e;6r8S39Kf$P1V|JkZX&)D=*-t{Ent(E25}G}8q8oyhzg z-K3?ON3Ukp;CN~oUT|93K#i`)Ad*lXesXZ4TVh7lQ=OVut$8h_AIR>{P0cK=QLCP- zG4mZ?s%lKLT(53I;Mtl*^{n9Yu;eSA#>&(yTGZ2Y&C~o|)9@(M%;Jh+>#pCU8=47; zP;{!EyOd4(iSXWiCD^4IHm-oT7)Gt1YI}tZ;Jj~zLe3ODrrE9*q?0eRnqBNTIm@wr zkaQ6>)U!8ly|Vl+WRJW2GsCW%w&Qx5SvIug*Qoox=Tw(R>sUxP zg~5g}c+{gDkIWwpjU5e5-+%q#;I;px;_<{|nMi%j)N~^?eVCd#7L}%WxglOY5Y_+k zM4D};W*VuPGZ#~1H#N98&+pF17hg#F5?oFTR7x$71Q9|oSA%^2D-e+H?X^lPb5JO1 zki}7%AP=}sZguUtAoru346?VQzl{fx_H)^b#fEfFsFSC%m$b4swv>&rOQ0n1dNv*e z0~?7LY!s<6=)l`37J8eF1L^naPVu~_*d*}3i(&}rTcqud`(vLM4~3t8lgj-6+fT#R z&juG|epik!4kt@6Z8r?nd+?p!fvdO;Fw}kNUi;E3Z8&Z3Bwec)U32Sdb*=-ol?%Z1 zL2L&d4(%?vLn^%?-Q7nHTOjv1o&N<$e;V87e;tpLyWAbFqIAoKw3mr^J1r~j!CN8N z$%i`uka_%zTzU_RK(}TNZNaqtR*P(D>uXs8@Xg;OAHNCTci@}C@CbB5fPNaBd6UG? z!TrbA!+-&4^)EcYzVN|E7h%5r6y(2Q1LOflyCrIC%y0meLwrT-L8J2bP%=T;QdVNlcEz`6T>&w58-qF0NuaH6M!_C3_N!%%1v>sA&&j=Mss|jF}|=bzI>!i9QO}x zKmW<|kCVbcV(a!v56O%-mFb2u-Be}{l$k$CNAgHho@&Tb2h%U@%L`3$;i0$yz?PW> ziIW6L4>hGr4e8RpG}#m;8^YvC64ss^i3?9oQ0fVY!GA-UIRojqdgiWzUB6uH&%es; zKEvfF6T5>v(j%k!S$_9Am!C@Pz9=AlBMtmLj>}&W_F}_Gk8=4}guSr@(o-DB>|NoI zo=xO$CikA>^0}V9Ye}SE5`hmWB*S4#*$F=xyx9!LmrbV(HK?2mJ;X5ae5vE=&`o_E zMlO8kG0c8{sGGm(LIFc$ zVptp)=6x7TnO6AVW3-3`+83jW96)MNw1RL6-0drd8`ZNS9^ms38-FIJppXQ?awfhC z{F+IBz`h4OLMw-P`K zQj;HR@M918xi1Czx9d%1uA$5w2=lNuFpP1urJQC4xv7&E1a9j7vnK?G&z?EO;Z)9W s>d71t# 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 0000000000000000000000000000000000000000..d7bd02fd95c42a035b9a4a46dc061f7db62b045f GIT binary patch literal 172 zcmX@j%ge<81fi7=(?RrO5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!@^-e02`x@7Dvk-u z%&W}F%P%fT%t_BojB!aV&MwI>h)GE;i%H4PPpyn8D9X=DO)iNqE-5NaE-5WajY%vh th>4HS%*!l^kJl@x{Ka9Do1apelWJGQ3N)J$h>JmtkIamWj77{q764eEE@l7# literal 0 HcmV?d00001 diff --git a/app/app/__pycache__/main.cpython-312.pyc b/app/app/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7122bf21b4cf00f17b9d1111f81bd7ef32b7ce4 GIT binary patch literal 3582 zcmcImT})fa6`uR!AN+^GfPwI1C=i^DASMk75O$YMvOuF43UoK?rLrvF!I;{36{){!`{piD)$r6a_Xm?Sq`dS> zGjryg?|yUcIWuSOZ{2POg6GMXe@g7y5&AQom`{Zv?Ei~H=pho2z@$)`i7;t4!ZLKu zrnod8;We2{S<=>sRg?LYEp3lf7_tLory;vQb{nz>-7Jrw?JLhr>2oAlAF*H9Hd$R0X#`xqX)TXZKWFr(B4CI)e&?%phr!{@`x*rjcN5GR`9_$ z)ojAmZKIp>Q2N_`$`V~i8v}>$x(kI4)-fL8cxLFJC>sn;&JLk9L{4o znt-GEzc|MA93Ab2mcyOUdSoYbE(PLny6MhcM8URy&{u*ivArFWm66GDVw=K0&S6;z zGQ@LbVru$EQV>#jF(zTqSW{vmfh8(QVouQ{yNnejnMugY?p`sAGqG&4XF(P-Kx$Ev zN(vLFp^oOH)C^(H1bO0)rBdQzRFsm5WJV@c`ZO+K0cVtCEG3(WG*;$C;cz;SV*-|B z!p|#8me?fS*SK-?wa>Rge;?ZZE_Byn+df2!acB`*IEvKZ2+H0HTHuYzkpT(|3l_@v z!t*(L#GApT1#1Nu%=vvNoFBlE`5F6=`-#PZ?y&!3b#8@SfYSWp18cA*CxcelX><{N z()~+rkqPqQ{ecYOo_!~oSxRL@AV?*WvDvFw{*fYP zX9av~RuDzJG@F&g1sqqRvLfZ;N>0MFv21oW9ZP0{oeJ};M(Xf!aonIr(W9#ZM^&C`;Xt>xS@Incln{;)wZlH?$n+xRGz*& z_Im?sZD*V_xvvZ-J;WXccNrL-tGgr64*Z+8tGex#1uLa@n4qp2w_~2&o20AESNfk<4M=rbvy{|CC;~h!++3uFX^ssB0tp-8QtiT9tl9|@C#)XM3jb%BOz{^eV;#6ErBBnP6Bi{}qJI*k5>64F;vT*5>Sxfk{Nv`6rG-#A{?wjYTg>Ta{XHP zYLwn}(TSwARW znZ|WTZ=-YCfjY2XPK!b=gXf66q6?|QJUzb{adi{>-z3|k++1R^W$%S}VfeU=i1&X|_ zz}MyZx}v?V7#b*q!e58NJ9QKJPWOKUs!pr^bd7l}c9A zxUj=CP-6!*Hd13p$&MJlfU5JTdUa@{yKrVWe`Z(>zWxNOr>}0my?seV)w^h>h&Tf@ zyfIcd`$qok8|rIsZrRk}m^yiLds#z84pBwEVf|*I^HRR^lG-uw#ILrGsN?UcW04Yv z7~fOGGAwvyE?ne&YqtxnefictwdMR*7uBYrt--Cc$6%}I^>Z%Yd`@i&edSjh2e#_B z952PL4sO&Jf|v8b%WC)VlNq(^t?kZjzvk-qkGSeBdg=*CmESu|G#lnj`lnL1M2N$Hvj+t literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fd123d19a74672639785f6234d0a0cf622107dec GIT binary patch literal 181 zcmX@j%ge<81fi7=(?RrO5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!3URiI2`x@7Dvk-u z%&W}F%P%fT%t_BojB!aV&MwI>h)GE;i%H4PPpyn8D9X=DO)iNqE-5NaE-5WajY%vh z0OHJ;#L|+CnE3e2yv&mLc)fzkUmP~M`6;D2sdh!IKuZ{bxERFv$jr#dSi}ru0RU9G BG7|s* literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..df2ca375bcc73f2b4569efdf621976a9afcd0675 GIT binary patch literal 4667 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 E00fW~VgLXD literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fc31d3cb87e59091c4061e993a49053d4e2f5be5 GIT binary patch literal 183 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 E7Zw=%P5=M^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..49ad10b92e719e226c282e39ccf608af7d7391bf GIT binary patch literal 185 zcmX@j%ge<81fi7=(?RrO5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!ig31y2`x@7Dvk-u z%&W}F%P%fT%t_BojB!aV&MwI>h)GE;i%H4PPpyn8D9X=DO)iNqE-5NaE-5WajY%vh z0OHJ;)QW=CyyDd2nE3e2yv&mLc)fzkUmP~M`6;D2sdh!IK#LfGxERFv$jr#dSi}ru F0RTpqGtdA4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..22ee8678949147dcce39a5a92d384a5ad778d386 GIT binary patch literal 12498 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 P{0aRHR>r>wYvg|cTyF1_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..697d64cd917af61695bddf9ea3e7c31caf9f7db6 GIT binary patch literal 183 zcmX@j%ge<81fi7=(?RrO5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!3UjuK2`x@7Dvk-u z%&W}F%P%fT%t_BojB!aV&MwI>h)GE;i%H4PPpyn8D9X=DO)iNqE-5NaE-5WajY%vh z0OHJ;jQrB#)Z&=<_{_Y_lK6PNg34bUHo5sJr8%i~MXW$;7=gGL#Q4a}$jDg43}gWS D&SWzB literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c8ad1d2e00974dae3d2757752f302ff90d33ad73 GIT binary patch literal 19552 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 b*z 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 0000000000000000000000000000000000000000..b15b5bb2d339974f819b7cb88d3a46bf0616b471 GIT binary patch literal 180 zcmX@j%ge<81fi7=(?RrO5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!3U;=N2`x@7Dvk-u z%&W}F%P%fT%t_BojB!aV&MwI>h)GE;i%H4PPpyn8D9X=DO)iNqE-5NaE-5WajY%vh z0OHJ;{N$pT`1s7c%#!$cy@JYL95%W6DWy57c15f}I~aku7{vI<%*e=C#0+Es02y*I AqyPW_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ad08c96c1fbd9823e8ab163b26be000008623dbf GIT binary patch literal 4721 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 OKzDtKkLG6qDE}Xn{|D{> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7608a02782682ec18bc186fab61e417f1a762755 GIT binary patch literal 182 zcmX@j%ge<81fi7=(?RrO5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!3U#)M2`x@7Dvk-u z%&W}F%P%fT%t_BojB!aV&MwI>h)GE;i%H4PPpyn8D9X=DO)iNqE-5NaE-5WajY%vh z0OHJ;Qm_H>@tJv7%Q6rTOF9sk?;bCM#KD3G{m9R!H}kkW>_D8=y$fL4^NF)KgJ<+1!06a6=__Y;~34Wye7m2O^l106qhtd z++p(t)v3ASE}ItBCe0mp+q9&5G;iE%(+<_A`QrhbcB(<3{aPppdRF-U+gCSo1R6!%E4a3kpSq4#JdRbvytID>>2ys6*sL# zz}oZn_G43nH9raczeOAXUoP91yTO+O2eKExX0l)q3OsR~{qmHh({i)LcpigW*a|W6e9ACpbc&NxDPxLRQ}xM7LT7~mjmh{a zG4guOAW2QfM76wI&8KTOYhRrN^LhBz$6y*k9Mp`txu31&#g?p1m@A>IxRq0-KZNS~ zNT}Z=he~J^eJ;#!QDHXbmSLPCCRNBZkLs+pnh;?iApY`TfBm!91Q})U?IyJqr>|XOxH-fuH=epPKVo9CzVv*nwQcQryEJ>S5!=#0@S)e45%ID!z5-cPmLG8T8@5+RI}5zXrRtbIbfNX zi-RBq^3DcfSl01&7`+)rC;`=QMm!^#O?Al~=X*e?Ib;&u&_CC46Fh`VuMM~Ta}G;$Gw3U)qL z!Q3d?gD$@UU%y?+u0jE{>|(B>aa$?ho4eLf%*QiwO0J_-R_xtr1rxkWR!n(DQB`?@ z$PC2<1C`M!kyEOn%LRsaB{x~c#2A3uM^2~J0w&1<0Nb=&b*q-IPTAey1Z8KQ4Q`^x zwge5!W4Y~aki-QoPgx2qA+P5xNim6LSWYmSGEHg;DU2;gTGuqiw49V=pqWhBxTDRW zMA_9v9kiX%416>KljW(pNkY^qOUfuj#g;UkQVYajRn0npEYnsd+FTRe+LUN7?CgNg z_!=e~QJP|XhuM1@WwOSc7 z4fPbm_S-~2o*0zbKxl7d~yi6~&Y11H(J;f|m z>&Uie`>MV})h_L*Hc?A}7t8LMP;Uj(&h%$!9qb{y9yVNq5K2KMFVj81W8Kd92BwO@ zaol~h;}3LT868+d1NYHWYv{Q(wEGvd8rLH*#WRrJY=sepwf8=`4T<0>^ zTj8BtWHsD%_4tjM^1#96-b3XbhnB)a4+P+^3yANtC7{H%DYk>6ue~C`@2}epR3zYo izoziQt{cuz9iIfs;ejRBvk#oWw+;Wz14Uni1N;XYO=i6S literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9b8c077c21376f14dde3f2ca2ca93b2b809b3b73 GIT binary patch literal 177 zcmX@j%ge<81fi7=(?RrO5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!3UIcH2`x@7Dvk-u z%&W}F%P%fT%t_BojB!aV&MwI>h)GE;i%H4PPpyn8D9X=DO)iNqE-5NaE-5WajY%vh xh)K=|GUDSi^D;}~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b226740fa1167cf5461a6ff419031ee4f99347b7 GIT binary patch literal 3176 zcmai0UrZdw8K3=g$NlAu;eg|hjqMnGb`8YfIzHjyq;cx&!+y3Nl*czMNn3=K@l|k%>ky7m`98;?X6fdF~?3i$j}2Wqm2k zBOKnWZYI?)aPhjY7&jL8P$6M1P;ppSZ7N>TEt~qr3$~s&6@z-FOc=xk8UULS+b&EI zg9az^mYpTo8n#U8zoi(urr3EB=V)k1v2X_4wr*xEwYrszFz5n| z%Oa^BM{qa>V~_=z4E8#p1+~zp;Y}V`Xj(mPfE6NGWOlPWqgn)HRS6a=vRP3(a96C| z=0UAaYIJsPaCSZ@H91_P!!=1!hl^IZW`}EbxE86!;aVLoCbi-iZmas(#$=9- zR`E}3$L%n?qvmJFtXJ#U=U1l|lMca(&S~J(oz-0q?~@&>>;i~642bD+Z$j!?h{cak zpBu|Xu}>-JfP#rpdHQ8JGnxLX3*4iDOe&d9P0E*3Z|UeX2-*|LWGa)9C&w?R#^lts ziP3aQzB)QKHJQpRhDY-A79PnPS{E!tgPBx%I+d2k5?51;(K$fZ`My3QuPVkI0POrL z{r&wlo00L%ByAc>Bw^WMIXRxr$m8kJOQU0%Nx+>@AeeHusFz@So|tMip5P%uEW=n9 z?EVwTePma0gwPUa*X-PLlh>EH8*Wa5z1uX-#h-N=EL=3ZYUnlIwHQV}=T$z7RQ~OQ z2;JeA_#6IxhSi~qo>l&5O(NLd;WN+s=F|*k{LIT)zUC*t#DRU-U3aPG-;X(!=bZI! zqqs1($NgZB`&PV;26Rg|En6{FOa;p(Gyn{-t=qaiM?F@-&~4~!q8F%Wup$tcR*ZR^ zA_Ov9pn_^xq#3rOoNlq}?CR>;8%v&_nX)jkW~Oy>!7w3eijmconPF_*wDW}-4d0s4 z@_D>4Qy}>pShZ!?VP1t8;Tfe+m{IdEQ1j-jp6x9x(5PZrICtH^GAJxLPxLJKym-h( zVedEUo4^}u2FNn{doa49eAxIX*txEJY&i#XGBK3Mq~xjeDD^_+oyV41E%D$V zLJTwx*dN2-|AM^&q8i+9cwy4ln#O%Ie4oMn1^QaSet}+e(?#>Vz{2ZVyvtbtX|3)9 z^h^Fz-)7a{42t{577w~t{El%-u@+3VkR>PK4CE9eCxGm6eFQ2Kq6nTkucgl69Hu_F zYho&FQ~*%j&J&G_tm;s)pxAS94{2xfI~ZZfOFh@;bqI}+#dcgEN12>u1L*_e<^y?& zSvJ=|s$B;hn-q6Lv|BK@14A^9A7RY`WSFjjq`#jRM9qB4J_LQUT_oY>7JyMGH zSI5P!fl_p^+|Y8rZMCh~ak|vd3)5Dn9lLNT+WW`o;MUP%)7fIfx1R>R2Le0D>ksV0 zPKXA_lWCb1btvF8!WahK(Ii9I6_uH|%`Ka|EZv?x;OzOD~Z#ITfEOb)Oq4M3=%n>K-xjz}XTK1SGW5Qa#?Qjf5Q zfgER~n-PY;eQ;zjB)yEVOml!mL^fh6OU^UGo@4SIM%V#Jijmsc39I)GVVJZoLQH;w z%FP|iZ>$IwFGh}U3_kMo@9-W^$2#6fZZ&PGzm9E;ZFgVT zK_GkRd8mCh@O!=Z=Hx?j`{L9SCfpT}_ho0wPCbg9SiZ7ytR(h4Ztq?mU&*jdW5<@S zt_+mKW981Cq8MGVKwWM*TJh;<`RuDjv3>2@`pxamGbQm=$Bz>m+9S`v4)6B_*5BO{ z9^BbMF#N6daNslH;VZv09TvQJ!o>f0qT z{%?`@v^_yUDwoUdH69@&&{40wP=MdpN=%&SIN^07#=;8~V?k3)TURRxV4-k=Pa9kX zV7T%NOoB|wR;Rw2?D?4P`ChA}!|P-m>{yyv3@1AR$8lex`oExoKcRttpmW>k+@I0O br{2?C+r6cy2)cj$kVB!S_s;*Q0{Q;}>hucs literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..526a8fe5de80bcb85febd846052e33fddaa5acdd GIT binary patch literal 2967 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e79a7637ff7b9efdc12592b910fb3b328a5de98a GIT binary patch literal 175 zcmX@j%ge<81fi7=(?RrO5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!@^iL|2`x@7Dvk-u z%&W}F%P%fT%t_BojB!aV&MwI>h)GE;i%H4PPpyn8D9X=DO)iNqE-5NaE-5WajY%v3 v$|uFd$7kkcmc+;F6;%G>u*uC&Da}c>D`Ev2&j`fDAjU^#Mn=XWW*`dy`g<>n literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f1320aff3ca1a5b96dbe5d0cd73a48d5129e4a01 GIT binary patch literal 691 zcmZ8e&ui2`6rM?DlkBhlKwGPL5fr>aVaqzV;GazOf1L3NJF5>QwD4M@`dcb zyDvK`3rk^X-{|Wj@z=)cEQ{5x#DVkRFn8Sz$CUf#*_{<{{mBDi zjD{A|<9lh-z>fu4l9Rtjr&0u+KU}`!u5)1AeHx`joKl|qaX0ea7Vw>%W^M?(Zb&H< zZpP>~1i2^s_5!sKSH8;)H(a&JW>%bAiwO;TG2CTyQd)g1UzP5cjxqj@T1RN%2;KgM zZXTDfe<9{8>$5fg$(lbTjX}9Ss7{`lq@ukrPAX{T#(-4bEPl9krfW0W0h&IskTrK` LUVTZ}tW^9Bej~_O literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d74a059a8887df597955e453081f0a6c9291a2cd GIT binary patch literal 1211 zcmah|&ubGw6n?Wmv+c&%RHRkWbQ6^z#vc@_qSda72Y(=lf@Ps>cE^~y$;RESZBRk1 zc+rDZY_7dZ5xw{aBv&s+g~*}@k%HcW6|BXBZ<3~HFTRl3Z{ED`+i&L0zK%w_0Av2f z=iDa(@P$n}A*_JKVFVSh!6ptA$S5HUa}HPFNBI`!9ibqOiiibDC~u2bxPIhZ;cV%i zFe;VBwET^UrAfGM#4i`EQ?y7uGjKi3sIANe>ic=Oi1VJm=Vx5YbVA9a!Gu>lXEBjZ z$Ow22VDTOrKMe#e28h}*zw#V}X=1}o9$=QM@b^U#Zi521;JGj>RB$7=U>w)hMu$C( z8*OpLHkY)x0N2#wU{; zfy?+d=Dz*5=lmo|lXGd&6VYubS#F__4?^YSltoJcHa3iSuH$4)>slyUj_XtGwCuN= z?z6Tfmd_qPWL)&A=Np&u#j;a${lIi`dDA#S{cC|+GHiOouw9pyjgse%Q!5zr18>5@ zt3eI3gzH&jMKMzF4yE_Mf(u%e{dFu zN@gbWF0M6Ht*&a#F6iA<**&xS$)1Kf@J1c@*#qm7$g9V+rKSYYDfDs;;I!s69;)SC*R*h-$wc?OYXls}3wTnbHzUwbg$6bLi}D>^P7aGO|^A z%p-bA^j&kdcJ+Qb6>o7jxAJ(G{tx2n?gYstu8!+ zMTFo*aafKVF1YrDLx;T$IAu4$PvOwy2_XwGumF1&Al;M{qCG0k87~LlY#*v`Jn#cB I;q_wSA3y;ZY5)KL literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..253efdcd01c11d829fbfa55faf424edbabbf5b58 GIT binary patch literal 179 zcmX@j%ge<81fi7=(?RrO5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!3UaoJ2`x@7Dvk-u z%&W}F%P%fT%t_BojB!aV&MwI>h)GE;i%H4PPpyn8D9X=DO)iNqE-5NaE-5WajY%vh zh{?@QNzEyaiI30B%PfhH*DI*}#bE;!EX_%^D`Ev&!3e~~AjU^#Mn=XWW*`dy<0~;u literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b56099a9e8fcd6f335b723a8cddc618266760f06 GIT binary patch literal 1573 zcmaJ>OKjXk7#@3Vuh;9{&9g~p3sQ?{H*Gfp6%GL*k)$C_X(K_(!K`FCJ451R{K(iY zY)=d7fg@25EVmvLB@#KdC*V{B$E;K`QVvyxP;a2Xr5E_e&MrZsjO3Z`f6o6J|5Pd& z2-c6c9xp%2A@oEL-K8FZLk8e0gb|h~vcxG<$&)Ne5;#X?FK6XE#Zo-gQa#Pmyu6k7 zbW2a=3N3hsWu#c8MbEU%6l=8Pm94UbWb`h=`8x>fq`VC=h-FopQnm0@z#CPG>)44& z?0TfC@%&T}P~!Mpn+|B=`COj?;UWmRF&i-AF8kL=lWQNu%=MSKvPeFS!G&`3=G7TE zO-85_yMZ6AxS`-rIT2}yi@HlY0*9YVB5i^!2_s9!l9iJYQQ%Xtyp!7osaRMh+G;t& z$M+qjmyG^jcEBrOwdY0K$@lb)BmE-Q|0S0*|CTA_q|}vJ<>&e@bal47ww2Vjugkak zQ+fbvJ4R1uFw-GJtK-Kr%J4D06aT089P45)%Mq|1#U_?^O1+!MQeL?i-3!f1wU4XQ zE6`Z3HN+7uLf7(6WG9J>t1>T!%=H}BwAY~Pc}zZb5*ptwtOQ9!qMD0mL0$I~N}VN2 zp8f?%+}1(FgsdvD0bo?+^x%5nC!rWMuFeT_uq-b*jWs(A!UU#$dGX z9mKR(-joAiL7o-}y zF(s@Za1oQJ!Q4;`E-yM!1XE719S2NIE4+_{*dlPI2-0*TyAgOHB{9KFJSnaval;Qj z;YNcI(8IP9b0bSuU{(}LY8n97kNlG@bO>zpY+-t`F7l4*AG&^%`au*sblG+4GbCDz zgRqXttvU_@(yWIpSS5|vj$)QH;)Id96NYuD51~;#?O!cyGVxr}%>)oVfb9CblLz{gbWvWAt}(4;Q{)==3kP=6}~vX{}PZAVmFs+A3H5@T5ls-rw(nb5U-i`Bx}BCR@&>d!&z%z{718fU57&TqFM+g zUWQp;r`f=ik0}Mr%i2|h($?@&Z3+HhOjI(f>ji<*s4f9|roWX<=iBTxAjMBBdIh%M na*`xHLB#`f`d2jn2+bd$i34=*7j)_mts=er#mt`wj?~ZJP+yxO literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e265c675be4993801dabeb232a891441af08a489 GIT binary patch literal 2301 zcmds2O>ERw5PsetuXnxP%_f__0wJVO*eG4pk{*gGT7{560s+JZwbn|mv+p%=u-E3< zrYusWMdHxjh&Xh6OHXCRN<=-;Q+wzEsh3D`rLH9o2vrrgR?rKlzOgq;C?NIRC;59b z^JboB#xvtzBq@d1`uWPk!HBOBMwru)40}Z93{K`aprd-kh{{nT zCdZ7p95;BGHxhCpIE(3mk(84G9oJKaD2oBj>uE!hB@RWgZoQi%KfaZ`7VsxM44S&7N~)Qm}qwp^VjH9xW78iZ;U zKUN}F9cCI^Ts$?+_B18B>S$)wUePR&PpLMkgg%7G5)!N@n1cimBy$+a5zNWa2qH1| z#Brn*y~9pn;RxYZGa)#@YdIV6JtViff2YqO-4eTYeg9D}&-D6n8gnhFYda9gp1*T2;Ip^~=UTnp zSchtTg}k3Qan&MKoA?3=3A@zf{Df*NuB+i@#1}2987i$Qm*C@K@|o)D&b6GqVp^6~ zT~>6>cI={tXPB8#b#+}`(#bQywFw3BW2pDXkc}os``G5EFLX>#GfQTaSyC&P6w9<+ zRvyG=^}sS}oSfY%oZP(dv6uOzKHJXrui^DcZ{SdKd_&$E z|LFdEUiN(bWLxT6n{7<3bGH_5FM86E`b@ibsFA-lQJ-&%xtoX9Pkwjy+q0fHS)Xp_ z1{=cdLw6514|}3r-vE?Tj|mIiMBM{DBc_1JpHLBsn=)d zk?kN}JxT`tccX-4S4UaQ|5`gnM~=07FB=I`E7KhbFiv`LwmXLMwVpzbj<8GoOs680 zu--~E%cL~N;F{Pu3i_MoK|TO52rvXN3=q`OJ3#FM*u!9<&`RK2CQwF0eu(vpe81Pf(Qcur|hp7 zK#A{veTnA=zlp7vyrJ>t+{PDM6DKy$Eqb|&^|_t3zcIH}`0)M#FLRDn{>v5qav^7T zHQwJ~opOxni-Qe*OWb>XI`k48EcArg2xdjfzO1WyWrY~EFk3;?ke*=PgmY%(xl$2p zs?NIIE?R6PEK|ac!a0`Np=44coHZgF1x376Tw*_8D#ES7*9-FWV(y*^AB2H#|Z03G}t?S9H7%Q6rQ!$>$TTEahf0cOK8)d4aMQ7;!qIuCr$c8Q$$@3i>1}ZGsK(jdbhLd zf_rkM3UMUrfrwKjE?BihxWNfHQHf)Xl#J9vRUySg(sSx=jj3}SD{+ja=RHd7<=Q#YV9Ldi+O=gSm&m6f zkCZQ6Iy1-D3?+^g+FmWF+CJbXtbkOa6e8KL|4j+%C&-X6GGbUV;xR;I{!8FkJARv6 z;lVMYtmGnnP`2fMWokDrg%kZKrJcMTi~Nl=*ooG&85yz9lHAIk!4o6Bt@h(a{>$$< z(W!n`1s*+$(^$iq_S0L~CeYjvIwsKk5IQco#u1nC4U4wP?e<%x@}*$;VdR2_OE=3!LYUYOU@#){;Y765YlzJth{WdbPR_4Zt*U z_<7S3kkeZtZeN_fuLv_h7S z3J0CS&@4YyhzwVP z@3^>uHpX@}&)-kwSLL-*XJqfX{5}40<%g9{>P+**rdC)zu(s{y%iknATCq8|Ir_rd z*v-SubDM?9HT9cA-_EvXJB6dovjh8|PXBV@=L?-esd@I1g7P~z)bWkUJsTs_TS+;u zG#4JF(Zt>jb)pYWD9uF<=5}nT`97Fan)4hS4!^!YU+dNI3;}RL6wl+a|C<1wAr3w~ z!-UYILO}@J{%?P^M-mtar01`3Um&IoPWA2Sw$>+#EZ9koPaJHT6 z$3}#Z@11LmHAeIT-OdkSS0J^bxvRZ+eJ)B7u0bD(z63M_O9xjR%c)d}+laoD;1&2Zf_@_6+4Iz? zh;7T^!yXiU&Irq#Ij%SnedPw6JnBZYqj4rE;-%se|I?%zoGLC4(ir-1Pt>;~!hDAw z<5u{y4Bp`4&$uK>50G{b?fwHD{|z0#hi31ggTJF)Tgq-p`Fw5*@n!3nq)Ag>O#g-W HB1-)SQZXX_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a92ae48fd2c7ef3ddce6bce278706b458ce6725a GIT binary patch literal 1397 zcmZ`(%WoS+7@uA5uGi~V-1I>aMX0SxoK}vI3Wo+Ek)}-(M1^R=VX?H@c&5%evyaT~ zmO3XR^$#FI0{509Q7qvjoIsq4IA#xIq#UXWRlOAjF1^4vyLOsGJCbLA-*3M6H=aMr zWdp(b?cwA0?>U5?2w}X`5jgAs_#R<|C5lY(m8s-PrX&fRqq3VbbFN}4u4<~TW@;&~ z(7dagdWuzAa1GN)u||t-$t+1oMhgh%A0Vug(k{dn%Pe=Ldf}OXx9bwuu^o}faYJ zN@imkuksCC5$i`b+Msacv-DbhN9MB4~b2J_NERo*oc?_ zf~#o`JU@yHVJoY4YuyU`AcoPC5bgq)+^(PIx@CE`ODu~UmgV|5rUI8NYa_Pl=%j2} zk;otunV1*TMI1#`!d0tq2$&UwlA1ahSHpJDA+YGhXVf#67YTfhp8WDc4m^=wXytu zQhTqr^tX=6XZvS{XOq%w@5X-R&He{_mGfUOu=+UBdWD@6IBlj8Zcv-H)`;89W|a0> z%psf0+b_=>*s&>eBy0rGwl;KogJvVG98>a{n@yky?P}oF#wt7%OgxKOtEL1>bD0M8 zLjN$kKOeCf5XIXNUV`m;PLiZ2sCay7X!Zcj9H4jqLT~+}m8A>cT>KZoC-w6` De;9Z4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8806ef6e0c3190821e93b9360af8e4a77fc9268d GIT binary patch literal 2620 zcmd5;OKj9e7`8Y2@V>K~7YUCB3WQB*5@-O4R!D>Z36Df7p$~T@%h{PG4t_EAQkF{> zi9@AcNOR~Ob4*0k=Gb0ZIJHtQk>W!$5(k8O$gNdCNLqGbC9u;&R+b$O%6#tS60>obu&}o;EUa#+Rdd)@YWSMIj_y6iDozKqT7y zpecB_ob#=5uqGO;`HEObu4BkpLF5uqUFvEEEl4~zWLY{@O&&dG>1D&@(l9tDz{wLA zEJn2{^D?dQ*r@5!Dav?ktZY!Gm3Z`;%QSO}hbQQ57bXo~zdkw)V~SB-bv4U$rZpSw zL#jhd!Ri8xZYmp-B9@5?vPc9uL`1nMB+xMYA|$lb^Z>m0kPwZ|WCFPnm%{Z;OK-{p zTbxAdvqYC-_4veQe3D4p{GnieIfZ_j-tx&AU(b^0QvAVIOtWvx(frKe##IG!LtN{> z#qE%tsIRmQvfD|Lh)b#Zn>&14nq)|J>D9sS@~yeoteus7p@pZeO~0Bk+tIx@&_(iX4%FY#v>z`1o4?kwRA(VExQa?gF0Nob&R^y;OT8^xsz9Q{b)C# zI(6y07PF^Svk+o$!R$O%Qk{}YD36`LZBx^sV}&j*DT=8YR8e?BQ4EWeb(B+zaIwhvrc)MBH zw`h){veLA6ixP(SnqeREXxS~9))zccViYz^6xHR4peQJ_GzLZd7x9P&b%2u4U{}tf zumN7-<;Ukv7q2^%ImM4Ov!a`paGzaa%`m$UZ6G+`0o)UQZ#`JO`Xrv64c8{T<^#*&AIQ%$KhAjZ z(dy7zwr!To4|?rK7JHXJTkSpj>v=ExVRd9Jm7hIXdwcHvg*GqMUp>E;-&Jd!J6gSr zLdU`(FFycsuA?R`9QnR~vER#`u3l=;ACCMo{%G9GO;j)a6&2E*wT_izXT8)A%#_+y z>slUPxvF}pNl>%9Yop87%6Q4klDi}9%qrjfuUQ4UG)>|pvDM8a+0fZO&p)#lQ0tEDsje3UDX4?V9>L!#dF039ycxxQ@iTj^6~< zh0<}9b|d%~`aP8P0gM&)u!E=^LO^b2JqY!_XGc&iAoL>mT^|dUwb}Gx+Hn*L9eZ6v1?`U;%Z@J<=Q7+=F{rMwPbGgaIIzTorQ>( z?5hrA3s2Q1=l9Q<3#ykLfadih@^ihb$-Ur*Rz6ibUb{JWW;JyHI)sr@^D+{>w}9BZ(l?uDZI zwB4(m^cen7-T$nvo}8lDtJ!tXEnz|$8Q5eI^edCZ%QBzZnykgb4~Xb_w^>h?DZ527 zp1ytuWUi^p$U=WDW6}yU&dD-o((psd?$}r~p#+=~(bjS&ttiZ5b(IVDh_pN}Bmwh0 zS@paiq8V4X?s;n&;YXQ<=cTG^5eg-%D1(u@Vh4o5j!Vs_Lfx-4SU`L(8!<_ejYv>$ zl_aaOatpZSdkOXD3iXQt@w5|08?xFF3%J`&DG6}4ZW V_8;iezfB7-;d|q6kn(w(e*k2METjMc literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..955fa7eb7edf09aef067e30a111046b0c006b165 GIT binary patch literal 180 zcmX@j%ge<81fi7=(?RrO5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!3U;=N2`x@7Dvk-u z%&W}F%P%fT%t_BojB!aV&MwI>h)GE;i%H4PPpyn8D9X=DO)iNqE-5NaE-5WajY%vh zh$&9aNX<JmtkIamWj77{q762QB BF|Ggr literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7b06818f39338412bbf22ff77365bcf4dee5ae1e GIT binary patch literal 2116 zcmbVN&2Jk;6rb^ac-NmvP~6axq@PJvLOCIXP(eV7XbLqP2oftxtF32hFRZ<0W?kW1 zq!cN=Ij8ntK>Y{!A2^}pV6D^xLOpOp*r-xYeQ(xI9Lorak@mOGznPtR@Auvt|5_~O z2#i;cesA2@2>AnlOolNwhQEOElrX|*NLs`t6zxn%TNyW_Y%SDUx~nT&4~>@Tnv`V7 zXM`D#2{XC*9QMZHW)+(S*5cVS)>5nuY;KCR6`KdPFttNYu|;4@Q*2(bWndSkb|~}~ zouxnVV236a^J5+dEspk=oj8cveh9X{x3_x--u5TH8yo zh@06XT;n>Uw}E9D*Wf1C-UJ&sZOmnXv&Ni`TrcAoiM`W|1jn-yBM#z_SCc&BvMvJU z61dsr9=ht-iRF21zr{T-$$4HYV%-q!g6I9v^~3RuCS#G5J5ut=oK~TD2b&CarMFi!{8)z+0pbUn@6%ft!4Hd}EqqP%#BVCN^=6ik* zX8y}_h$aisr1fErYlgZuaJg}WY0XfmnKZ(^4wgk#pvo$cq!3829<@3lk2!Plv%#u` zDp(994OR)DV#iYw{XrCkvkI)>Y6R3nz6}5hTv^}$^zgm@&XW(0_4O$|j`a;p>=*s5 zXYU+spXC)lpRW$iOsjL=N6;UWY<{egsG_Vo#%y5!n!C4PZ-IcpNi0e5d~x|iUruv| zQ~%4WVp?Nktp#pB$FIvk&#kF7oIqHGXX4jV6JYxguKml!I}Bona2YDQhjy?|BIu2J=p ztE)C+o*yT7Y8(1xuAIo4NFe5F%k{ci%0dW9QRouayp6Ep60f_6h) zMv30-22A;vSe+j8+i_4ACM-|yR&^z*uEYXXg_OE>@#;FYZl`||9|~+*%!B+E;4h6* z`jV`^AXi?H<(Fh_WNLJ)Z;uGPMg>YY`_72KYh)o0_L#4vd}U-&da1vQYc7o}o30=3 K{zKrU!g>uoC#jeK literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a0574cc1124836495acd6d50667c24ecc35474a1 GIT binary patch literal 2795 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..61febb086a630d54660d3e2fdb73b83048f6c609 GIT binary patch literal 1281 zcmZ8gO>fjj7@o1mUhgJhL-?-LL>0=xpc2HPhYIyT5Rd~aAvHZ%R!3%M5(fOSof#v1 z4jg*p2I?;;O8-eONN`w@&3zX9)cH$qXaXQcnyY={X12y_(u6yXl^eV#J-6rEwh4CYI>9w6{~X_&r|wa zo#pMyD>Si}?%iAeG2K*Ls`M98HhEboo#opiOK)=ZNLQnj@y98vD&C|cSv}-KO_i4Q zP}h>D*=Uri;R9Y|DiyARE2Bn6aIXU!C@c9>G$xT;IN*=;wl61|T!Q7Vy={W%+-LYN zZ+E}@gz$ie%)bu%8WIQQ77=KX1HUUVBZ> zzVzRcwRXGRd8AnagdLM#0cM-!J|_&@@}Wt>JQrEcwd+=i<3iwq!zi?ob6f`;7wv-I z?MOOJH}09a8G(!^m?1-On28=($543hwI5Boc<3Z#G}EScAX}!7lz-?$+t}Cg8BYAh zi~YT&nS@u?t8z_hTJ(fV8vz&#K@Tl(ro}qCxzjJ5YTiDqcpIF)%j2BP8 zA*aWS=iiX?E&fMvu<{QAXlt#vjqQ@6ee~XmuK5+k)T0nWohMlr^&*u*UYY4 zEb$O2MM~b>r}hW%(5n0sURpU1W~B-tgizlQHbUwX=giuUy-Fn{R@$>O-(1f(=Q}$; zWwR*)&-2^AZeCUh`3)=m2kPSKj7fytCnhl^n>a)#5|(9Ka%5c=<(M6Fs7@V4S45rK zaVMcCL|L&_C#ff$l%A4^OxB4Rze~&nOFoW;)9Y!0s(>b0dVppGngTREgk}Yr0W>>= zjtO)O(A?0xIf0G?I>B;-dB+7h3Fy=iIw8;_fKCsglLDOq^ytvMQ;k{e*mK;xCIzZl z_L*-v49bZ$RS_w+e_9(2b3&YtWMXu-P zqKBo1`l&rE4OphvFNs(quBCzu?*FRK9NcX{t7V5hhbSN%mjrL*O%|x%)XP=h@*Bl~ z)~l8uC^c?X0I#{8g??++L!Fbe5bB&>sqzDwFv4SQhY$kIJUlFqx(aDw#*=bQDsJ7&tFAvvZ}Lo!GhFo}6jXnfCZ> zi_V7jA1W#NzX+Icc^o>z3a*4t=QS_~tPJN#Q`QwYpMDMu<5PzC9RIv0ab!;9HwCho z&5A+VWL||^B57-hsIv!wc?K2@gEb=M=cIm8NRBDB7T7@9ZW?aTc{UZmF5y$@P6-=1F-{JwH;Zl5lO9^-%! zj|Xz!f|kRB=D*5)lR)xQSyCiKYQ`}ch!l{3%Yr&c0jWk3f2PA{$1yYlb<3@L3=@HH z8Wq7cY)~xt>3SpE9)!h9GQiaE^n)zGQ5jJpg*Goj1MZh2n;})9ywlN$KTO_YB>qT3 zQ6HxE^Vo{!EW93uS6}U8lRL%s#B_^J539X>at@C2Y*XF42>;Be_Hm*SK4Jji)4^2r zl4HbNEwXka28vJPP3AM^a+C2OfeZulL8i*?82!6SQH#Ia`&n2hO64WQ(BSK3!GM!t zHk;n3(;=_eR@9D&Z@OmijGC|yL;5CB_D)EW~m#8|#19A>msD_Dms|r`48s3G( zJw*J^@jn)_FY=90ypV_>{t~v13h0G;A-sEC5fZMvRUd9_;giSTE;Sbx+zu4?|#=M@afLT(qglL_+mE&9iJT2 i1_PEvMV6Mjsic&DGF9jj_;zPyX{M?D4MS!GPyYfI%8*F_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..51eb65553df3601ee13b701b4fc16e067971ff22 GIT binary patch literal 1990 zcmZ`(&2Jk;6rb5IuN}KilaQo^Bt;2AE48V_fkr4os7WC?;0S>^tSqe-&m>vc4>Pka zSOO_hTgkcQ)*d-hwFmwL4%|3Z^&wrULI@$$TSbhJa^k(&d^m0x$#0*3GxOg2y^r~| zQYjHwzuo?Q>lclXKk(1-GLMvlpFz1#7-2Ld5pfAcT?=WXxth}TP>&4PP`VMCk>y%Z z!7W6#Yg3|;j|emG5@vDhk)GRhiwZ3OYV+a=T2g2c(9!@pqR7wFodna%u^r$NL~-)N;l68jgZYVrZ4%&3FBdA--`So z{4^Dr`C-6A*36^NAo9!_4Vjlu5X11p^&&x`l7bUD6a?s13_jTJ3@r>t*5wIl;6a^4j z)${x~PEsG4N!TwVs>@V7BnP09!;O{q8t7IwJ`dtf7$-9I!>zz?taABPnzR~>-)^uZ z;hjcHBwzAo>cQ=HGi?jr@LR2hY;HrWve5=3i>;2p9#WzRikt&+kL+Jr*tykR+FN@v zGV|0}*q@u5q*^(66{$i$iLe zsZwe=g{*|!Rs@GKtK2NUu;JK4nw6p;_Cg+SrQ4a^@}>MH5v=Ceq5}Kk0*WdMOrsb@ zffvPP6cfY z6i8K~)qCXG?EDX_KYh}hy#bWA_vCKWGj8Ox@Z|ZlK7gG;TBl_MGDbcbN>lToM%RGR zHBU%e9V}=W3%VY!-o!b{w18!$rYbM<{4^`I6@AptL_xtht^=fa8U;>gM|1SU@j<>fy=`A^*Bo;=p8suD2V4jg3-FWJ;*g^8$(}Kp2ZviVrz$X}%t|}0Al_21WoES7 z0aI0g*Pz4vI1QS@f@Apupr%E=Y--NcP^fySTqw6^lXPe==P&g8ViBxh|H%f3mpY~N zPcrkIy#AcL_7}PSgk0}i8eQmi`UF1xl18uXO{0FTU!rsgb@=pci_YyW^$C3XW0V%V T%ZL~IHlh)GE;i%H4PPpyn8D9X=DO)iNqE-5NaE-5WajY%vh zhym&?%S=u!j){-Y%*!l^kJl@x{Ka9Do1apelWJGQ3bcd~h>JmtkIamWj77{q764k5 BGA{rC literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..eb6af0f186a66fab5d286e51211b85e5bdcabadb GIT binary patch literal 789 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 BadQ9w literal 0 HcmV?d00001 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 @@ + + +