diff --git a/.env b/.env index 98c8196862..4e5f20c55b 100644 --- a/.env +++ b/.env @@ -5,33 +5,35 @@ DOMAIN=localhost # Environment: local, staging, production ENVIRONMENT=local -PROJECT_NAME="Full Stack FastAPI Project" +PROJECT_NAME="SOCIA" STACK_NAME=full-stack-fastapi-project # Backend BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173,http://localhost.tiangolo.com" -SECRET_KEY=changethis -FIRST_SUPERUSER=admin@example.com -FIRST_SUPERUSER_PASSWORD=changethis +SECRET_KEY=0_vuFnI9SIxZ4dxA5TcIroZRKp_ljxMuknEAcXDsGgo +FIRST_SUPERUSER=arpit.singh@example.com +FIRST_SUPERUSER_PASSWORD=SOciaSOcia +# Otpless +CLIENT_ID = "CLIENT_ID" +CLIENT_SECRET = "CLIENT_SECRET" # Emails -SMTP_HOST= -SMTP_USER= -SMTP_PASSWORD= -EMAILS_FROM_EMAIL=info@example.com +SMTP_HOST=arpit.singh@example.com +SMTP_USER=arpit.singh@example.com +SMTP_PASSWORD=SOciaSOcia +EMAILS_FROM_EMAIL=arpit.singh@example.com SMTP_TLS=True SMTP_SSL=False SMTP_PORT=587 # Postgres -POSTGRES_SERVER=localhost -POSTGRES_PORT=5432 -POSTGRES_DB=app -POSTGRES_USER=postgres -POSTGRES_PASSWORD=changethis +POSTGRES_SERVER=aws-0-ap-south-1.pooler.supabase.com +POSTGRES_PORT=6543 +POSTGRES_USER=postgres.uiwsgdtnmovxahfgxfkj +POSTGRES_PASSWORD=Aa1sociaaicos +POSTGRES_DB=sociadb SENTRY_DSN= # Configure these with your own Docker registry images -DOCKER_IMAGE_BACKEND=backend -DOCKER_IMAGE_FRONTEND=frontend +DOCKER_IMAGE_BACKEND=backend \ No newline at end of file diff --git a/.github/workflows/generate-client.yml b/.github/workflows/generate-client.yml index a81f78cb51..e69de29bb2 100644 --- a/.github/workflows/generate-client.yml +++ b/.github/workflows/generate-client.yml @@ -1,49 +0,0 @@ -name: Generate Client - -on: - pull_request: - types: - - opened - - synchronize - -jobs: - generate-client: - permissions: - contents: write - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - token: ${{ secrets.FULL_STACK_FASTAPI_TEMPLATE_REPO_TOKEN }} - - uses: actions/setup-node@v4 - with: - node-version: lts/* - - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - name: Install dependencies - run: npm ci - working-directory: frontend - - run: pip install ./backend - - run: bash scripts/generate-client.sh - - name: Commit changes - run: | - git config --local user.email "github-actions@github.com" - git config --local user.name "github-actions" - git add frontend/src/client - git diff --staged --quiet || git commit -m "โœจ Autogenerate frontend client" - git push - - # https://github.com/marketplace/actions/alls-green#why - generate-client-alls-green: # This job does nothing and is only used for the branch protection - if: always() - needs: - - generate-client - runs-on: ubuntu-latest - steps: - - name: Decide whether the needed jobs succeeded or failed - uses: re-actors/alls-green@release/v1 - with: - jobs: ${{ toJSON(needs) }} - \ No newline at end of file diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml deleted file mode 100644 index fc208993f6..0000000000 --- a/.github/workflows/playwright.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Playwright Tests - -on: - push: - branches: - - master - pull_request: - types: - - opened - - synchronize - workflow_dispatch: - inputs: - debug_enabled: - description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' - required: false - default: 'false' - -jobs: - - test: - timeout-minutes: 60 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: lts/* - - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} - with: - limit-access-to-actor: true - - name: Install dependencies - run: npm ci - working-directory: frontend - - name: Install Playwright Browsers - run: npx playwright install --with-deps - working-directory: frontend - - run: docker compose build - - run: docker compose down -v --remove-orphans - - run: docker compose up -d - - name: Run Playwright tests - run: npx playwright test - working-directory: frontend - - run: docker compose down -v --remove-orphans - - uses: actions/upload-artifact@v4 - if: always() - with: - name: playwright-report - path: frontend/playwright-report/ - retention-days: 30 - include-hidden-files: true - - # https://github.com/marketplace/actions/alls-green#why - e2e-alls-green: # This job does nothing and is only used for the branch protection - if: always() - needs: - - test - runs-on: ubuntu-latest - steps: - - name: Decide whether the needed jobs succeeded or failed - uses: re-actors/alls-green@release/v1 - with: - jobs: ${{ toJSON(needs) }} diff --git a/.gitignore b/.gitignore index a6dd346572..530b37eca8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ node_modules/ /playwright-report/ /blob-report/ /playwright/.cache/ +.history/ +.DS_Store + diff --git a/.vscode/launch.json b/.vscode/launch.json index 24eae850d0..c1888bf5cd 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,13 +16,6 @@ "cwd": "${workspaceFolder}/backend", "jinja": true, "envFile": "${workspaceFolder}/.env", - }, - { - "type": "chrome", - "request": "launch", - "name": "Debug Frontend: Launch Chrome against http://localhost:5173", - "url": "http://localhost:5173", - "webRoot": "${workspaceFolder}/frontend" - }, + } ] } diff --git a/README.md b/README.md deleted file mode 100644 index afe124f3fb..0000000000 --- a/README.md +++ /dev/null @@ -1,239 +0,0 @@ -# Full Stack FastAPI Template - -Test -Coverage - -## Technology Stack and Features - -- โšก [**FastAPI**](https://fastapi.tiangolo.com) for the Python backend API. - - ๐Ÿงฐ [SQLModel](https://sqlmodel.tiangolo.com) for the Python SQL database interactions (ORM). - - ๐Ÿ” [Pydantic](https://docs.pydantic.dev), used by FastAPI, for the data validation and settings management. - - ๐Ÿ’พ [PostgreSQL](https://www.postgresql.org) as the SQL database. -- ๐Ÿš€ [React](https://react.dev) for the frontend. - - ๐Ÿ’ƒ Using TypeScript, hooks, Vite, and other parts of a modern frontend stack. - - ๐ŸŽจ [Chakra UI](https://chakra-ui.com) for the frontend components. - - ๐Ÿค– An automatically generated frontend client. - - ๐Ÿงช [Playwright](https://playwright.dev) for End-to-End testing. - - ๐Ÿฆ‡ Dark mode support. -- ๐Ÿ‹ [Docker Compose](https://www.docker.com) for development and production. -- ๐Ÿ”’ Secure password hashing by default. -- ๐Ÿ”‘ JWT (JSON Web Token) authentication. -- ๐Ÿ“ซ Email based password recovery. -- โœ… Tests with [Pytest](https://pytest.org). -- ๐Ÿ“ž [Traefik](https://traefik.io) as a reverse proxy / load balancer. -- ๐Ÿšข Deployment instructions using Docker Compose, including how to set up a frontend Traefik proxy to handle automatic HTTPS certificates. -- ๐Ÿญ CI (continuous integration) and CD (continuous deployment) based on GitHub Actions. - -### Dashboard Login - -[![API docs](img/login.png)](https://github.com/fastapi/full-stack-fastapi-template) - -### Dashboard - Admin - -[![API docs](img/dashboard.png)](https://github.com/fastapi/full-stack-fastapi-template) - -### Dashboard - Create User - -[![API docs](img/dashboard-create.png)](https://github.com/fastapi/full-stack-fastapi-template) - -### Dashboard - Items - -[![API docs](img/dashboard-items.png)](https://github.com/fastapi/full-stack-fastapi-template) - -### Dashboard - User Settings - -[![API docs](img/dashboard-user-settings.png)](https://github.com/fastapi/full-stack-fastapi-template) - -### Dashboard - Dark Mode - -[![API docs](img/dashboard-dark.png)](https://github.com/fastapi/full-stack-fastapi-template) - -### Interactive API Documentation - -[![API docs](img/docs.png)](https://github.com/fastapi/full-stack-fastapi-template) - -## How To Use It - -You can **just fork or clone** this repository and use it as is. - -โœจ It just works. โœจ - -### How to Use a Private Repository - -If you want to have a private repository, GitHub won't allow you to simply fork it as it doesn't allow changing the visibility of forks. - -But you can do the following: - -- Create a new GitHub repo, for example `my-full-stack`. -- Clone this repository manually, set the name with the name of the project you want to use, for example `my-full-stack`: - -```bash -git clone git@github.com:fastapi/full-stack-fastapi-template.git my-full-stack -``` - -- Enter into the new directory: - -```bash -cd my-full-stack -``` - -- Set the new origin to your new repository, copy it from the GitHub interface, for example: - -```bash -git remote set-url origin git@github.com:octocat/my-full-stack.git -``` - -- Add this repo as another "remote" to allow you to get updates later: - -```bash -git remote add upstream git@github.com:fastapi/full-stack-fastapi-template.git -``` - -- Push the code to your new repository: - -```bash -git push -u origin master -``` - -### Update From the Original Template - -After cloning the repository, and after doing changes, you might want to get the latest changes from this original template. - -- Make sure you added the original repository as a remote, you can check it with: - -```bash -git remote -v - -origin git@github.com:octocat/my-full-stack.git (fetch) -origin git@github.com:octocat/my-full-stack.git (push) -upstream git@github.com:fastapi/full-stack-fastapi-template.git (fetch) -upstream git@github.com:fastapi/full-stack-fastapi-template.git (push) -``` - -- Pull the latest changes without merging: - -```bash -git pull --no-commit upstream master -``` - -This will download the latest changes from this template without committing them, that way you can check everything is right before committing. - -- If there are conflicts, solve them in your editor. - -- Once you are done, commit the changes: - -```bash -git merge --continue -``` - -### Configure - -You can then update configs in the `.env` files to customize your configurations. - -Before deploying it, make sure you change at least the values for: - -- `SECRET_KEY` -- `FIRST_SUPERUSER_PASSWORD` -- `POSTGRES_PASSWORD` - -You can (and should) pass these as environment variables from secrets. - -Read the [deployment.md](./deployment.md) docs for more details. - -### Generate Secret Keys - -Some environment variables in the `.env` file have a default value of `changethis`. - -You have to change them with a secret key, to generate secret keys you can run the following command: - -```bash -python -c "import secrets; print(secrets.token_urlsafe(32))" -``` - -Copy the content and use that as password / secret key. And run that again to generate another secure key. - -## How To Use It - Alternative With Copier - -This repository also supports generating a new project using [Copier](https://copier.readthedocs.io). - -It will copy all the files, ask you configuration questions, and update the `.env` files with your answers. - -### Install Copier - -You can install Copier with: - -```bash -pip install copier -``` - -Or better, if you have [`pipx`](https://pipx.pypa.io/), you can run it with: - -```bash -pipx install copier -``` - -**Note**: If you have `pipx`, installing copier is optional, you could run it directly. - -### Generate a Project With Copier - -Decide a name for your new project's directory, you will use it below. For example, `my-awesome-project`. - -Go to the directory that will be the parent of your project, and run the command with your project's name: - -```bash -copier copy https://github.com/fastapi/full-stack-fastapi-template my-awesome-project --trust -``` - -If you have `pipx` and you didn't install `copier`, you can run it directly: - -```bash -pipx run copier copy https://github.com/fastapi/full-stack-fastapi-template my-awesome-project --trust -``` - -**Note** the `--trust` option is necessary to be able to execute a [post-creation script](https://github.com/fastapi/full-stack-fastapi-template/blob/master/.copier/update_dotenv.py) that updates your `.env` files. - -### Input Variables - -Copier will ask you for some data, you might want to have at hand before generating the project. - -But don't worry, you can just update any of that in the `.env` files afterwards. - -The input variables, with their default values (some auto generated) are: - -- `project_name`: (default: `"FastAPI Project"`) The name of the project, shown to API users (in .env). -- `stack_name`: (default: `"fastapi-project"`) The name of the stack used for Docker Compose labels and project name (no spaces, no periods) (in .env). -- `secret_key`: (default: `"changethis"`) The secret key for the project, used for security, stored in .env, you can generate one with the method above. -- `first_superuser`: (default: `"admin@example.com"`) The email of the first superuser (in .env). -- `first_superuser_password`: (default: `"changethis"`) The password of the first superuser (in .env). -- `smtp_host`: (default: "") The SMTP server host to send emails, you can set it later in .env. -- `smtp_user`: (default: "") The SMTP server user to send emails, you can set it later in .env. -- `smtp_password`: (default: "") The SMTP server password to send emails, you can set it later in .env. -- `emails_from_email`: (default: `"info@example.com"`) The email account to send emails from, you can set it later in .env. -- `postgres_password`: (default: `"changethis"`) The password for the PostgreSQL database, stored in .env, you can generate one with the method above. -- `sentry_dsn`: (default: "") The DSN for Sentry, if you are using it, you can set it later in .env. - -## Backend Development - -Backend docs: [backend/README.md](./backend/README.md). - -## Frontend Development - -Frontend docs: [frontend/README.md](./frontend/README.md). - -## Deployment - -Deployment docs: [deployment.md](./deployment.md). - -## Development - -General development docs: [development.md](./development.md). - -This includes using Docker Compose, custom local domains, `.env` configurations, etc. - -## Release Notes - -Check the file [release-notes.md](./release-notes.md). - -## License - -The Full Stack FastAPI Template is licensed under the terms of the MIT license. diff --git a/acme.json b/acme.json new file mode 100644 index 0000000000..0fa97337f5 --- /dev/null +++ b/acme.json @@ -0,0 +1,2 @@ +touch letsencrypt/acme.json +chmod 600 letsencrypt/acme.json diff --git a/backend/.gitignore b/backend/.gitignore index 63f67bcd21..101092a3ec 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -5,4 +5,4 @@ app.egg-info .coverage htmlcov .cache -.venv +.venv \ No newline at end of file diff --git a/backend/app/alembic/versions/.keep b/backend/6a0826f924e8, old mode 100755 new mode 100644 similarity index 100% rename from backend/app/alembic/versions/.keep rename to backend/6a0826f924e8, diff --git a/backend/Dockerfile b/backend/Dockerfile index c3187aeb28..57fa537edd 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -26,3 +26,5 @@ COPY ./prestart.sh /app/ COPY ./tests-start.sh /app/ COPY ./app /app/app + +RUN poetry run pip list \ No newline at end of file diff --git a/backend/README.md b/backend/README.md index 7e7829677f..cb82724abf 100644 --- a/backend/README.md +++ b/backend/README.md @@ -17,8 +17,6 @@ docker compose up -d Frontend, built with Docker, with routes handled based on the path: http://localhost -Backend, JSON based web API based on OpenAPI: http://localhost/api/ - Automatic interactive documentation with Swagger UI (from the OpenAPI backend): http://localhost/docs Adminer, database web administration: http://localhost:8080 @@ -97,110 +95,104 @@ and then `exec` inside the running container: $ docker compose exec backend bash ``` -You should see an output like: - -```console -root@7f2607af31c3:/app# -``` - that means that you are in a `bash` session inside your container, as a `root` user, under the `/app` directory, this directory has another directory called "app" inside, that's where your code lives inside the container: `/app/app`. -There you can use the script `/start-reload.sh` to run the debug live reloading server. You can run that script from inside the container with: - -```console -$ bash /start-reload.sh -``` - -...it will look like: - -```console -root@7f2607af31c3:/app# bash /start-reload.sh -``` - -and then hit enter. That runs the live reloading server that auto reloads when it detects code changes. - -Nevertheless, if it doesn't detect a change but a syntax error, it will just stop with an error. But as the container is still alive and you are in a Bash session, you can quickly restart it after fixing the error, running the same command ("up arrow" and "Enter"). - -...this previous detail is what makes it useful to have the container alive doing nothing and then, in a Bash session, make it run the live reload server. - -### Backend tests - -To test the backend run: - -```console -$ bash ./scripts/test.sh -``` - -The tests run with Pytest, modify and add tests to `./backend/app/tests/`. - -If you use GitHub Actions the tests will run automatically. - -#### Test running stack - -If your stack is already up and you just want to run the tests, you can use: - -```bash -docker compose exec backend bash /app/tests-start.sh -``` - -That `/app/tests-start.sh` script just calls `pytest` after making sure that the rest of the stack is running. If you need to pass extra arguments to `pytest`, you can pass them to that command and they will be forwarded. - -For example, to stop on first error: - -```bash -docker compose exec backend bash /app/tests-start.sh -x -``` - -#### Test Coverage - -When the tests are run, a file `htmlcov/index.html` is generated, you can open it in your browser to see the coverage of the tests. - ### Migrations -As during local development your app directory is mounted as a volume inside the container, you can also run the migrations with `alembic` commands inside the container and the migration code will be in your app directory (instead of being only inside the container). So you can add it to your git repository. - -Make sure you create a "revision" of your models and that you "upgrade" your database with that revision every time you change them. As this is what will update the tables in your database. Otherwise, your application will have errors. - -* Start an interactive session in the backend container: - -```console -$ docker compose exec backend bash -``` - -* Alembic is already configured to import your SQLModel models from `./backend/app/models.py`. +Make sure you create a "revision" of your models and upgrade your database with that revision every time you change them. This updates your database schema to reflect model changes and prevents application errors. -* After changing a model (for example, adding a column), inside the container, create a revision, e.g.: +#### Steps to Perform Migration -```console -$ alembic revision --autogenerate -m "Add column last_name to User model" -``` - -* Commit to the git repository the files generated in the alembic directory. - -* After creating the revision, run the migration in the database (this is what will actually change the database): - -```console -$ alembic upgrade head -``` +1. **Start an Interactive Session:** + Open a shell inside the backend container: + ```bash + docker compose exec backend bash + ``` -If you don't want to use migrations at all, uncomment the lines in the file at `./backend/app/core/db.py` that end in: +2. **Generate a New Migration Revision:** + Automatically create a new migration based on your model changes: + ```bash + alembic revision --autogenerate -m "Describe your schema changes" + ``` -```python -SQLModel.metadata.create_all(engine) -``` - -and comment the line in the file `prestart.sh` that contains: +3. **Review the Migration Script:** + Check the generated migration file in the migrations directory to confirm the changes. Modify the migration file if necessary. -```console -$ alembic upgrade head -``` +4. **Apply the Migration:** + Run the migration to update your database schema: + ```bash + alembic upgrade head + ``` -If you don't want to start with the default models and want to remove them / modify them, from the beginning, without having any previous revision, you can remove the revision files (`.py` Python files) under `./backend/app/alembic/versions/`. And then create a first migration as described above. +5. **Verify the Update:** + Ensure that your database reflects the changes by reviewing logs or using a database client. -## Email Templates - -The email templates are in `./backend/app/email-templates/`. Here, there are two directories: `build` and `src`. The `src` directory contains the source files that are used to build the final email templates. The `build` directory contains the final email templates that are used by the application. - -Before continuing, ensure you have the [MJML extension](https://marketplace.visualstudio.com/items?itemName=attilabuti.vscode-mjml) installed in your VS Code. +* Start an interactive session in the backend container: -Once you have the MJML extension installed, you can create a new email template in the `src` directory. After creating the new email template and with the `.mjml` file open in your editor, open the command palette with `Ctrl+Shift+P` and search for `MJML: Export to HTML`. This will convert the `.mjml` file to a `.html` file and now you can save it in the build directory. +## Additional Backend Documentation + +### Project Structure + +- **app/** + - **api/**: Contains the RESTful API endpoint definitions using FastAPI. Each module here relates to a specific resource by: + - Defining route paths for HTTP methods (GET, POST, PUT, DELETE). + - Validating input data using Pydantic models. + - Handling exceptions and returning standardized JSON responses compliant with OpenAPI. + - **models.py**: Contains the SQLModel-based database models that define: + - The table schema including columns, data types, and constraints. + - Relationships between tables along with validation rules and default values. + **Note:** Always generate a new Alembic migration and upgrade your database after modifying this file. + - **crud.py**: Implements a set of CRUD functions that abstract database interactions, including: + - Creating and inserting new records. + - Fetching single or multiple records. + - Updating records with new data. + - Deleting records with robust error handling. +- **Docker Setup** + - `docker-compose.yml` & `docker-compose.override.yml`: Used to start the entire stack for development. The volume mappings allow live reloading of code changes. + - `.dockerignore`: Lists files and directories that are not needed during the Docker build process. +- **Version Control** + - `.gitignore`: Ensures that generated files (like caches, virtual environments, etc.) are not committed to version control. + +### Development Workflow + +- **Dependency management:** Uses [Poetry](https://python-poetry.org/) to handle dependencies and virtual environments. +- **Running the application:** Start the services with: + ```bash + docker compose up -d + ``` + Then, for local development and debugging, you can attach to the backend container using: + ```bash + docker compose exec backend bash + ``` +- **Migrations:** Use Alembic for database schema migrations. Always update your migration scripts when modifying models. +- **Debugging & Testing:** Leverage the VS Code configurations to set breakpoints and run tests directly from the editor. + +### Extending the Application + +- **Adding New APIs:** When creating new endpoints, add a new module under the `app/api/` directory. Each endpoint should: + - Define RESTful routes implementing the required CRUD functionalities. + - Use dependency injection to handle authentication and request validation. + - Return structured responses according to OpenAPI standards. +- **Modifying Data Models:** When altering `app/models.py`: + - Update table schemas, add or modify columns, and adjust relationships. + - Create and apply a corresponding Alembic migration to keep the database schema synchronized. + +This section is intended to serve as a high-level guide for both new and experienced developers on understanding and extending the backend of this FastAPI project. + +## Modules Overview + +### Schema + +The `app/schema/` directory contains Pydantic models that are responsible for: + - **Data Structure Definition:** Outlining the data formats used for API requests and responses. + - **Validation & Serialization:** Enforcing business logic and ensuring that incoming and outgoing data adhere to expected formats. For example, custom validators (such as in `carousel_poster.py`) ensure that specific business rules are followed. + - **ORM Integration:** Converting ORM objects into serializable formats using the `from_attributes = True` configuration. + +### Core + +The `app/core/` directory includes essential modules that form the backbone of the application: + - **config.py:** Manages application configuration and environment variables using Pydantic's BaseSettings. It also constructs derived settings (like the SQLAlchemy database URI and server host). + - **db.py:** Initializes and provides access to the database engine and sessions, ensuring smooth database connectivity and supporting operations like database initialization. + - **security.py:** Implements security measures such as JWT-based token creation and validation, ensuring secure API operations. + +These modules together ensure a modular, secure, and maintainable backend system. \ No newline at end of file diff --git a/backend/app/alembic/env.py b/backend/app/alembic/env.py index 7f29c04680..ddab70f856 100755 --- a/backend/app/alembic/env.py +++ b/backend/app/alembic/env.py @@ -22,7 +22,7 @@ from app.core.config import settings # noqa target_metadata = SQLModel.metadata - +print("target_metadata in env:", SQLModel.metadata.tables) # other values from the config, defined by the needs of env.py, # can be acquired: # my_important_option = config.get_main_option("my_important_option") @@ -78,7 +78,7 @@ def run_migrations_online(): context.run_migrations() -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() +# if context.is_offline_mode(): +# run_migrations_offline() +# else: +run_migrations_online() diff --git a/backend/app/alembic/versions/15b7e49466ec_your_new_changes.py b/backend/app/alembic/versions/15b7e49466ec_your_new_changes.py new file mode 100644 index 0000000000..532f52e1b3 --- /dev/null +++ b/backend/app/alembic/versions/15b7e49466ec_your_new_changes.py @@ -0,0 +1,29 @@ +"""your_new_changes + +Revision ID: 15b7e49466ec +Revises: 6a0826f924e8 +Create Date: 2025-02-06 12:56:25.950098 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = '15b7e49466ec' +down_revision = '6a0826f924e8' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/1a31ce608336_add_cascade_delete_relationships.py b/backend/app/alembic/versions/1a31ce608336_add_cascade_delete_relationships.py deleted file mode 100644 index 10e47a1456..0000000000 --- a/backend/app/alembic/versions/1a31ce608336_add_cascade_delete_relationships.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Add cascade delete relationships - -Revision ID: 1a31ce608336 -Revises: d98dd8ec85a3 -Create Date: 2024-07-31 22:24:34.447891 - -""" -from alembic import op -import sqlalchemy as sa -import sqlmodel.sql.sqltypes - - -# revision identifiers, used by Alembic. -revision = '1a31ce608336' -down_revision = 'd98dd8ec85a3' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('item', 'owner_id', - existing_type=sa.UUID(), - nullable=False) - op.drop_constraint('item_owner_id_fkey', 'item', type_='foreignkey') - op.create_foreign_key(None, 'item', 'user', ['owner_id'], ['id'], ondelete='CASCADE') - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, 'item', type_='foreignkey') - op.create_foreign_key('item_owner_id_fkey', 'item', 'user', ['owner_id'], ['id']) - op.alter_column('item', 'owner_id', - existing_type=sa.UUID(), - nullable=True) - # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/20cc9e9039fd_create_qr_codes_table.py b/backend/app/alembic/versions/20cc9e9039fd_create_qr_codes_table.py new file mode 100644 index 0000000000..1ec5e79bcc --- /dev/null +++ b/backend/app/alembic/versions/20cc9e9039fd_create_qr_codes_table.py @@ -0,0 +1,29 @@ +"""Create qrcode table + +Revision ID: 20cc9e9039fd +Revises: 91f6f2bc36a4 +Create Date: 2024-11-01 10:17:32.083891 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = '20cc9e9039fd' +down_revision = '91f6f2bc36a4' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/3078d16ee962_add_explicit_uservenueassociation_for_.py b/backend/app/alembic/versions/3078d16ee962_add_explicit_uservenueassociation_for_.py new file mode 100644 index 0000000000..1e37e4bab3 --- /dev/null +++ b/backend/app/alembic/versions/3078d16ee962_add_explicit_uservenueassociation_for_.py @@ -0,0 +1,31 @@ +"""Add explicit UserVenueAssociation for UserBusiness and Venue + +Revision ID: 3078d16ee962 +Revises: 8c693686becd +Create Date: 2024-10-24 14:55:49.010006 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = '3078d16ee962' +down_revision = '8c693686becd' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user_venue_association', sa.Column('id', sa.Uuid(), nullable=False)) + op.add_column('user_venue_association', sa.Column('role', sqlmodel.sql.sqltypes.AutoString(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user_venue_association', 'role') + op.drop_column('user_venue_association', 'id') + # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/4951a24acf4c_make_phone_number_nullable_in_user_.py b/backend/app/alembic/versions/4951a24acf4c_make_phone_number_nullable_in_user_.py new file mode 100644 index 0000000000..b5d6bc57bd --- /dev/null +++ b/backend/app/alembic/versions/4951a24acf4c_make_phone_number_nullable_in_user_.py @@ -0,0 +1,51 @@ +"""Make phone_number nullable in user_business + +Revision ID: 4951a24acf4c +Revises: e2955dcf9b00 +Create Date: 2024-10-26 15:19:32.622093 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = '4951a24acf4c' +down_revision = 'e2955dcf9b00' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('user_business', 'email', + existing_type=sa.VARCHAR(length=255), + nullable=False) + op.alter_column('user_business', 'phone_number', + existing_type=sa.VARCHAR(), + nullable=True) + op.drop_index('ix_user_business_phone_number', table_name='user_business') + op.drop_index('ix_user_public_email', table_name='user_public') + op.add_column('user_venue_association', sa.Column('user_id', sa.Uuid(), nullable=False)) + op.drop_constraint('user_venue_association_user_business_id_fkey', 'user_venue_association', type_='foreignkey') + op.create_foreign_key(None, 'user_venue_association', 'user_business', ['user_id'], ['id']) + op.drop_column('user_venue_association', 'user_business_id') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user_venue_association', sa.Column('user_business_id', sa.UUID(), autoincrement=False, nullable=False)) + op.drop_constraint(None, 'user_venue_association', type_='foreignkey') + op.create_foreign_key('user_venue_association_user_business_id_fkey', 'user_venue_association', 'user_business', ['user_business_id'], ['id']) + op.drop_column('user_venue_association', 'user_id') + op.create_index('ix_user_public_email', 'user_public', ['email'], unique=True) + op.create_index('ix_user_business_phone_number', 'user_business', ['phone_number'], unique=True) + op.alter_column('user_business', 'phone_number', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('user_business', 'email', + existing_type=sa.VARCHAR(length=255), + nullable=True) + # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/6a0826f924e8_create_carousel_poster_table.py b/backend/app/alembic/versions/6a0826f924e8_create_carousel_poster_table.py new file mode 100644 index 0000000000..5e0a00d968 --- /dev/null +++ b/backend/app/alembic/versions/6a0826f924e8_create_carousel_poster_table.py @@ -0,0 +1,66 @@ +"""Create carousel_poster table + +Revision ID: 6a0826f924e8 +Revises: bc0a875f3f51 +Create Date: 2024-11-01 22:48:54.989213 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = '6a0826f924e8' +down_revision = 'bc0a875f3f51' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('carousel_poster', + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('h3_index', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('image_url', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('deep_link', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('expires_at', sa.DateTime(), nullable=False), + sa.Column('event_id', sa.Uuid(), nullable=True), + sa.Column('venue_id', sa.Uuid(), nullable=True), + sa.ForeignKeyConstraint(['event_id'], ['event.id'], ), + sa.ForeignKeyConstraint(['venue_id'], ['venue.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_carousel_poster_h3_index'), 'carousel_poster', ['h3_index'], unique=False) + op.create_index(op.f('ix_carousel_poster_id'), 'carousel_poster', ['id'], unique=False) + op.add_column('event', sa.Column('venue_id', sa.Uuid(), nullable=False)) + op.drop_constraint('event_nightclub_id_fkey', 'event', type_='foreignkey') + op.create_foreign_key(None, 'event', 'venue', ['venue_id'], ['id']) + op.drop_column('event', 'nightclub_id') + op.alter_column('venue', 'latitude', + existing_type=sa.DOUBLE_PRECISION(precision=53), + nullable=False) + op.alter_column('venue', 'longitude', + existing_type=sa.DOUBLE_PRECISION(precision=53), + nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('venue', 'longitude', + existing_type=sa.DOUBLE_PRECISION(precision=53), + nullable=True) + op.alter_column('venue', 'latitude', + existing_type=sa.DOUBLE_PRECISION(precision=53), + nullable=True) + op.add_column('event', sa.Column('nightclub_id', sa.UUID(), autoincrement=False, nullable=False)) + op.drop_constraint(None, 'event', type_='foreignkey') + op.create_foreign_key('event_nightclub_id_fkey', 'event', 'nightclub', ['nightclub_id'], ['id']) + op.drop_column('event', 'venue_id') + op.drop_index(op.f('ix_carousel_poster_id'), table_name='carousel_poster') + op.drop_index(op.f('ix_carousel_poster_h3_index'), table_name='carousel_poster') + op.drop_table('carousel_poster') + # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/7fd1b196213e_initial_migration.py b/backend/app/alembic/versions/7fd1b196213e_initial_migration.py new file mode 100644 index 0000000000..0565126562 --- /dev/null +++ b/backend/app/alembic/versions/7fd1b196213e_initial_migration.py @@ -0,0 +1,486 @@ +"""Initial migration + +Revision ID: 7fd1b196213e +Revises: +Create Date: 2024-10-23 17:54:34.693255 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = '7fd1b196213e' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user_business', + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('email', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True), + sa.Column('phone_number', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('is_superuser', sa.Boolean(), nullable=False), + sa.Column('full_name', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True), + sa.Column('refresh_token', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('registration_date', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_user_business_email'), 'user_business', ['email'], unique=True) + op.create_index(op.f('ix_user_business_id'), 'user_business', ['id'], unique=False) + op.create_index(op.f('ix_user_business_phone_number'), 'user_business', ['phone_number'], unique=True) + op.create_table('user_public', + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('email', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True), + sa.Column('phone_number', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('is_superuser', sa.Boolean(), nullable=False), + sa.Column('full_name', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True), + sa.Column('refresh_token', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('date_of_birth', sa.DateTime(), nullable=True), + sa.Column('gender', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('registration_date', sa.DateTime(), nullable=False), + sa.Column('profile_picture', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('preferences', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_user_public_email'), 'user_public', ['email'], unique=True) + op.create_index(op.f('ix_user_public_id'), 'user_public', ['id'], unique=False) + op.create_index(op.f('ix_user_public_phone_number'), 'user_public', ['phone_number'], unique=True) + op.create_table('venue', + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('address', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('latitude', sa.Float(), nullable=True), + sa.Column('longitude', sa.Float(), nullable=True), + sa.Column('capacity', sa.Integer(), nullable=True), + sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('google_rating', sa.Float(), nullable=True), + sa.Column('instagram_handle', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('instagram_token', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('google_map_link', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('mobile_number', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('email', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('opening_time', sa.Time(), nullable=True), + sa.Column('closing_time', sa.Time(), nullable=True), + sa.Column('avg_expense_for_two', sa.Float(), nullable=True), + sa.Column('zomato_link', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('swiggy_link', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_venue_id'), 'venue', ['id'], unique=False) + op.create_index(op.f('ix_venue_name'), 'venue', ['name'], unique=False) + op.create_table('foodcourt', + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('total_qsrs', sa.Integer(), nullable=True), + sa.Column('seating_capacity', sa.Integer(), nullable=True), + sa.Column('venue_id', sa.Uuid(), nullable=False), + sa.ForeignKeyConstraint(['venue_id'], ['venue.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_foodcourt_id'), 'foodcourt', ['id'], unique=False) + op.create_index(op.f('ix_foodcourt_venue_id'), 'foodcourt', ['venue_id'], unique=False) + op.create_table('menu', + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('menu_type', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('venue_id', sa.Uuid(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), + sa.ForeignKeyConstraint(['venue_id'], ['venue.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_menu_id'), 'menu', ['id'], unique=False) + op.create_table('nightclub', + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('venue_id', sa.Uuid(), nullable=False), + sa.ForeignKeyConstraint(['venue_id'], ['venue.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_nightclub_id'), 'nightclub', ['id'], unique=False) + op.create_index(op.f('ix_nightclub_venue_id'), 'nightclub', ['venue_id'], unique=False) + op.create_table('payment_source_nightclub', + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.Column('source_type', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('gateway_transaction_id', sa.Uuid(), nullable=True), + sa.Column('payment_time', sa.DateTime(), nullable=False), + sa.Column('amount', sa.Float(), nullable=False), + sa.Column('status', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('retry_count', sa.Integer(), nullable=False), + sa.Column('last_attempt_time', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user_public.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_payment_source_nightclub_id'), 'payment_source_nightclub', ['id'], unique=False) + op.create_table('payment_source_qsr', + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.Column('source_type', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('gateway_transaction_id', sa.Uuid(), nullable=True), + sa.Column('payment_time', sa.DateTime(), nullable=False), + sa.Column('amount', sa.Float(), nullable=False), + sa.Column('status', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('retry_count', sa.Integer(), nullable=False), + sa.Column('last_attempt_time', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user_public.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_payment_source_qsr_id'), 'payment_source_qsr', ['id'], unique=False) + op.create_table('payment_source_restaurant', + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.Column('source_type', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('gateway_transaction_id', sa.Uuid(), nullable=True), + sa.Column('payment_time', sa.DateTime(), nullable=False), + sa.Column('amount', sa.Float(), nullable=False), + sa.Column('status', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('retry_count', sa.Integer(), nullable=False), + sa.Column('last_attempt_time', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user_public.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_payment_source_restaurant_id'), 'payment_source_restaurant', ['id'], unique=False) + op.create_table('pickup_location', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('venue_id', sa.Uuid(), nullable=False), + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.ForeignKeyConstraint(['venue_id'], ['venue.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_pickup_location_id'), 'pickup_location', ['id'], unique=False) + op.create_table('restaurant', + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('venue_id', sa.Uuid(), nullable=False), + sa.Column('cuisine_type', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.ForeignKeyConstraint(['venue_id'], ['venue.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_restaurant_id'), 'restaurant', ['id'], unique=False) + op.create_index(op.f('ix_restaurant_venue_id'), 'restaurant', ['venue_id'], unique=False) + op.create_table('user_venue_association', + sa.Column('user_business_id', sa.Uuid(), nullable=False), + sa.Column('venue_id', sa.Uuid(), nullable=False), + sa.ForeignKeyConstraint(['user_business_id'], ['user_business.id'], ), + sa.ForeignKeyConstraint(['venue_id'], ['venue.id'], ), + sa.PrimaryKeyConstraint('user_business_id', 'venue_id') + ) + op.create_table('event', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('nightclub_id', sa.Uuid(), nullable=False), + sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('start_time', sa.DateTime(), nullable=False), + sa.Column('end_time', sa.DateTime(), nullable=False), + sa.Column('image_url', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('age_restriction', sa.Integer(), nullable=True), + sa.Column('dress_code', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.ForeignKeyConstraint(['nightclub_id'], ['nightclub.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_event_id'), 'event', ['id'], unique=False) + op.create_table('group', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('nightclub_id', sa.Uuid(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('admin_user_id', sa.Uuid(), nullable=False), + sa.Column('table_number', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.ForeignKeyConstraint(['admin_user_id'], ['user_public.id'], ), + sa.ForeignKeyConstraint(['nightclub_id'], ['nightclub.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_group_id'), 'group', ['id'], unique=False) + op.create_table('menu_category', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('menu_id', sa.Uuid(), nullable=False), + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.ForeignKeyConstraint(['menu_id'], ['menu.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_menu_category_id'), 'menu_category', ['id'], unique=False) + op.create_table('nightclub_order', + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.Column('note', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('order_time', sa.DateTime(), nullable=False), + sa.Column('total_amount', sa.Float(), nullable=False), + sa.Column('taxes_and_charges', sa.Float(), nullable=True), + sa.Column('cover_charge_used', sa.Float(), nullable=True), + sa.Column('status', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('service_type', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('venue_id', sa.Uuid(), nullable=True), + sa.Column('payment_id', sa.Uuid(), nullable=True), + sa.Column('pickup_location_id', sa.Uuid(), nullable=True), + sa.ForeignKeyConstraint(['payment_id'], ['payment_source_nightclub.id'], ), + sa.ForeignKeyConstraint(['pickup_location_id'], ['pickup_location.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user_public.id'], ), + sa.ForeignKeyConstraint(['venue_id'], ['nightclub.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_nightclub_order_id'), 'nightclub_order', ['id'], unique=False) + op.create_table('qsr', + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('foodcourt_id', sa.Uuid(), nullable=True), + sa.Column('drive_thru', sa.Boolean(), nullable=True), + sa.Column('venue_id', sa.Uuid(), nullable=False), + sa.ForeignKeyConstraint(['foodcourt_id'], ['foodcourt.id'], ), + sa.ForeignKeyConstraint(['venue_id'], ['venue.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_qsr_foodcourt_id'), 'qsr', ['foodcourt_id'], unique=False) + op.create_index(op.f('ix_qsr_id'), 'qsr', ['id'], unique=False) + op.create_index(op.f('ix_qsr_venue_id'), 'qsr', ['venue_id'], unique=False) + op.create_table('restaurant_order', + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.Column('pickup_location_id', sa.Uuid(), nullable=True), + sa.Column('note', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('order_time', sa.DateTime(), nullable=False), + sa.Column('total_amount', sa.Float(), nullable=False), + sa.Column('taxes_and_charges', sa.Float(), nullable=True), + sa.Column('cover_charge_used', sa.Float(), nullable=True), + sa.Column('status', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('service_type', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('venue_id', sa.Uuid(), nullable=True), + sa.Column('payment_id', sa.Uuid(), nullable=True), + sa.ForeignKeyConstraint(['payment_id'], ['payment_source_restaurant.id'], ), + sa.ForeignKeyConstraint(['pickup_location_id'], ['pickup_location.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user_public.id'], ), + sa.ForeignKeyConstraint(['venue_id'], ['restaurant.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_restaurant_order_id'), 'restaurant_order', ['id'], unique=False) + op.create_table('clubvisit', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.Column('group_id', sa.Uuid(), nullable=True), + sa.Column('nightclub_id', sa.Uuid(), nullable=False), + sa.Column('entry_time', sa.DateTime(), nullable=False), + sa.Column('exit_time', sa.DateTime(), nullable=True), + sa.Column('cover_charge', sa.Float(), nullable=True), + sa.Column('total_bill', sa.Float(), nullable=True), + sa.ForeignKeyConstraint(['group_id'], ['group.id'], ), + sa.ForeignKeyConstraint(['nightclub_id'], ['nightclub.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user_public.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('event_booking', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.Column('event_id', sa.Uuid(), nullable=False), + sa.Column('booking_time', sa.DateTime(), nullable=False), + sa.Column('total_amount', sa.Float(), nullable=False), + sa.Column('status', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.ForeignKeyConstraint(['event_id'], ['event.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user_public.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('group_nightclub_order_link', + sa.Column('group_id', sa.Uuid(), nullable=False), + sa.Column('nightclub_order_id', sa.Uuid(), nullable=False), + sa.ForeignKeyConstraint(['group_id'], ['group.id'], ), + sa.ForeignKeyConstraint(['nightclub_order_id'], ['nightclub_order.id'], ), + sa.PrimaryKeyConstraint('group_id', 'nightclub_order_id') + ) + op.create_table('group_wallet', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('group_id', sa.Uuid(), nullable=False), + sa.Column('balance', sa.Float(), nullable=False), + sa.ForeignKeyConstraint(['group_id'], ['group.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('group_id') + ) + op.create_index(op.f('ix_group_wallet_id'), 'group_wallet', ['id'], unique=False) + op.create_table('groupmembers', + sa.Column('group_id', sa.Uuid(), nullable=False), + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.ForeignKeyConstraint(['group_id'], ['group.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user_public.id'], ), + sa.PrimaryKeyConstraint('group_id', 'user_id') + ) + op.create_table('menu_sub_category', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('category_id', sa.Uuid(), nullable=False), + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.ForeignKeyConstraint(['category_id'], ['menu_category.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_menu_sub_category_id'), 'menu_sub_category', ['id'], unique=False) + op.create_table('qsr_order', + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.Column('pickup_location_id', sa.Uuid(), nullable=True), + sa.Column('note', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('order_time', sa.DateTime(), nullable=False), + sa.Column('total_amount', sa.Float(), nullable=False), + sa.Column('taxes_and_charges', sa.Float(), nullable=True), + sa.Column('cover_charge_used', sa.Float(), nullable=True), + sa.Column('status', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('service_type', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('venue_id', sa.Uuid(), nullable=False), + sa.Column('payment_id', sa.Uuid(), nullable=False), + sa.ForeignKeyConstraint(['payment_id'], ['payment_source_qsr.id'], ), + sa.ForeignKeyConstraint(['pickup_location_id'], ['pickup_location.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user_public.id'], ), + sa.ForeignKeyConstraint(['venue_id'], ['qsr.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_qsr_order_id'), 'qsr_order', ['id'], unique=False) + op.create_table('event_offering', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('event_id', sa.Uuid(), nullable=False), + sa.Column('event_booking_id', sa.Uuid(), nullable=False), + sa.Column('offering_type', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('price', sa.Float(), nullable=False), + sa.Column('total_guests_per_pass', sa.Integer(), nullable=False), + sa.Column('cover_charge', sa.Float(), nullable=True), + sa.Column('additional_charges', sa.Float(), nullable=True), + sa.Column('availability', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['event_booking_id'], ['event_booking.id'], ), + sa.ForeignKeyConstraint(['event_id'], ['event.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_event_offering_id'), 'event_offering', ['id'], unique=False) + op.create_table('groupwallettopup', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('group_wallet_id', sa.Uuid(), nullable=False), + sa.Column('amount', sa.Float(), nullable=False), + sa.Column('topup_time', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['group_wallet_id'], ['group_wallet.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_groupwallettopup_id'), 'groupwallettopup', ['id'], unique=False) + op.create_table('menu_item', + sa.Column('category_id', sa.Uuid(), nullable=False), + sa.Column('sub_category_id', sa.Uuid(), nullable=True), + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('price', sa.Float(), nullable=False), + sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('image_url', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('is_veg', sa.Boolean(), nullable=True), + sa.Column('ingredients', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('abv', sa.Float(), nullable=True), + sa.Column('ibu', sa.Integer(), nullable=True), + sa.Column('id', sa.Uuid(), nullable=False), + sa.ForeignKeyConstraint(['category_id'], ['menu_category.id'], ), + sa.ForeignKeyConstraint(['sub_category_id'], ['menu_sub_category.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_menu_item_id'), 'menu_item', ['id'], unique=False) + op.create_table('payment_event', + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.Column('source_type', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('gateway_transaction_id', sa.Uuid(), nullable=True), + sa.Column('payment_time', sa.DateTime(), nullable=False), + sa.Column('amount', sa.Float(), nullable=False), + sa.Column('status', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('event_booking_id', sa.Uuid(), nullable=True), + sa.ForeignKeyConstraint(['event_booking_id'], ['event_booking.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user_public.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_payment_event_id'), 'payment_event', ['id'], unique=False) + op.create_table('orderitem', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('nightclub_order_id', sa.Uuid(), nullable=True), + sa.Column('restaurant_order_id', sa.Uuid(), nullable=True), + sa.Column('qsr_order_id', sa.Uuid(), nullable=True), + sa.Column('item_id', sa.Uuid(), nullable=False), + sa.Column('quantity', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['item_id'], ['menu_item.id'], ), + sa.ForeignKeyConstraint(['nightclub_order_id'], ['nightclub_order.id'], ), + sa.ForeignKeyConstraint(['qsr_order_id'], ['qsr_order.id'], ), + sa.ForeignKeyConstraint(['restaurant_order_id'], ['restaurant_order.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_orderitem_id'), 'orderitem', ['id'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_orderitem_id'), table_name='orderitem') + op.drop_table('orderitem') + op.drop_index(op.f('ix_payment_event_id'), table_name='payment_event') + op.drop_table('payment_event') + op.drop_index(op.f('ix_menu_item_id'), table_name='menu_item') + op.drop_table('menu_item') + op.drop_index(op.f('ix_groupwallettopup_id'), table_name='groupwallettopup') + op.drop_table('groupwallettopup') + op.drop_index(op.f('ix_event_offering_id'), table_name='event_offering') + op.drop_table('event_offering') + op.drop_index(op.f('ix_qsr_order_id'), table_name='qsr_order') + op.drop_table('qsr_order') + op.drop_index(op.f('ix_menu_sub_category_id'), table_name='menu_sub_category') + op.drop_table('menu_sub_category') + op.drop_table('groupmembers') + op.drop_index(op.f('ix_group_wallet_id'), table_name='group_wallet') + op.drop_table('group_wallet') + op.drop_table('group_nightclub_order_link') + op.drop_table('event_booking') + op.drop_table('clubvisit') + op.drop_index(op.f('ix_restaurant_order_id'), table_name='restaurant_order') + op.drop_table('restaurant_order') + op.drop_index(op.f('ix_qsr_venue_id'), table_name='qsr') + op.drop_index(op.f('ix_qsr_id'), table_name='qsr') + op.drop_index(op.f('ix_qsr_foodcourt_id'), table_name='qsr') + op.drop_table('qsr') + op.drop_index(op.f('ix_nightclub_order_id'), table_name='nightclub_order') + op.drop_table('nightclub_order') + op.drop_index(op.f('ix_menu_category_id'), table_name='menu_category') + op.drop_table('menu_category') + op.drop_index(op.f('ix_group_id'), table_name='group') + op.drop_table('group') + op.drop_index(op.f('ix_event_id'), table_name='event') + op.drop_table('event') + op.drop_table('user_venue_association') + op.drop_index(op.f('ix_restaurant_venue_id'), table_name='restaurant') + op.drop_index(op.f('ix_restaurant_id'), table_name='restaurant') + op.drop_table('restaurant') + op.drop_index(op.f('ix_pickup_location_id'), table_name='pickup_location') + op.drop_table('pickup_location') + op.drop_index(op.f('ix_payment_source_restaurant_id'), table_name='payment_source_restaurant') + op.drop_table('payment_source_restaurant') + op.drop_index(op.f('ix_payment_source_qsr_id'), table_name='payment_source_qsr') + op.drop_table('payment_source_qsr') + op.drop_index(op.f('ix_payment_source_nightclub_id'), table_name='payment_source_nightclub') + op.drop_table('payment_source_nightclub') + op.drop_index(op.f('ix_nightclub_venue_id'), table_name='nightclub') + op.drop_index(op.f('ix_nightclub_id'), table_name='nightclub') + op.drop_table('nightclub') + op.drop_index(op.f('ix_menu_id'), table_name='menu') + op.drop_table('menu') + op.drop_index(op.f('ix_foodcourt_venue_id'), table_name='foodcourt') + op.drop_index(op.f('ix_foodcourt_id'), table_name='foodcourt') + op.drop_table('foodcourt') + op.drop_index(op.f('ix_venue_name'), table_name='venue') + op.drop_index(op.f('ix_venue_id'), table_name='venue') + op.drop_table('venue') + op.drop_index(op.f('ix_user_public_phone_number'), table_name='user_public') + op.drop_index(op.f('ix_user_public_id'), table_name='user_public') + op.drop_index(op.f('ix_user_public_email'), table_name='user_public') + op.drop_table('user_public') + op.drop_index(op.f('ix_user_business_phone_number'), table_name='user_business') + op.drop_index(op.f('ix_user_business_id'), table_name='user_business') + op.drop_index(op.f('ix_user_business_email'), table_name='user_business') + op.drop_table('user_business') + # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/8c693686becd_description_of_changes.py b/backend/app/alembic/versions/8c693686becd_description_of_changes.py new file mode 100644 index 0000000000..3b39fcda0e --- /dev/null +++ b/backend/app/alembic/versions/8c693686becd_description_of_changes.py @@ -0,0 +1,37 @@ +"""Description of changes + +Revision ID: 8c693686becd +Revises: 9e690425af2e +Create Date: 2024-10-23 23:16:40.093380 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = '8c693686becd' +down_revision = '9e690425af2e' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('nightclub', sa.Column('age_limit', sa.Integer(), nullable=True)) + op.add_column('venue', sa.Column('zomato_link', sqlmodel.sql.sqltypes.AutoString(), nullable=True)) + op.add_column('venue', sa.Column('swiggy_link', sqlmodel.sql.sqltypes.AutoString(), nullable=True)) + op.drop_column('venue', 'swiggylink') + op.drop_column('venue', 'zomatolink') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('venue', sa.Column('zomatolink', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column('venue', sa.Column('swiggylink', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.drop_column('venue', 'swiggy_link') + op.drop_column('venue', 'zomato_link') + op.drop_column('nightclub', 'age_limit') + # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/91f6f2bc36a4_add_registration_date_to_user_public.py b/backend/app/alembic/versions/91f6f2bc36a4_add_registration_date_to_user_public.py new file mode 100644 index 0000000000..701da3f637 --- /dev/null +++ b/backend/app/alembic/versions/91f6f2bc36a4_add_registration_date_to_user_public.py @@ -0,0 +1,63 @@ +"""Add registration_date to user_public + +Revision ID: 91f6f2bc36a4 +Revises: 4951a24acf4c +Create Date: 2024-10-30 21:14:14.300480 + +""" +from alembic import op +from sqlalchemy.dialects.postgresql import ENUM +import sqlalchemy as sa +import sqlmodel.sql.sqltypes +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '91f6f2bc36a4' +down_revision = '4951a24acf4c' +branch_labels = None +depends_on = None + +gender_enum = ENUM('male', 'female', 'others', name='gender') + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + gender_enum.create(op.get_bind()) + op.drop_table('carousel_poster') + op.drop_column('user_business', 'registration_date') + op.alter_column('user_public', 'gender', + type_=gender_enum, + existing_type=sa.String(), # Change to the existing type + existing_nullable=True, + postgresql_using='gender::gender') # If you need to convert existing data + + op.drop_column('user_public', 'registration_date') + op.drop_column('venue', 'h3_index') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('venue', sa.Column('h3_index', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column('user_public', sa.Column('registration_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=False)) + op.drop_column('user_public', 'gender') + gender_enum.drop(op.get_bind()) + op.add_column('user_business', sa.Column('registration_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=False)) + op.create_table('carousel_poster', + sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), autoincrement=False, nullable=False), + sa.Column('image_url', sa.VARCHAR(length=255), autoincrement=False, nullable=False), + sa.Column('deep_link', sa.VARCHAR(length=255), autoincrement=False, nullable=False), + sa.Column('expires_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), + sa.Column('event_id', sa.UUID(), autoincrement=False, nullable=True), + sa.Column('nightclub_id', sa.UUID(), autoincrement=False, nullable=True), + sa.Column('foodcourt_id', sa.UUID(), autoincrement=False, nullable=True), + sa.Column('qsr_id', sa.UUID(), autoincrement=False, nullable=True), + sa.Column('restaurant_id', sa.UUID(), autoincrement=False, nullable=True), + sa.Column('h3_index', sa.VARCHAR(length=255), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['event_id'], ['event.id'], name='carousel_poster_event_id_fkey'), + sa.ForeignKeyConstraint(['foodcourt_id'], ['foodcourt.id'], name='carousel_poster_foodcourt_id_fkey'), + sa.ForeignKeyConstraint(['nightclub_id'], ['nightclub.id'], name='carousel_poster_nightclub_id_fkey'), + sa.ForeignKeyConstraint(['qsr_id'], ['qsr.id'], name='carousel_poster_qsr_id_fkey'), + sa.ForeignKeyConstraint(['restaurant_id'], ['restaurant.id'], name='carousel_poster_restaurant_id_fkey'), + sa.PrimaryKeyConstraint('id', name='carousel_poster_pkey') + ) + # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/9c0a54914c78_add_max_length_for_string_varchar_.py b/backend/app/alembic/versions/9c0a54914c78_add_max_length_for_string_varchar_.py deleted file mode 100755 index 78a41773b9..0000000000 --- a/backend/app/alembic/versions/9c0a54914c78_add_max_length_for_string_varchar_.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Add max length for string(varchar) fields in User and Items models - -Revision ID: 9c0a54914c78 -Revises: e2412789c190 -Create Date: 2024-06-17 14:42:44.639457 - -""" -from alembic import op -import sqlalchemy as sa -import sqlmodel.sql.sqltypes - - -# revision identifiers, used by Alembic. -revision = '9c0a54914c78' -down_revision = 'e2412789c190' -branch_labels = None -depends_on = None - - -def upgrade(): - # Adjust the length of the email field in the User table - op.alter_column('user', 'email', - existing_type=sa.String(), - type_=sa.String(length=255), - existing_nullable=False) - - # Adjust the length of the full_name field in the User table - op.alter_column('user', 'full_name', - existing_type=sa.String(), - type_=sa.String(length=255), - existing_nullable=True) - - # Adjust the length of the title field in the Item table - op.alter_column('item', 'title', - existing_type=sa.String(), - type_=sa.String(length=255), - existing_nullable=False) - - # Adjust the length of the description field in the Item table - op.alter_column('item', 'description', - existing_type=sa.String(), - type_=sa.String(length=255), - existing_nullable=True) - - -def downgrade(): - # Revert the length of the email field in the User table - op.alter_column('user', 'email', - existing_type=sa.String(length=255), - type_=sa.String(), - existing_nullable=False) - - # Revert the length of the full_name field in the User table - op.alter_column('user', 'full_name', - existing_type=sa.String(length=255), - type_=sa.String(), - existing_nullable=True) - - # Revert the length of the title field in the Item table - op.alter_column('item', 'title', - existing_type=sa.String(length=255), - type_=sa.String(), - existing_nullable=False) - - # Revert the length of the description field in the Item table - op.alter_column('item', 'description', - existing_type=sa.String(length=255), - type_=sa.String(), - existing_nullable=True) diff --git a/backend/app/alembic/versions/9e690425af2e_add_zomato_link_to_venue.py b/backend/app/alembic/versions/9e690425af2e_add_zomato_link_to_venue.py new file mode 100644 index 0000000000..0abc04de47 --- /dev/null +++ b/backend/app/alembic/versions/9e690425af2e_add_zomato_link_to_venue.py @@ -0,0 +1,25 @@ +"""add zomato_link to venue + +Revision ID: 9e690425af2e +Revises: 7fd1b196213e +Create Date: 2024-10-23 23:11:38.666505 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = '9e690425af2e' +down_revision = '7fd1b196213e' +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/backend/app/alembic/versions/a1c01df04ba4_create_qrcode_table.py b/backend/app/alembic/versions/a1c01df04ba4_create_qrcode_table.py new file mode 100644 index 0000000000..c404b005de --- /dev/null +++ b/backend/app/alembic/versions/a1c01df04ba4_create_qrcode_table.py @@ -0,0 +1,47 @@ +"""Create qrcode table + +Revision ID: a1c01df04ba4 +Revises: bef89635a1b9 +Create Date: 2024-11-01 13:29:27.200014 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'a1c01df04ba4' +down_revision = 'bef89635a1b9' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('qrcode', + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('venue_id', sa.Uuid(), nullable=True), + sa.Column('table_number', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.ForeignKeyConstraint(['venue_id'], ['venue.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.drop_table('qr_codes') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('qr_codes', + sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), + sa.Column('updated_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), + sa.Column('id', sa.UUID(), autoincrement=False, nullable=False), + sa.Column('venue_id', sa.UUID(), autoincrement=False, nullable=True), + sa.Column('table_number', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['venue_id'], ['venue.id'], name='qr_codes_venue_id_fkey'), + sa.PrimaryKeyConstraint('id', name='qr_codes_pkey') + ) + op.drop_table('qrcode') + # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/bc0a875f3f51_make_latitude_and_longitude_not_nullable.py b/backend/app/alembic/versions/bc0a875f3f51_make_latitude_and_longitude_not_nullable.py new file mode 100644 index 0000000000..200aca7380 --- /dev/null +++ b/backend/app/alembic/versions/bc0a875f3f51_make_latitude_and_longitude_not_nullable.py @@ -0,0 +1,25 @@ +"""Make latitude and longitude not nullable + +Revision ID: bc0a875f3f51 +Revises: a1c01df04ba4 +Create Date: 2024-11-01 22:45:19.154522 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = 'bc0a875f3f51' +down_revision = 'a1c01df04ba4' +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/backend/app/alembic/versions/bef89635a1b9_create_qr_codes_table.py b/backend/app/alembic/versions/bef89635a1b9_create_qr_codes_table.py new file mode 100644 index 0000000000..c412895ff4 --- /dev/null +++ b/backend/app/alembic/versions/bef89635a1b9_create_qr_codes_table.py @@ -0,0 +1,37 @@ +"""Create qrcode table + +Revision ID: bef89635a1b9 +Revises: 20cc9e9039fd +Create Date: 2024-11-01 10:26:05.045546 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = 'bef89635a1b9' +down_revision = '20cc9e9039fd' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('qrcode', + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('venue_id', sa.Uuid(), nullable=True), + sa.Column('table_number', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.ForeignKeyConstraint(['venue_id'], ['venue.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('qrcode') + # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/d98dd8ec85a3_edit_replace_id_integers_in_all_models_.py b/backend/app/alembic/versions/d98dd8ec85a3_edit_replace_id_integers_in_all_models_.py deleted file mode 100755 index 37af1fa215..0000000000 --- a/backend/app/alembic/versions/d98dd8ec85a3_edit_replace_id_integers_in_all_models_.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Edit replace id integers in all models to use UUID instead - -Revision ID: d98dd8ec85a3 -Revises: 9c0a54914c78 -Create Date: 2024-07-19 04:08:04.000976 - -""" -from alembic import op -import sqlalchemy as sa -import sqlmodel.sql.sqltypes -from sqlalchemy.dialects import postgresql - - -# revision identifiers, used by Alembic. -revision = 'd98dd8ec85a3' -down_revision = '9c0a54914c78' -branch_labels = None -depends_on = None - - -def upgrade(): - # Ensure uuid-ossp extension is available - op.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"') - - # Create a new UUID column with a default UUID value - op.add_column('user', sa.Column('new_id', postgresql.UUID(as_uuid=True), default=sa.text('uuid_generate_v4()'))) - op.add_column('item', sa.Column('new_id', postgresql.UUID(as_uuid=True), default=sa.text('uuid_generate_v4()'))) - op.add_column('item', sa.Column('new_owner_id', postgresql.UUID(as_uuid=True), nullable=True)) - - # Populate the new columns with UUIDs - op.execute('UPDATE "user" SET new_id = uuid_generate_v4()') - op.execute('UPDATE item SET new_id = uuid_generate_v4()') - op.execute('UPDATE item SET new_owner_id = (SELECT new_id FROM "user" WHERE "user".id = item.owner_id)') - - # Set the new_id as not nullable - op.alter_column('user', 'new_id', nullable=False) - op.alter_column('item', 'new_id', nullable=False) - - # Drop old columns and rename new columns - op.drop_constraint('item_owner_id_fkey', 'item', type_='foreignkey') - op.drop_column('item', 'owner_id') - op.alter_column('item', 'new_owner_id', new_column_name='owner_id') - - op.drop_column('user', 'id') - op.alter_column('user', 'new_id', new_column_name='id') - - op.drop_column('item', 'id') - op.alter_column('item', 'new_id', new_column_name='id') - - # Create primary key constraint - op.create_primary_key('user_pkey', 'user', ['id']) - op.create_primary_key('item_pkey', 'item', ['id']) - - # Recreate foreign key constraint - op.create_foreign_key('item_owner_id_fkey', 'item', 'user', ['owner_id'], ['id']) - -def downgrade(): - # Reverse the upgrade process - op.add_column('user', sa.Column('old_id', sa.Integer, autoincrement=True)) - op.add_column('item', sa.Column('old_id', sa.Integer, autoincrement=True)) - op.add_column('item', sa.Column('old_owner_id', sa.Integer, nullable=True)) - - # Populate the old columns with default values - # Generate sequences for the integer IDs if not exist - op.execute('CREATE SEQUENCE IF NOT EXISTS user_id_seq AS INTEGER OWNED BY "user".old_id') - op.execute('CREATE SEQUENCE IF NOT EXISTS item_id_seq AS INTEGER OWNED BY item.old_id') - - op.execute('SELECT setval(\'user_id_seq\', COALESCE((SELECT MAX(old_id) + 1 FROM "user"), 1), false)') - op.execute('SELECT setval(\'item_id_seq\', COALESCE((SELECT MAX(old_id) + 1 FROM item), 1), false)') - - op.execute('UPDATE "user" SET old_id = nextval(\'user_id_seq\')') - op.execute('UPDATE item SET old_id = nextval(\'item_id_seq\'), old_owner_id = (SELECT old_id FROM "user" WHERE "user".id = item.owner_id)') - - # Drop new columns and rename old columns back - op.drop_constraint('item_owner_id_fkey', 'item', type_='foreignkey') - op.drop_column('item', 'owner_id') - op.alter_column('item', 'old_owner_id', new_column_name='owner_id') - - op.drop_column('user', 'id') - op.alter_column('user', 'old_id', new_column_name='id') - - op.drop_column('item', 'id') - op.alter_column('item', 'old_id', new_column_name='id') - - # Create primary key constraint - op.create_primary_key('user_pkey', 'user', ['id']) - op.create_primary_key('item_pkey', 'item', ['id']) - - # Recreate foreign key constraint - op.create_foreign_key('item_owner_id_fkey', 'item', 'user', ['owner_id'], ['id']) diff --git a/backend/app/alembic/versions/e2412789c190_initialize_models.py b/backend/app/alembic/versions/e2412789c190_initialize_models.py deleted file mode 100644 index 7529ea91fa..0000000000 --- a/backend/app/alembic/versions/e2412789c190_initialize_models.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Initialize models - -Revision ID: e2412789c190 -Revises: -Create Date: 2023-11-24 22:55:43.195942 - -""" -import sqlalchemy as sa -import sqlmodel.sql.sqltypes -from alembic import op - -# revision identifiers, used by Alembic. -revision = "e2412789c190" -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "user", - sa.Column("email", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("is_active", sa.Boolean(), nullable=False), - sa.Column("is_superuser", sa.Boolean(), nullable=False), - sa.Column("full_name", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column("id", sa.Integer(), nullable=False), - sa.Column( - "hashed_password", sqlmodel.sql.sqltypes.AutoString(), nullable=False - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True) - op.create_table( - "item", - sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True), - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("title", sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column("owner_id", sa.Integer(), nullable=False), - sa.ForeignKeyConstraint( - ["owner_id"], - ["user.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table("item") - op.drop_index(op.f("ix_user_email"), table_name="user") - op.drop_table("user") - # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/e2955dcf9b00_updated_menu_and_category_models.py b/backend/app/alembic/versions/e2955dcf9b00_updated_menu_and_category_models.py new file mode 100644 index 0000000000..288f13039b --- /dev/null +++ b/backend/app/alembic/versions/e2955dcf9b00_updated_menu_and_category_models.py @@ -0,0 +1,29 @@ +"""Updated menu and category models + +Revision ID: e2955dcf9b00 +Revises: ea33a3e76248 +Create Date: 2024-10-24 23:47:03.184543 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = 'e2955dcf9b00' +down_revision = 'ea33a3e76248' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/ea33a3e76248_create_menu_subcategory_table.py b/backend/app/alembic/versions/ea33a3e76248_create_menu_subcategory_table.py new file mode 100644 index 0000000000..12c6f31eee --- /dev/null +++ b/backend/app/alembic/versions/ea33a3e76248_create_menu_subcategory_table.py @@ -0,0 +1,57 @@ +"""create menu_subcategory table + +Revision ID: ea33a3e76248 +Revises: 3078d16ee962 +Create Date: 2024-10-24 23:25:14.486071 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = 'ea33a3e76248' +down_revision = '3078d16ee962' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('menu_subcategory', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('category_id', sa.Uuid(), nullable=False), + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('is_alcoholic', sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint(['category_id'], ['menu_category.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_menu_subcategory_id'), 'menu_subcategory', ['id'], unique=False) + op.add_column('menu_item', sa.Column('subcategory_id', sa.Uuid(), nullable=False)) + op.drop_constraint('menu_item_category_id_fkey', 'menu_item', type_='foreignkey') + op.create_foreign_key(None, 'menu_item', 'menu_subcategory', ['subcategory_id'], ['id']) + op.drop_column('menu_item', 'category_id') + op.drop_column('menu_item', 'sub_category_id') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('menu_item', sa.Column('sub_category_id', sa.UUID(), autoincrement=False, nullable=True)) + op.add_column('menu_item', sa.Column('category_id', sa.UUID(), autoincrement=False, nullable=False)) + op.drop_constraint(None, 'menu_item', type_='foreignkey') + op.create_foreign_key('menu_item_category_id_fkey', 'menu_item', 'menu_category', ['category_id'], ['id']) + op.create_foreign_key('menu_item_sub_category_id_fkey', 'menu_item', 'menu_sub_category', ['sub_category_id'], ['id']) + op.drop_column('menu_item', 'subcategory_id') + op.create_table('menu_sub_category', + sa.Column('id', sa.UUID(), autoincrement=False, nullable=False), + sa.Column('category_id', sa.UUID(), autoincrement=False, nullable=False), + sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['category_id'], ['menu_category.id'], name='menu_sub_category_category_id_fkey'), + sa.PrimaryKeyConstraint('id', name='menu_sub_category_pkey') + ) + op.create_index('ix_menu_sub_category_id', 'menu_sub_category', ['id'], unique=False) + op.drop_index(op.f('ix_menu_subcategory_id'), table_name='menu_subcategory') + op.drop_table('menu_subcategory') + # ### end Alembic commands ### diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py index c2b83c841d..57bfe25812 100644 --- a/backend/app/api/deps.py +++ b/backend/app/api/deps.py @@ -1,22 +1,19 @@ from collections.abc import Generator from typing import Annotated -import jwt from fastapi import Depends, HTTPException, status -from fastapi.security import OAuth2PasswordBearer -from jwt.exceptions import InvalidTokenError -from pydantic import ValidationError -from sqlmodel import Session +from fastapi.security import ( + HTTPAuthorizationCredentials, + HTTPBearer, +) +from sqlalchemy.orm import Session -from app.core import security -from app.core.config import settings from app.core.db import engine -from app.models import TokenPayload, User - -reusable_oauth2 = OAuth2PasswordBearer( - tokenUrl=f"{settings.API_V1_STR}/login/access-token" -) +from app.core.security import get_jwt_payload +from app.models.user import UserBusiness, UserPublic +# OAuth2PasswordBearer to extract the token from the request header +bearer_scheme = HTTPBearer() def get_db() -> Generator[Session, None, None]: with Session(engine) as session: @@ -24,34 +21,87 @@ def get_db() -> Generator[Session, None, None]: SessionDep = Annotated[Session, Depends(get_db)] -TokenDep = Annotated[str, Depends(reusable_oauth2)] - -def get_current_user(session: SessionDep, token: TokenDep) -> User: +# Dependency to get the current user +async def get_current_user( + credentials: Annotated[HTTPAuthorizationCredentials, Depends(bearer_scheme)], + session: SessionDep +) -> UserPublic | UserBusiness: + # print('credentials.credentials ', credentials.credentials) try: - payload = jwt.decode( - token, settings.SECRET_KEY, algorithms=[security.ALGORITHM] - ) - token_data = TokenPayload(**payload) - except (InvalidTokenError, ValidationError): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Could not validate credentials", + print('credentials.credentials ', credentials.credentials) + # Verify and decode the token (ensure this is as fast as possible) + token_data = get_jwt_payload(credentials.credentials) + user_id = token_data.sub + print('hhuuuser_id ', user_id) + # Query both user types in a single call, using a union if possible + user = ( + session.query(UserPublic) + .filter(UserPublic.id == user_id) + .first() + ) or ( + session.query(UserBusiness) + .filter(UserBusiness.id == user_id) + .first() ) - user = session.get(User, token_data.sub) - if not user: - raise HTTPException(status_code=404, detail="User not found") - if not user.is_active: - raise HTTPException(status_code=400, detail="Inactive user") - return user + # If no user is found or the user is inactive, raise an error + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found", + ) + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Inactive user", + ) + + return user -CurrentUser = Annotated[User, Depends(get_current_user)] + except Exception: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials" + ) +# Dependency to get the business user +async def get_business_user( + credentials: Annotated[HTTPAuthorizationCredentials, Depends(bearer_scheme)], + session: SessionDep +) -> UserBusiness: + current_user = await get_current_user(credentials, session) + if not isinstance(current_user, UserBusiness): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a business user", + ) + return current_user -def get_current_active_superuser(current_user: CurrentUser) -> User: +# Dependency to get the superuser +async def get_super_user( + credentials: Annotated[HTTPAuthorizationCredentials, Depends(bearer_scheme)], + session: SessionDep +) -> UserBusiness: + current_user = await get_business_user(credentials, session) if not current_user.is_superuser: raise HTTPException( - status_code=403, detail="The user doesn't have enough privileges" + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a superuser", + ) + return current_user + +# Dependency to get any public user +async def get_public_user( + credentials: Annotated[HTTPAuthorizationCredentials, Depends(bearer_scheme)], + session: SessionDep +) -> UserPublic: + print('credentials.credentials ', credentials.credentials) + current_user = await get_current_user(credentials, session) + # print('current_user ', current_user) + if not isinstance(current_user, UserPublic): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not a public user", ) return current_user diff --git a/backend/app/api/main.py b/backend/app/api/main.py index 09e0663fc3..71bd7cb7d2 100644 --- a/backend/app/api/main.py +++ b/backend/app/api/main.py @@ -1,9 +1,12 @@ from fastapi import APIRouter -from app.api.routes import items, login, users, utils +from app.api.routes import carousel, login, menu, qrcode, users, venues,scrapper api_router = APIRouter() +api_router.include_router(venues.router, prefix="/venue", tags=["venue"]) +api_router.include_router(menu.router, prefix="/menu", tags=["menu"]) +api_router.include_router(users.router, prefix="/user", tags=["user"]) api_router.include_router(login.router, tags=["login"]) -api_router.include_router(users.router, prefix="/users", tags=["users"]) -api_router.include_router(utils.router, prefix="/utils", tags=["utils"]) -api_router.include_router(items.router, prefix="/items", tags=["items"]) +api_router.include_router(qrcode.router, tags=["qrcode"]) +api_router.include_router(carousel.router, prefix="/carousel", tags=["carousel"]) +api_router.include_router(scrapper.router, prefix="/scrapper", tags=["scrapper"]) \ No newline at end of file diff --git a/backend/app/api/routes/carousel.py b/backend/app/api/routes/carousel.py new file mode 100644 index 0000000000..994a7a0063 --- /dev/null +++ b/backend/app/api/routes/carousel.py @@ -0,0 +1,102 @@ +from datetime import datetime + +import h3 +from fastapi import APIRouter, Depends, HTTPException +from sqlmodel import Session, select + +from app.api.deps import SessionDep, get_business_user, get_current_user, get_db +from app.models.carousel_poster import CarouselPoster +from app.models.event import Event +from app.models.user import UserBusiness, UserPublic +from app.models.venue import Venue +from app.schema.carousel_poster import CarouselPosterCreate, CarouselPosterRead +from app.util import check_user_permission, create_record, get_record_by_id +from app.utils import get_h3_index + +router = APIRouter() + + +@router.get("/poster/", response_model=list[CarouselPosterRead]) +async def get_carousel_posters( + latitude: float, + longitude: float, + session: SessionDep, + radius: int = 3000, + current_user: UserPublic = Depends(get_current_user), # noqa: ARG001 +): + user_h3_index = get_h3_index(latitude=latitude, longitude=longitude) + + distance_in_km = radius / 1000 + k_ring_size = int(distance_in_km / 1.2) + + nearby_h3_indexes = h3.k_ring(user_h3_index, k_ring_size) + + posters = ( + session.execute( + select(CarouselPoster) + .where(CarouselPoster.h3_index.in_(nearby_h3_indexes)) + .where(CarouselPoster.expires_at > datetime.now()) + ) + .scalars() + .all() + ) + + return [poster.to_read_schema() for poster in posters] + + +@router.post("/poster/", response_model=CarouselPosterRead) +async def create_carousel_poster( + poster: CarouselPosterCreate, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_business_user), +): + print("Creating carousel poster, poster: ", poster) + poster_instance = CarouselPoster.from_create_schema(poster) + print("Creating carousel poster, poster_instance: ", poster_instance) + venue = None + if poster_instance.venue_id: + print("Creating carousel poster, venue_id: ", poster_instance.venue_id) + try: + venue = get_record_by_id(db, Venue, poster_instance.venue_id) + except Exception: + raise HTTPException(status_code=404, detail="Venue not found") + print("Creating carousel poster, venue: ", venue) + elif poster_instance.event_id: + try: + event = db.get(poster_instance.event_id, Event) + except Exception: + raise HTTPException(status_code=404, detail="Event not found") + venue = event.venue + else: + raise ValueError("Either event_id or venue_id must be provided") + + check_user_permission(db, current_user, venue.id) + + h3_index = get_h3_index( + latitude=venue.latitude, longitude=venue.longitude, resolution=9 + ) + + poster_instance.h3_index = h3_index + + created_poster = create_record(db, poster_instance) + + assert isinstance( + created_poster, CarouselPoster + ), "The returned object is not of type CarouselPoster" + + x = created_poster.to_read_schema() + return x + + +# @router.put("/poster/{poster_id}", response_model=CarouselPosterRead) +# async def update_carousel_poster( +# poster_id: uuid.UUID, updated_data: CarouselPosterCreate, session: SessionDep +# ): +# return update_record( +# session=session, record_id=poster_id, obj_in=updated_data, model=CarouselPoster +# ) + + +# @router.delete("/poster/{poster_id}") +# async def delete_carousel_poster(poster_id: uuid.UUID, session: SessionDep): +# return delete_record(session=session, model=CarouselPoster, record_id=poster_id) diff --git a/backend/app/api/routes/items.py b/backend/app/api/routes/items.py deleted file mode 100644 index 67196c2366..0000000000 --- a/backend/app/api/routes/items.py +++ /dev/null @@ -1,109 +0,0 @@ -import uuid -from typing import Any - -from fastapi import APIRouter, HTTPException -from sqlmodel import func, select - -from app.api.deps import CurrentUser, SessionDep -from app.models import Item, ItemCreate, ItemPublic, ItemsPublic, ItemUpdate, Message - -router = APIRouter() - - -@router.get("/", response_model=ItemsPublic) -def read_items( - session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100 -) -> Any: - """ - Retrieve items. - """ - - if current_user.is_superuser: - count_statement = select(func.count()).select_from(Item) - count = session.exec(count_statement).one() - statement = select(Item).offset(skip).limit(limit) - items = session.exec(statement).all() - else: - count_statement = ( - select(func.count()) - .select_from(Item) - .where(Item.owner_id == current_user.id) - ) - count = session.exec(count_statement).one() - statement = ( - select(Item) - .where(Item.owner_id == current_user.id) - .offset(skip) - .limit(limit) - ) - items = session.exec(statement).all() - - return ItemsPublic(data=items, count=count) - - -@router.get("/{id}", response_model=ItemPublic) -def read_item(session: SessionDep, current_user: CurrentUser, id: uuid.UUID) -> Any: - """ - Get item by ID. - """ - item = session.get(Item, id) - if not item: - raise HTTPException(status_code=404, detail="Item not found") - if not current_user.is_superuser and (item.owner_id != current_user.id): - raise HTTPException(status_code=400, detail="Not enough permissions") - return item - - -@router.post("/", response_model=ItemPublic) -def create_item( - *, session: SessionDep, current_user: CurrentUser, item_in: ItemCreate -) -> Any: - """ - Create new item. - """ - item = Item.model_validate(item_in, update={"owner_id": current_user.id}) - session.add(item) - session.commit() - session.refresh(item) - return item - - -@router.put("/{id}", response_model=ItemPublic) -def update_item( - *, - session: SessionDep, - current_user: CurrentUser, - id: uuid.UUID, - item_in: ItemUpdate, -) -> Any: - """ - Update an item. - """ - item = session.get(Item, id) - if not item: - raise HTTPException(status_code=404, detail="Item not found") - if not current_user.is_superuser and (item.owner_id != current_user.id): - raise HTTPException(status_code=400, detail="Not enough permissions") - update_dict = item_in.model_dump(exclude_unset=True) - item.sqlmodel_update(update_dict) - session.add(item) - session.commit() - session.refresh(item) - return item - - -@router.delete("/{id}") -def delete_item( - session: SessionDep, current_user: CurrentUser, id: uuid.UUID -) -> Message: - """ - Delete an item. - """ - item = session.get(Item, id) - if not item: - raise HTTPException(status_code=404, detail="Item not found") - if not current_user.is_superuser and (item.owner_id != current_user.id): - raise HTTPException(status_code=400, detail="Not enough permissions") - session.delete(item) - session.commit() - return Message(message="Item deleted successfully") diff --git a/backend/app/api/routes/login.py b/backend/app/api/routes/login.py index fe7e94d5c1..643515827b 100644 --- a/backend/app/api/routes/login.py +++ b/backend/app/api/routes/login.py @@ -1,124 +1,182 @@ -from datetime import timedelta -from typing import Annotated, Any +import hashlib +from datetime import datetime, timedelta, timezone +from typing import Annotated from fastapi import APIRouter, Depends, HTTPException -from fastapi.responses import HTMLResponse -from fastapi.security import OAuth2PasswordRequestForm - -from app import crud -from app.api.deps import CurrentUser, SessionDep, get_current_active_superuser -from app.core import security -from app.core.config import settings -from app.core.security import get_password_hash -from app.models import Message, NewPassword, Token, UserPublic -from app.utils import ( - generate_password_reset_token, - generate_reset_password_email, - send_email, - verify_password_reset_token, + +from app.api.deps import SessionDep, get_current_user +from app.core.security import ( # Adjust the import based on your structure + create_access_token, + create_refresh_token, + get_jwt_payload, ) +from app.models.auth import OtplessToken, RefreshTokenPayload, UserAuthResponse +from app.models.user import UserBusiness, UserPublic # Import your UserPublic model router = APIRouter() -@router.post("/login/access-token") -def login_access_token( - session: SessionDep, form_data: Annotated[OAuth2PasswordRequestForm, Depends()] -) -> Token: - """ - OAuth2 compatible token login, get an access token for future requests - """ - user = crud.authenticate( - session=session, email=form_data.username, password=form_data.password - ) - if not user: - raise HTTPException(status_code=400, detail="Incorrect email or password") - elif not user.is_active: - raise HTTPException(status_code=400, detail="Inactive user") - access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) - return Token( - access_token=security.create_access_token( - user.id, expires_delta=access_token_expires +def generate_number_from_string(s): + return int(hashlib.sha256(s.encode()).hexdigest(), 16) % 10 ** 8 + + +@router.post("/verify_token/business", response_model=UserAuthResponse) +async def business_user_google_login(request: OtplessToken, session: SessionDep): + try: + # Verify the Google token with Google + # token_info_url = "https://oauth2.googleapis.com/tokeninfo" + # async with httpx.AsyncClient() as client: + # response = await client.get(f"{token_info_url}?id_token={request.google_token}") + + # if response.status_code != 200: + # raise HTTPException(status_code=400, detail="Invalid Google token") + + # # Parse user info + # user_info = response.json() + # email = user_info.get("email") + # user_id = user_info.get("sub") # Google user ID + + # Check for user by Google ID or email + email = request.otpless_token+ "test@gmail.com" # Replace this with the actual email from the SDK response + user = session.query(UserBusiness).filter(UserBusiness.email == email).first() + + if not user: + # Create new user if not found + user = UserBusiness( + email=email, + is_active=True + ) + session.add(user) + session.commit() + session.refresh(user) + + # Create tokens + access_token = create_access_token(subject=str(user.id), expires_delta=timedelta(minutes=30)) + refresh_token = create_refresh_token(subject=str(user.id)) + + # Store refresh token + user.refresh_token = refresh_token.token + session.add(user) + session.commit() + + return UserAuthResponse( + access_token=access_token, + refresh_token=refresh_token, + issued_at=datetime.now(timezone.utc) + ) + + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/verify_token/public", response_model=UserAuthResponse) +async def verify_token(request: OtplessToken, session: SessionDep): + try: + # Verify the token using OTPLess SDK + # Uncomment and implement token verification with OTPLess + # user_details = OTPLessAuthSDK.UserDetail.verify_token( + # request.otpless_token, + # settings.CLIENT_ID, + # settings.CLIENT_SECRET + # ) + + # Simulated user details for demonstration + # phone_number = "8130181469" # Replace this with the actual phone number from the SDK response + phone_number = str(generate_number_from_string(request.otpless_token)) + + # Check for the user by phone number + user = session.query(UserPublic).filter(UserPublic.phone_number == phone_number).first() + + if not user: + # Create a new user if not found + user = UserPublic( + phone_number=phone_number, + is_active=True + ) + session.add(user) + session.commit() + session.refresh(user) + + # Create tokens + access_token = create_access_token(subject=str(user.id), expires_delta=timedelta(minutes=30)) + refresh_token = create_refresh_token(subject=str(user.id)) + + # Store the new refresh token in the user's record + user.refresh_token = refresh_token.token + session.add(user) # Add the updated user to the session + session.commit() # Commit the changes to the database + + return UserAuthResponse( + access_token=access_token, + refresh_token=refresh_token, + issued_at=datetime.now(timezone.utc) ) - ) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) -@router.post("/login/test-token", response_model=UserPublic) -def test_token(current_user: CurrentUser) -> Any: - """ - Test access token - """ - return current_user +@router.post("/refresh_token", response_model=UserAuthResponse) +async def refresh_token(request: RefreshTokenPayload, session: SessionDep): + try: + # Decode and validate the refresh token payload + payload = get_jwt_payload(request.refresh_token) -@router.post("/password-recovery/{email}") -def recover_password(email: str, session: SessionDep) -> Message: - """ - Password Recovery - """ - user = crud.get_user_by_email(session=session, email=email) + # Extract the user ID (sub) from the payload + user_id = payload.sub - if not user: - raise HTTPException( - status_code=404, - detail="The user with this email does not exist in the system.", - ) - password_reset_token = generate_password_reset_token(email=email) - email_data = generate_reset_password_email( - email_to=user.email, email=email, token=password_reset_token - ) - send_email( - email_to=user.email, - subject=email_data.subject, - html_content=email_data.html_content, - ) - return Message(message="Password recovery email sent") - - -@router.post("/reset-password/") -def reset_password(session: SessionDep, body: NewPassword) -> Message: - """ - Reset password - """ - email = verify_password_reset_token(token=body.token) - if not email: - raise HTTPException(status_code=400, detail="Invalid token") - user = crud.get_user_by_email(session=session, email=email) - if not user: - raise HTTPException( - status_code=404, - detail="The user with this email does not exist in the system.", + # Fetch the user from the database using the user ID + user = ( + session.query(UserPublic) + .filter(UserPublic.id == user_id) + .first() + ) or ( + session.query(UserBusiness) + .filter(UserBusiness.id == user_id) + .first() ) - elif not user.is_active: - raise HTTPException(status_code=400, detail="Inactive user") - hashed_password = get_password_hash(password=body.new_password) - user.hashed_password = hashed_password - session.add(user) - session.commit() - return Message(message="Password updated successfully") - - -@router.post( - "/password-recovery-html-content/{email}", - dependencies=[Depends(get_current_active_superuser)], - response_class=HTMLResponse, -) -def recover_password_html_content(email: str, session: SessionDep) -> Any: - """ - HTML Content for Password Recovery - """ - user = crud.get_user_by_email(session=session, email=email) - - if not user: - raise HTTPException( - status_code=404, - detail="The user with this username does not exist in the system.", + + if not user: + raise HTTPException(status_code=404, detail="User not found") + + # Verify if the refresh token in the database matches the provided token + if user.refresh_token != request.refresh_token: + raise HTTPException(status_code=401, detail="Invalid refresh token") + + # Check if the token has expired + current_time = datetime.now(timezone.utc) + if payload.exp and datetime.fromtimestamp(payload.exp, timezone.utc) < current_time: + raise HTTPException(status_code=401, detail="Refresh token expired") + + # If valid, generate new tokens + new_access_token = create_access_token(subject=user_id, expires_delta=timedelta(minutes=30)) + new_refresh_token = create_refresh_token(subject=user_id) + + # Update the user's refresh token in the database + user.refresh_token = new_refresh_token.token + session.add(user) + session.commit() + + # Return the new tokens and issue time + return UserAuthResponse( + access_token=new_access_token, + refresh_token=new_refresh_token, + issued_at=current_time ) - password_reset_token = generate_password_reset_token(email=email) - email_data = generate_reset_password_email( - email_to=user.email, email=email, token=password_reset_token - ) - - return HTMLResponse( - content=email_data.html_content, headers={"subject:": email_data.subject} - ) + + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.get("/logout") +async def logout( + session: SessionDep, + current_user: Annotated[UserPublic | UserBusiness, Depends(get_current_user)]): + try: + # Invalidate the refresh token by setting it to None or an empty string + current_user.refresh_token = None + session.add(current_user) + session.commit() + + return {"message": "Logout successful, refresh token invalidated"} + + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) diff --git a/backend/app/api/routes/menu.py b/backend/app/api/routes/menu.py new file mode 100644 index 0000000000..f845b7f7a1 --- /dev/null +++ b/backend/app/api/routes/menu.py @@ -0,0 +1,440 @@ +import uuid + +from fastapi import APIRouter, Depends, HTTPException +from sqlmodel import Session, select + +from app.api.deps import get_current_user, get_db +from app.models.menu import Menu, MenuCategory, MenuItem, MenuSubCategory +from app.models.user import UserBusiness, UserPublic +from app.models.venue import Venue +from app.schema.menu import ( + MenuCategoryCreate, + MenuCategoryRead, + MenuCategoryUpdate, + MenuCreate, + MenuItemCreate, + MenuItemRead, + MenuItemUpdate, + MenuRead, + MenuSubCategoryCreate, + MenuSubCategoryRead, + MenuSubCategoryUpdate, + MenuUpdate, +) +from app.util import ( + check_user_permission, + create_record, + delete_record, + get_record_by_id, + update_record, +) + +router = APIRouter() + + +# Get all menus of a specific venue +@router.get("/all/{venue_id}", response_model=list[MenuRead]) +async def read_menus( + venue_id: uuid.UUID, + db: Session = Depends(get_db), + current_user: UserPublic = Depends(get_current_user), # noqa: ARG001 +): + """ + Retrieve all menus for a specific venue. + """ + # Query the Menu table for menus associated with the specified venue + statement = select(Menu).where(Menu.venue_id == venue_id) + menus = db.execute(statement).scalars().all() # Execute the query + + if not menus: + raise HTTPException(status_code=404, detail="No menus found for this venue.") + + assert isinstance(menus[0], Menu), "Fetched Menu object is not of type Menu" + + return [menu.to_read_schema() for menu in menus] + + +@router.get("/menu/{menu_id}", response_model=MenuRead) +async def read_menu( + menu_id: uuid.UUID, + db: Session = Depends(get_db), + current_user: UserPublic = Depends(get_current_user), # noqa: ARG001 +): + """ + Retrieve a specific menu by its ID. + """ + menu = get_record_by_id(db, Menu, menu_id) + + assert isinstance(menu, Menu), "The returned object is not of type Menu" + return menu.to_read_schema() + + +@router.post("/", response_model=MenuRead) +async def create_menu( + menu_create: MenuCreate, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_current_user), +): + """ + Create a new menu for a specific venue. + """ + # Check if the venue exists + venue = get_record_by_id(db, Venue, menu_create.venue_id) + if not venue: + raise HTTPException(status_code=404, detail="Venue not found.") + + # Check if the user has permission to create a menu for this venue + check_user_permission(db, current_user, menu_create.venue_id) + + try: + # Create the Menu object + menu_instance = Menu.from_create_schema(menu_create) + + # Use the create_record helper to save the menu to the database + created_menu = create_record(db, menu_instance) + + assert isinstance(created_menu, Menu), "The returned object is not of type Menu" + return created_menu.to_read_schema() + + except Exception as e: + db.rollback() # Rollback in case of error + raise HTTPException(status_code=400, detail=f"Error creating menu: {str(e)}") + + +@router.patch("/{menu_id}", response_model=MenuRead) +async def update_menu( + menu_id: uuid.UUID, + menu_update: MenuUpdate, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_current_user), +): + """ + Update an existing menu's details using a partial update (PATCH). + + :param menu_id: The ID of the menu to update. + :param menu_update: The fields to update, provided as a Pydantic model. + :param db: Active database session. + :return: The updated Menu as a response. + """ + # Retrieve the menu by its ID + menu_instance = get_record_by_id(db, Menu, menu_id) + # Check if the user has permission to update a menu for this venue + + if not menu_instance: + raise HTTPException(status_code=404, detail="Menu not found.") + + check_user_permission(db, current_user, menu_instance.venue_id) + + # Update the menu using the validated fields from MenuUpdate + updated_menu = update_record(db, menu_instance, menu_update) + assert isinstance(updated_menu, Menu), "The returned object is not of type Menu" + return updated_menu.to_read_schema() + + +@router.delete("/{menu_id}", response_model=dict) +async def delete_menu( + menu_id: uuid.UUID, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_current_user), +): + """ + Delete a menu by its ID. + + :param menu_id: The ID of the menu to delete. + :param db: Active database session. + :return: Confirmation message on successful deletion. + """ + menu_instance = get_record_by_id(db, Menu, menu_id) + if not menu_instance: + raise HTTPException(status_code=404, detail="Menu not found.") + + # Check if the user has permission to delete a menu for this venue + check_user_permission(db, current_user, menu_instance.venue_id) + + delete_record(db, menu_instance) + + return {"detail": "Menu deleted successfully."} + + +############################################################################################################## + + +@router.post("/category", response_model=MenuCategoryRead) +async def create_menu_category( + category_create: MenuCategoryCreate, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_current_user), +): + """ + Create a new menu category associated with a menu. + + :param category_create: The details for the new menu category, provided as a Pydantic model. + :param db: Active database session. + :return: The created MenuCategory as a response. + """ + # Check if the menu exists + menu = get_record_by_id(db, Menu, category_create.menu_id) + + if not menu: + raise HTTPException(status_code=404, detail="Menu not found.") + # Check if the user has permission to update a menu for this venue + check_user_permission(db, current_user, menu.venue_id) + + # Create a new MenuCategory instance from the provided data + category_instance = MenuCategory.from_create_schema(category_create) + + # Persist the new category in the database + created_category = create_record(db, category_instance) + + assert isinstance( + created_category, MenuCategory + ), "The returned object is not of type MenuCategory" + return created_category.to_read_schema() + + +@router.patch("/category/{category_id}", response_model=MenuCategoryRead) +async def update_menu_category( + category_id: uuid.UUID, + category_update: MenuCategoryUpdate, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_current_user), +): + """ + Update an existing menu category's details using a partial update (PATCH). + + :param category_id: The ID of the menu category to update. + :param category_update: The fields to update, provided as a Pydantic model. + :param db: Active database session. + :return: The updated MenuCategory as a response. + """ + # Retrieve the category by its ID + category_instance = get_record_by_id(db, MenuCategory, category_id) + + if not category_instance: + raise HTTPException(status_code=404, detail="Menu category not found.") + + check_user_permission(db, current_user, category_instance.menu.venue_id) + + # Update the category using the validated fields from MenuCategoryUpdate + updated_category = update_record(db, category_instance, category_update) + + assert isinstance( + updated_category, MenuCategory + ), "The returned object is not of type MenuCategory" + return updated_category.to_read_schema() + + +@router.delete("/category/{category_id}", response_model=dict) +async def delete_category( + category_id: uuid.UUID, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_current_user), +): + """ + Delete a menu category by its ID. + + :param category_id: The ID of the category to delete. + :param db: Active database session. + :return: Confirmation message on successful deletion. + """ + category = get_record_by_id(db, MenuCategory, category_id) + + if not category: + raise HTTPException(status_code=404, detail="Category not found.") + + check_user_permission(db, current_user, category.menu.venue_id) + + delete_record(db, category) + + return {"detail": "Category deleted successfully."} + + +############################################################################################################## + + +@router.post("/subcategory/", response_model=MenuSubCategoryRead) +async def create_menu_subcategory( + subcategory_create: MenuSubCategoryCreate, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_current_user), +): + """ + Create a new menu subcategory associated with a category. + + :param subcategory_create: The details for the new menu subcategory, provided as a Pydantic model. + :param db: Active database session. + :return: The created MenuSubCategory as a response. + """ + # Check if the category exists + category = get_record_by_id(db, MenuCategory, subcategory_create.category_id) + + if not category: + raise HTTPException(status_code=404, detail="Category not found.") + + check_user_permission(db, current_user, category.menu.venue_id) + + # Create a new MenuSubCategory instance from the provided data + subcategory_instance = MenuSubCategory.from_create_schema(subcategory_create) + + # Persist the new subcategory in the database + created_subcategory = create_record(db, subcategory_instance) + + assert isinstance( + created_subcategory, MenuSubCategory + ), "The returned object is not of type MenuSubCategory" + return created_subcategory.to_read_schema() + + +@router.patch("/subcategory/{subcategory_id}", response_model=MenuSubCategoryRead) +async def update_menu_subcategory( + subcategory_id: uuid.UUID, + subcategory_update: MenuSubCategoryUpdate, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_current_user), +): + """ + Update an existing menu subcategory. + + :param subcategory_id: The unique identifier for the subcategory to update. + :param subcategory_update: The details to update, provided as a Pydantic model. + :param db: Active database session. + :return: The updated MenuSubCategory as a response. + """ + # Check if the subcategory exists + subcategory = get_record_by_id(db, MenuSubCategory, subcategory_id) + + if not subcategory: + raise HTTPException(status_code=404, detail="Subcategory not found.") + + check_user_permission(db, current_user, subcategory.category.menu.venue_id) + + # Update the subcategory with provided data + update_data = subcategory_update.dict( + exclude_unset=True + ) # Exclude unset fields for partial update + updated_subcategory = update_record(db, subcategory, update_data) + + assert isinstance( + updated_subcategory, MenuSubCategory + ), "The returned object is not of type MenuSubCategory" + return updated_subcategory.to_read_schema() + + +@router.delete("/subcategory/{subcategory_id}", response_model=dict) +async def delete_subcategory( + subcategory_id: uuid.UUID, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_current_user), +): + """ + Delete a menu subcategory by its ID. + + :param subcategory_id: The ID of the subcategory to delete. + :param db: Active database session. + :return: Confirmation message on successful deletion. + """ + subcategory = get_record_by_id(db, MenuSubCategory, subcategory_id) + + if not subcategory: + raise HTTPException(status_code=404, detail="Subcategory not found.") + + check_user_permission(db, current_user, subcategory.category.menu.venue_id) + + delete_record(db, subcategory) + + return {"detail": "Subcategory deleted successfully."} + + +############################################################################################################## + + +@router.post("/item/", response_model=MenuItemRead) +async def create_menu_item( + item_create: MenuItemCreate, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_current_user), +): + """ + Create a new menu item associated with a subcategory. + + :param item_create: The details for the new menu item, provided as a Pydantic model. + :param db: Active database session. + :return: The created MenuItem as a response. + """ + # Check if the subcategory exists + subcategory = get_record_by_id(db, MenuSubCategory, item_create.subcategory_id) + + if not subcategory: + raise HTTPException(status_code=404, detail="Subcategory not found.") + + check_user_permission(db, current_user, subcategory.category.menu.venue_id) + + # Create a new MenuItem instance from the provided data + item_instance = MenuItem.from_create_schema(item_create) + + # Persist the new item in the database + created_item = create_record(db, item_instance) + + assert isinstance( + created_item, MenuItem + ), "The returned object is not of type MenuItem" + return created_item.to_read_schema() + + +@router.patch("/item/{item_id}", response_model=MenuItemRead) +async def update_menu_item( + item_id: uuid.UUID, + item_update: MenuItemUpdate, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_current_user), +): + """ + Update an existing menu item. + + :param item_id: The unique identifier for the item to update. + :param item_update: The details to update, provided as a Pydantic model. + :param db: Active database session. + :return: The updated MenuItem as a response. + """ + # Check if the item exists + item = get_record_by_id(db, MenuItem, item_id) + + if not item: + raise HTTPException(status_code=404, detail="Menu item not found.") + + check_user_permission(db, current_user, item.subcategory.category.menu.venue_id) + + # Update the item with provided data + updated_item = update_record(db, item, item_update) + assert isinstance( + updated_item, MenuItem + ), "The returned object is not of type MenuItem" + return updated_item.to_read_schema() + + +@router.delete("/item/{item_id}", response_model=dict) +async def delete_menu_item( + item_id: uuid.UUID, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_current_user), +): + """ + Delete a menu item by its ID. + + :param item_id: The ID of the item to delete. + :param db: Active database session. + :return: Confirmation message on successful deletion. + """ + item = get_record_by_id(db, MenuItem, item_id) + + if not item: + raise HTTPException(status_code=404, detail="Menu item not found.") + + check_user_permission(db, current_user, item.subcategory.category.menu.venue_id) + + delete_record(db, item) + + return {"detail": "Menu item deleted successfully."} + + +######################################################################################################### diff --git a/backend/app/api/routes/qrcode.py b/backend/app/api/routes/qrcode.py new file mode 100644 index 0000000000..1eb66b76c2 --- /dev/null +++ b/backend/app/api/routes/qrcode.py @@ -0,0 +1,179 @@ +import uuid +from pathlib import Path + +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import HTMLResponse +from jinja2 import Template +from sqlmodel import Session, select + +from app.api.deps import SessionDep, get_current_user, get_db +from app.models.qrcode import QRCode # Ensure you have this import for your model +from app.models.user import UserBusiness +from app.schema.qrcode import ( + QRCodeCreate, + QRCodeRead, + QRCodeUpdate, +) + +# Ensure you have these imports for your schemas +from app.util import ( + check_user_permission, + delete_record, + get_record_by_id, + update_record, +) + +router = APIRouter() + + +# Return all QR codes for a specific venue +@router.get("/venue/{venue_id}", response_model=list[QRCodeRead]) +async def read_qrcode_by_venue( + venue_id: uuid.UUID, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_current_user), +): + """ + Retrieve all QR codes associated with a specific venue. + """ + qrcodes = ( + db.execute(select(QRCode).where(QRCode.venue_id == venue_id)).scalars().all() + ) + # if qrcode is empty, raise an error + if not qrcodes: + raise HTTPException(status_code=404, detail="No QR codes found for this venue.") + + check_user_permission(db, current_user, venue_id) + return [qr_code.to_read_schema() for qr_code in qrcodes] + + +# Get a specific QR code +@router.get("/{qr_code_id}", response_model=QRCodeRead) +async def read_qr_code( + qr_code_id: uuid.UUID, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_current_user), +): + """ + Retrieve a specific QR code by ID. + """ + qr_code_instance = get_record_by_id(db, QRCode, qr_code_id) + check_user_permission(db, current_user, qr_code_instance.venue_id) + assert isinstance( + qr_code_instance, QRCode + ), "The returned object is not of type QRCode" + return qr_code_instance.to_read_schema() + + +# Create a new QR code +@router.post("/", response_model=QRCodeRead) +async def create_qr_code( + qr_code: QRCodeCreate, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_current_user), +): + """ + Create a new QR code. + """ + check_user_permission(db, current_user, qr_code.venue_id) + try: + qr_code_instance = QRCode.from_create_schema(qr_code) + db.add(qr_code_instance) # Persist the new QR code + db.commit() # Commit the session + return ( + qr_code_instance.to_read_schema() + ) # Call the instance method to convert to QRCodeRead + except Exception as e: + db.rollback() # Rollback the session in case of any error + raise HTTPException(status_code=500, detail=str(e)) from e + + +# Patch a QR code for partial updates +@router.patch("/{qr_code_id}", response_model=QRCodeRead) +async def update_qr_code( + qr_code_id: uuid.UUID, + updated_qr_code: QRCodeUpdate, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_current_user), +): + qr_code_instance = get_record_by_id(db, QRCode, qr_code_id) + + if not qr_code_instance: + raise HTTPException(status_code=404, detail="QR code not found.") + + # Check user permission before updating the QR code for this venue + check_user_permission(db, current_user, qr_code_instance.venue_id) + + updated_qr_code = update_record(db, qr_code_instance, updated_qr_code) + assert isinstance( + updated_qr_code, QRCode + ), "The returned object is not of type QRCode" + return updated_qr_code.to_read_schema() + + +# Delete a QR code +@router.delete("/qrcode/{qr_code_id}", response_model=None) +async def delete_qr_code( + qr_code_id: uuid.UUID, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_current_user), +): + """ + Delete a QR code by ID. + """ + qr_code_instance = get_record_by_id(db, QRCode, qr_code_id) + + if not qr_code_instance: + raise HTTPException(status_code=404, detail="QR code not found.") + + check_user_permission(db, current_user, qr_code_instance.venue_id) + return delete_record(db, qr_code_instance) + + +@router.get("/scan/{qr_id}", response_class=HTMLResponse) +async def scan_qr_code(qr_id: uuid.UUID, session: SessionDep): + """ + Scan a QR code to determine its associated venue and redirect appropriately. + """ + # Retrieve the QR code from the database + qr_code_result = session.execute( + select(QRCode).where(QRCode.id == qr_id) + ).one_or_none() + + if not qr_code_result: + raise HTTPException(status_code=404, detail="QR code not found.") + + qr_code = qr_code_result[0] + # Determine the venue type and ID from the QR code + venue_type, venue_id = None, None + + if qr_code.foodcourt_id: + venue_type = "foodcourt" + venue_id = qr_code.foodcourt_id + elif qr_code.qsr_id: + venue_type = "qsr" + venue_id = qr_code.qsr_id + elif qr_code.nightclub_id: + venue_type = "nightclub" + venue_id = qr_code.nightclub_id + elif qr_code.restaurant_id: + venue_type = "restaurant" + venue_id = qr_code.restaurant_id + + if not venue_type or not venue_id: + raise HTTPException(status_code=400, detail="No associated venue found.") + + # Load the landing page HTML template + template_path = Path(__file__).parent.parent.parent / "static" / "landing_page.html" + print("template_path:", template_path) + try: + template_str = template_path.read_text() + except FileNotFoundError as exc: + raise HTTPException( + status_code=500, detail="Landing page template not found." + ) from exc + + # Use string.Template to replace placeholders in the HTML + template = Template(template_str) + html_content = template.render(venueId=venue_id, venueType=venue_type) + return HTMLResponse(content=html_content) diff --git a/backend/app/api/routes/scrapper.py b/backend/app/api/routes/scrapper.py new file mode 100644 index 0000000000..96c5c62554 --- /dev/null +++ b/backend/app/api/routes/scrapper.py @@ -0,0 +1,309 @@ +from fastapi import APIRouter, HTTPException, Depends +from pydantic import BaseModel, HttpUrl +import json +import requests +from bs4 import BeautifulSoup +import logging +from datetime import datetime, time +import httpx +from app.api.deps import SessionDep, get_business_user +from app.models.user import UserBusiness +from app.schema.venue import RestaurantCreate, VenueCreate +from app.schema.menu import ( + MenuCreate, + MenuCategoryCreate, + MenuSubCategoryCreate, + MenuItemCreate +) +from app.api.routes.utils import transform_restaurant_data + +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +router = APIRouter() + +class ZomatoUrl(BaseModel): + url: HttpUrl + +def fetch_zomato_data(url: str) -> str: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + } + + try: + logger.info(f"Fetching data from URL: {url}") + response = requests.get(url, headers=headers) + response.raise_for_status() + return response.text + except Exception as e: + logger.error(f"Error fetching data: {str(e)}") + raise + +def parse_zomato_page(html_content: str) -> dict: + try: + soup = BeautifulSoup(html_content, 'html.parser') + scripts = soup.find_all('script') + + for script in scripts: + if script.string and 'window.__PRELOADED_STATE__' in script.string: + json_str = script.string.split('window.__PRELOADED_STATE__ = JSON.parse(')[1] + json_str = json_str.split(');')[0].strip() + json_str = json_str.strip('"').replace('\\"', '"').replace('\\\\', '\\') + return json.loads(json_str) + + raise ValueError("Could not find PRELOADED_STATE in page") + except Exception as e: + logger.error(f"Error parsing page: {str(e)}") + raise + +def extract_menu_data(json_data: dict) -> dict: + try: + with open('data.json', 'w') as f: + json.dump(json_data, f, indent=4) + restaurant_data = json_data.get('pages', {}).get('current', {}) + restaurant_details = json_data.get('pages', {}).get('restaurant', {}) + + restaurant_id = next(iter(restaurant_details)) if restaurant_details else None + if not restaurant_id: + raise ValueError("No restaurant ID found") + + res_info = restaurant_details[restaurant_id].get('sections', {}) + basic_info = res_info.get('SECTION_BASIC_INFO', {}) + menu_widget = res_info.get('SECTION_MENU_WIDGET', {}) + + + # Validate required name field + if not basic_info.get('name'): + raise ValueError("Restaurant name is required") + + restaurant_info = { + 'cuisine_type': basic_info.get('cuisine_string', ''), + 'venue': { + 'name': basic_info.get('name', ''), + 'address': basic_info.get('address', ''), + 'locality': basic_info.get('locality_verbose', ''), + 'city': basic_info.get('city', ''), + 'latitude': basic_info.get('latitude', '0'), + 'longitude': basic_info.get('longitude', '0'), + 'zipcode': basic_info.get('zipcode', ''), + 'rating': basic_info.get('rating', {}).get('aggregate_rating', '0'), + 'timing': basic_info.get('timing', {}).get('timing', ''), + 'avg_cost_for_two': basic_info.get('average_cost_for_two', 0) + } + } + + + menu_categories = [] + print("Catorgies", menu_widget.get('menu', {}).get('categories', [])) + for category in menu_widget.get('menu', {}).get('categories', []): + print("category", category) + category_data = { + 'name': category.get('name', ''), + 'description': category.get('description', ''), + 'subcategories': [] + } + + # Group items by subcategory + subcategories = {} + for item in category.get('items', []): + subcategory_name = item.get('category', 'Other') + + if subcategory_name not in subcategories: + subcategories[subcategory_name] = { + 'name': subcategory_name, + 'description': '', + 'items': [] + } + + menu_item = { + 'name': item.get('name', ''), + 'description': item.get('desc', ''), + 'is_veg': item.get('isVeg', True), + 'image_url': item.get('itemImage', ''), + 'variants': [] + } + + # Handle variants + if item.get('variantsV2'): + for variant in item['variantsV2']: + menu_item['variants'].append({ + 'name': variant.get('variantName', ''), + 'price': float(variant.get('price', 0)) / 100, + 'is_default': variant.get('isDefault', False) + }) + else: + menu_item['variants'].append({ + 'name': 'Regular', + 'price': float(item.get('defaultPrice', 0)) / 100, + 'is_default': True + }) + + subcategories[subcategory_name]['items'].append(menu_item) + + # Add non-empty subcategories to category + category_data['subcategories'] = [ + subcat for subcat in subcategories.values() + if subcat['items'] + ] + + if category_data['subcategories']: + menu_categories.append(category_data) + + return { + 'status': 'success', + 'restaurant_info': restaurant_info, + 'menu': menu_categories + } + + except Exception as e: + logger.error(f"Error extracting menu data: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Failed to extract menu data: {str(e)}" + ) + + +async def create_restaurant(client: httpx.AsyncClient, restaurant_data: RestaurantCreate): + response = await client.post("/venue/restaurants/", json=restaurant_data.dict()) + response.raise_for_status() + return response.json()["id"] + +async def create_menu(client: httpx.AsyncClient, menu_data: MenuCreate): + response = await client.post("/menu/", json=menu_data.dict()) + response.raise_for_status() + return response.json()["menu_id"] + +async def create_category(client: httpx.AsyncClient, category_data: MenuCategoryCreate): + response = await client.post("/menu/category/", json=category_data.dict()) + response.raise_for_status() + return response.json()["category_id"] + +async def create_subcategory(client: httpx.AsyncClient, subcategory_data: MenuSubCategoryCreate): + response = await client.post("/menu/subcategory/", json=subcategory_data.dict()) + response.raise_for_status() + return response.json()["subcategory_id"] + +async def create_menu_item(client: httpx.AsyncClient, item_data: MenuItemCreate): + response = await client.post("/menu/item/", json=item_data.dict()) + response.raise_for_status() + +@router.post("/menu") +async def scrape_and_create_menu( + request: ZomatoUrl, + session: SessionDep, + current_user: UserBusiness = Depends(get_business_user) +): + try: + # 1. Scrape and log data + html_content = fetch_zomato_data(str(request.url)) + json_data = parse_zomato_page(html_content) + scraped_data = extract_menu_data(json_data) + + logger.info("Scraped Restaurant Info:") + logger.info(f"Name: {scraped_data['restaurant_info']['name']}") + logger.info(f"Cuisines: {scraped_data['restaurant_info']['cuisines']}") + + async with httpx.AsyncClient() as client: + # 2. Create Restaurant + venue_data = VenueCreate( + name=scraped_data['restaurant_info']['name'], + description="Restaurant imported from Zomato", + avg_expense_for_two=scraped_data['restaurant_info']['avg_cost_for_two'], + zomato_link=str(request.url) + ) + + restaurant_data = RestaurantCreate( + venue=venue_data, + cuisine_type=scraped_data['restaurant_info']['cuisines'] + ) + + venue_id = await create_restaurant(client, restaurant_data) + logger.info(f"Created restaurant with venue_id: {venue_id}") + + # 3. Create Menu + menu_data = MenuCreate( + name=f"{scraped_data['restaurant_info']['name']} Menu", + description="Imported from Zomato", + venue_id=venue_id, + menu_type="Food" + ) + menu_id = await create_menu(client, menu_data) + logger.info(f"Created menu with menu_id: {menu_id}") + + # 4. Create Categories, Subcategories, and Items sequentially + for category in scraped_data['menu']: + category_data = MenuCategoryCreate( + name=category['category'], + menu_id=menu_id + ) + category_id = await create_category(client, category_data) + logger.info(f"Created category: {category['category']}") + + subcategory_data = MenuSubCategoryCreate( + name=f"{category['category']} Items", + category_id=category_id, + is_alcoholic=False + ) + subcategory_id = await create_subcategory(client, subcategory_data) + logger.info(f"Created subcategory for {category['category']}") + + for item in category['items']: + item_data = MenuItemCreate( + name=item['name'], + description=item['description'], + price=float(item['price']), + subcategory_id=subcategory_id, + is_veg=item['is_veg'], + image_url=item.get('image_url') + ) + await create_menu_item(client, item_data) + logger.info(f"Created item: {item['name']}") + + return { + "message": "Menu successfully created", + "venue_id": str(venue_id), + "menu_id": str(menu_id), + "restaurant_name": scraped_data['restaurant_info']['name'], + "scraped_data": scraped_data + } + + except Exception as e: + logger.error(f"Menu creation failed: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to create menu: {str(e)}") + + +@router.get("/menu/scrape") +async def get_scraped_menu(url: str): + try: + # Validate URL + zomato_url = ZomatoUrl(url=url) + + # Scrape data using existing functions + html_content = fetch_zomato_data(str(zomato_url.url)) + json_data = parse_zomato_page(html_content) + # print(json_data) + # scraped_data = extract_menu_data(json_data) + print("==== cleaning the data ====") + scraped_data = transform_restaurant_data(json_data) + print(scraped_data) + + return { + "status": "success", + "restaurant_info": scraped_data, + "menu": scraped_data['menu'] + } + + except ValueError as ve: + logger.error(f"Invalid URL format: {str(ve)}") + raise HTTPException( + status_code=400, + detail=f"Invalid Zomato URL: {str(ve)}" + ) + except Exception as e: + logger.error(f"Scraping failed: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Failed to scrape menu data: {str(e)}" + ) diff --git a/backend/app/api/routes/users.py b/backend/app/api/routes/users.py index c636b094ee..6a757720c0 100644 --- a/backend/app/api/routes/users.py +++ b/backend/app/api/routes/users.py @@ -1,228 +1,214 @@ import uuid -from typing import Any -from fastapi import APIRouter, Depends, HTTPException -from sqlmodel import col, delete, func, select +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlmodel import Session -from app import crud from app.api.deps import ( - CurrentUser, SessionDep, - get_current_active_superuser, + get_business_user, + get_current_user, + get_db, + get_public_user, + get_super_user, ) -from app.core.config import settings -from app.core.security import get_password_hash, verify_password -from app.models import ( - Item, - Message, - UpdatePassword, - User, - UserCreate, - UserPublic, - UserRegister, - UsersPublic, - UserUpdate, - UserUpdateMe, +from app.models import UserBusiness, UserPublic +from app.schema.user import ( + UserBusinessCreate, + UserBusinessRead, + UserBusinessUpdate, + UserPublicRead, + UserPublicUpdate, +) +from app.util import ( + create_record, + delete_record, + get_all_records, + get_record_by_id, + update_record, ) -from app.utils import generate_new_account_email, send_email router = APIRouter() -@router.get( - "/", - dependencies=[Depends(get_current_active_superuser)], - response_model=UsersPublic, -) -def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any: +@router.get("/me", response_model=UserPublicRead | UserBusinessRead) +async def read_user_me( + current_user: UserPublic | UserBusiness = Depends(get_current_user), +): + print("current_user", current_user) """ - Retrieve users. + Retrieve profile information of the currently authenticated user. """ + x = current_user.to_read_schema() + print("hui", x) + return x - count_statement = select(func.count()).select_from(User) - count = session.exec(count_statement).one() - statement = select(User).offset(skip).limit(limit) - users = session.exec(statement).all() +@router.get("/all-user-business/", response_model=list[UserBusinessRead]) +async def all_read_user_business( + db: Session = Depends(get_db), + skip: int = Query(0, alias="page", ge=0), + limit: int = Query(10, le=100), + current_user: UserBusiness = Depends(get_super_user), # noqa: ARG001 +): + """ + Retrieve a paginated list of user businesses. + - **skip**: The page number (starting from 0) + - **limit**: The number of items per page + """ + all_users = get_all_records(db, UserBusiness, skip=skip, limit=limit) - return UsersPublic(data=users, count=count) + if all_users is None: + raise HTTPException(status_code=404, detail="No user businesses found.") + # Convert each record to its read schema + assert all( + hasattr(user, "to_read_schema") for user in all_users + ), "Each user must implement 'to_read_schema'" -@router.post( - "/", dependencies=[Depends(get_current_active_superuser)], response_model=UserPublic -) -def create_user(*, session: SessionDep, user_in: UserCreate) -> Any: - """ - Create new user. - """ - user = crud.get_user_by_email(session=session, email=user_in.email) - if user: - raise HTTPException( - status_code=400, - detail="The user with this email already exists in the system.", - ) + return [user.to_read_schema() for user in all_users] - user = crud.create_user(session=session, user_create=user_in) - if settings.emails_enabled and user_in.email: - email_data = generate_new_account_email( - email_to=user_in.email, username=user_in.email, password=user_in.password - ) - send_email( - email_to=user_in.email, - subject=email_data.subject, - html_content=email_data.html_content, - ) - return user - - -@router.patch("/me", response_model=UserPublic) -def update_user_me( - *, session: SessionDep, user_in: UserUpdateMe, current_user: CurrentUser -) -> Any: - """ - Update own user. - """ - - if user_in.email: - existing_user = crud.get_user_by_email(session=session, email=user_in.email) - if existing_user and existing_user.id != current_user.id: - raise HTTPException( - status_code=409, detail="User with this email already exists" - ) - user_data = user_in.model_dump(exclude_unset=True) - current_user.sqlmodel_update(user_data) - session.add(current_user) - session.commit() - session.refresh(current_user) - return current_user - - -@router.patch("/me/password", response_model=Message) -def update_password_me( - *, session: SessionDep, body: UpdatePassword, current_user: CurrentUser -) -> Any: - """ - Update own password. - """ - if not verify_password(body.current_password, current_user.hashed_password): - raise HTTPException(status_code=400, detail="Incorrect password") - if body.current_password == body.new_password: - raise HTTPException( - status_code=400, detail="New password cannot be the same as the current one" - ) - hashed_password = get_password_hash(body.new_password) - current_user.hashed_password = hashed_password - session.add(current_user) - session.commit() - return Message(message="Password updated successfully") - - -@router.get("/me", response_model=UserPublic) -def read_user_me(current_user: CurrentUser) -> Any: - """ - Get current user. - """ - return current_user - - -@router.delete("/me", response_model=Message) -def delete_user_me(session: SessionDep, current_user: CurrentUser) -> Any: - """ - Delete own user. - """ - if current_user.is_superuser: - raise HTTPException( - status_code=403, detail="Super users are not allowed to delete themselves" - ) - statement = delete(Item).where(col(Item.owner_id) == current_user.id) - session.exec(statement) # type: ignore - session.delete(current_user) - session.commit() - return Message(message="User deleted successfully") - - -@router.post("/signup", response_model=UserPublic) -def register_user(session: SessionDep, user_in: UserRegister) -> Any: - """ - Create new user without the need to be logged in. - """ - user = crud.get_user_by_email(session=session, email=user_in.email) - if user: - raise HTTPException( - status_code=400, - detail="The user with this email already exists in the system", - ) - user_create = UserCreate.model_validate(user_in) - user = crud.create_user(session=session, user_create=user_create) - return user - - -@router.get("/{user_id}", response_model=UserPublic) -def read_user_by_id( - user_id: uuid.UUID, session: SessionDep, current_user: CurrentUser -) -> Any: - """ - Get a specific user by id. - """ - user = session.get(User, user_id) - if user == current_user: - return user - if not current_user.is_superuser: - raise HTTPException( - status_code=403, - detail="The user doesn't have enough privileges", - ) - return user +@router.get("/user-businesses/{user_business_id}", response_model=UserBusinessRead) +async def read_user_business( + user_business_id: uuid.UUID, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_super_user), # noqa: ARG001 +): + """ + Retrieve a single user business by ID. + - **user_business_id**: The ID of the user business to retrieve + """ + user_instance = get_record_by_id(db, UserBusiness, user_business_id) -@router.patch( - "/{user_id}", - dependencies=[Depends(get_current_active_superuser)], - response_model=UserPublic, -) -def update_user( - *, + return user_instance.to_read_schema() + + +@router.post("/user-businesses/", response_model=UserBusinessRead) +async def create_user_business( + user_business: UserBusinessCreate, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_super_user), +): + """ + Create a new user business. + - **user_business**: The user business data to create + """ + + try: + user_instance = UserBusiness.from_create_schema(user_business) + user_instance.id = current_user.id + return create_record(db, user_instance) + except Exception as e: + db.rollback() + raise HTTPException(status_code=400, detail=str(e)) + + +@router.patch("/user-businesses/{user_business_id}", response_model=UserBusinessRead) +async def update_user_business( + user_business: UserBusinessUpdate, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_current_user), +): + """ + Update an existing user business. + - **user_business_id**: The ID of the user business to update + - **user_business**: The updated user business data + """ + try: + user_instance = UserBusiness.from_create_schema(user_business) + return update_record(db, current_user, user_instance) + except Exception as e: + db.rollback() + raise HTTPException(status_code=400, detail=str(e)) + + +@router.delete("/user-businesses/{user_business_id}", response_model=dict) +async def delete_user_business( + user_business_id: uuid.UUID, + session: SessionDep, + current_user: UserBusiness = Depends(get_super_user), # noqa: ARG001 +): + """ + Delete a user business by ID. + - **user_business_id**: The ID of the user business to delete + """ + user_instance = get_record_by_id(session, UserBusiness, user_business_id) + + delete_record(session, user_instance) + return {"message": f"UserBusiness with ID {user_business_id} has been deleted."} + + +@router.get("/all-user-public/", response_model=list[UserPublicRead]) +async def all_read_user_public( + db: Session = Depends(get_db), + skip: int = Query(0, alias="page", ge=0), + limit: int = Query(10, le=100), + current_user: UserBusiness = Depends(get_business_user), # noqa: ARG001 +): + """ + Retrieve a paginated list of user public. + - **skip**: The page number (starting from 0) + - **limit**: The number of items per page + """ + all_users = get_all_records(db, UserPublic, skip=skip, limit=limit) + + if all_users is None: + raise HTTPException(status_code=404, detail="No user found.") + + assert all( + hasattr(user, "to_read_schema") for user in all_users + ), "Each user must implement 'to_read_schema'" + + # Convert each record to its read schema + return [user.to_read_schema() for user in all_users] + + +@router.get("/user-public/{user_public_id}", response_model=UserPublicRead) +async def read_user_public( + user_public_id: uuid.UUID, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_business_user), # noqa: ARG001 +): + """ + Retrieve a single user public by ID. + - **user_public_id**: The ID of the user public to retrieve + """ + user_instance = get_record_by_id(db, UserBusiness, user_public_id) + + return user_instance.to_read_schema() + + +@router.patch("/user-public/", response_model=UserPublicRead) +async def update_user_public_me( + user_public: UserPublicUpdate, + db: Session = Depends(get_db), + current_user: UserPublic = Depends(get_public_user), +): + """ + Update an existing user public. + - **user_public_id**: The ID of the user public to update + - **user_public**: The updated user public data + """ + print("user_public", user_public) + try: + user_instance = UserPublic.from_create_schema(user_public) + return update_record(db, current_user, user_instance) + except Exception as e: + db.rollback() + raise HTTPException(status_code=400, detail=str(e)) + + +@router.delete("/user-public/{user_public_id}", response_model=dict) +async def delete_user_public( + user_public_id: uuid.UUID, session: SessionDep, - user_id: uuid.UUID, - user_in: UserUpdate, -) -> Any: - """ - Update a user. - """ - - db_user = session.get(User, user_id) - if not db_user: - raise HTTPException( - status_code=404, - detail="The user with this id does not exist in the system", - ) - if user_in.email: - existing_user = crud.get_user_by_email(session=session, email=user_in.email) - if existing_user and existing_user.id != user_id: - raise HTTPException( - status_code=409, detail="User with this email already exists" - ) - - db_user = crud.update_user(session=session, db_user=db_user, user_in=user_in) - return db_user - - -@router.delete("/{user_id}", dependencies=[Depends(get_current_active_superuser)]) -def delete_user( - session: SessionDep, current_user: CurrentUser, user_id: uuid.UUID -) -> Message: - """ - Delete a user. - """ - user = session.get(User, user_id) - if not user: - raise HTTPException(status_code=404, detail="User not found") - if user == current_user: - raise HTTPException( - status_code=403, detail="Super users are not allowed to delete themselves" - ) - statement = delete(Item).where(col(Item.owner_id) == user_id) - session.exec(statement) # type: ignore - session.delete(user) - session.commit() - return Message(message="User deleted successfully") + current_user: UserBusiness = Depends(get_super_user), # noqa: ARG001 +): + """ + Delete a user public by ID. + - **user_public_id**: The ID of the user public to delete + """ + user_instance = get_record_by_id(session, UserBusiness, user_public_id) + + delete_record(session, user_instance) + return {"message": f"UserPublic with ID {user_public_id} has been deleted."} diff --git a/backend/app/api/routes/utils.py b/backend/app/api/routes/utils.py index 82f6d2b821..6b169536db 100644 --- a/backend/app/api/routes/utils.py +++ b/backend/app/api/routes/utils.py @@ -1,26 +1,184 @@ -from fastapi import APIRouter, Depends -from pydantic.networks import EmailStr +def transform_restaurant_data(zomato_data): + """ + Transform Zomato restaurant data into the specified MenuData format. + + Args: + zomato_data (dict): The Zomato restaurant data in its original format + + Returns: + dict: The transformed data in MenuData format + """ + try: + # Extract the menu data from the Zomato data structure + restaurant = zomato_data.get('pages', {}).get('restaurant', {}) + + resId = list(restaurant)[0] + + menu_list = zomato_data.get('pages', {}).get('restaurant', {}).get(resId, {}).get('order', {}).get('menuList', {}) + + if not menu_list: + menu_list = zomato_data.get('order', {}).get('menuList', {}) + + menus = menu_list.get('menus', []) + + # Initialize the result structure + result = { + "menu": [] + } + + # Process each menu category + + categories = [] + for menu_entry in menus: + -from app.api.deps import get_current_active_superuser -from app.models import Message -from app.utils import generate_test_email, send_email -router = APIRouter() + menu = menu_entry.get('menu', {}) + subcategories = menu.get('categories', []) + + for sub_cat_entry in subcategories: + subcategory = sub_cat_entry.get('category', {}) + subcategory_name = subcategory.get('name', '') + items = subcategory.get('items', []) + + # Check if we need to create a subcategory + has_subcategories = False + subcategories_map = {} + + # First, scan items to see if they have any group information that could be used as subcategories + for item_entry in items: + item = item_entry.get('item', {}) + # Look for potential subcategory indicators in the item data + # This could be customized based on the actual data structure + groups = item.get('groups', []) + if groups: + has_subcategories = True + for group_entry in groups: + group = group_entry.get('group', {}) + subcategory_name = group.get('name', 'Other') + if subcategory_name not in subcategories_map: + subcategories_map[subcategory_name] = [] + + # If no subcategories found, create a default one + if not has_subcategories: + subcategories_map['General'] = [] + + # Process each menu item and assign to appropriate subcategory + for item_entry in items: + item = item_entry.get('item', {}) + + # Transform the item to match the MenuItem interface + transformed_item = { + "id": item.get('id', ''), + "name": item.get('name', ''), + "description": item.get('desc', ''), + "price": item.get('price', 0), + "is_veg": any(tag == "pure_veg" for tag in item.get('tag_slugs', [])), + "spice_level": determine_spice_level(item), + "image_url": extract_image_url(item) + } + + # Determine which subcategory this item belongs to + assigned = False + groups = item.get('groups', []) + if groups: + for group_entry in groups: + group = group_entry.get('group', {}) + subcategory_name = group.get('name', 'Other') + if subcategory_name in subcategories_map: + subcategories_map[subcategory_name].append(transformed_item) + assigned = True + break + + # If no subcategory was assigned, put in the first available subcategory + if not assigned: + default_subcategory = next(iter(subcategories_map.keys())) + subcategories_map[default_subcategory].append(transformed_item) + + # Create the category entry with its subcategories + category_entry = { + "category": menu.get('name', ''), + "subcategories": [ + { + "subcategory": subcategory_name, + "items": items_list + } + for subcategory_name, items_list in subcategories_map.items() + ] + } + + result["menu"].append(category_entry) + + return result + + except Exception as e: + print(f"Error transforming data: {e}") + # Return a minimal valid structure in case of error + return {"menu": []} -@router.post( - "/test-email/", - dependencies=[Depends(get_current_active_superuser)], - status_code=201, -) -def test_email(email_to: EmailStr) -> Message: +def determine_spice_level(item): """ - Test emails. + Determine the spice level of an item based on available tags or description. + + Args: + item (dict): The menu item data + + Returns: + str: One of 'None', 'Mild', 'Medium', 'Spicy', or 'Hot' """ - email_data = generate_test_email(email_to=email_to) - send_email( - email_to=email_to, - subject=email_data.subject, - html_content=email_data.html_content, - ) - return Message(message="Test email sent") + # Check tags for spice indicators + tags = item.get('tag_slugs', []) + desc = item.get('desc', '').lower() + + # This is a simple heuristic and could be enhanced based on actual data patterns + if any(spicy_tag in tags for spicy_tag in ['extra_spicy', 'very_hot']): + return 'Hot' + elif any(spicy_tag in tags for spicy_tag in ['spicy', 'hot']): + return 'Spicy' + elif any(medium_tag in tags for medium_tag in ['medium_spicy', 'medium_hot']): + return 'Medium' + elif any(mild_tag in tags for mild_tag in ['mild_spicy', 'slightly_spicy']): + return 'Mild' + + # Check description for spice indicators + if any(hot_term in desc for hot_term in ['very spicy', 'extra hot', 'extremely spicy']): + return 'Hot' + elif any(spicy_term in desc for spicy_term in ['spicy', 'hot']): + return 'Spicy' + elif any(medium_term in desc for medium_term in ['medium spicy', 'moderately spiced']): + return 'Medium' + elif any(mild_term in desc for mild_term in ['mild spice', 'slightly spicy']): + return 'Mild' + + # Default value + return 'None' + +def extract_image_url(item): + """ + Extract the image URL from an item. + + Args: + item (dict): The menu item data + + Returns: + str: The image URL, or None if not available + """ + # First try to get the specific item image URL + image_url = item.get('item_image_url') + if image_url: + return image_url + + # Then check the media array for images + media = item.get('media', []) + for media_item in media: + if media_item.get('mediaType') == 'image': + image_data = media_item.get('image', {}) + if image_data and image_data.get('url'): + return image_data.get('url') + + # If no image found, return None + return None + +# Example usage: +# transformed_data = transform_restaurant_data(zomato_data) diff --git a/backend/app/api/routes/venues.py b/backend/app/api/routes/venues.py new file mode 100644 index 0000000000..ea2601a5bf --- /dev/null +++ b/backend/app/api/routes/venues.py @@ -0,0 +1,218 @@ +from fastapi import APIRouter, Depends, FastAPI, HTTPException +from sqlmodel import Session + +from app.api.deps import ( + get_business_user, + get_db, +) +from app.models.user import UserBusiness, UserVenueAssociation +from app.models.venue import QSR, Foodcourt, Nightclub, Restaurant, Venue +from app.schema.venue import ( + FoodcourtCreate, + FoodcourtRead, + NightclubCreate, + NightclubRead, + QSRCreate, + QSRRead, + RestaurantCreate, + RestaurantRead, + VenueListResponse, +) + +# Assuming you have a dependency to get the database session +from app.util import ( + create_record, + get_all_records, +) + +app = FastAPI() +router = APIRouter() + + +# POST endpoint for Foodcourt +@router.post("/foodcourts/", response_model=FoodcourtRead) +def create_foodcourt( + foodcourt: FoodcourtCreate, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_business_user), +): + try: + # Check if the venue exists + venue_instance = Venue.from_create_schema(foodcourt.venue) + create_record(db, venue_instance) # Persist the new venue + # Use the newly created venue instance + foodcourt_instance = Foodcourt.from_create_schema(venue_instance.id, foodcourt) + # Create the new Foodcourt record in the database + create_record(db, foodcourt_instance) + association = UserVenueAssociation( + user_id=current_user.id, venue_id=venue_instance.id + ) + create_record(db, association) + + return foodcourt_instance.to_read_schema() + + except Exception as e: + # Rollback the session in case of any error + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) # Respond with a 500 error + + +# GET endpoint for Foodcourt +@router.get("/foodcourts/", response_model=list[FoodcourtRead]) +def read_foodcourts(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)): + return get_all_records(db, Foodcourt, skip=skip, limit=limit) + + +# POST endpoint for QSR +@router.post("/qsrs/", response_model=QSRRead) +def create_qsr( + qsr: QSRCreate, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_business_user), +): + try: + # Check if the venue exists + venue_instance = Venue.from_create_schema(qsr.venue) + create_record(db, venue_instance) # Persist the new venue + # Use the newly created venue instance + qsr_instance = QSR.from_create_schema(venue_instance.id, qsr) + # Create the new Foodcourt record in the database + create_record(db, qsr_instance) + association = UserVenueAssociation( + user_id=current_user.id, venue_id=venue_instance.id + ) + create_record(db, association) + return qsr_instance.to_read_schema() + + except Exception as e: + # Rollback the session in case of any error + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) # Respond with a 500 error + + +# GET endpoint for QSR +@router.get("/qsrs/", response_model=list[QSRRead]) +def read_qsrs(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)): + return get_all_records(db, QSR, skip=skip, limit=limit) + + +# POST endpoint for Restaurant +@router.post("/restaurants/", response_model=RestaurantRead) +def create_restaurant( + restaurant: RestaurantCreate, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_business_user), +): + try: + # Check if the venue exists + venue_instance = Venue.from_create_schema(restaurant.venue) + create_record(db, venue_instance) # Persist the new venue + # Use the newly created venue instance + restaurant_instance = Restaurant.from_create_schema( + venue_instance.id, restaurant + ) + # Create the new Foodcourt record in the database + create_record(db, restaurant_instance) + association = UserVenueAssociation( + user_id=current_user.id, venue_id=venue_instance.id + ) + create_record(db, association) + return restaurant_instance.to_read_schema() + + except Exception as e: + # Rollback the session in case of any error + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) # Respond with a 500 error + + +# GET endpoint for Restaurant +@router.get("/restaurants/", response_model=list[RestaurantRead]) +def read_restaurants(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)): + return get_all_records(db, Restaurant, skip=skip, limit=limit) + + +# POST endpoint for Nightclub +@router.post("/nightclubs/", response_model=NightclubRead) +def create_nightclub( + nightclub: NightclubCreate, + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_business_user), +): + try: + # Check if the venue exists + venue_instance = Venue.from_create_schema(nightclub.venue) + create_record(db, venue_instance) # Persist the new venue + # Use the newly created venue instance + nightclub_instance = Nightclub.from_create_schema(venue_instance.id, nightclub) + # Create the new Foodcourt record in the database + create_record(db, nightclub_instance) + association = UserVenueAssociation( + user_id=current_user.id, venue_id=venue_instance.id + ) + create_record(db, association) + return nightclub_instance.to_read_schema() + + except Exception as e: + # Rollback the session in case of any error + db.rollback() + raise HTTPException(status_code=500, detail=str(e)) # Respond with a 500 error + + +# GET endpoint for Nightclub +@router.get("/nightclubs/", response_model=list[NightclubRead]) +def read_nightclubs(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)): + return get_all_records(db, Nightclub, skip=skip, limit=limit) + + +@router.get("/my-venues/", response_model=VenueListResponse) +async def get_my_venues( + db: Session = Depends(get_db), + current_user: UserBusiness = Depends(get_business_user), +): + """ + Retrieve the venues managed by the current user, organized by venue type. + + This method leverages SQLAlchemy's efficient querying capabilities to minimize database load + while ensuring data integrity through the association table, UserVenueAssociation. + """ + + # Fetch all venues managed by the current user + managed_venues = ( + db.query(Venue) + .join(UserVenueAssociation) + .filter(UserVenueAssociation.user_id == current_user.id) + .all() + ) + + # Create a set for fast membership testing + managed_venue_ids = {venue.id for venue in managed_venues} + + # Initialize lists for categorized venues + nightclubs, qsrs, foodcourts, restaurants = [], [], [], [] + + # Efficiently query and convert Nightclubs + for nightclub in ( + db.query(Nightclub).filter(Nightclub.venue_id.in_(managed_venue_ids)).all() + ): + nightclubs.append(nightclub.to_read_schema()) + + # Efficiently query and convert QSRs + for qsr in db.query(QSR).filter(QSR.venue_id.in_(managed_venue_ids)).all(): + qsrs.append(qsr.to_read_schema()) + + # Efficiently query and convert Foodcourts + for foodcourt in ( + db.query(Foodcourt).filter(Foodcourt.venue_id.in_(managed_venue_ids)).all() + ): + foodcourts.append(foodcourt.to_read_schema()) + + # Efficiently query and convert Restaurants + for restaurant in ( + db.query(Restaurant).filter(Restaurant.venue_id.in_(managed_venue_ids)).all() + ): + restaurants.append(restaurant.to_read_schema()) + + # Construct and return the response + return VenueListResponse( + nightclubs=nightclubs, qsrs=qsrs, foodcourts=foodcourts, restaurants=restaurants + ) diff --git a/backend/app/backend_pre_start.py b/backend/app/backend_pre_start.py index c2f8e29ae1..9b9a3c3b81 100644 --- a/backend/app/backend_pre_start.py +++ b/backend/app/backend_pre_start.py @@ -28,7 +28,6 @@ def init(db_engine: Engine) -> None: logger.error(e) raise e - def main() -> None: logger.info("Initializing service") init(engine) diff --git a/backend/app/constants.py b/backend/app/constants.py new file mode 100644 index 0000000000..5ce00e9968 --- /dev/null +++ b/backend/app/constants.py @@ -0,0 +1,8 @@ +# constants.py +from enum import Enum + + +class Gender(Enum): + MALE = "male" + FEMALE = "female" + OTHERS = ("others",) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 1e3a440c1c..c5392033df 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,16 +1,14 @@ import secrets import warnings -from typing import Annotated, Any, Literal +from typing import Annotated, Any, ClassVar, Literal from pydantic import ( AnyUrl, BeforeValidator, HttpUrl, - PostgresDsn, computed_field, model_validator, ) -from pydantic_core import MultiHostUrl from pydantic_settings import BaseSettings, SettingsConfigDict from typing_extensions import Self @@ -30,10 +28,15 @@ class Settings(BaseSettings): API_V1_STR: str = "/api/v1" SECRET_KEY: str = secrets.token_urlsafe(32) # 60 minutes * 24 hours * 8 days = 8 days - ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 DOMAIN: str = "localhost" ENVIRONMENT: Literal["local", "staging", "production"] = "local" + # Add CLIENT_ID and CLIENT_SECRET + CLIENT_ID: str + CLIENT_SECRET: str + REFRESH_TOKEN_EXPIRE_DAYS: ClassVar[int] = 365 # Use ClassVar if it's a constant + ALGORITHM: str = "HS256" @computed_field # type: ignore[prop-decorator] @property def server_host(self) -> str: @@ -56,15 +59,8 @@ def server_host(self) -> str: @computed_field # type: ignore[prop-decorator] @property - def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: - return MultiHostUrl.build( - scheme="postgresql+psycopg", - username=self.POSTGRES_USER, - password=self.POSTGRES_PASSWORD, - host=self.POSTGRES_SERVER, - port=self.POSTGRES_PORT, - path=self.POSTGRES_DB, - ) + def SQLALCHEMY_DATABASE_URI(self) -> str: + return f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_SERVER}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}" SMTP_TLS: bool = True SMTP_SSL: bool = False @@ -113,8 +109,10 @@ def _enforce_non_default_secrets(self) -> Self: self._check_default_secret( "FIRST_SUPERUSER_PASSWORD", self.FIRST_SUPERUSER_PASSWORD ) + print(f"FIRST_SUPERUSER_PASSWORD: {self.FIRST_SUPERUSER_PASSWORD}") return self - -settings = Settings() # type: ignore +print("About to initialize Settings...") +settings = Settings() +print("Settings initialized.") diff --git a/backend/app/core/db.py b/backend/app/core/db.py index d260a856d2..6b8554581e 100644 --- a/backend/app/core/db.py +++ b/backend/app/core/db.py @@ -1,34 +1,49 @@ -from sqlmodel import Session, create_engine, select +from sqlmodel import Session, create_engine -from app import crud from app.core.config import settings -from app.models import User, UserCreate +print("SQLALCHEMY_DATABASE_URI : ", str(settings.SQLALCHEMY_DATABASE_URI)) engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI)) - - -# make sure all SQLModel models are imported (app.models) before initializing DB -# otherwise, SQLModel might fail to initialize relationships properly -# for more details: https://github.com/fastapi/full-stack-fastapi-template/issues/28 - - -def init_db(session: Session) -> None: - # Tables should be created with Alembic migrations - # But if you don't want to use migrations, create - # the tables un-commenting the next lines - # from sqlmodel import SQLModel - - # from app.core.engine import engine - # This works because the models are already imported and registered from app.models - # SQLModel.metadata.create_all(engine) - - user = session.exec( - select(User).where(User.email == settings.FIRST_SUPERUSER) - ).first() - if not user: - user_in = UserCreate( - email=settings.FIRST_SUPERUSER, - password=settings.FIRST_SUPERUSER_PASSWORD, - is_superuser=True, - ) - user = crud.create_user(session=session, user_create=user_in) +print("engine created : ") + +connection = engine.connect() +print("Connection successful!") +connection.close() + +def init_db() -> None: + """ + Initialize the database with the necessary default data. + Assumes that database schema is up-to-date due to Alembic migrations. + """ + # Example: Create the superuser if it does not exist + with Session(engine) as session: + print("here") + # Check for existing superuser + # superuser = session.exec( + # select(UserBusiness).where(UserBusiness.email == settings.FIRST_SUPERUSER) + # ).first() + # print("heree") + + # if not superuser: + # print("here1") + # user_in = UserBusiness( + # email=settings.FIRST_SUPERUSER, + # phone_number=None, + # is_active=True, + # is_superuser=True, + # full_name="Superuser", + # registration_date=datetime.utcnow() + # ) + # print("here2") + # # Create superuser in the database + # session.add(user_in) + # print("here3") + # session.commit() + # print("here4") + # session.refresh(user_in) + + # Other initial setup tasks can go here + # Example: Create default Nightclub, Foodcourt, etc. + # ... + + print("Database initialization complete.") diff --git a/backend/app/core/security.py b/backend/app/core/security.py index 7aff7cfb32..cdae82e4cd 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -1,27 +1,68 @@ +import uuid # For generating unique token ID from datetime import datetime, timedelta, timezone -from typing import Any import jwt -from passlib.context import CryptContext +from fastapi import HTTPException, status from app.core.config import settings +from app.models.auth import AccessToken, RefreshToken, TokenModel -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +ALGORITHM = "HS256" +def get_jwt_payload(token: str) -> TokenModel: + try: + # Decode the token using the secret key and algorithm + payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + print('payload ', payload) + # Create and return a TokenModel instance with the decoded payload + return TokenModel(sub=payload.get("sub"), exp=payload.get("exp")) -ALGORITHM = "HS256" + except jwt.ExpiredSignatureError: + # Handle expired token + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token has expired" + ) + except jwt.InvalidTokenError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid token" + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Token validation failed: {str(e)}" + ) +def create_access_token(subject: str, expires_delta: timedelta | None = None) -> AccessToken: + if expires_delta: + expire = datetime.now(timezone.utc) + expires_delta + else: + expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) -def create_access_token(subject: str | Any, expires_delta: timedelta) -> str: - expire = datetime.now(timezone.utc) + expires_delta - to_encode = {"exp": expire, "sub": str(subject)} + jti = str(uuid.uuid4()) + to_encode = {"exp": expire, "sub": str(subject), "jti": jti} encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM) - return encoded_jwt + # Create an instance of AccessToken with the encoded JWT and expiration time + access_token = AccessToken( + token=encoded_jwt, + expires_at=expire, + token_type="Bearer" + ) -def verify_password(plain_password: str, hashed_password: str) -> bool: - return pwd_context.verify(plain_password, hashed_password) + return access_token + +def create_refresh_token(subject: str) -> RefreshToken: + expire = datetime.now(timezone.utc) + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS) + jti = str(uuid.uuid4()) + to_encode = {"sub": str(subject), "exp": expire, "jti": jti} + encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM) + # Create an instance of RefreshToken with the encoded JWT and expiration time + refresh_token = RefreshToken( + token=encoded_jwt, + expires_at=expire + ) -def get_password_hash(password: str) -> str: - return pwd_context.hash(password) + return refresh_token diff --git a/backend/app/crud.py b/backend/app/crud.py deleted file mode 100644 index 905bf48724..0000000000 --- a/backend/app/crud.py +++ /dev/null @@ -1,54 +0,0 @@ -import uuid -from typing import Any - -from sqlmodel import Session, select - -from app.core.security import get_password_hash, verify_password -from app.models import Item, ItemCreate, User, UserCreate, UserUpdate - - -def create_user(*, session: Session, user_create: UserCreate) -> User: - db_obj = User.model_validate( - user_create, update={"hashed_password": get_password_hash(user_create.password)} - ) - session.add(db_obj) - session.commit() - session.refresh(db_obj) - return db_obj - - -def update_user(*, session: Session, db_user: User, user_in: UserUpdate) -> Any: - user_data = user_in.model_dump(exclude_unset=True) - extra_data = {} - if "password" in user_data: - password = user_data["password"] - hashed_password = get_password_hash(password) - extra_data["hashed_password"] = hashed_password - db_user.sqlmodel_update(user_data, update=extra_data) - session.add(db_user) - session.commit() - session.refresh(db_user) - return db_user - - -def get_user_by_email(*, session: Session, email: str) -> User | None: - statement = select(User).where(User.email == email) - session_user = session.exec(statement).first() - return session_user - - -def authenticate(*, session: Session, email: str, password: str) -> User | None: - db_user = get_user_by_email(session=session, email=email) - if not db_user: - return None - if not verify_password(password, db_user.hashed_password): - return None - return db_user - - -def create_item(*, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) -> Item: - db_item = Item.model_validate(item_in, update={"owner_id": owner_id}) - session.add(db_item) - session.commit() - session.refresh(db_item) - return db_item diff --git a/backend/app/email-templates/build/new_account.html b/backend/app/email-templates/build/new_account.html deleted file mode 100644 index 344505033b..0000000000 --- a/backend/app/email-templates/build/new_account.html +++ /dev/null @@ -1,25 +0,0 @@ -
{{ project_name }} - New Account
Welcome to your new account!
Here are your account details:
Username: {{ username }}
Password: {{ password }}
Go to Dashboard

\ No newline at end of file diff --git a/backend/app/email-templates/build/reset_password.html b/backend/app/email-templates/build/reset_password.html deleted file mode 100644 index 4148a5b773..0000000000 --- a/backend/app/email-templates/build/reset_password.html +++ /dev/null @@ -1,25 +0,0 @@ -
{{ project_name }} - Password Recovery
Hello {{ username }}
We've received a request to reset your password. You can do it by clicking the button below:
Reset password
Or copy and paste the following link into your browser:
This password will expire in {{ valid_hours }} hours.

If you didn't request a password recovery you can disregard this email.
\ No newline at end of file diff --git a/backend/app/email-templates/build/test_email.html b/backend/app/email-templates/build/test_email.html deleted file mode 100644 index 04d0d85092..0000000000 --- a/backend/app/email-templates/build/test_email.html +++ /dev/null @@ -1,25 +0,0 @@ -
{{ project_name }}
Test email for: {{ email }}

\ No newline at end of file diff --git a/backend/app/email-templates/src/new_account.mjml b/backend/app/email-templates/src/new_account.mjml deleted file mode 100644 index f41a3e3cf1..0000000000 --- a/backend/app/email-templates/src/new_account.mjml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - {{ project_name }} - New Account - Welcome to your new account! - Here are your account details: - Username: {{ username }} - Password: {{ password }} - Go to Dashboard - - - - - diff --git a/backend/app/email-templates/src/reset_password.mjml b/backend/app/email-templates/src/reset_password.mjml deleted file mode 100644 index 743f5d77f4..0000000000 --- a/backend/app/email-templates/src/reset_password.mjml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - {{ project_name }} - Password Recovery - Hello {{ username }} - We've received a request to reset your password. You can do it by clicking the button below: - Reset password - Or copy and paste the following link into your browser: - {{ link }} - This password will expire in {{ valid_hours }} hours. - - If you didn't request a password recovery you can disregard this email. - - - - diff --git a/backend/app/email-templates/src/test_email.mjml b/backend/app/email-templates/src/test_email.mjml deleted file mode 100644 index 45d58d6bac..0000000000 --- a/backend/app/email-templates/src/test_email.mjml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - {{ project_name }} - Test email for: {{ email }} - - - - - diff --git a/backend/app/initial_data.py b/backend/app/initial_data.py index d806c3d381..43552c5299 100644 --- a/backend/app/initial_data.py +++ b/backend/app/initial_data.py @@ -1,16 +1,13 @@ import logging -from sqlmodel import Session - -from app.core.db import engine, init_db +from app.core.db import init_db logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def init() -> None: - with Session(engine) as session: - init_db(session) + init_db() def main() -> None: diff --git a/backend/app/main.py b/backend/app/main.py index 4c252a1722..29a97ad94d 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -20,6 +20,8 @@ def custom_generate_unique_id(route: APIRoute) -> str: generate_unique_id_function=custom_generate_unique_id, ) +# app.mount("/static", StaticFiles(directory="app/static"), name="static") + # Set all CORS enabled origins if settings.BACKEND_CORS_ORIGINS: app.add_middleware( diff --git a/backend/app/models.py b/backend/app/models.py deleted file mode 100644 index 90ef5559e3..0000000000 --- a/backend/app/models.py +++ /dev/null @@ -1,114 +0,0 @@ -import uuid - -from pydantic import EmailStr -from sqlmodel import Field, Relationship, SQLModel - - -# Shared properties -class UserBase(SQLModel): - email: EmailStr = Field(unique=True, index=True, max_length=255) - is_active: bool = True - is_superuser: bool = False - full_name: str | None = Field(default=None, max_length=255) - - -# Properties to receive via API on creation -class UserCreate(UserBase): - password: str = Field(min_length=8, max_length=40) - - -class UserRegister(SQLModel): - email: EmailStr = Field(max_length=255) - password: str = Field(min_length=8, max_length=40) - full_name: str | None = Field(default=None, max_length=255) - - -# Properties to receive via API on update, all are optional -class UserUpdate(UserBase): - email: EmailStr | None = Field(default=None, max_length=255) # type: ignore - password: str | None = Field(default=None, min_length=8, max_length=40) - - -class UserUpdateMe(SQLModel): - full_name: str | None = Field(default=None, max_length=255) - email: EmailStr | None = Field(default=None, max_length=255) - - -class UpdatePassword(SQLModel): - current_password: str = Field(min_length=8, max_length=40) - new_password: str = Field(min_length=8, max_length=40) - - -# Database model, database table inferred from class name -class User(UserBase, table=True): - id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) - hashed_password: str - items: list["Item"] = Relationship(back_populates="owner", cascade_delete=True) - - -# Properties to return via API, id is always required -class UserPublic(UserBase): - id: uuid.UUID - - -class UsersPublic(SQLModel): - data: list[UserPublic] - count: int - - -# Shared properties -class ItemBase(SQLModel): - title: str = Field(min_length=1, max_length=255) - description: str | None = Field(default=None, max_length=255) - - -# Properties to receive on item creation -class ItemCreate(ItemBase): - pass - - -# Properties to receive on item update -class ItemUpdate(ItemBase): - title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore - - -# Database model, database table inferred from class name -class Item(ItemBase, table=True): - id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) - title: str = Field(max_length=255) - owner_id: uuid.UUID = Field( - foreign_key="user.id", nullable=False, ondelete="CASCADE" - ) - owner: User | None = Relationship(back_populates="items") - - -# Properties to return via API, id is always required -class ItemPublic(ItemBase): - id: uuid.UUID - owner_id: uuid.UUID - - -class ItemsPublic(SQLModel): - data: list[ItemPublic] - count: int - - -# Generic message -class Message(SQLModel): - message: str - - -# JSON payload containing access token -class Token(SQLModel): - access_token: str - token_type: str = "bearer" - - -# Contents of JWT token -class TokenPayload(SQLModel): - sub: str | None = None - - -class NewPassword(SQLModel): - token: str - new_password: str = Field(min_length=8, max_length=40) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000000..b20db96ec2 --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,57 @@ +from sqlmodel import SQLModel + +# Import all your models here +from .carousel_poster import CarouselPoster +from .club_visit import ClubVisit +from .event import Event +from .event_booking import EventBooking +from .event_offering import EventOffering +from .group import Group +from .group_wallet import GroupWallet +from .group_wallet_topup import GroupWalletTopup +from .menu import Menu +from .order import NightclubOrder, QSROrder, RestaurantOrder +from .order_item import OrderItem +from .payment import ( + PaymentEvent, + PaymentOrderNightclub, + PaymentOrderQSR, + PaymentOrderRestaurant, +) +from .pickup_location import PickupLocation +from .qrcode import QRCode +from .user import UserBusiness, UserPublic +from .venue import QSR, Foodcourt, Nightclub, Restaurant + +# Make all models accessible when importing app.models +__all__ = [ + "SQLModel", + "ClubVisit", + "Event", + "EventBooking", + "EventOffering", + "Group", + "GroupWallet", + "GroupWalletTopup", + "Menu", + "NightclubOrder", + "RestaurantOrder", + "QSROrder", + "OrderItem", + "PickupLocation", + "UserBusiness", + "UserPublic", + "Nightclub", + "QSR", + "Restaurant", + "Foodcourt", + "PaymentOrderNightclub", + "PaymentOrderRestaurant", + "PaymentOrderQSR", + "PaymentEvent", + "QRCode", + "CarouselPoster", +] + + +print("target_metadata in init:", SQLModel.metadata.tables) diff --git a/backend/app/models/auth.py b/backend/app/models/auth.py new file mode 100644 index 0000000000..39b3286a07 --- /dev/null +++ b/backend/app/models/auth.py @@ -0,0 +1,76 @@ +import uuid +from datetime import datetime, timezone + +from pydantic import EmailStr +from sqlmodel import Field, SQLModel + + +class TokenBlacklist(SQLModel, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + jti: str = Field(index=True, unique=True) # Store JWT ID (jti) + user_id: uuid.UUID = Field(nullable=False) + created_at: datetime = Field( + default_factory=lambda: datetime.now(timezone.utc), nullable=False + ) + + +class OtplessPhoneAuthDetails(SQLModel): + mode: str + phone_number: str + country_code: str + auth_state: str + + +class OtplessEmailAuthDetails(SQLModel): + email: EmailStr + mode: str + auth_state: str + + +class AuthenticationDetails(SQLModel): + phone: OtplessPhoneAuthDetails + email: OtplessEmailAuthDetails + + +class OtplessVerifyTokenResponse(SQLModel): + name: str + email: EmailStr + first_name: str + last_name: str + family_name: str + phone_number: str + national_phone_number: str + country_code: str + email_verified: bool + auth_time: str + authentication_details: AuthenticationDetails + + +class OtplessToken(SQLModel): + otpless_token: str + + +class TokenModel(SQLModel): + sub: str + exp: int | None = None + + +class RefreshTokenPayload(SQLModel): + refresh_token: str + + +class AccessToken(SQLModel): + token: str + expires_at: datetime + token_type: str = "Bearer" + + +class RefreshToken(SQLModel): + token: str + expires_at: datetime + + +class UserAuthResponse(SQLModel): + access_token: AccessToken + refresh_token: RefreshToken + issued_at: datetime diff --git a/backend/app/models/base_model.py b/backend/app/models/base_model.py new file mode 100644 index 0000000000..e61e2e0719 --- /dev/null +++ b/backend/app/models/base_model.py @@ -0,0 +1,34 @@ +""" +Base model class for all models in the application. +""" + +from abc import ABC, abstractmethod +from datetime import datetime, timezone + +from sqlmodel import Field, SQLModel + + +class BaseTimeModel(SQLModel, ABC): + """ + Base class for models that require timestamp fields. + + Attributes: + created_at (Optional[datetime]): The timestamp when the model instance was created. + updated_at (Optional[datetime]): The timestamp when the model instance was last updated. + """ + + created_at: datetime | None = Field( + default_factory=lambda: datetime.now(timezone.utc), nullable=True + ) + updated_at: datetime | None = Field( + default_factory=lambda: datetime.now(timezone.utc), nullable=True + ) + + @abstractmethod + def to_read_schema(self): + """Convert the model instance to its read schema representation.""" + + @classmethod + @abstractmethod + def from_create_schema(cls, schema): + """Create a model instance from the provided create schema.""" diff --git a/backend/app/models/carousel_poster.py b/backend/app/models/carousel_poster.py new file mode 100644 index 0000000000..fbc1627a7f --- /dev/null +++ b/backend/app/models/carousel_poster.py @@ -0,0 +1,50 @@ +import uuid +from datetime import datetime +from typing import TYPE_CHECKING, Optional + +from sqlmodel import Field, Relationship + +from app.models.base_model import BaseTimeModel +from app.schema.carousel_poster import CarouselPosterCreate, CarouselPosterRead + +if TYPE_CHECKING: + from app.models.event import Event + from app.models.venue import Venue + + +class CarouselPoster(BaseTimeModel, table=True): + __tablename__ = "carousel_poster" + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + h3_index: str = Field(nullable=True, index=True) + image_url: str = Field(nullable=False) + deep_link: str = Field(nullable=False) + expires_at: datetime = Field(nullable=False) + + # Foreign keys [Optional] + event_id: uuid.UUID | None = Field(default=None, foreign_key="event.id") + venue_id: uuid.UUID | None = Field(default=None, foreign_key="venue.id") + + # Relationships [Optional] + event: Optional["Event"] = Relationship(back_populates="carousel_posters") + venue: Optional["Venue"] = Relationship(back_populates="carousel_posters") + + @classmethod + def from_create_schema(cls, carousel_poster_create: CarouselPosterCreate): + return cls( + image_url=carousel_poster_create.image_url, + deep_link=carousel_poster_create.deep_link, + expires_at=carousel_poster_create.expires_at, + event_id=carousel_poster_create.event_id, + venue_id=carousel_poster_create.venue_id, + ) + + def to_read_schema(self) -> CarouselPosterRead: + return CarouselPosterRead( + courosel_id=self.id, + h3_index=self.h3_index, + image_url=self.image_url, + deep_link=self.deep_link, + expires_at=self.expires_at, + event_id=self.event_id, + venue_id=self.venue_id, + ) diff --git a/backend/app/models/club_visit.py b/backend/app/models/club_visit.py new file mode 100644 index 0000000000..7b9a6b5a86 --- /dev/null +++ b/backend/app/models/club_visit.py @@ -0,0 +1,26 @@ +import uuid +from datetime import datetime +from typing import TYPE_CHECKING, Optional + +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from app.models.group import Group + from app.models.user import UserPublic + from app.models.venue import Nightclub + + +class ClubVisit(SQLModel, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + user_id: uuid.UUID | None = Field(foreign_key="user_public.id", nullable=False) + group_id: uuid.UUID | None = Field(foreign_key="group.id", nullable=True) + nightclub_id: uuid.UUID | None = Field(foreign_key="nightclub.id", nullable=False) + entry_time: datetime = Field(nullable=False) + exit_time: datetime | None = Field(nullable=True) + cover_charge: float | None = Field(nullable=True) + total_bill: float | None = Field(nullable=True) + + # Relationships + user: Optional["UserPublic"] = Relationship(back_populates="club_visits") + group: Optional["Group"] = Relationship(back_populates="club_visits") + nightclub: Optional["Nightclub"] = Relationship(back_populates="club_visits") diff --git a/backend/app/models/event.py b/backend/app/models/event.py new file mode 100644 index 0000000000..15e9a50a68 --- /dev/null +++ b/backend/app/models/event.py @@ -0,0 +1,30 @@ +import uuid +from datetime import datetime +from typing import TYPE_CHECKING, Optional + +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from app.models.carousel_poster import CarouselPoster + from app.models.event_booking import EventBooking + from app.models.event_offering import EventOffering + from app.models.venue import Venue + + + + +class Event(SQLModel, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + venue_id: uuid.UUID = Field(foreign_key="venue.id", nullable=False) + title: str = Field(nullable=False) + start_time: datetime = Field(nullable=False) + end_time: datetime = Field(nullable=False) + image_url: str | None = Field(nullable=True) + age_restriction: int | None = Field(nullable=True) + dress_code: str | None = Field(nullable=True) + + # Relationships + venue: Optional["Venue"] = Relationship(back_populates="events") + offerings: list["EventOffering"] = Relationship(back_populates="event") + event_bookings: list["EventBooking"] = Relationship(back_populates="event") + carousel_posters: list["CarouselPoster"] | None = Relationship(back_populates="event") \ No newline at end of file diff --git a/backend/app/models/event_booking.py b/backend/app/models/event_booking.py new file mode 100644 index 0000000000..24e456d232 --- /dev/null +++ b/backend/app/models/event_booking.py @@ -0,0 +1,32 @@ +import uuid +from datetime import datetime +from typing import TYPE_CHECKING, Optional + +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from app.models.event import Event + from app.models.event_offering import EventOffering + from app.models.payment import PaymentEvent + from app.models.user import UserPublic + + +class EventBooking(SQLModel, table=True): + __tablename__ = "event_booking" + + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + user_id: uuid.UUID | None = Field(foreign_key="user_public.id", nullable=False) + event_id: uuid.UUID | None = Field(foreign_key="event.id", nullable=False) + booking_time: datetime = Field(nullable=False) + total_amount: float = Field(nullable=False) + status: str = Field(nullable=False) + + # Relationships + user: Optional["UserPublic"] = Relationship(back_populates="event_bookings") + event: Optional["Event"] = Relationship(back_populates="event_bookings") + payment: Optional["PaymentEvent"] = Relationship( + back_populates="event_booking", sa_relationship_kwargs={"uselist": False} + ) + event_offerings: list["EventOffering"] = Relationship( + back_populates="event_booking" + ) diff --git a/backend/app/models/event_offering.py b/backend/app/models/event_offering.py new file mode 100644 index 0000000000..f7466ba95f --- /dev/null +++ b/backend/app/models/event_offering.py @@ -0,0 +1,29 @@ +import uuid +from typing import TYPE_CHECKING, Optional + +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from app.models.event import Event + from app.models.event_booking import EventBooking + + +# Stag, couple etc +class EventOffering(SQLModel, table=True): + __tablename__ = "event_offering" + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + event_id: uuid.UUID = Field(foreign_key="event.id", nullable=False) + event_booking_id: uuid.UUID = Field(foreign_key="event_booking.id", nullable=False) + offering_type: str = Field(nullable=False) + description: str = Field(nullable=False) + price: float = Field(nullable=False) + total_guests_per_pass: int = Field(nullable=False) + cover_charge: float | None = Field(nullable=True) + additional_charges: float | None = Field(nullable=True) + availability: int = Field(nullable=False) + + # Relationships + event: Optional["Event"] = Relationship(back_populates="offerings") + event_booking: Optional["EventBooking"] = Relationship( + back_populates="event_offerings" + ) diff --git a/backend/app/models/group.py b/backend/app/models/group.py new file mode 100644 index 0000000000..8c19fbe24c --- /dev/null +++ b/backend/app/models/group.py @@ -0,0 +1,48 @@ +import uuid +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Optional + +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from app.models.club_visit import ClubVisit + from app.models.group_wallet import GroupWallet + from app.models.order import NightclubOrder + from app.models.user import UserPublic + from app.models.venue import Nightclub + + +class GroupMembers(SQLModel, table=True): + group_id: uuid.UUID = Field(foreign_key="group.id", primary_key=True) + user_id: uuid.UUID = Field(foreign_key="user_public.id", primary_key=True) + + +class GroupNightclubOrderLink(SQLModel, table=True): + __tablename__ = "group_nightclub_order_link" + + group_id: uuid.UUID = Field(foreign_key="group.id", primary_key=True) + nightclub_order_id: uuid.UUID = Field( + foreign_key="nightclub_order.id", primary_key=True + ) + + +class Group(SQLModel, table=True): + __tablename__ = "group" + + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + nightclub_id: uuid.UUID | None = Field(foreign_key="nightclub.id") + created_at: datetime = Field(default=datetime.now(timezone.utc)) + admin_user_id: uuid.UUID = Field(foreign_key="user_public.id", nullable=False) + table_number: str | None = Field(default=None) + + # Relationships + admin_user: Optional["UserPublic"] = Relationship(back_populates="managed_groups") + wallet: Optional["GroupWallet"] = Relationship(back_populates="group") + members: list["UserPublic"] = Relationship( + back_populates="groups", link_model=GroupMembers + ) + club_visits: list["ClubVisit"] = Relationship(back_populates="group") + nightclub_orders: list["NightclubOrder"] = Relationship( + back_populates="groups", link_model=GroupNightclubOrderLink + ) + nightclubs: list["Nightclub"] = Relationship(back_populates="group") diff --git a/backend/app/models/group_wallet.py b/backend/app/models/group_wallet.py new file mode 100644 index 0000000000..43565545fb --- /dev/null +++ b/backend/app/models/group_wallet.py @@ -0,0 +1,20 @@ +import uuid +from typing import TYPE_CHECKING, Optional + +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from app.models.group import Group + from app.models.group_wallet_topup import GroupWalletTopup + + +# (TODO) Added another model : Group wallet transactions +class GroupWallet(SQLModel, table=True): + __tablename__ = "group_wallet" + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + group_id: uuid.UUID = Field(foreign_key="group.id", nullable=False, unique=True) + balance: float = Field(default=0.0, nullable=False) + + # Relationships + group: Optional["Group"] = Relationship(back_populates="wallet") + topups: list["GroupWalletTopup"] = Relationship(back_populates="group_wallet") diff --git a/backend/app/models/group_wallet_topup.py b/backend/app/models/group_wallet_topup.py new file mode 100644 index 0000000000..b73e697ba0 --- /dev/null +++ b/backend/app/models/group_wallet_topup.py @@ -0,0 +1,18 @@ +import uuid +from datetime import datetime +from typing import TYPE_CHECKING, Optional + +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from app.models.group_wallet import GroupWallet + + +class GroupWalletTopup(SQLModel, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + group_wallet_id: uuid.UUID = Field(foreign_key="group_wallet.id", nullable=False) + amount: float = Field(nullable=False) + topup_time: datetime = Field(nullable=False) + + # Relationships + group_wallet: Optional["GroupWallet"] = Relationship(back_populates="topups") diff --git a/backend/app/models/menu.py b/backend/app/models/menu.py new file mode 100644 index 0000000000..f8d5ce70fd --- /dev/null +++ b/backend/app/models/menu.py @@ -0,0 +1,166 @@ +import uuid +from typing import TYPE_CHECKING, Optional + +from sqlmodel import Field, Relationship + +from app.models.base_model import BaseTimeModel + +if TYPE_CHECKING: + from app.models.venue import Venue + +from app.schema.menu import ( + MenuCategoryCreate, + MenuCategoryRead, + MenuCreate, + MenuItemCreate, + MenuItemRead, + MenuRead, + MenuSubCategoryCreate, + MenuSubCategoryRead, +) + + +class MenuItem(BaseTimeModel, table=True): + __tablename__ = "menu_item" + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + subcategory_id: uuid.UUID = Field(foreign_key="menu_subcategory.id", nullable=False) + name: str = Field(nullable=False) + price: float = Field(nullable=False) + description: str | None = Field(default=None) + image_url: str | None = Field(default=None) + is_veg: bool | None = Field(default=None) + ingredients: str | None = Field(default=None) + abv: float | None = Field(default=None) + ibu: int | None = Field(default=None) + + # Relationships + subcategory: Optional["MenuSubCategory"] = Relationship(back_populates="menu_items") + + @classmethod + def from_create_schema(cls, schema: MenuItemCreate) -> "MenuItem": + return cls( + subcategory_id=schema.subcategory_id, + name=schema.name, + price=schema.price, + description=schema.description, + image_url=schema.image_url, + is_veg=schema.is_veg, + ingredients=schema.ingredients, + abv=schema.abv, + ibu=schema.ibu, + ) + + @classmethod + def to_read_schema(self) -> MenuItemRead: + return MenuItemRead( + item_id=self.id, + subcategory_id=self.subcategory_id, + name=self.name, + price=self.price, + description=self.description, + image_url=self.image_url, + is_veg=self.is_veg, + ingredients=self.ingredients, + abv=self.abv, + ibu=self.ibu, + ) + + +######################################################################################################### + + +class MenuSubCategory(BaseTimeModel, table=True): + __tablename__ = "menu_subcategory" + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + category_id: uuid.UUID = Field(foreign_key="menu_category.id", nullable=False) + name: str = Field(nullable=False) + is_alcoholic: bool = Field(default=False) + + # Relationships + category: "MenuCategory" = Relationship(back_populates="sub_categories") + menu_items: list["MenuItem"] = Relationship(back_populates="subcategory") + + @classmethod + def from_create_schema(cls, schema: "MenuSubCategoryCreate") -> "MenuSubCategory": + return cls( + name=schema.name, + category_id=schema.category_id, + is_alcoholic=schema.is_alcoholic, + ) + + def to_read_schema(self) -> MenuSubCategoryRead: + return MenuSubCategoryRead( + subcategory_id=self.id, + category_id=self.category_id, + name=self.name, + is_alcoholic=self.is_alcoholic, + menu_items=[item.to_read_schema() for item in self.menu_items], + ) + + +######################################################################################################### + + +class MenuCategory(BaseTimeModel, table=True): + __tablename__ = "menu_category" + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + menu_id: uuid.UUID = Field(foreign_key="menu.id", nullable=False) + name: str = Field(nullable=False) + + # Relationships + menu: "Menu" = Relationship(back_populates="categories") + sub_categories: list["MenuSubCategory"] = Relationship(back_populates="category") + + @classmethod + def from_create_schema(cls, schema: MenuCategoryCreate) -> "MenuCategory": + return cls(name=schema.name, menu_id=schema.menu_id) + + def to_read_schema(self) -> MenuCategoryRead: + return MenuCategoryRead( + category_id=self.id, + menu_id=self.menu_id, + name=self.name, + sub_categories=[ + subcategory.to_read_schema() for subcategory in self.sub_categories + ], + ) + + +######################################################################################################### + + +class Menu(BaseTimeModel, table=True): + __tablename__ = "menu" + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + name: str = Field(nullable=False) + description: str | None = Field(default=None) + menu_type: str | None = Field(default=None) # Type of menu (e.g., "Food", "Drink") + venue_id: uuid.UUID = Field(foreign_key="venue.id", nullable=False) + + # Relationships + categories: list["MenuCategory"] = Relationship(back_populates="menu") + venue: "Venue" = Relationship(back_populates="menu") + + @classmethod + def from_create_schema(cls, schema: MenuCreate) -> "Menu": + return cls( + name=schema.name, + description=schema.description, + menu_type=schema.menu_type, + venue_id=schema.venue_id, + ) + + def to_read_schema(self) -> MenuRead: + return MenuRead( + menu_id=self.id, + name=self.name, + description=self.description, + menu_type=self.menu_type, + venue_id=self.venue_id, + categories=[ + MenuCategory.to_read_schema(category) for category in self.categories + ], + ) + + +######################################################################################################### diff --git a/backend/app/models/order.py b/backend/app/models/order.py new file mode 100644 index 0000000000..9a45eb4789 --- /dev/null +++ b/backend/app/models/order.py @@ -0,0 +1,91 @@ +import uuid +from datetime import datetime +from typing import TYPE_CHECKING, Optional + +from sqlmodel import Field, Relationship, SQLModel + +from app.models.group import GroupNightclubOrderLink + +if TYPE_CHECKING: + from app.models.group import Group + from app.models.order_item import OrderItem + from app.models.payment import ( + PaymentOrderNightclub, + PaymentOrderQSR, + PaymentOrderRestaurant, + ) + from app.models.pickup_location import PickupLocation + from app.models.user import UserPublic + from app.models.venue import QSR, Nightclub, Restaurant + + +class OrderBase(SQLModel): + user_id: uuid.UUID = Field(foreign_key="user_public.id") + pickup_location_id: uuid.UUID | None = Field( + default=None, foreign_key="pickup_location.id" + ) + note: str | None = Field(nullable=True) + order_time: datetime = Field(nullable=False) + total_amount: float = Field(nullable=False) + taxes_and_charges: float | None = Field(default=None) + cover_charge_used: float | None = Field(default=None) + status: str = Field(nullable=False) + service_type: str | None = Field(default=None) + + +class NightclubOrder(OrderBase, table=True): + __tablename__ = "nightclub_order" + + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + venue_id: uuid.UUID | None = Field(default=None, foreign_key="nightclub.id") + payment_id: uuid.UUID | None = Field( + default=None, foreign_key="payment_source_nightclub.id" + ) + pickup_location_id: uuid.UUID | None = Field( + default=None, foreign_key="pickup_location.id" + ) + # Relationships + user: Optional["UserPublic"] = Relationship(back_populates="nightclub_orders") + nightclub: Optional["Nightclub"] = Relationship(back_populates="orders") + pickup_location: Optional["PickupLocation"] = Relationship(back_populates="orders") + payment: Optional["PaymentOrderNightclub"] = Relationship( + back_populates="order", sa_relationship_kwargs={"uselist": False} + ) + groups: list["Group"] = Relationship( + back_populates="nightclub_orders", link_model=GroupNightclubOrderLink + ) # Many-to-many + order_items: list["OrderItem"] = Relationship(back_populates="nightclub_order") + + +class RestaurantOrder(OrderBase, table=True): + __tablename__ = "restaurant_order" + + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + venue_id: uuid.UUID | None = Field(default=None, foreign_key="restaurant.id") + payment_id: uuid.UUID | None = Field( + default=None, foreign_key="payment_source_restaurant.id" + ) + + # Relationships + user: Optional["UserPublic"] = Relationship(back_populates="restaurant_orders") + restaurant: Optional["Restaurant"] = Relationship(back_populates="orders") + payment: Optional["PaymentOrderRestaurant"] = Relationship( + back_populates="order", sa_relationship_kwargs={"uselist": False} + ) + order_items: list["OrderItem"] = Relationship(back_populates="restaurant_order") + + +class QSROrder(OrderBase, table=True): + __tablename__ = "qsr_order" + + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + venue_id: uuid.UUID = Field(default=None, foreign_key="qsr.id") + payment_id: uuid.UUID = Field(default=None, foreign_key="payment_source_qsr.id") + + # Relationships + user: Optional["UserPublic"] = Relationship(back_populates="qsr_orders") + qsr: Optional["QSR"] = Relationship(back_populates="orders") + payment: Optional["PaymentOrderQSR"] = Relationship( + back_populates="order", sa_relationship_kwargs={"uselist": False} + ) + order_items: list["OrderItem"] = Relationship(back_populates="qsr_order") diff --git a/backend/app/models/order_item.py b/backend/app/models/order_item.py new file mode 100644 index 0000000000..56d9998487 --- /dev/null +++ b/backend/app/models/order_item.py @@ -0,0 +1,29 @@ +import uuid +from typing import TYPE_CHECKING, Optional + +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from app.models.order import NightclubOrder, QSROrder, RestaurantOrder + + +class OrderItem(SQLModel, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + nightclub_order_id: uuid.UUID | None = Field( + default=None, foreign_key="nightclub_order.id" + ) + restaurant_order_id: uuid.UUID | None = Field( + default=None, foreign_key="restaurant_order.id" + ) + qsr_order_id: uuid.UUID | None = Field(default=None, foreign_key="qsr_order.id") + item_id: uuid.UUID = Field(foreign_key="menu_item.id", nullable=False) + quantity: int = Field(nullable=False) + + # Relationships + nightclub_order: Optional["NightclubOrder"] = Relationship( + back_populates="order_items" + ) + restaurant_order: Optional["RestaurantOrder"] = Relationship( + back_populates="order_items" + ) + qsr_order: Optional["QSROrder"] = Relationship(back_populates="order_items") diff --git a/backend/app/models/payment.py b/backend/app/models/payment.py new file mode 100644 index 0000000000..341e1b5bea --- /dev/null +++ b/backend/app/models/payment.py @@ -0,0 +1,64 @@ +import uuid +from datetime import datetime +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from app.models.event_booking import EventBooking + from app.models.order import NightclubOrder, QSROrder, RestaurantOrder + from app.models.user import UserPublic +from sqlmodel import Field, Relationship, SQLModel + + +class PaymentBase(SQLModel): + user_id: uuid.UUID = Field(foreign_key="user_public.id", nullable=False) + source_type: str = Field(nullable=False) # Changed to str + gateway_transaction_id: uuid.UUID | None = Field(default=None) + payment_time: datetime = Field(nullable=False) + amount: float = Field(nullable=False) + status: str = Field(nullable=False) # e.g., Paid, Pending, Failed + source_type: str = Field(nullable=False) + + +class PaymentOrderNightclub(PaymentBase, table=True): + __tablename__ = "payment_source_nightclub" + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + retry_count: int = Field(default=0) + last_attempt_time: datetime | None = Field(default=None) + order: "NightclubOrder" = Relationship( + back_populates="payment", sa_relationship_kwargs={"uselist": False} + ) + user: Optional["UserPublic"] = Relationship(back_populates="nightclub_payments") + + +class PaymentOrderQSR(PaymentBase, table=True): + __tablename__ = "payment_source_qsr" + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + retry_count: int = Field(default=0) + last_attempt_time: datetime | None = Field(default=None) + order: "QSROrder" = Relationship( + back_populates="payment", sa_relationship_kwargs={"uselist": False} + ) + user: Optional["UserPublic"] = Relationship(back_populates="qsr_payments") + + +class PaymentOrderRestaurant(PaymentBase, table=True): + __tablename__ = "payment_source_restaurant" + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + retry_count: int = Field(default=0) + last_attempt_time: datetime | None = Field(default=None) + order: "RestaurantOrder" = Relationship( + back_populates="payment", sa_relationship_kwargs={"uselist": False} + ) + user: Optional["UserPublic"] = Relationship(back_populates="restaurant_payments") + + +class PaymentEvent(PaymentBase, table=True): + __tablename__ = "payment_event" + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + event_booking_id: uuid.UUID | None = Field( + default=None, foreign_key="event_booking.id" + ) + event_booking: Optional["EventBooking"] = Relationship( + back_populates="payment", sa_relationship_kwargs={"uselist": False} + ) + user: Optional["UserPublic"] = Relationship(back_populates="event_payments") diff --git a/backend/app/models/pickup_location.py b/backend/app/models/pickup_location.py new file mode 100644 index 0000000000..31b4ac345a --- /dev/null +++ b/backend/app/models/pickup_location.py @@ -0,0 +1,22 @@ +import uuid +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from app.models.order import NightclubOrder + from app.models.venue import Venue +from sqlmodel import Field, Relationship, SQLModel + + +class PickupLocation(SQLModel, table=True): + __tablename__ = "pickup_location" + + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + venue_id: uuid.UUID = Field(foreign_key="venue.id", nullable=False) + name: str = Field(nullable=False) + description: str | None = Field(default=None) + + # Relationships + orders: list["NightclubOrder"] = Relationship(back_populates="pickup_location") + + # Optionally, if you have a specific type of venue for PickupLocation + venue: Optional["Venue"] = Relationship(back_populates="pickup_locations") diff --git a/backend/app/models/qrcode.py b/backend/app/models/qrcode.py new file mode 100644 index 0000000000..4fca5dd7af --- /dev/null +++ b/backend/app/models/qrcode.py @@ -0,0 +1,31 @@ +import uuid +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from app.models.venue import Venue +from sqlmodel import Field, Relationship + +from app.models.base_model import BaseTimeModel +from app.schema.qrcode import QRCodeCreate, QRCodeRead + + +class QRCode(BaseTimeModel, table=True): + __tablename__ = "qrcode" + + id: uuid.UUID | None = Field(default_factory=uuid.uuid4, primary_key=True) + venue_id: uuid.UUID | None = Field(default=None, foreign_key="venue.id") + table_number: str | None = Field(default=None, nullable=True) + + # Relationships + venue: Optional["Venue"] = Relationship(back_populates="qrcode") + + @classmethod + def from_create_schema(cls, schema: "QRCodeCreate") -> "QRCode": + return cls(venue_id=schema.venue_id, table_number=schema.table_number) + + def to_read_schema(self) -> "QRCodeRead": + return QRCodeRead( + id=self.id, # Access the actual value of the id + venue_id=self.venue_id, # Access the actual value of venue_id + table_number=self.table_number, # Provide a default empty string if None + ) diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 0000000000..0877cbd102 --- /dev/null +++ b/backend/app/models/user.py @@ -0,0 +1,131 @@ +import uuid +from datetime import datetime +from typing import TYPE_CHECKING + +from pydantic import EmailStr +from sqlmodel import Field, Relationship, SQLModel + +from app.constants import Gender +from app.models.base_model import BaseTimeModel +from app.models.club_visit import ClubVisit +from app.models.event_booking import EventBooking +from app.models.group import GroupMembers +from app.models.order import NightclubOrder, QSROrder, RestaurantOrder +from app.models.payment import ( + PaymentEvent, + PaymentOrderNightclub, + PaymentOrderQSR, + PaymentOrderRestaurant, +) +from app.models.venue import Venue + +if TYPE_CHECKING: + from app.models.group import Group + +from app.schema.user import ( + UserBusinessCreate, + UserBusinessRead, + UserPublicCreate, + UserPublicRead, +) + + +# Shared properties +class UserBase(BaseTimeModel): + is_active: bool = True + is_superuser: bool = False + full_name: str | None = Field(default=None, max_length=255) + refresh_token: str = Field(nullable=True) + + +class UserPublic(UserBase, table=True): + __tablename__ = "user_public" + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + phone_number: str | None = Field( + unique=True, nullable=False, index=True, default=None + ) + email: EmailStr | None = Field(default=None) + date_of_birth: datetime | None = Field(default=None) + gender: Gender | None = Field(default=None) + profile_picture: str | None = Field(default=None) + preferences: str | None = Field(default=None) + + # Relationships + nightclub_orders: list["NightclubOrder"] = Relationship(back_populates="user") + restaurant_orders: list["RestaurantOrder"] = Relationship(back_populates="user") + qsr_orders: list["QSROrder"] = Relationship(back_populates="user") + + club_visits: list["ClubVisit"] = Relationship(back_populates="user") + event_bookings: list["EventBooking"] = Relationship(back_populates="user") + groups: list["Group"] = Relationship( + back_populates="members", link_model=GroupMembers + ) + managed_groups: list["Group"] = Relationship(back_populates="admin_user") + nightclub_payments: list["PaymentOrderNightclub"] = Relationship( + back_populates="user" + ) + qsr_payments: list["PaymentOrderQSR"] = Relationship(back_populates="user") + restaurant_payments: list["PaymentOrderRestaurant"] = Relationship( + back_populates="user" + ) + event_payments: list["PaymentEvent"] = Relationship(back_populates="user") + + @classmethod + def from_create_schema(cls, schema: UserPublicCreate) -> "UserPublic": + return cls( + full_name=schema.full_name, + phone_number=schema.phone_number, + email=schema.email, + date_of_birth=schema.date_of_birth, + gender=schema.gender, + profile_picture=schema.profile_picture, + preferences=schema.preferences, + ) + + def to_read_schema(self) -> UserPublicRead: + return UserPublicRead( + id=self.id, + full_name=self.full_name, + phone_number=self.phone_number, + email=self.email, + date_of_birth=self.date_of_birth, + gender=self.gender, + profile_picture=self.profile_picture, + preferences=self.preferences, + ) + + +class UserVenueAssociation(SQLModel, table=True): + __tablename__ = "user_venue_association" + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + user_id: uuid.UUID = Field(foreign_key="user_business.id", primary_key=True) + venue_id: uuid.UUID = Field(foreign_key="venue.id", primary_key=True) + role: str | None = Field(default=None) # e.g., 'manager', 'owner' + + user: "UserBusiness" = Relationship(back_populates="venues_association") + venue: "Venue" = Relationship(back_populates="managing_users") + + +class UserBusiness(UserBase, table=True): + __tablename__ = "user_business" + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + email: EmailStr = Field(unique=True, nullable=False, index=True, max_length=255) + phone_number: str | None = Field(default=None) + # Relationships + venues_association: list[UserVenueAssociation] = Relationship(back_populates="user") + + @classmethod + def from_create_schema(cls, schema: UserBusinessCreate) -> "UserBusiness": + return cls( + full_name=schema.full_name, + email=schema.email, + phone_number=schema.phone_number, + ) + + def to_read_schema(self) -> UserBusinessRead: + return UserBusinessRead( + id=self.id, + full_name=self.full_name, + email=self.email, + phone_number=self.phone_number, + ) diff --git a/backend/app/models/venue.py b/backend/app/models/venue.py new file mode 100644 index 0000000000..31c89195c0 --- /dev/null +++ b/backend/app/models/venue.py @@ -0,0 +1,233 @@ +import uuid +from datetime import time +from typing import TYPE_CHECKING, Optional + +from sqlmodel import Field, Relationship + +from app.models.base_model import BaseTimeModel + +if TYPE_CHECKING: + from app.models.carousel_poster import CarouselPoster + from app.models.club_visit import ClubVisit + from app.models.event import Event + from app.models.group import Group + from app.models.menu import Menu + from app.models.order import NightclubOrder, QSROrder, RestaurantOrder + from app.models.pickup_location import PickupLocation + from app.models.qrcode import QRCode + from app.models.user import UserVenueAssociation + +from app.schema.venue import ( + FoodcourtCreate, + FoodcourtRead, + NightclubCreate, + NightclubRead, + QSRCreate, + QSRRead, + RestaurantCreate, + RestaurantRead, + VenueCreate, + VenueRead, +) + + +class Venue(BaseTimeModel, table=True): + __tablename__ = "venue" + id: uuid.UUID = Field( + default_factory=uuid.uuid4, primary_key=True, index=True + ) # Missing id field + name: str = Field(nullable=False, index=True) + address: str | None = Field(default=None) + latitude: float = Field(default=0) + longitude: float = Field(default=0) + capacity: int | None = Field(default=None) + description: str | None = Field(default=None) + google_rating: float | None = Field(default=None) + instagram_handle: str | None = Field(default=None) + instagram_token: str | None = Field(default=None) + google_map_link: str | None = Field(default=None) + mobile_number: str | None = Field(default=None) + email: str | None = Field(default=None) + opening_time: time | None = Field(default=None) + closing_time: time | None = Field(default=None) + avg_expense_for_two: float | None = Field(default=None) + zomato_link: str | None = Field(default=None) + swiggy_link: str | None = Field(default=None) + + managing_users: list["UserVenueAssociation"] = Relationship(back_populates="venue") + qrcode: list["QRCode"] = Relationship(back_populates="venue") + menu: list["Menu"] = Relationship(back_populates="venue") + pickup_locations: list["PickupLocation"] = Relationship(back_populates="venue") + carousel_posters: list["CarouselPoster"] | None = Relationship(back_populates="venue") + + # Back-references for specific venue types + foodcourt: Optional["Foodcourt"] = Relationship(back_populates="venue") + qsr: Optional["QSR"] = Relationship(back_populates="venue") + restaurant: Optional["Restaurant"] = Relationship(back_populates="venue") + nightclub: Optional["Nightclub"] = Relationship(back_populates="venue") + events: list["Event"] = Relationship(back_populates="venue") + + @classmethod + def from_create_schema(cls, venue_create: VenueCreate) -> "Venue": + return cls( + name=venue_create.name, + capacity=venue_create.capacity, + description=venue_create.description, + instagram_handle=venue_create.instagram_handle, + instagram_token=venue_create.instagram_token, + google_map_link=venue_create.google_map_link, + mobile_number=venue_create.mobile_number, + email=venue_create.email, + opening_time=venue_create.opening_time, + closing_time=venue_create.closing_time, + avg_expense_for_two=venue_create.avg_expense_for_two, + zomato_link=venue_create.zomato_link, + swiggy_link=venue_create.swiggy_link, + ) + + def to_read_schema(self) -> VenueRead: + return VenueRead( + id=self.id, + name=self.name, + address=self.address, + latitude=self.latitude, + longitude=self.longitude, + capacity=self.capacity, + description=self.description, + google_rating=self.google_rating, + instagram_handle=self.instagram_handle, + google_map_link=self.google_map_link, + mobile_number=self.mobile_number, + email=self.email, + opening_time=self.opening_time, + closing_time=self.closing_time, + avg_expense_for_two=self.avg_expense_for_two, + zomato_link=self.zomato_link, + swiggy_link=self.swiggy_link, + ) + + +# Specific Venue Types +class Foodcourt(BaseTimeModel, table=True): + __tablename__ = "foodcourt" + + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + total_qsrs: int | None = Field(default=None) # Example specific field for foodcourt + seating_capacity: int | None = Field(default=None) # Specific to foodcourts + venue_id: uuid.UUID = Field(foreign_key="venue.id", nullable=False, index=True) + + # Relationships + venue: Venue = Relationship(back_populates="foodcourt") + qsrs: list["QSR"] = Relationship(back_populates="foodcourt") + + @classmethod + def from_create_schema( + cls, venue_id: uuid, foodcourt_create: FoodcourtCreate + ) -> "Foodcourt": + return cls( + total_qsrs=foodcourt_create.total_qsrs, + seating_capacity=foodcourt_create.seating_capacity, + venue_id=venue_id, + ) + + def to_read_schema(self) -> FoodcourtRead: + venue_read = self.venue.to_read_schema() + return FoodcourtRead( + id=self.id, + total_qsrs=self.total_qsrs, + seating_capacity=self.seating_capacity, + venue=venue_read, + qsrs=[qsr.to_read_schema() for qsr in self.qsrs], + ) + + +class QSR(BaseTimeModel, table=True): + __tablename__ = "qsr" + + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + foodcourt_id: uuid.UUID | None = Field( + foreign_key="foodcourt.id", nullable=True, index=True + ) + + drive_thru: bool | None = Field(default=False) # Specific field for QSR + venue_id: uuid.UUID = Field(foreign_key="venue.id", nullable=False, index=True) + + venue: Venue = Relationship(back_populates="qsr") + foodcourt: Foodcourt | None = Relationship(back_populates="qsrs") + orders: list["QSROrder"] = Relationship(back_populates="qsr") + + @classmethod + def from_create_schema(cls, venue_id: uuid, qsr_create: QSRCreate) -> "QSR": + return cls( + foodcourt_id=qsr_create.foodcourt_id, + drive_thru=qsr_create.drive_thru, + venue_id=venue_id, + ) + + def to_read_schema(self) -> QSRRead: + venue_read = self.venue.to_read_schema() + return QSRRead( + id=self.id, + foodcourt_id=self.foodcourt_id, + drive_thru=self.drive_thru, + venue=venue_read, + ) + + +class Restaurant(BaseTimeModel, table=True): + __tablename__ = "restaurant" + + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + venue_id: uuid.UUID = Field(foreign_key="venue.id", nullable=False, index=True) + venue: Venue = Relationship(back_populates="restaurant") + cuisine_type: str | None = Field( + default=None + ) # Example specific field for restaurant + orders: list["RestaurantOrder"] = Relationship(back_populates="restaurant") + + @classmethod + def from_create_schema( + cls, venue_id, restaurant_create: RestaurantCreate + ) -> "Restaurant": + return cls( + venue_id=venue_id, + cuisine_type=restaurant_create.cuisine_type, + ) + + def to_read_schema(self) -> RestaurantRead: + venue_read = self.venue.to_read_schema() + return RestaurantRead( + id=self.id, + venue=venue_read, + cuisine_type=self.cuisine_type, + ) + + +class Nightclub(BaseTimeModel, table=True): + __tablename__ = "nightclub" + + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True) + venue_id: uuid.UUID = Field(foreign_key="venue.id", nullable=False, index=True) + age_limit: int | None = Field(default=None) + # Relationships + club_visits: list["ClubVisit"] = Relationship(back_populates="nightclub") + orders: list["NightclubOrder"] = Relationship(back_populates="nightclub") + group: list["Group"] = Relationship(back_populates="nightclubs") + venue: Venue = Relationship(back_populates="nightclub") + + @classmethod + def from_create_schema( + cls, venue_id, nightclub_create: NightclubCreate + ) -> "Nightclub": + return cls( + venue_id=venue_id, + age_limit=nightclub_create.age_limit, + ) + + def to_read_schema(self) -> NightclubRead: + venue_read = self.venue.to_read_schema() + return NightclubRead( + age_limit=self.age_limit, + id=self.id, + venue=venue_read, + ) diff --git a/backend/app/tests/__init__.py b/backend/app/schema/__init__.py similarity index 100% rename from backend/app/tests/__init__.py rename to backend/app/schema/__init__.py diff --git a/backend/app/schema/carousel_poster.py b/backend/app/schema/carousel_poster.py new file mode 100644 index 0000000000..9b2d963fb1 --- /dev/null +++ b/backend/app/schema/carousel_poster.py @@ -0,0 +1,34 @@ +import uuid +from datetime import datetime + +from pydantic import BaseModel, model_validator + + +class CarouselPosterCreate(BaseModel): + image_url: str + deep_link: str + expires_at: datetime + event_id: uuid.UUID | None = None + venue_id: uuid.UUID | None = None + + class Config: + from_attributes = True + + @model_validator(mode="before") + def validate_event_or_venue(cls, values): + event_id, venue_id = values.get('event_id'), values.get('venue_id') + if bool(event_id) == bool(venue_id): # Both or neither are set + raise ValueError("Exactly one of 'event_id' or 'venue_id' must be provided.") + return values + +class CarouselPosterRead(BaseModel): + courosel_id: uuid.UUID + image_url: str + deep_link: str + h3_index: str | None + expires_at: datetime + event_id: uuid.UUID | None + venue_id: uuid.UUID | None + + class Config: + from_attributes = True diff --git a/backend/app/schema/menu.py b/backend/app/schema/menu.py new file mode 100644 index 0000000000..1d3d45d609 --- /dev/null +++ b/backend/app/schema/menu.py @@ -0,0 +1,137 @@ +import uuid + +from pydantic import BaseModel, Field + + +class MenuItemCreate(BaseModel): + subcategory_id: uuid.UUID + name: str + price: float + description: str | None = None + image_url: str | None = None + is_veg: bool | None = None + ingredients: str | None = None + abv: float | None = None + ibu: int | None = None + + class Config: + from_attributes = True + + +# Response schema for a menu item +class MenuItemRead(BaseModel): + item_id: uuid.UUID + subcategory_id: uuid.UUID + name: str + price: float + description: str | None = None + image_url: str | None = None + is_veg: bool | None = None + ingredients: str | None = None + abv: float | None = None + ibu: int | None = None + + class Config: + from_attributes = True + + +class MenuItemUpdate(BaseModel): + name: str | None = None # Name can be updated + price: float | None = None # Price can be updated + description: str | None = None # Description is optional and updatable + image_url: str | None = None # Image URL is optional and updatable + is_veg: bool | None = None # Optionally update veg/non-veg status + ingredients: str | None = None # Ingredients list is optional and updatable + abv: float | None = None # Alcohol by volume can be updated + ibu: int | None = None # International Bitterness Units can be updated + + class Config: + from_attributes = True + + +######################################################################################################### + + +class MenuSubCategoryCreate(BaseModel): + name: str # Name of the subcategory + is_alcoholic: bool = Field(default=False) + category_id: uuid.UUID # Foreign key to the parent category + + class Config: + from_attributes = True + + +class MenuSubCategoryRead(BaseModel): + subcategory_id: uuid.UUID # Unique identifier for the subcategory + category_id: uuid.UUID + is_alcoholic: bool + name: str # Name of the subcategory + menu_items: list[MenuItemRead] | None = [] # list of items under this subcategory + + +class MenuSubCategoryUpdate(BaseModel): + name: str | None = None # Optional name for update + is_alcoholic: bool | None = None # Optional field for update + category_id: uuid.UUID | None = None # Optional foreign key to update category + menu_items: list["MenuItemUpdate"] | None = ( + [] + ) # Optional list for updating menu items + + class Config: + from_attributes = True + + +######################################################################################################### +class MenuCategoryRead(BaseModel): + category_id: uuid.UUID # Unique identifier for the category + name: str # Name of the category + menu_id: uuid.UUID + sub_categories: list[MenuSubCategoryRead] = [] # list of subcategories + + +class MenuCategoryCreate(BaseModel): + name: str # Name of the category + menu_id: uuid.UUID + + class Config: + from_attributes = True + + +class MenuCategoryUpdate(BaseModel): + name: str | None = None # Category name can be updated + menu_id: uuid.UUID | None = None # Menu ID can be updated + + class Config: + from_attributes = True + + +######################################################################################################### +class MenuRead(BaseModel): + menu_id: uuid.UUID # Unique identifier for the menu + name: str # Name of the menu (could be a restaurant menu or type of menu) + description: str | None = None # Description of the menu + categories: list[MenuCategoryRead] = [] # Nested list of categories + venue_id: uuid.UUID # Foreign key to the venue + menu_type: str | None = None + + class Config: + from_attributes = True + + +class MenuCreate(BaseModel): + name: str # Name of the menu (could be a restaurant menu or type of menu) + description: str | None = None # Description of the menu + venue_id: uuid.UUID # Foreign key to the venue + menu_type: str | None = None # Type of menu (e.g., "Food", "Drink") + + class Config: + from_attributes = True + + +class MenuUpdate(BaseModel): + name: str # Name of the menu (could be a restaurant menu or type of menu) + description: str | None = None # Description of the menu + menu_type: str | None = None # Type of menu (e.g., "Food", "Drink") + + class Config: + from_attributes = True diff --git a/backend/app/schema/qrcode.py b/backend/app/schema/qrcode.py new file mode 100644 index 0000000000..d0decde0b7 --- /dev/null +++ b/backend/app/schema/qrcode.py @@ -0,0 +1,32 @@ +from uuid import UUID + +from pydantic import BaseModel + + +class QRCodeCreate(BaseModel): + """ + Schema for creating a new QR code. + """ + + table_number: str | None = None + venue_id: UUID # Foreign key to the venue + + +class QRCodeUpdate(BaseModel): + """ + Schema for creating a new QR code. + """ + + table_number: str | None = None + +class QRCodeRead(BaseModel): + """ + Schema for reading a QR code. + """ + + id: UUID # Automatically added by the model + venue_id: UUID + table_number: str | None + + class Config: + from_attributes = True diff --git a/backend/app/schema/user.py b/backend/app/schema/user.py new file mode 100644 index 0000000000..6331cb9a6b --- /dev/null +++ b/backend/app/schema/user.py @@ -0,0 +1,66 @@ +# Create and Read Schemas + +import uuid +from datetime import datetime + +from pydantic import BaseModel, EmailStr + +from app.constants import Gender + + +class UserPublicCreate(BaseModel): + full_name: str + phone_number: str | None = None + email: EmailStr | None = None + date_of_birth: datetime | None = None + gender: Gender | None = None + profile_picture: str | None = None + preferences: str | None = None + + +class UserPublicUpdate(BaseModel): + full_name: str | None = None + phone_number: str | None = None + email: EmailStr | None = None + date_of_birth: datetime | None + gender: Gender | None = None + profile_picture: str | None = None + preferences: str | None = None + + +class UserPublicRead(BaseModel): + id: uuid.UUID + phone_number: str + full_name: str | None = None + email: EmailStr | None = None + date_of_birth: datetime | None = None + gender: Gender | None = None + profile_picture: str | None = None + preferences: str | None = None + + class Config: + from_attributes = True + extra = "forbid" + + +class UserBusinessCreate(BaseModel): + full_name: str | None = None + email: EmailStr + phone_number: str | None = None + + +class UserBusinessUpdate(BaseModel): + full_name: str | None = None + email: EmailStr | None = None + phone_number: str | None = None + + +class UserBusinessRead(BaseModel): + id: uuid.UUID + full_name: str | None = None + email: EmailStr + phone_number: str | None = None + + class Config: + from_attributes = True + extra = "forbid" diff --git a/backend/app/schema/venue.py b/backend/app/schema/venue.py new file mode 100644 index 0000000000..3f78fa4bef --- /dev/null +++ b/backend/app/schema/venue.py @@ -0,0 +1,132 @@ +import uuid +from datetime import time + +from pydantic import BaseModel, Field, validator + + +# Venue base details (composition) +class VenueCreate(BaseModel): + name: str + capacity: int | None = None + description: str | None = None + instagram_handle: str | None = None + instagram_token: str | None = None + mobile_number: str | None = None + email: str | None = None + opening_time: time | None = None + closing_time: time | None = None + avg_expense_for_two: float | None = None + zomato_link: str | None = None + swiggy_link: str | None = None + google_map_link: str | None = None + + +class FoodcourtCreate(BaseModel): + total_qsrs: int | None = None + seating_capacity: int | None = None + venue: VenueCreate + + class Config: + from_attributes = True + + +class QSRCreate(BaseModel): + drive_thru: bool | None = False + foodcourt_id: uuid.UUID | None = None + venue: VenueCreate + + class Config: + from_attributes = True + + +# Restaurant Schemas +class RestaurantCreate(BaseModel): + cuisine_type: str | None = Field( + default=None, + description="Comma-separated list of cuisine types" + ) + venue: VenueCreate + + @validator('cuisine_type', pre=True) + def format_cuisine_type(cls, v): + if isinstance(v, list): + return ', '.join(v) # Convert list to comma-separated string + return v + + class Config: + from_attributes = True + + +# Nightclub Schemas +class NightclubCreate(BaseModel): + venue: VenueCreate + age_limit: int | None = None + + class Config: + from_attributes = True + + +class VenueRead(BaseModel): + id: uuid.UUID + name: str + address: str | None + latitude: float + longitude: float + capacity: int | None + description: str | None + google_rating: float | None + instagram_handle: str | None + google_map_link: str | None + mobile_number: str | None + email: str | None + opening_time: time | None + closing_time: time | None + avg_expense_for_two: float | None + zomato_link: str | None + swiggy_link: str | None + + +class FoodcourtRead(BaseModel): + id: uuid.UUID + total_qsrs: int | None = None # Specific field for foodcourt + seating_capacity: int | None = None # Specific to foodcourts + venue: VenueRead + qsrs: list["QSRRead"] = [] # list of QSRs in the foodcourt + + class Config: + from_attributes = True + + +class QSRRead(BaseModel): + id: uuid.UUID + # Add any specific fields for QSR if needed + foodcourt_id: uuid.UUID | None = None # Reference to the associated foodcourt + venue: VenueRead + + class Config: + from_attributes = True + + +class RestaurantRead(BaseModel): + id: uuid.UUID + cuisine_type: str | None = None + venue: VenueRead + + class Config: + from_attributes = True + + +class NightclubRead(BaseModel): + id: uuid.UUID + age_limit: int | None = None + venue: VenueRead + + class Config: + from_attributes = True + + +class VenueListResponse(BaseModel): + nightclubs: list[NightclubRead] + qsrs: list[QSRRead] + foodcourts: list[FoodcourtRead] + restaurants: list[RestaurantRead] diff --git a/backend/app/static/landing_page.html b/backend/app/static/landing_page.html new file mode 100644 index 0000000000..2bed0ef319 --- /dev/null +++ b/backend/app/static/landing_page.html @@ -0,0 +1,48 @@ + + + + + + Redirecting... + + + +

Redirecting...

+ + \ No newline at end of file diff --git a/backend/app/tests/api/routes/test_items.py b/backend/app/tests/api/routes/test_items.py deleted file mode 100644 index c215238a69..0000000000 --- a/backend/app/tests/api/routes/test_items.py +++ /dev/null @@ -1,164 +0,0 @@ -import uuid - -from fastapi.testclient import TestClient -from sqlmodel import Session - -from app.core.config import settings -from app.tests.utils.item import create_random_item - - -def test_create_item( - client: TestClient, superuser_token_headers: dict[str, str] -) -> None: - data = {"title": "Foo", "description": "Fighters"} - response = client.post( - f"{settings.API_V1_STR}/items/", - headers=superuser_token_headers, - json=data, - ) - assert response.status_code == 200 - content = response.json() - assert content["title"] == data["title"] - assert content["description"] == data["description"] - assert "id" in content - assert "owner_id" in content - - -def test_read_item( - client: TestClient, superuser_token_headers: dict[str, str], db: Session -) -> None: - item = create_random_item(db) - response = client.get( - f"{settings.API_V1_STR}/items/{item.id}", - headers=superuser_token_headers, - ) - assert response.status_code == 200 - content = response.json() - assert content["title"] == item.title - assert content["description"] == item.description - assert content["id"] == str(item.id) - assert content["owner_id"] == str(item.owner_id) - - -def test_read_item_not_found( - client: TestClient, superuser_token_headers: dict[str, str] -) -> None: - response = client.get( - f"{settings.API_V1_STR}/items/{uuid.uuid4()}", - headers=superuser_token_headers, - ) - assert response.status_code == 404 - content = response.json() - assert content["detail"] == "Item not found" - - -def test_read_item_not_enough_permissions( - client: TestClient, normal_user_token_headers: dict[str, str], db: Session -) -> None: - item = create_random_item(db) - response = client.get( - f"{settings.API_V1_STR}/items/{item.id}", - headers=normal_user_token_headers, - ) - assert response.status_code == 400 - content = response.json() - assert content["detail"] == "Not enough permissions" - - -def test_read_items( - client: TestClient, superuser_token_headers: dict[str, str], db: Session -) -> None: - create_random_item(db) - create_random_item(db) - response = client.get( - f"{settings.API_V1_STR}/items/", - headers=superuser_token_headers, - ) - assert response.status_code == 200 - content = response.json() - assert len(content["data"]) >= 2 - - -def test_update_item( - client: TestClient, superuser_token_headers: dict[str, str], db: Session -) -> None: - item = create_random_item(db) - data = {"title": "Updated title", "description": "Updated description"} - response = client.put( - f"{settings.API_V1_STR}/items/{item.id}", - headers=superuser_token_headers, - json=data, - ) - assert response.status_code == 200 - content = response.json() - assert content["title"] == data["title"] - assert content["description"] == data["description"] - assert content["id"] == str(item.id) - assert content["owner_id"] == str(item.owner_id) - - -def test_update_item_not_found( - client: TestClient, superuser_token_headers: dict[str, str] -) -> None: - data = {"title": "Updated title", "description": "Updated description"} - response = client.put( - f"{settings.API_V1_STR}/items/{uuid.uuid4()}", - headers=superuser_token_headers, - json=data, - ) - assert response.status_code == 404 - content = response.json() - assert content["detail"] == "Item not found" - - -def test_update_item_not_enough_permissions( - client: TestClient, normal_user_token_headers: dict[str, str], db: Session -) -> None: - item = create_random_item(db) - data = {"title": "Updated title", "description": "Updated description"} - response = client.put( - f"{settings.API_V1_STR}/items/{item.id}", - headers=normal_user_token_headers, - json=data, - ) - assert response.status_code == 400 - content = response.json() - assert content["detail"] == "Not enough permissions" - - -def test_delete_item( - client: TestClient, superuser_token_headers: dict[str, str], db: Session -) -> None: - item = create_random_item(db) - response = client.delete( - f"{settings.API_V1_STR}/items/{item.id}", - headers=superuser_token_headers, - ) - assert response.status_code == 200 - content = response.json() - assert content["message"] == "Item deleted successfully" - - -def test_delete_item_not_found( - client: TestClient, superuser_token_headers: dict[str, str] -) -> None: - response = client.delete( - f"{settings.API_V1_STR}/items/{uuid.uuid4()}", - headers=superuser_token_headers, - ) - assert response.status_code == 404 - content = response.json() - assert content["detail"] == "Item not found" - - -def test_delete_item_not_enough_permissions( - client: TestClient, normal_user_token_headers: dict[str, str], db: Session -) -> None: - item = create_random_item(db) - response = client.delete( - f"{settings.API_V1_STR}/items/{item.id}", - headers=normal_user_token_headers, - ) - assert response.status_code == 400 - content = response.json() - assert content["detail"] == "Not enough permissions" diff --git a/backend/app/tests/api/routes/test_login.py b/backend/app/tests/api/routes/test_login.py deleted file mode 100644 index 34fe8ee560..0000000000 --- a/backend/app/tests/api/routes/test_login.py +++ /dev/null @@ -1,104 +0,0 @@ -from unittest.mock import patch - -from fastapi.testclient import TestClient -from sqlmodel import Session, select - -from app.core.config import settings -from app.core.security import verify_password -from app.models import User -from app.utils import generate_password_reset_token - - -def test_get_access_token(client: TestClient) -> None: - login_data = { - "username": settings.FIRST_SUPERUSER, - "password": settings.FIRST_SUPERUSER_PASSWORD, - } - r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data) - tokens = r.json() - assert r.status_code == 200 - assert "access_token" in tokens - assert tokens["access_token"] - - -def test_get_access_token_incorrect_password(client: TestClient) -> None: - login_data = { - "username": settings.FIRST_SUPERUSER, - "password": "incorrect", - } - r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data) - assert r.status_code == 400 - - -def test_use_access_token( - client: TestClient, superuser_token_headers: dict[str, str] -) -> None: - r = client.post( - f"{settings.API_V1_STR}/login/test-token", - headers=superuser_token_headers, - ) - result = r.json() - assert r.status_code == 200 - assert "email" in result - - -def test_recovery_password( - client: TestClient, normal_user_token_headers: dict[str, str] -) -> None: - with ( - patch("app.core.config.settings.SMTP_HOST", "smtp.example.com"), - patch("app.core.config.settings.SMTP_USER", "admin@example.com"), - ): - email = "test@example.com" - r = client.post( - f"{settings.API_V1_STR}/password-recovery/{email}", - headers=normal_user_token_headers, - ) - assert r.status_code == 200 - assert r.json() == {"message": "Password recovery email sent"} - - -def test_recovery_password_user_not_exits( - client: TestClient, normal_user_token_headers: dict[str, str] -) -> None: - email = "jVgQr@example.com" - r = client.post( - f"{settings.API_V1_STR}/password-recovery/{email}", - headers=normal_user_token_headers, - ) - assert r.status_code == 404 - - -def test_reset_password( - client: TestClient, superuser_token_headers: dict[str, str], db: Session -) -> None: - token = generate_password_reset_token(email=settings.FIRST_SUPERUSER) - data = {"new_password": "changethis", "token": token} - r = client.post( - f"{settings.API_V1_STR}/reset-password/", - headers=superuser_token_headers, - json=data, - ) - assert r.status_code == 200 - assert r.json() == {"message": "Password updated successfully"} - - user_query = select(User).where(User.email == settings.FIRST_SUPERUSER) - user = db.exec(user_query).first() - assert user - assert verify_password(data["new_password"], user.hashed_password) - - -def test_reset_password_invalid_token( - client: TestClient, superuser_token_headers: dict[str, str] -) -> None: - data = {"new_password": "changethis", "token": "invalid"} - r = client.post( - f"{settings.API_V1_STR}/reset-password/", - headers=superuser_token_headers, - json=data, - ) - response = r.json() - - assert "detail" in response - assert r.status_code == 400 - assert response["detail"] == "Invalid token" diff --git a/backend/app/tests/api/routes/test_users.py b/backend/app/tests/api/routes/test_users.py deleted file mode 100644 index ba9be65426..0000000000 --- a/backend/app/tests/api/routes/test_users.py +++ /dev/null @@ -1,486 +0,0 @@ -import uuid -from unittest.mock import patch - -from fastapi.testclient import TestClient -from sqlmodel import Session, select - -from app import crud -from app.core.config import settings -from app.core.security import verify_password -from app.models import User, UserCreate -from app.tests.utils.utils import random_email, random_lower_string - - -def test_get_users_superuser_me( - client: TestClient, superuser_token_headers: dict[str, str] -) -> None: - r = client.get(f"{settings.API_V1_STR}/users/me", headers=superuser_token_headers) - current_user = r.json() - assert current_user - assert current_user["is_active"] is True - assert current_user["is_superuser"] - assert current_user["email"] == settings.FIRST_SUPERUSER - - -def test_get_users_normal_user_me( - client: TestClient, normal_user_token_headers: dict[str, str] -) -> None: - r = client.get(f"{settings.API_V1_STR}/users/me", headers=normal_user_token_headers) - current_user = r.json() - assert current_user - assert current_user["is_active"] is True - assert current_user["is_superuser"] is False - assert current_user["email"] == settings.EMAIL_TEST_USER - - -def test_create_user_new_email( - client: TestClient, superuser_token_headers: dict[str, str], db: Session -) -> None: - with ( - patch("app.utils.send_email", return_value=None), - patch("app.core.config.settings.SMTP_HOST", "smtp.example.com"), - patch("app.core.config.settings.SMTP_USER", "admin@example.com"), - ): - username = random_email() - password = random_lower_string() - data = {"email": username, "password": password} - r = client.post( - f"{settings.API_V1_STR}/users/", - headers=superuser_token_headers, - json=data, - ) - assert 200 <= r.status_code < 300 - created_user = r.json() - user = crud.get_user_by_email(session=db, email=username) - assert user - assert user.email == created_user["email"] - - -def test_get_existing_user( - client: TestClient, superuser_token_headers: dict[str, str], db: Session -) -> None: - username = random_email() - password = random_lower_string() - user_in = UserCreate(email=username, password=password) - user = crud.create_user(session=db, user_create=user_in) - user_id = user.id - r = client.get( - f"{settings.API_V1_STR}/users/{user_id}", - headers=superuser_token_headers, - ) - assert 200 <= r.status_code < 300 - api_user = r.json() - existing_user = crud.get_user_by_email(session=db, email=username) - assert existing_user - assert existing_user.email == api_user["email"] - - -def test_get_existing_user_current_user(client: TestClient, db: Session) -> None: - username = random_email() - password = random_lower_string() - user_in = UserCreate(email=username, password=password) - user = crud.create_user(session=db, user_create=user_in) - user_id = user.id - - login_data = { - "username": username, - "password": password, - } - r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data) - tokens = r.json() - a_token = tokens["access_token"] - headers = {"Authorization": f"Bearer {a_token}"} - - r = client.get( - f"{settings.API_V1_STR}/users/{user_id}", - headers=headers, - ) - assert 200 <= r.status_code < 300 - api_user = r.json() - existing_user = crud.get_user_by_email(session=db, email=username) - assert existing_user - assert existing_user.email == api_user["email"] - - -def test_get_existing_user_permissions_error( - client: TestClient, normal_user_token_headers: dict[str, str] -) -> None: - r = client.get( - f"{settings.API_V1_STR}/users/{uuid.uuid4()}", - headers=normal_user_token_headers, - ) - assert r.status_code == 403 - assert r.json() == {"detail": "The user doesn't have enough privileges"} - - -def test_create_user_existing_username( - client: TestClient, superuser_token_headers: dict[str, str], db: Session -) -> None: - username = random_email() - # username = email - password = random_lower_string() - user_in = UserCreate(email=username, password=password) - crud.create_user(session=db, user_create=user_in) - data = {"email": username, "password": password} - r = client.post( - f"{settings.API_V1_STR}/users/", - headers=superuser_token_headers, - json=data, - ) - created_user = r.json() - assert r.status_code == 400 - assert "_id" not in created_user - - -def test_create_user_by_normal_user( - client: TestClient, normal_user_token_headers: dict[str, str] -) -> None: - username = random_email() - password = random_lower_string() - data = {"email": username, "password": password} - r = client.post( - f"{settings.API_V1_STR}/users/", - headers=normal_user_token_headers, - json=data, - ) - assert r.status_code == 403 - - -def test_retrieve_users( - client: TestClient, superuser_token_headers: dict[str, str], db: Session -) -> None: - username = random_email() - password = random_lower_string() - user_in = UserCreate(email=username, password=password) - crud.create_user(session=db, user_create=user_in) - - username2 = random_email() - password2 = random_lower_string() - user_in2 = UserCreate(email=username2, password=password2) - crud.create_user(session=db, user_create=user_in2) - - r = client.get(f"{settings.API_V1_STR}/users/", headers=superuser_token_headers) - all_users = r.json() - - assert len(all_users["data"]) > 1 - assert "count" in all_users - for item in all_users["data"]: - assert "email" in item - - -def test_update_user_me( - client: TestClient, normal_user_token_headers: dict[str, str], db: Session -) -> None: - full_name = "Updated Name" - email = random_email() - data = {"full_name": full_name, "email": email} - r = client.patch( - f"{settings.API_V1_STR}/users/me", - headers=normal_user_token_headers, - json=data, - ) - assert r.status_code == 200 - updated_user = r.json() - assert updated_user["email"] == email - assert updated_user["full_name"] == full_name - - user_query = select(User).where(User.email == email) - user_db = db.exec(user_query).first() - assert user_db - assert user_db.email == email - assert user_db.full_name == full_name - - -def test_update_password_me( - client: TestClient, superuser_token_headers: dict[str, str], db: Session -) -> None: - new_password = random_lower_string() - data = { - "current_password": settings.FIRST_SUPERUSER_PASSWORD, - "new_password": new_password, - } - r = client.patch( - f"{settings.API_V1_STR}/users/me/password", - headers=superuser_token_headers, - json=data, - ) - assert r.status_code == 200 - updated_user = r.json() - assert updated_user["message"] == "Password updated successfully" - - user_query = select(User).where(User.email == settings.FIRST_SUPERUSER) - user_db = db.exec(user_query).first() - assert user_db - assert user_db.email == settings.FIRST_SUPERUSER - assert verify_password(new_password, user_db.hashed_password) - - # Revert to the old password to keep consistency in test - old_data = { - "current_password": new_password, - "new_password": settings.FIRST_SUPERUSER_PASSWORD, - } - r = client.patch( - f"{settings.API_V1_STR}/users/me/password", - headers=superuser_token_headers, - json=old_data, - ) - db.refresh(user_db) - - assert r.status_code == 200 - assert verify_password(settings.FIRST_SUPERUSER_PASSWORD, user_db.hashed_password) - - -def test_update_password_me_incorrect_password( - client: TestClient, superuser_token_headers: dict[str, str] -) -> None: - new_password = random_lower_string() - data = {"current_password": new_password, "new_password": new_password} - r = client.patch( - f"{settings.API_V1_STR}/users/me/password", - headers=superuser_token_headers, - json=data, - ) - assert r.status_code == 400 - updated_user = r.json() - assert updated_user["detail"] == "Incorrect password" - - -def test_update_user_me_email_exists( - client: TestClient, normal_user_token_headers: dict[str, str], db: Session -) -> None: - username = random_email() - password = random_lower_string() - user_in = UserCreate(email=username, password=password) - user = crud.create_user(session=db, user_create=user_in) - - data = {"email": user.email} - r = client.patch( - f"{settings.API_V1_STR}/users/me", - headers=normal_user_token_headers, - json=data, - ) - assert r.status_code == 409 - assert r.json()["detail"] == "User with this email already exists" - - -def test_update_password_me_same_password_error( - client: TestClient, superuser_token_headers: dict[str, str] -) -> None: - data = { - "current_password": settings.FIRST_SUPERUSER_PASSWORD, - "new_password": settings.FIRST_SUPERUSER_PASSWORD, - } - r = client.patch( - f"{settings.API_V1_STR}/users/me/password", - headers=superuser_token_headers, - json=data, - ) - assert r.status_code == 400 - updated_user = r.json() - assert ( - updated_user["detail"] == "New password cannot be the same as the current one" - ) - - -def test_register_user(client: TestClient, db: Session) -> None: - username = random_email() - password = random_lower_string() - full_name = random_lower_string() - data = {"email": username, "password": password, "full_name": full_name} - r = client.post( - f"{settings.API_V1_STR}/users/signup", - json=data, - ) - assert r.status_code == 200 - created_user = r.json() - assert created_user["email"] == username - assert created_user["full_name"] == full_name - - user_query = select(User).where(User.email == username) - user_db = db.exec(user_query).first() - assert user_db - assert user_db.email == username - assert user_db.full_name == full_name - assert verify_password(password, user_db.hashed_password) - - -def test_register_user_already_exists_error(client: TestClient) -> None: - password = random_lower_string() - full_name = random_lower_string() - data = { - "email": settings.FIRST_SUPERUSER, - "password": password, - "full_name": full_name, - } - r = client.post( - f"{settings.API_V1_STR}/users/signup", - json=data, - ) - assert r.status_code == 400 - assert r.json()["detail"] == "The user with this email already exists in the system" - - -def test_update_user( - client: TestClient, superuser_token_headers: dict[str, str], db: Session -) -> None: - username = random_email() - password = random_lower_string() - user_in = UserCreate(email=username, password=password) - user = crud.create_user(session=db, user_create=user_in) - - data = {"full_name": "Updated_full_name"} - r = client.patch( - f"{settings.API_V1_STR}/users/{user.id}", - headers=superuser_token_headers, - json=data, - ) - assert r.status_code == 200 - updated_user = r.json() - - assert updated_user["full_name"] == "Updated_full_name" - - user_query = select(User).where(User.email == username) - user_db = db.exec(user_query).first() - db.refresh(user_db) - assert user_db - assert user_db.full_name == "Updated_full_name" - - -def test_update_user_not_exists( - client: TestClient, superuser_token_headers: dict[str, str] -) -> None: - data = {"full_name": "Updated_full_name"} - r = client.patch( - f"{settings.API_V1_STR}/users/{uuid.uuid4()}", - headers=superuser_token_headers, - json=data, - ) - assert r.status_code == 404 - assert r.json()["detail"] == "The user with this id does not exist in the system" - - -def test_update_user_email_exists( - client: TestClient, superuser_token_headers: dict[str, str], db: Session -) -> None: - username = random_email() - password = random_lower_string() - user_in = UserCreate(email=username, password=password) - user = crud.create_user(session=db, user_create=user_in) - - username2 = random_email() - password2 = random_lower_string() - user_in2 = UserCreate(email=username2, password=password2) - user2 = crud.create_user(session=db, user_create=user_in2) - - data = {"email": user2.email} - r = client.patch( - f"{settings.API_V1_STR}/users/{user.id}", - headers=superuser_token_headers, - json=data, - ) - assert r.status_code == 409 - assert r.json()["detail"] == "User with this email already exists" - - -def test_delete_user_me(client: TestClient, db: Session) -> None: - username = random_email() - password = random_lower_string() - user_in = UserCreate(email=username, password=password) - user = crud.create_user(session=db, user_create=user_in) - user_id = user.id - - login_data = { - "username": username, - "password": password, - } - r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data) - tokens = r.json() - a_token = tokens["access_token"] - headers = {"Authorization": f"Bearer {a_token}"} - - r = client.delete( - f"{settings.API_V1_STR}/users/me", - headers=headers, - ) - assert r.status_code == 200 - deleted_user = r.json() - assert deleted_user["message"] == "User deleted successfully" - result = db.exec(select(User).where(User.id == user_id)).first() - assert result is None - - user_query = select(User).where(User.id == user_id) - user_db = db.execute(user_query).first() - assert user_db is None - - -def test_delete_user_me_as_superuser( - client: TestClient, superuser_token_headers: dict[str, str] -) -> None: - r = client.delete( - f"{settings.API_V1_STR}/users/me", - headers=superuser_token_headers, - ) - assert r.status_code == 403 - response = r.json() - assert response["detail"] == "Super users are not allowed to delete themselves" - - -def test_delete_user_super_user( - client: TestClient, superuser_token_headers: dict[str, str], db: Session -) -> None: - username = random_email() - password = random_lower_string() - user_in = UserCreate(email=username, password=password) - user = crud.create_user(session=db, user_create=user_in) - user_id = user.id - r = client.delete( - f"{settings.API_V1_STR}/users/{user_id}", - headers=superuser_token_headers, - ) - assert r.status_code == 200 - deleted_user = r.json() - assert deleted_user["message"] == "User deleted successfully" - result = db.exec(select(User).where(User.id == user_id)).first() - assert result is None - - -def test_delete_user_not_found( - client: TestClient, superuser_token_headers: dict[str, str] -) -> None: - r = client.delete( - f"{settings.API_V1_STR}/users/{uuid.uuid4()}", - headers=superuser_token_headers, - ) - assert r.status_code == 404 - assert r.json()["detail"] == "User not found" - - -def test_delete_user_current_super_user_error( - client: TestClient, superuser_token_headers: dict[str, str], db: Session -) -> None: - super_user = crud.get_user_by_email(session=db, email=settings.FIRST_SUPERUSER) - assert super_user - user_id = super_user.id - - r = client.delete( - f"{settings.API_V1_STR}/users/{user_id}", - headers=superuser_token_headers, - ) - assert r.status_code == 403 - assert r.json()["detail"] == "Super users are not allowed to delete themselves" - - -def test_delete_user_without_privileges( - client: TestClient, normal_user_token_headers: dict[str, str], db: Session -) -> None: - username = random_email() - password = random_lower_string() - user_in = UserCreate(email=username, password=password) - user = crud.create_user(session=db, user_create=user_in) - - r = client.delete( - f"{settings.API_V1_STR}/users/{user.id}", - headers=normal_user_token_headers, - ) - assert r.status_code == 403 - assert r.json()["detail"] == "The user doesn't have enough privileges" diff --git a/backend/app/tests/conftest.py b/backend/app/tests/conftest.py deleted file mode 100644 index 90ab39a357..0000000000 --- a/backend/app/tests/conftest.py +++ /dev/null @@ -1,42 +0,0 @@ -from collections.abc import Generator - -import pytest -from fastapi.testclient import TestClient -from sqlmodel import Session, delete - -from app.core.config import settings -from app.core.db import engine, init_db -from app.main import app -from app.models import Item, User -from app.tests.utils.user import authentication_token_from_email -from app.tests.utils.utils import get_superuser_token_headers - - -@pytest.fixture(scope="session", autouse=True) -def db() -> Generator[Session, None, None]: - with Session(engine) as session: - init_db(session) - yield session - statement = delete(Item) - session.execute(statement) - statement = delete(User) - session.execute(statement) - session.commit() - - -@pytest.fixture(scope="module") -def client() -> Generator[TestClient, None, None]: - with TestClient(app) as c: - yield c - - -@pytest.fixture(scope="module") -def superuser_token_headers(client: TestClient) -> dict[str, str]: - return get_superuser_token_headers(client) - - -@pytest.fixture(scope="module") -def normal_user_token_headers(client: TestClient, db: Session) -> dict[str, str]: - return authentication_token_from_email( - client=client, email=settings.EMAIL_TEST_USER, db=db - ) diff --git a/backend/app/tests/crud/__init__.py b/backend/app/tests/crud/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/app/tests/crud/test_user.py b/backend/app/tests/crud/test_user.py deleted file mode 100644 index e9eb4a0391..0000000000 --- a/backend/app/tests/crud/test_user.py +++ /dev/null @@ -1,91 +0,0 @@ -from fastapi.encoders import jsonable_encoder -from sqlmodel import Session - -from app import crud -from app.core.security import verify_password -from app.models import User, UserCreate, UserUpdate -from app.tests.utils.utils import random_email, random_lower_string - - -def test_create_user(db: Session) -> None: - email = random_email() - password = random_lower_string() - user_in = UserCreate(email=email, password=password) - user = crud.create_user(session=db, user_create=user_in) - assert user.email == email - assert hasattr(user, "hashed_password") - - -def test_authenticate_user(db: Session) -> None: - email = random_email() - password = random_lower_string() - user_in = UserCreate(email=email, password=password) - user = crud.create_user(session=db, user_create=user_in) - authenticated_user = crud.authenticate(session=db, email=email, password=password) - assert authenticated_user - assert user.email == authenticated_user.email - - -def test_not_authenticate_user(db: Session) -> None: - email = random_email() - password = random_lower_string() - user = crud.authenticate(session=db, email=email, password=password) - assert user is None - - -def test_check_if_user_is_active(db: Session) -> None: - email = random_email() - password = random_lower_string() - user_in = UserCreate(email=email, password=password) - user = crud.create_user(session=db, user_create=user_in) - assert user.is_active is True - - -def test_check_if_user_is_active_inactive(db: Session) -> None: - email = random_email() - password = random_lower_string() - user_in = UserCreate(email=email, password=password, disabled=True) - user = crud.create_user(session=db, user_create=user_in) - assert user.is_active - - -def test_check_if_user_is_superuser(db: Session) -> None: - email = random_email() - password = random_lower_string() - user_in = UserCreate(email=email, password=password, is_superuser=True) - user = crud.create_user(session=db, user_create=user_in) - assert user.is_superuser is True - - -def test_check_if_user_is_superuser_normal_user(db: Session) -> None: - username = random_email() - password = random_lower_string() - user_in = UserCreate(email=username, password=password) - user = crud.create_user(session=db, user_create=user_in) - assert user.is_superuser is False - - -def test_get_user(db: Session) -> None: - password = random_lower_string() - username = random_email() - user_in = UserCreate(email=username, password=password, is_superuser=True) - user = crud.create_user(session=db, user_create=user_in) - user_2 = db.get(User, user.id) - assert user_2 - assert user.email == user_2.email - assert jsonable_encoder(user) == jsonable_encoder(user_2) - - -def test_update_user(db: Session) -> None: - password = random_lower_string() - email = random_email() - user_in = UserCreate(email=email, password=password, is_superuser=True) - user = crud.create_user(session=db, user_create=user_in) - new_password = random_lower_string() - user_in_update = UserUpdate(password=new_password, is_superuser=True) - if user.id is not None: - crud.update_user(session=db, db_user=user, user_in=user_in_update) - user_2 = db.get(User, user.id) - assert user_2 - assert user.email == user_2.email - assert verify_password(new_password, user_2.hashed_password) diff --git a/backend/app/tests/scripts/__init__.py b/backend/app/tests/scripts/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/app/tests/scripts/test_backend_pre_start.py b/backend/app/tests/scripts/test_backend_pre_start.py deleted file mode 100644 index 631690fcf6..0000000000 --- a/backend/app/tests/scripts/test_backend_pre_start.py +++ /dev/null @@ -1,33 +0,0 @@ -from unittest.mock import MagicMock, patch - -from sqlmodel import select - -from app.backend_pre_start import init, logger - - -def test_init_successful_connection() -> None: - engine_mock = MagicMock() - - session_mock = MagicMock() - exec_mock = MagicMock(return_value=True) - session_mock.configure_mock(**{"exec.return_value": exec_mock}) - - with ( - patch("sqlmodel.Session", return_value=session_mock), - patch.object(logger, "info"), - patch.object(logger, "error"), - patch.object(logger, "warn"), - ): - try: - init(engine_mock) - connection_successful = True - except Exception: - connection_successful = False - - assert ( - connection_successful - ), "The database connection should be successful and not raise an exception." - - assert session_mock.exec.called_once_with( - select(1) - ), "The session should execute a select statement once." diff --git a/backend/app/tests/scripts/test_test_pre_start.py b/backend/app/tests/scripts/test_test_pre_start.py deleted file mode 100644 index a176f380de..0000000000 --- a/backend/app/tests/scripts/test_test_pre_start.py +++ /dev/null @@ -1,33 +0,0 @@ -from unittest.mock import MagicMock, patch - -from sqlmodel import select - -from app.tests_pre_start import init, logger - - -def test_init_successful_connection() -> None: - engine_mock = MagicMock() - - session_mock = MagicMock() - exec_mock = MagicMock(return_value=True) - session_mock.configure_mock(**{"exec.return_value": exec_mock}) - - with ( - patch("sqlmodel.Session", return_value=session_mock), - patch.object(logger, "info"), - patch.object(logger, "error"), - patch.object(logger, "warn"), - ): - try: - init(engine_mock) - connection_successful = True - except Exception: - connection_successful = False - - assert ( - connection_successful - ), "The database connection should be successful and not raise an exception." - - assert session_mock.exec.called_once_with( - select(1) - ), "The session should execute a select statement once." diff --git a/backend/app/tests/utils/__init__.py b/backend/app/tests/utils/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/app/tests/utils/item.py b/backend/app/tests/utils/item.py deleted file mode 100644 index 6e32b3a84a..0000000000 --- a/backend/app/tests/utils/item.py +++ /dev/null @@ -1,16 +0,0 @@ -from sqlmodel import Session - -from app import crud -from app.models import Item, ItemCreate -from app.tests.utils.user import create_random_user -from app.tests.utils.utils import random_lower_string - - -def create_random_item(db: Session) -> Item: - user = create_random_user(db) - owner_id = user.id - assert owner_id is not None - title = random_lower_string() - description = random_lower_string() - item_in = ItemCreate(title=title, description=description) - return crud.create_item(session=db, item_in=item_in, owner_id=owner_id) diff --git a/backend/app/tests/utils/user.py b/backend/app/tests/utils/user.py deleted file mode 100644 index 9c1b073109..0000000000 --- a/backend/app/tests/utils/user.py +++ /dev/null @@ -1,49 +0,0 @@ -from fastapi.testclient import TestClient -from sqlmodel import Session - -from app import crud -from app.core.config import settings -from app.models import User, UserCreate, UserUpdate -from app.tests.utils.utils import random_email, random_lower_string - - -def user_authentication_headers( - *, client: TestClient, email: str, password: str -) -> dict[str, str]: - data = {"username": email, "password": password} - - r = client.post(f"{settings.API_V1_STR}/login/access-token", data=data) - response = r.json() - auth_token = response["access_token"] - headers = {"Authorization": f"Bearer {auth_token}"} - return headers - - -def create_random_user(db: Session) -> User: - email = random_email() - password = random_lower_string() - user_in = UserCreate(email=email, password=password) - user = crud.create_user(session=db, user_create=user_in) - return user - - -def authentication_token_from_email( - *, client: TestClient, email: str, db: Session -) -> dict[str, str]: - """ - Return a valid token for the user with given email. - - If the user doesn't exist it is created first. - """ - password = random_lower_string() - user = crud.get_user_by_email(session=db, email=email) - if not user: - user_in_create = UserCreate(email=email, password=password) - user = crud.create_user(session=db, user_create=user_in_create) - else: - user_in_update = UserUpdate(password=password) - if not user.id: - raise Exception("User id not set") - user = crud.update_user(session=db, db_user=user, user_in=user_in_update) - - return user_authentication_headers(client=client, email=email, password=password) diff --git a/backend/app/tests/utils/utils.py b/backend/app/tests/utils/utils.py deleted file mode 100644 index 184bac44d9..0000000000 --- a/backend/app/tests/utils/utils.py +++ /dev/null @@ -1,26 +0,0 @@ -import random -import string - -from fastapi.testclient import TestClient - -from app.core.config import settings - - -def random_lower_string() -> str: - return "".join(random.choices(string.ascii_lowercase, k=32)) - - -def random_email() -> str: - return f"{random_lower_string()}@{random_lower_string()}.com" - - -def get_superuser_token_headers(client: TestClient) -> dict[str, str]: - login_data = { - "username": settings.FIRST_SUPERUSER, - "password": settings.FIRST_SUPERUSER_PASSWORD, - } - r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data) - tokens = r.json() - a_token = tokens["access_token"] - headers = {"Authorization": f"Bearer {a_token}"} - return headers diff --git a/backend/app/util.py b/backend/app/util.py new file mode 100644 index 0000000000..717c23fb02 --- /dev/null +++ b/backend/app/util.py @@ -0,0 +1,171 @@ +import logging +import uuid +from typing import TypeVar + +from fastapi import HTTPException +from pydantic import BaseModel +from sqlmodel import Session, SQLModel, select + +from app.models.user import UserBusiness, UserVenueAssociation + +# Generic CRUD function to get all records with pagination +T = TypeVar("T", bound=SQLModel) + + +def get_all_records( + session: Session, model: type[T], skip: int = 0, limit: int = 10 +) -> list[SQLModel]: + """ + Retrieve a paginated list of records as schemas. + - **session**: Database session + - **model**: SQLModel class (e.g., Nightclub, Restaurant, QSR, Foodcourt) + - **skip**: Number of records to skip + - **limit**: Number of records to return + """ + + try: + # Fetch raw model instances + statement = select(model).offset(skip).limit(limit) + result = session.execute(statement).scalars().all() + + # Convert each record to its read schema + return result + except ValueError as ve: + # Handle errors (optional, add specific error handling as needed) + print(f"Error fetching records: {ve}") + return [] + + +def get_record_by_id(db: Session, model: type[T], record_id: uuid.UUID) -> T | None: + """ + Generic function to retrieve a record by its ID. + + Args: + db (Session): The database session. + model (Type[T]): The SQLModel class representing the table. + record_id (uuid.UUID): The ID of the record to retrieve. + + Returns: + Optional[T]: The retrieved record if found, otherwise None. + + Raises: + HTTPException: If the record is not found, raises a 404 error. + """ + record = db.get(model, record_id) + if not record: + raise HTTPException( + status_code=404, detail=f"{model.__name__} with ID {record_id} not found." + ) + return record + + +# Function to create a new record +# Create a new record +def create_record(db: Session, instance: SQLModel) -> SQLModel: + """ + Create a new record in the database with automatic timestamp management. + + :param db: The active database session. + :param model: The SQLModel class representing the database model. + :param instance: An instance of the model to be persisted. + :return: The created instance with updated attributes. + """ + # Current UTC time for timestamping + # current_time = datetime.now() + + # # Setting timestamps for the record + # instance.created_at = current_time + # instance.updated_at = current_time + + # Persisting the new record in the database + db.add(instance) + db.commit() # Commit the changes + db.refresh(instance) # Refresh to load any generated attributes + + return instance # Return the created instance + + +def update_record(db: Session, instance: SQLModel, update_data: BaseModel) -> SQLModel: + """ + Update an existing record in the database, applying only the changes provided by a Pydantic model. + This approach ensures validation of input data and prevents partial updates with invalid fields. + + :param db: Active database session. + :param instance: Existing model instance to be updated. + :param update_data: Pydantic model containing the fields to update. + :return: The updated model instance with changes committed. + """ + try: + # Convert the Pydantic model to a dictionary, excluding unset fields + update_dict = update_data.dict(exclude_unset=True) + + # Apply updates to only the fields that are set in the update_data Pydantic model + for key, value in update_dict.items(): + if hasattr(instance, key): + setattr(instance, key, value) + else: + raise ValueError(f"Field '{key}' does not exist on the model.") + + # Persist the changes in a single transaction + db.add(instance) + db.commit() + db.refresh(instance) + + return instance + + except ValueError as ve: + logging.error("Validation error: %s", ve) + db.rollback() # Undo any changes in case of failure + raise HTTPException(status_code=400, detail=str(ve)) + + except Exception as e: + logging.error("Unexpected error during record update: %s", e) + db.rollback() # Rollback any transaction in case of failure + raise HTTPException( + status_code=500, + detail="An internal error occurred while updating the record.", + ) from e + + +# Function to delete a record +def delete_record(db: Session, instance: SQLModel) -> None: + """ + Delete a record from the database. + + :param db: The active database session. + :param instance: The instance of the model to be deleted. + :return: None + """ + db.delete(instance) + db.commit() + + +def check_user_permission(db: Session, current_user: UserBusiness, venue_id: uuid.UUID): + """ + Check if the user has permission to manage the specified venue. + + Args: + db: Database session. + current_user: The current user object. + venue_id: The ID of the venue to check permissions for. + + Raises: + HTTPException: If the user does not have permission. + + Returns: + UserVenueAssociation: The association record if it exists. + """ + statement = select(UserVenueAssociation).where( + UserVenueAssociation.user_id == current_user.id, + UserVenueAssociation.venue_id == venue_id, + ) + + user_venue_association = db.execute(statement).scalars().first() + + if user_venue_association is None: + raise HTTPException( + status_code=403, + detail="User does not have permission to manage this venue.", + ) + + return user_venue_association diff --git a/backend/app/utils.py b/backend/app/utils.py index d5ccf3153f..34f7324bbc 100644 --- a/backend/app/utils.py +++ b/backend/app/utils.py @@ -1,117 +1,58 @@ -import logging -from dataclasses import dataclass -from datetime import datetime, timedelta, timezone -from pathlib import Path -from typing import Any - -import emails # type: ignore -import jwt -from jinja2 import Template -from jwt.exceptions import InvalidTokenError - -from app.core.config import settings - - -@dataclass -class EmailData: - html_content: str - subject: str - - -def render_email_template(*, template_name: str, context: dict[str, Any]) -> str: - template_str = ( - Path(__file__).parent / "email-templates" / "build" / template_name - ).read_text() - html_content = Template(template_str).render(context) - return html_content - - -def send_email( - *, - email_to: str, - subject: str = "", - html_content: str = "", -) -> None: - assert settings.emails_enabled, "no provided configuration for email variables" - message = emails.Message( - subject=subject, - html=html_content, - mail_from=(settings.EMAILS_FROM_NAME, settings.EMAILS_FROM_EMAIL), - ) - smtp_options = {"host": settings.SMTP_HOST, "port": settings.SMTP_PORT} - if settings.SMTP_TLS: - smtp_options["tls"] = True - elif settings.SMTP_SSL: - smtp_options["ssl"] = True - if settings.SMTP_USER: - smtp_options["user"] = settings.SMTP_USER - if settings.SMTP_PASSWORD: - smtp_options["password"] = settings.SMTP_PASSWORD - response = message.send(to=email_to, smtp=smtp_options) - logging.info(f"send email result: {response}") - - -def generate_test_email(email_to: str) -> EmailData: - project_name = settings.PROJECT_NAME - subject = f"{project_name} - Test email" - html_content = render_email_template( - template_name="test_email.html", - context={"project_name": settings.PROJECT_NAME, "email": email_to}, - ) - return EmailData(html_content=html_content, subject=subject) - - -def generate_reset_password_email(email_to: str, email: str, token: str) -> EmailData: - project_name = settings.PROJECT_NAME - subject = f"{project_name} - Password recovery for user {email}" - link = f"{settings.server_host}/reset-password?token={token}" - html_content = render_email_template( - template_name="reset_password.html", - context={ - "project_name": settings.PROJECT_NAME, - "username": email, - "email": email_to, - "valid_hours": settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS, - "link": link, - }, - ) - return EmailData(html_content=html_content, subject=subject) - - -def generate_new_account_email( - email_to: str, username: str, password: str -) -> EmailData: - project_name = settings.PROJECT_NAME - subject = f"{project_name} - New account for user {username}" - html_content = render_email_template( - template_name="new_account.html", - context={ - "project_name": settings.PROJECT_NAME, - "username": username, - "password": password, - "email": email_to, - "link": settings.server_host, - }, - ) - return EmailData(html_content=html_content, subject=subject) - - -def generate_password_reset_token(email: str) -> str: - delta = timedelta(hours=settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS) - now = datetime.now(timezone.utc) - expires = now + delta - exp = expires.timestamp() - encoded_jwt = jwt.encode( - {"exp": exp, "nbf": now, "sub": email}, - settings.SECRET_KEY, - algorithm="HS256", - ) - return encoded_jwt - - -def verify_password_reset_token(token: str) -> str | None: - try: - decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) - return str(decoded_token["sub"]) - except InvalidTokenError: - return None +import re + +import unshortenit + + +class CoordinatesNotFoundError(Exception): + """Custom exception raised when coordinates cannot be found in the Google Maps link.""" + + pass + + +def extract_coordinates_from_full_link(link: str) -> tuple[float, float] | None: + """ + Extracts latitude and longitude from a full Google Maps link. + + Args: + link (str): The full Google Maps URL. + + Returns: + Optional[Tuple[float, float]]: A tuple containing latitude and longitude, or None if not found. + """ + lat_long_regex = r"@([-+]?\d*\.\d+),([-+]?\d*\.\d+)" + match = re.search(lat_long_regex, link) + + if match: + latitude = float(match.group(1)) + longitude = float(match.group(2)) + return latitude, longitude + + return None + + +def get_coordinates_from_google_maps( + link: str, +) -> list[tuple[str, float, float]] | None: + """ + Retrieves latitude and longitude from a Google Maps link by unshortening it and extracting coordinates. + + Args: + link (str): The Google Maps URL. + + Raises: + CoordinatesNotFoundError: If coordinates are not found in the link. + + Returns: + Optional[list[Tuple[str, float, float]]]: A list of tuples containing the link, latitude, and longitude, + or raises an exception if coordinates are not found. + """ + # Unshorten the URL if it's a shortened link + unshortened_url = unshortenit.unshorten(link) + + # Attempt to extract coordinates from the full link + coordinates = extract_coordinates_from_full_link(unshortened_url) + if coordinates: + latitude, longitude = coordinates + return [(unshortened_url, latitude, longitude)] + + raise CoordinatesNotFoundError(f"No coordinates found in the provided link: {link}") diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py new file mode 100644 index 0000000000..73b91aab81 --- /dev/null +++ b/backend/app/utils/__init__.py @@ -0,0 +1 @@ +from .h3_utils import get_h3_index, is_within_radius \ No newline at end of file diff --git a/backend/app/utils/h3_utils.py b/backend/app/utils/h3_utils.py new file mode 100644 index 0000000000..3d6fc57817 --- /dev/null +++ b/backend/app/utils/h3_utils.py @@ -0,0 +1,8 @@ +import h3 + + +def get_h3_index(latitude: float, longitude: float, resolution: int = 9) -> str: + return h3.geo_to_h3(latitude, longitude, resolution) + +def is_within_radius(user_h3_index: str, poster_h3_index: str, radius: int) -> bool: + return h3.h3_distance(user_h3_index, poster_h3_index) <= radius diff --git a/backend/data.json b/backend/data.json new file mode 100644 index 0000000000..04b6f88589 --- /dev/null +++ b/backend/data.json @@ -0,0 +1,6190 @@ +{ + "pages": { + "current": { + "name": "restaurant", + "pageTitle": "Schezwan Spicy Food, Modinagar Locality order online - Zomato", + "pageDescription": "Order food online from Schezwan Spicy Food, Modinagar Locality, Modinagar. Get great offers and super fast food delivery when you order food online from Schezwan Spicy Food on Zomato.", + "resId": 19713383, + "pageUrl": "/modinagar/schezwan-spicy-food-modinagar-locality/order", + "canonicalUrl": "https://www.zomato.com/modinagar/schezwan-spicy-food-modinagar-locality/order", + "title": "Order Online", + "subType": "order", + "key": "order", + "ogTitle": "Schezwan Spicy Food, Modinagar Locality order online - Zomato", + "ogDescription": "Order food online from Schezwan Spicy Food, Modinagar Locality, Modinagar. Get great offers and super fast food delivery when you order food online from Schezwan Spicy Food on Zomato.", + "ogUrl": "/modinagar/schezwan-spicy-food-modinagar-locality/order", + "ampHtmlUrl": "", + "isFloodReliefRes": false, + "isNoIndex": false, + "checkoutUrl": "/modinagar/schezwan-spicy-food-modinagar-locality/order/verify", + "show_rating_v15": true, + "showBookingV2": true, + "isRestaurantPageV2": true, + "isMobile": 0, + "isOAuthV2Enabled": false, + "useAuthSdkForLogin": true, + "useAuthSdkForLogout": false, + "gaPageType": "Restaurant" + }, + "contact": { + "contactPageBannerData": [], + "snippetData": [], + "formData": {}, + "buisinessEnquiriesData": {} + }, + "gift": { + "crystalData": {} + }, + "goodbye": {}, + "restaurant": { + "19713383": { + "sections": { + "SECTION_IMAGE_CAROUSEL": { + "entities": [ + { + "entity_type": "IMAGES", + "entity_ids": [ + "r_YwMjUyMTA4OTIw" + ] + } + ], + "has_more_photo": false, + "obpImage": { + "entity_type": "IMAGES", + "entity_ids": [] + }, + "is_partner": false + }, + "SECTION_BASIC_INFO": { + "res_id": 19713383, + "name": "Schezwan Spicy Food", + "cuisine_string": "Fast Food, Chinese", + "rating": { + "has_fake_reviews": 0, + "aggregate_rating": "4.3", + "rating_text": "4.3", + "rating_subtitle": "Very Good", + "rating_color": "5BA829", + "votes": 156, + "subtext": "REVIEWS", + "is_new": false + }, + "rating_new": { + "newlyOpenedObj": null, + "suspiciousReviewObj": null, + "ratings": { + "DINING": { + "rating_type": "DINING", + "rating": "", + "reviewCount": "0", + "reviewTextSmall": "0 Reviews", + "subtext": "Does not offer Dining", + "color": "", + "ratingV2": "-", + "subtitle": "DINING", + "sideSubTitle": "Dining Ratings", + "bgColorV2": { + "type": "green", + "tint": "100" + }, + "textColorV2": { + "type": "green", + "tint": "500" + }, + "newOnDining": false + }, + "DELIVERY": { + "rating_type": "DELIVERY", + "rating": "4.3", + "reviewCount": "156", + "reviewTextSmall": "156 Reviews", + "subtext": "156 Delivery Reviews", + "color": "#E23744", + "ratingV2": "4.3", + "subtitle": "DELIVERY", + "sideSubTitle": "Delivery Ratings", + "bgColorV2": { + "type": "green", + "tint": "700" + }, + "newOnDelivery": false + } + } + }, + "res_status_text": "Closed", + "timing": { + "timing_desc": "Opens tomorrow at 11:22am", + "customised_timings": { + "opening_hours": [ + { + "timing": "11:22am \u2013 9:26pm", + "days": "Mon" + }, + { + "timing": "Closed", + "days": "Tue-Sun" + } + ] + }, + "show_open_now": false, + "show_timing_info": false + }, + "is_delivery_only": true, + "is_perm_closed": false, + "is_temp_closed": false, + "is_opening_soon": 0, + "should_ban_ugc": false, + "is_shelled": false, + "media_alert": 0, + "learn_more_text": "Learn More", + "res_thumb": "https://b.zmtcdn.com/images/res_avatar_476_320_1x_new.png?output-format=webp", + "disclaimer_text": "", + "resUrl": "/modinagar/schezwan-spicy-food-modinagar-locality", + "enableClientSideAds": false, + "is_partner": false, + "disable_open_app": 0, + "backToHomeUrl": "https://www.zomato.com/modinagar/restaurants?order-online=1" + }, + "SECTION_FEATURE_RAIL": [], + "SECTION_RES_HEADER_DETAILS": { + "LOCALITY": { + "text": "Modinagar Locality, Modinagar", + "url": "https://www.zomato.com/modinagar/modinagar-locality-restaurants" + }, + "CUISINES": [ + { + "deeplink": "zomato://search?deeplink_filters=WyJ7XCJjb250ZXh0XCI6XCJhbGxcIn0iLCJ7XCJjdWlzaW5lX2lkXCI6W1wiNDBcIl19Il0%3D", + "url": "https://www.zomato.com/modinagar/restaurants/fast-food/", + "name": "Fast Food" + }, + { + "deeplink": "zomato://search?deeplink_filters=WyJ7XCJjb250ZXh0XCI6XCJhbGxcIn0iLCJ7XCJjdWlzaW5lX2lkXCI6W1wiMjVcIl19Il0%3D", + "url": "https://www.zomato.com/modinagar/restaurants/chinese/", + "name": "Chinese" + } + ], + "ESTABLISHMENTS": [] + }, + "SECTION_RES_CONTACT": { + "city_id": 11595, + "city_name": "Modinagar", + "country_id": 1, + "country_name": "India", + "zipcode": "201204", + "is_dark_kitchen": false, + "locality_verbose": "Modinagar Locality, Modinagar", + "latitude": "28.8302229471", + "longitude": "77.5706658886", + "static_map_url": "https://maps.zomato.com/php/staticmap?center=28.8302229471,77.5706658886&maptype=zomato&markers=28.8302229471,77.5706658886,pin_res32&sensor=false&scale=2&zoom=16&language=en&size=240x150&size=400x240", + "address": "Upper Bazar, near Jain shikanji Modinagar Locality", + "is_phone_available": 1, + "phoneDetails": { + "title": "Phone Numbers", + "phoneStr": "+918979041283" + }, + "res_chain_text": "", + "res_group_text": "", + "res_chain_url": "", + "res_group_url": "", + "show_res_map": true + }, + "SECTION_MAGIC_LINKS": [ + { + "title": "Related to Schezwan Spicy Food, Modinagar Locality", + "magicLinks": [ + { + "url": "https://www.zomato.com/modinagar", + "title": "Modinagar Restaurants", + "displayName": "Restaurants in Modinagar, Modinagar Restaurants" + }, + { + "url": "https://www.zomato.com/modinagar/modinagar-locality-restaurants", + "title": "Modinagar Locality restaurants", + "displayName": "Modinagar Locality restaurants" + }, + { + "url": "https://www.zomato.com/modinagar/modinagar-locality-restaurants?sort=best", + "title": "Best Modinagar Locality restaurants", + "displayName": "Best Modinagar Locality restaurants" + }, + { + "url": "https://www.zomato.com/modinagar/modinagar-city-restaurants", + "title": "Modinagar City restaurants", + "displayName": "Modinagar City restaurants" + }, + { + "url": "https://www.zomato.com/modinagar", + "title": " in Modinagar", + "displayName": " in Modinagar" + }, + { + "url": "https://www.zomato.com/restaurants-near-me", + "title": " near me", + "displayName": " near me" + }, + { + "url": "https://www.zomato.com/modinagar/modinagar-locality-restaurants", + "title": " in Modinagar Locality", + "displayName": " in Modinagar Locality" + }, + { + "url": "https://www.zomato.com/modinagar", + "title": " in Modinagar", + "displayName": " in Modinagar" + }, + { + "url": "https://www.zomato.com/restaurants-near-me", + "title": " near me", + "displayName": " near me" + }, + { + "url": "https://www.zomato.com/modinagar/modinagar-locality-restaurants", + "title": " in Modinagar Locality", + "displayName": " in Modinagar Locality" + }, + { + "url": "https://www.zomato.com/modinagar/modinagar-locality-restaurants?order-online=1", + "title": "Order food online in Modinagar Locality", + "displayName": "Order food online in Modinagar Locality" + }, + { + "url": "https://www.zomato.com/modinagar/restaurants?order-online=1", + "title": "Order food online in Modinagar", + "displayName": "Order food online in Modinagar" + } + ] + }, + { + "title": "Restaurants around Modinagar Locality", + "magicLinks": [ + { + "url": "https://www.zomato.com/modinagar/muradnagar-locality-restaurants", + "title": "List of Muradnagar Locality restaurants", + "displayName": "Muradnagar Locality restaurants" + } + ] + }, + { + "title": "Frequent searches leading to this page", + "magicLinks": [ + { + "url": "https://www.zomato.com/modinagar/schezwan-spicy-food-modinagar-locality", + "title": "", + "displayName": "schezwan spicy food menu" + }, + { + "url": "https://www.zomato.com/modinagar/schezwan-spicy-food-modinagar-locality", + "title": "", + "displayName": "schezwan spicy food modinagar locality menu" + }, + { + "url": "https://www.zomato.com/modinagar/schezwan-spicy-food-modinagar-locality", + "title": "", + "displayName": "schezwan spicy food modinagar" + }, + { + "url": "https://www.zomato.com/modinagar/schezwan-spicy-food-modinagar-locality", + "title": "", + "displayName": "schezwan spicy food modinagar menu" + }, + { + "url": "https://www.zomato.com/modinagar/schezwan-spicy-food-modinagar-locality", + "title": "", + "displayName": "schezwan spicy food restaurant" + } + ] + }, + { + "title": "Top Stores", + "magicLinks": [] + } + ], + "SECTION_RATING_DATA": { + "header": "Rate your experience for", + "options": [ + { + "value": "dining", + "label": "Dining" + }, + { + "value": "delivery", + "label": "Delivery" + } + ], + "selected": "dining" + }, + "SECTION_OBP_TAGS": [ + { + "entityType": "isDeliveryOnly", + "isImage": false, + "title": "Delivery Only", + "imageUrl": "" + } + ], + "SECTION_BREADCRUMBS": [ + { + "title": "Home", + "url": "https://www.zomato.com" + }, + { + "title": "India", + "url": "https://www.zomato.com/india" + }, + { + "title": "Modinagar", + "url": "https://www.zomato.com/modinagar/restaurants" + }, + { + "title": "Modinagar Locality", + "url": "https://www.zomato.com/modinagar/modinagar-locality-restaurants" + }, + { + "title": "Schezwan Spicy Food", + "url": "https://www.zomato.com/modinagar/schezwan-spicy-food-modinagar-locality/order" + }, + { + "title": "Order Online", + "url": "" + } + ], + "SECTION_USER_ACTIONS": { + "share": { + "url": "http://zoma.to/r/19713383" + }, + "review": { + "reviewed": false + }, + "photo": [], + "bookmark": { + "count": 0, + "bookmarked": false + } + } + }, + "navbarSection": [ + { + "name": "restaurant", + "pageTitle": "Schezwan Spicy Food, Modinagar Locality order online - Zomato", + "pageDescription": "Order food online from Schezwan Spicy Food, Modinagar Locality, Modinagar. Get great offers and super fast food delivery when you order food online from Schezwan Spicy Food on Zomato.", + "resId": 19713383, + "pageUrl": "/modinagar/schezwan-spicy-food-modinagar-locality/order", + "canonicalUrl": "https://www.zomato.com/modinagar/schezwan-spicy-food-modinagar-locality/order", + "title": "Order Online", + "subType": "order", + "key": "order", + "ogTitle": "Schezwan Spicy Food, Modinagar Locality order online - Zomato", + "ogDescription": "Order food online from Schezwan Spicy Food, Modinagar Locality, Modinagar. Get great offers and super fast food delivery when you order food online from Schezwan Spicy Food on Zomato.", + "ogUrl": "/modinagar/schezwan-spicy-food-modinagar-locality/order", + "ampHtmlUrl": "", + "isFloodReliefRes": false, + "isNoIndex": false, + "checkoutUrl": "/modinagar/schezwan-spicy-food-modinagar-locality/order/verify", + "children": [ + { + "key": "ctg_22246933", + "title": "Starters (6)" + }, + { + "key": "ctg_22246934", + "title": "Main Course (1)" + }, + { + "key": "ctg_22246916", + "title": "Fried Rice and Noodles (7)" + }, + { + "key": "ctg_22246915", + "title": "Burgers (2)" + }, + { + "key": "ctg_22246917", + "title": "Rolls (2)" + }, + { + "key": "ctg_22246914", + "title": "Momos (4)" + }, + { + "key": "ctg_22246911", + "title": "Snacks (2)" + } + ] + }, + { + "name": "restaurant", + "pageTitle": "Reviews of Schezwan Spicy Food, Modinagar Locality, Modinagar | Zomato ", + "pageDescription": "Reviews of Schezwan Spicy Food Modinagar Locality; see all unbiased reviews of Schezwan Spicy Food Modinagar for delivery and dining on Zomato.", + "resId": 19713383, + "pageUrl": "/modinagar/schezwan-spicy-food-modinagar-locality/reviews", + "canonicalUrl": "https://www.zomato.com/modinagar/schezwan-spicy-food-modinagar-locality/reviews", + "title": "Reviews", + "subType": "reviews", + "key": "reviews", + "ogTitle": "Reviews of Schezwan Spicy Food, Modinagar Locality, Modinagar | Zomato ", + "ogDescription": "Reviews of Schezwan Spicy Food Modinagar Locality; see all unbiased reviews of Schezwan Spicy Food Modinagar for delivery and dining on Zomato.", + "ogUrl": "/modinagar/schezwan-spicy-food-modinagar-locality/reviews", + "ampHtmlUrl": "/modinagar/schezwan-spicy-food-modinagar-locality/reviews?amp=1", + "isFloodReliefRes": false, + "isNoIndex": false + } + ], + "trackingData": { + "googleAdsPayload": { + "addToCart": { + "eventName": "conversion", + "payload": { + "send_to": "AW-958674130/1QC4COu6ne0BENLpkMkD" + } + } + } + }, + "orderDetails": { + "hasOnlineOrdering": true, + "isServiceable": false, + "promoOffer": "", + "promoSubText": "", + "deeplink": "zomato://order/19713383", + "deliveryTime": "", + "onlineStatusCode": 710, + "statusReasonCode": 7210, + "trackingText": "Live tracking not available", + "minOrderAmountDetails": { + "minOrderAmount": 0, + "minOrderDisplayAmount": "\u20b9 0", + "minOrderAmountDisplayText": "\u20b9 0 minimum item total required to place an order" + }, + "isTrackingAvailableOnApp": null, + "isO2Active": false + }, + "takeAwayDetails": null, + "experimentParams": { + "promo_blocker_on_page_load": false, + "show_native_promo_blocker": false + }, + "metaData": { + "currencyDetails": { + "currency": "\u20b9", + "currencyISOCode": "INR", + "currency_affix": "prefix", + "currency_on_right": 0 + }, + "offers": [] + }, + "order": { + "menuList": { + "menus": [ + { + "menu": { + "id": "ctg_22246933", + "name": "Starters", + "cart_category_id": 0, + "categories": [ + { + "category": { + "id": "s_ctg_33446769", + "name": "", + "items": [ + { + "item": { + "id": "ctl_339496832", + "name": "Manchurian Dry", + "price": 0, + "desc": "", + "min_price": 80, + "max_price": 140, + "default_price": 80, + "display_price": 80, + "variant_id": "", + "parent_menu_id": "ctg_22246933", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "manchuriandry", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Starters", + "fb_slug": "manchuriandry-starters-19713383", + "item_metadata": "{\"v\":0,\"s_ctg\":33446769,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"manchurian-dry\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1, + "groups": [ + { + "group": { + "id": "p_36442593", + "name": "Quantity", + "label": "Quantity", + "min": 1, + "max": 1, + "parent_menu_id": 0, + "hasFocus": 0, + "entity_type": "property", + "name_slug": "quantity", + "tracking_metadata": "{\"has_discounted_addon\":false,\"is_dynamic_modifier_group\":false,\"is_dynamic_modifier_group_v2\":false}", + "max_selections_per_item": 1, + "parent_visiblity": 1, + "show_customisation": true, + "items": [ + { + "item": { + "id": "pv_100061504", + "name": "Half", + "price": 80, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524524", + "parent_menu_id": "ctg_22246933", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "half", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "is_default": 1, + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Starters", + "fb_slug": "", + "item_metadata": "{\"v\":424524524,\"s_ctg\":33446769,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"manchurian-dry\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + }, + { + "item": { + "id": "pv_100061505", + "name": "Full", + "price": 140, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524525", + "parent_menu_id": "ctg_22246933", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "full", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Starters", + "fb_slug": "", + "item_metadata": "{\"v\":424524525,\"s_ctg\":33446769,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"manchurian-dry\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + } + ], + "reference_items": [] + } + } + ], + "enable_customisation_on_cart": false, + "customisation_tracking_metadata": "{\"exp_no\":1,\"default_v_price\":80,\"lowest_v_price\":80,\"highest_v_price\":140,\"total_mgs\":-1,\"total_v_count\":2,\"properties\":1}" + } + }, + { + "item": { + "id": "ctl_352829567", + "name": "Chilli Potato", + "price": 0, + "desc": "", + "min_price": 70, + "max_price": 110, + "default_price": 70, + "display_price": 70, + "variant_id": "", + "parent_menu_id": "ctg_22246933", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "chillipotato", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Starters", + "fb_slug": "chillipotato-starters-19713383", + "item_metadata": "{\"v\":0,\"s_ctg\":33446769,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"chilli-potato\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1, + "groups": [ + { + "group": { + "id": "p_37533689", + "name": "Quantity", + "label": "Quantity", + "min": 1, + "max": 1, + "parent_menu_id": 0, + "hasFocus": 0, + "entity_type": "property", + "name_slug": "quantity", + "tracking_metadata": "{\"has_discounted_addon\":false,\"is_dynamic_modifier_group\":false,\"is_dynamic_modifier_group_v2\":false}", + "max_selections_per_item": 1, + "parent_visiblity": 1, + "show_customisation": true, + "items": [ + { + "item": { + "id": "pv_103197755", + "name": "Half", + "price": 70, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_440195575", + "parent_menu_id": "ctg_22246933", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "half", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "is_default": 1, + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Starters", + "fb_slug": "", + "item_metadata": "{\"v\":440195575,\"s_ctg\":33446769,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"chilli-potato\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + }, + { + "item": { + "id": "pv_103197756", + "name": "Full", + "price": 110, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_440195576", + "parent_menu_id": "ctg_22246933", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "full", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Starters", + "fb_slug": "", + "item_metadata": "{\"v\":440195576,\"s_ctg\":33446769,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"chilli-potato\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + } + ], + "reference_items": [] + } + } + ], + "enable_customisation_on_cart": false, + "customisation_tracking_metadata": "{\"exp_no\":1,\"default_v_price\":70,\"lowest_v_price\":70,\"highest_v_price\":110,\"total_mgs\":-1,\"total_v_count\":2,\"properties\":1}" + } + }, + { + "item": { + "id": "ctl_432129807", + "name": "Honey Chilli Potato", + "price": 0, + "desc": "", + "min_price": 80, + "max_price": 135, + "default_price": 80, + "display_price": 80, + "variant_id": "", + "parent_menu_id": "ctg_22246933", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 3, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "honeychillipotato", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "sf-not-vegan", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services", + "sf-not-vegan" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Starters", + "fb_slug": "honeychillipotato-starters-19713383", + "item_metadata": "{\"v\":0,\"s_ctg\":33446769,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"honey-chilli-potato\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1, + "groups": [ + { + "group": { + "id": "p_43856069", + "name": "Quantity", + "label": "Quantity", + "min": 1, + "max": 1, + "parent_menu_id": 0, + "hasFocus": 0, + "entity_type": "property", + "name_slug": "quantity", + "tracking_metadata": "{\"has_discounted_addon\":false,\"is_dynamic_modifier_group\":false,\"is_dynamic_modifier_group_v2\":false}", + "max_selections_per_item": 1, + "parent_visiblity": 1, + "show_customisation": true, + "items": [ + { + "item": { + "id": "pv_120454631", + "name": "Half", + "price": 80, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_532968853", + "parent_menu_id": "ctg_22246933", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "half", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "is_default": 1, + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Starters", + "fb_slug": "", + "item_metadata": "{\"v\":532968853,\"s_ctg\":33446769,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"honey-chilli-potato\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + }, + { + "item": { + "id": "pv_120454632", + "name": "Full", + "price": 135, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_532968854", + "parent_menu_id": "ctg_22246933", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "full", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Starters", + "fb_slug": "", + "item_metadata": "{\"v\":532968854,\"s_ctg\":33446769,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"honey-chilli-potato\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + } + ], + "reference_items": [] + } + } + ], + "enable_customisation_on_cart": false, + "customisation_tracking_metadata": "{\"exp_no\":1,\"default_v_price\":80,\"lowest_v_price\":80,\"highest_v_price\":135,\"total_mgs\":-1,\"total_v_count\":2,\"properties\":1}" + } + }, + { + "item": { + "id": "ctl_432129808", + "name": "Paneer 65", + "price": 160, + "desc": "", + "min_price": 160, + "max_price": 160, + "default_price": 160, + "display_price": 160, + "variant_id": "v_532968855", + "parent_menu_id": "ctg_22246933", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 4, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "paneer65", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "sf-not-vegan", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services", + "sf-not-vegan" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Starters", + "fb_slug": "paneer65-starters-19713383", + "item_metadata": "{\"v\":532968855,\"s_ctg\":33446769,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"paneer-65\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1 + } + }, + { + "item": { + "id": "ctl_432129809", + "name": "Paneer Chilli", + "price": 180, + "desc": "", + "min_price": 180, + "max_price": 180, + "default_price": 180, + "display_price": 180, + "variant_id": "v_532968856", + "parent_menu_id": "ctg_22246933", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 5, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "paneerchilli", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "sf-not-vegan", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services", + "sf-not-vegan" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Starters", + "fb_slug": "paneerchilli-starters-19713383", + "item_metadata": "{\"v\":532968856,\"s_ctg\":33446769,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"paneer-chilli\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1 + } + }, + { + "item": { + "id": "ctl_437305137", + "name": "Chole Bhature", + "price": 100, + "desc": "O", + "min_price": 100, + "max_price": 100, + "default_price": 100, + "display_price": 100, + "variant_id": "v_539061466", + "parent_menu_id": "ctg_22246933", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 6, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "cholebhature", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Starters", + "fb_slug": "cholebhature-starters-19713383", + "item_metadata": "{\"v\":539061466,\"s_ctg\":33446769,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"chole-bhature\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1 + } + } + ], + "reference_items": [], + "tag_ids": "1", + "is_expanded": 0 + } + } + ], + "is_expanded": 1, + "should_disable_items": false, + "subtitle": null, + "stepper_disabled_text": "", + "is_hidden": false + } + }, + { + "menu": { + "id": "ctg_22246934", + "name": "Main Course", + "cart_category_id": 0, + "categories": [ + { + "category": { + "id": "s_ctg_33446770", + "name": "", + "items": [ + { + "item": { + "id": "ctl_339496835", + "name": "Manchurian Gravy", + "price": 0, + "desc": "", + "min_price": 95, + "max_price": 160, + "default_price": 95, + "display_price": 95, + "variant_id": "", + "parent_menu_id": "ctg_22246934", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "manchuriangravy", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Main Course", + "fb_slug": "manchuriangravy-maincourse-19713383", + "item_metadata": "{\"v\":0,\"s_ctg\":33446770,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"manchurian-gravy\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1, + "groups": [ + { + "group": { + "id": "p_36442594", + "name": "Quantity", + "label": "Quantity", + "min": 1, + "max": 1, + "parent_menu_id": 0, + "hasFocus": 0, + "entity_type": "property", + "name_slug": "quantity", + "tracking_metadata": "{\"has_discounted_addon\":false,\"is_dynamic_modifier_group\":false,\"is_dynamic_modifier_group_v2\":false}", + "max_selections_per_item": 1, + "parent_visiblity": 1, + "show_customisation": true, + "items": [ + { + "item": { + "id": "pv_100061506", + "name": "Half", + "price": 95, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524528", + "parent_menu_id": "ctg_22246934", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "half", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "is_default": 1, + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Main Course", + "fb_slug": "", + "item_metadata": "{\"v\":424524528,\"s_ctg\":33446770,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"manchurian-gravy\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + }, + { + "item": { + "id": "pv_100061507", + "name": "Full", + "price": 160, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524529", + "parent_menu_id": "ctg_22246934", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "full", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Main Course", + "fb_slug": "", + "item_metadata": "{\"v\":424524529,\"s_ctg\":33446770,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"manchurian-gravy\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + } + ], + "reference_items": [] + } + } + ], + "enable_customisation_on_cart": false, + "customisation_tracking_metadata": "{\"exp_no\":1,\"default_v_price\":95,\"lowest_v_price\":95,\"highest_v_price\":160,\"total_mgs\":-1,\"total_v_count\":2,\"properties\":1}" + } + } + ], + "reference_items": [], + "tag_ids": "1", + "is_expanded": 0 + } + } + ], + "is_expanded": 0, + "should_disable_items": false, + "subtitle": null, + "stepper_disabled_text": "", + "is_hidden": false + } + }, + { + "menu": { + "id": "ctg_22246916", + "name": "Fried Rice and Noodles", + "cart_category_id": 0, + "categories": [ + { + "category": { + "id": "s_ctg_33446768", + "name": "Fried Rice", + "items": [ + { + "item": { + "id": "ctl_339496829", + "name": "Fried Rice", + "price": 0, + "desc": "", + "min_price": 90, + "max_price": 150, + "default_price": 90, + "display_price": 90, + "variant_id": "", + "parent_menu_id": "ctg_22246916", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "friedrice", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Fried Rice and Noodles", + "fb_slug": "friedrice-friedriceandnoodles-19713383", + "item_metadata": "{\"v\":0,\"s_ctg\":33446768,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"fried-rice\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1, + "groups": [ + { + "group": { + "id": "p_36442591", + "name": "Quantity", + "label": "Quantity", + "min": 1, + "max": 1, + "parent_menu_id": 0, + "hasFocus": 0, + "entity_type": "property", + "name_slug": "quantity", + "tracking_metadata": "{\"has_discounted_addon\":false,\"is_dynamic_modifier_group\":false,\"is_dynamic_modifier_group_v2\":false}", + "max_selections_per_item": 1, + "parent_visiblity": 1, + "show_customisation": true, + "items": [ + { + "item": { + "id": "pv_100061500", + "name": "Half", + "price": 90, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524519", + "parent_menu_id": "ctg_22246916", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "half", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "is_default": 1, + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Fried Rice and Noodles", + "fb_slug": "", + "item_metadata": "{\"v\":424524519,\"s_ctg\":33446768,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"fried-rice\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + }, + { + "item": { + "id": "pv_100061501", + "name": "Full", + "price": 150, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524520", + "parent_menu_id": "ctg_22246916", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "full", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Fried Rice and Noodles", + "fb_slug": "", + "item_metadata": "{\"v\":424524520,\"s_ctg\":33446768,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"fried-rice\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + } + ], + "reference_items": [] + } + } + ], + "enable_customisation_on_cart": false, + "customisation_tracking_metadata": "{\"exp_no\":1,\"default_v_price\":90,\"lowest_v_price\":90,\"highest_v_price\":150,\"total_mgs\":-1,\"total_v_count\":2,\"properties\":1}" + } + }, + { + "item": { + "id": "ctl_339496830", + "name": "Triple Fried Rice", + "price": 180, + "desc": "", + "min_price": 180, + "max_price": 180, + "default_price": 180, + "display_price": 180, + "variant_id": "v_424524521", + "parent_menu_id": "ctg_22246916", + "item_image_url": "https://b.zmtcdn.com/data/dish_photos/f4b/7ba5a5491a1d6bdfe6df1e3b1342df4b.jpeg", + "item_image_thumb_url": "https://b.zmtcdn.com/data/dish_photos/f4b/7ba5a5491a1d6bdfe6df1e3b1342df4b.jpeg?fit=around%7C200%3A200&crop=200%3A200%3B%2A%2C%2A", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": true, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "triplefriedrice", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [ + { + "mediaType": "image", + "image": { + "url": "https://b.zmtcdn.com/data/dish_photos/f4b/7ba5a5491a1d6bdfe6df1e3b1342df4b.jpeg?fit=around%7C200%3A200&crop=200%3A200%3B%2A%2C%2A", + "data": null, + "bgColor": null, + "borderColor": null, + "bgColorHex": null, + "animation": null, + "animate": null, + "aspectRatio": null, + "type": null, + "height": null, + "width": null, + "clickAction": null, + "border": null, + "shouldIgnoreResizing": null, + "tracking": null, + "placeholderColor": null, + "heightRatio": null, + "scaleMode": null, + "filter": null, + "overlayTextData": null, + "overlayIcon": null, + "containerAnimations": null, + "overlayAnimation": null, + "offsetRatio": null, + "shouldExpand": null, + "cornerRadius": null, + "trackingData": null, + "gravity": null, + "repeatCount": null, + "themeConfig": null + }, + "video": null, + "audio": null, + "lottie": null + } + ], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Fried Rice and Noodles", + "fb_slug": "triplefriedrice-friedriceandnoodles-19713383", + "item_metadata": "{\"v\":424524521,\"s_ctg\":33446768,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"triple-fried-rice\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1 + } + }, + { + "item": { + "id": "ctl_339496831", + "name": "Schezwan Fried Rice", + "price": 0, + "desc": "", + "min_price": 120, + "max_price": 190, + "default_price": 120, + "display_price": 120, + "variant_id": "", + "parent_menu_id": "ctg_22246916", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 3, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "schezwanfriedrice", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "sf-spicy", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services", + "sf-spicy" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Fried Rice and Noodles", + "fb_slug": "schezwanfriedrice-friedriceandnoodles-19713383", + "item_metadata": "{\"v\":0,\"s_ctg\":33446768,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"schezwan-fried-rice\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1, + "groups": [ + { + "group": { + "id": "p_36442592", + "name": "Quantity", + "label": "Quantity", + "min": 1, + "max": 1, + "parent_menu_id": 0, + "hasFocus": 0, + "entity_type": "property", + "name_slug": "quantity", + "tracking_metadata": "{\"has_discounted_addon\":false,\"is_dynamic_modifier_group\":false,\"is_dynamic_modifier_group_v2\":false}", + "max_selections_per_item": 1, + "parent_visiblity": 1, + "show_customisation": true, + "items": [ + { + "item": { + "id": "pv_100061502", + "name": "Half", + "price": 120, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524522", + "parent_menu_id": "ctg_22246916", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "half", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "is_default": 1, + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Fried Rice and Noodles", + "fb_slug": "", + "item_metadata": "{\"v\":424524522,\"s_ctg\":33446768,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"schezwan-fried-rice\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + }, + { + "item": { + "id": "pv_100061503", + "name": "Full", + "price": 190, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524523", + "parent_menu_id": "ctg_22246916", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "full", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Fried Rice and Noodles", + "fb_slug": "", + "item_metadata": "{\"v\":424524523,\"s_ctg\":33446768,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"schezwan-fried-rice\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + } + ], + "reference_items": [] + } + } + ], + "enable_customisation_on_cart": false, + "customisation_tracking_metadata": "{\"exp_no\":1,\"default_v_price\":120,\"lowest_v_price\":120,\"highest_v_price\":190,\"total_mgs\":-1,\"total_v_count\":2,\"properties\":1}" + } + } + ], + "reference_items": [], + "tag_ids": "1", + "is_expanded": 0 + } + }, + { + "category": { + "id": "s_ctg_33446742", + "name": "Noodles", + "items": [ + { + "item": { + "id": "ctl_339496656", + "name": "Veg Noodles", + "price": 0, + "desc": "", + "min_price": 55, + "max_price": 95, + "default_price": 55, + "display_price": 55, + "variant_id": "", + "parent_menu_id": "ctg_22246916", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "vegnoodles", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Fried Rice and Noodles", + "fb_slug": "vegnoodles-friedriceandnoodles-19713383", + "item_metadata": "{\"v\":0,\"s_ctg\":33446742,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"veg-noodles\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1, + "groups": [ + { + "group": { + "id": "p_36442545", + "name": "Quantity", + "label": "Quantity", + "min": 1, + "max": 1, + "parent_menu_id": 0, + "hasFocus": 0, + "entity_type": "property", + "name_slug": "quantity", + "tracking_metadata": "{\"has_discounted_addon\":false,\"is_dynamic_modifier_group\":false,\"is_dynamic_modifier_group_v2\":false}", + "max_selections_per_item": 1, + "parent_visiblity": 1, + "show_customisation": true, + "items": [ + { + "item": { + "id": "pv_100061399", + "name": "Half", + "price": 55, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524291", + "parent_menu_id": "ctg_22246916", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "half", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "is_default": 1, + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Fried Rice and Noodles", + "fb_slug": "", + "item_metadata": "{\"v\":424524291,\"s_ctg\":33446742,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"veg-noodles\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + }, + { + "item": { + "id": "pv_100061400", + "name": "Full", + "price": 95, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524292", + "parent_menu_id": "ctg_22246916", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "full", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Fried Rice and Noodles", + "fb_slug": "", + "item_metadata": "{\"v\":424524292,\"s_ctg\":33446742,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"veg-noodles\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + } + ], + "reference_items": [] + } + } + ], + "enable_customisation_on_cart": false, + "customisation_tracking_metadata": "{\"exp_no\":1,\"default_v_price\":55,\"lowest_v_price\":55,\"highest_v_price\":95,\"total_mgs\":-1,\"total_v_count\":2,\"properties\":1}" + } + }, + { + "item": { + "id": "ctl_339496657", + "name": "Singapuri Noodles", + "price": 0, + "desc": "", + "min_price": 80, + "max_price": 135, + "default_price": 80, + "display_price": 80, + "variant_id": "", + "parent_menu_id": "ctg_22246916", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "singapurinoodles", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Fried Rice and Noodles", + "fb_slug": "singapurinoodles-friedriceandnoodles-19713383", + "item_metadata": "{\"v\":0,\"s_ctg\":33446742,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"singapuri-noodles\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1, + "groups": [ + { + "group": { + "id": "p_36442546", + "name": "Quantity", + "label": "Quantity", + "min": 1, + "max": 1, + "parent_menu_id": 0, + "hasFocus": 0, + "entity_type": "property", + "name_slug": "quantity", + "tracking_metadata": "{\"has_discounted_addon\":false,\"is_dynamic_modifier_group\":false,\"is_dynamic_modifier_group_v2\":false}", + "max_selections_per_item": 1, + "parent_visiblity": 1, + "show_customisation": true, + "items": [ + { + "item": { + "id": "pv_100061401", + "name": "Half", + "price": 80, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524293", + "parent_menu_id": "ctg_22246916", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "half", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "is_default": 1, + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Fried Rice and Noodles", + "fb_slug": "", + "item_metadata": "{\"v\":424524293,\"s_ctg\":33446742,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"singapuri-noodles\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + }, + { + "item": { + "id": "pv_100061402", + "name": "Full", + "price": 135, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524294", + "parent_menu_id": "ctg_22246916", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "full", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Fried Rice and Noodles", + "fb_slug": "", + "item_metadata": "{\"v\":424524294,\"s_ctg\":33446742,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"singapuri-noodles\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + } + ], + "reference_items": [] + } + } + ], + "enable_customisation_on_cart": false, + "customisation_tracking_metadata": "{\"exp_no\":1,\"default_v_price\":80,\"lowest_v_price\":80,\"highest_v_price\":135,\"total_mgs\":-1,\"total_v_count\":2,\"properties\":1}" + } + }, + { + "item": { + "id": "ctl_339496658", + "name": "Triple Noodles", + "price": 170, + "desc": "", + "min_price": 170, + "max_price": 170, + "default_price": 170, + "display_price": 170, + "variant_id": "v_424524295", + "parent_menu_id": "ctg_22246916", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 3, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "triplenoodles", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Fried Rice and Noodles", + "fb_slug": "triplenoodles-friedriceandnoodles-19713383", + "item_metadata": "{\"v\":424524295,\"s_ctg\":33446742,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"triple-noodles\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1 + } + }, + { + "item": { + "id": "ctl_339496659", + "name": "Schezwan Noodles", + "price": 0, + "desc": "", + "min_price": 100, + "max_price": 180, + "default_price": 100, + "display_price": 100, + "variant_id": "", + "parent_menu_id": "ctg_22246916", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 4, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "schezwannoodles", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "sf-spicy", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services", + "sf-spicy" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Fried Rice and Noodles", + "fb_slug": "schezwannoodles-friedriceandnoodles-19713383", + "item_metadata": "{\"v\":0,\"s_ctg\":33446742,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"schezwan-noodles\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1, + "groups": [ + { + "group": { + "id": "p_36442547", + "name": "Quantity", + "label": "Quantity", + "min": 1, + "max": 1, + "parent_menu_id": 0, + "hasFocus": 0, + "entity_type": "property", + "name_slug": "quantity", + "tracking_metadata": "{\"has_discounted_addon\":false,\"is_dynamic_modifier_group\":false,\"is_dynamic_modifier_group_v2\":false}", + "max_selections_per_item": 1, + "parent_visiblity": 1, + "show_customisation": true, + "items": [ + { + "item": { + "id": "pv_100061403", + "name": "Half", + "price": 100, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524296", + "parent_menu_id": "ctg_22246916", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "half", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "is_default": 1, + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Fried Rice and Noodles", + "fb_slug": "", + "item_metadata": "{\"v\":424524296,\"s_ctg\":33446742,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"schezwan-noodles\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + }, + { + "item": { + "id": "pv_100061404", + "name": "Full", + "price": 180, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524297", + "parent_menu_id": "ctg_22246916", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "full", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Fried Rice and Noodles", + "fb_slug": "", + "item_metadata": "{\"v\":424524297,\"s_ctg\":33446742,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"schezwan-noodles\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + } + ], + "reference_items": [] + } + } + ], + "enable_customisation_on_cart": false, + "customisation_tracking_metadata": "{\"exp_no\":1,\"default_v_price\":100,\"lowest_v_price\":100,\"highest_v_price\":180,\"total_mgs\":-1,\"total_v_count\":2,\"properties\":1}" + } + } + ], + "reference_items": [], + "tag_ids": "1", + "is_expanded": 0 + } + } + ], + "is_expanded": 0, + "should_disable_items": false, + "subtitle": null, + "stepper_disabled_text": "", + "is_hidden": false + } + }, + { + "menu": { + "id": "ctg_22246915", + "name": "Burgers", + "cart_category_id": 0, + "categories": [ + { + "category": { + "id": "s_ctg_33446741", + "name": "", + "items": [ + { + "item": { + "id": "ctl_339496654", + "name": "Veg Burger", + "price": 40, + "desc": "", + "min_price": 40, + "max_price": 40, + "default_price": 40, + "display_price": 40, + "variant_id": "v_424524289", + "parent_menu_id": "ctg_22246915", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "vegburger", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Burgers", + "fb_slug": "vegburger-burgers-19713383", + "item_metadata": "{\"v\":424524289,\"s_ctg\":33446741,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"veg-burger\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1 + } + }, + { + "item": { + "id": "ctl_339496655", + "name": "Cheese Burger", + "price": 65, + "desc": "", + "min_price": 65, + "max_price": 65, + "default_price": 65, + "display_price": 65, + "variant_id": "v_424524290", + "parent_menu_id": "ctg_22246915", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "cheeseburger", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "sf-not-vegan", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services", + "sf-not-vegan" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Burgers", + "fb_slug": "cheeseburger-burgers-19713383", + "item_metadata": "{\"v\":424524290,\"s_ctg\":33446741,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"cheese-burger\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1 + } + } + ], + "reference_items": [], + "tag_ids": "1", + "is_expanded": 0 + } + } + ], + "is_expanded": 0, + "should_disable_items": false, + "subtitle": null, + "stepper_disabled_text": "", + "is_hidden": false + } + }, + { + "menu": { + "id": "ctg_22246917", + "name": "Rolls", + "cart_category_id": 0, + "categories": [ + { + "category": { + "id": "s_ctg_33446743", + "name": "", + "items": [ + { + "item": { + "id": "ctl_339496660", + "name": "Veg Roll", + "price": 70, + "desc": "", + "min_price": 70, + "max_price": 70, + "default_price": 70, + "display_price": 70, + "variant_id": "v_424524298", + "parent_menu_id": "ctg_22246917", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "vegroll", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Rolls", + "fb_slug": "vegroll-rolls-19713383", + "item_metadata": "{\"v\":424524298,\"s_ctg\":33446743,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"veg-roll\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1 + } + }, + { + "item": { + "id": "ctl_339496661", + "name": "Paneer Roll", + "price": 95, + "desc": "", + "min_price": 95, + "max_price": 95, + "default_price": 95, + "display_price": 95, + "variant_id": "v_424524299", + "parent_menu_id": "ctg_22246917", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "paneerroll", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "sf-not-vegan", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services", + "sf-not-vegan" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Rolls", + "fb_slug": "paneerroll-rolls-19713383", + "item_metadata": "{\"v\":424524299,\"s_ctg\":33446743,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"paneer-roll\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1 + } + } + ], + "reference_items": [], + "tag_ids": "1", + "is_expanded": 0 + } + } + ], + "is_expanded": 0, + "should_disable_items": false, + "subtitle": null, + "stepper_disabled_text": "", + "is_hidden": false + } + }, + { + "menu": { + "id": "ctg_22246914", + "name": "Momos", + "cart_category_id": 0, + "categories": [ + { + "category": { + "id": "s_ctg_33446740", + "name": "", + "items": [ + { + "item": { + "id": "ctl_339496649", + "name": "Steamed Momos", + "price": 0, + "desc": "", + "min_price": 55, + "max_price": 95, + "default_price": 55, + "display_price": 55, + "variant_id": "", + "parent_menu_id": "ctg_22246914", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "steamedmomos", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Momos", + "fb_slug": "steamedmomos-momos-19713383", + "item_metadata": "{\"v\":0,\"s_ctg\":33446740,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"steamed-momos\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1, + "groups": [ + { + "group": { + "id": "p_36442540", + "name": "Quantity", + "label": "Quantity", + "min": 1, + "max": 1, + "parent_menu_id": 0, + "hasFocus": 0, + "entity_type": "property", + "name_slug": "quantity", + "tracking_metadata": "{\"has_discounted_addon\":false,\"is_dynamic_modifier_group\":false,\"is_dynamic_modifier_group_v2\":false}", + "max_selections_per_item": 1, + "parent_visiblity": 1, + "show_customisation": true, + "items": [ + { + "item": { + "id": "pv_100061389", + "name": "Half", + "price": 55, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524279", + "parent_menu_id": "ctg_22246914", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "half", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "is_default": 1, + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Momos", + "fb_slug": "", + "item_metadata": "{\"v\":424524279,\"s_ctg\":33446740,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"steamed-momos\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + }, + { + "item": { + "id": "pv_100061390", + "name": "Full", + "price": 95, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524280", + "parent_menu_id": "ctg_22246914", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "full", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Momos", + "fb_slug": "", + "item_metadata": "{\"v\":424524280,\"s_ctg\":33446740,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"steamed-momos\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + } + ], + "reference_items": [] + } + } + ], + "enable_customisation_on_cart": false, + "customisation_tracking_metadata": "{\"exp_no\":1,\"default_v_price\":55,\"lowest_v_price\":55,\"highest_v_price\":95,\"total_mgs\":-1,\"total_v_count\":2,\"properties\":1}" + } + }, + { + "item": { + "id": "ctl_339496650", + "name": "Fried Momos", + "price": 0, + "desc": "", + "min_price": 70, + "max_price": 110, + "default_price": 70, + "display_price": 70, + "variant_id": "", + "parent_menu_id": "ctg_22246914", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "friedmomos", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Momos", + "fb_slug": "friedmomos-momos-19713383", + "item_metadata": "{\"v\":0,\"s_ctg\":33446740,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"fried-momos\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1, + "groups": [ + { + "group": { + "id": "p_36442541", + "name": "Quantity", + "label": "Quantity", + "min": 1, + "max": 1, + "parent_menu_id": 0, + "hasFocus": 0, + "entity_type": "property", + "name_slug": "quantity", + "tracking_metadata": "{\"has_discounted_addon\":false,\"is_dynamic_modifier_group\":false,\"is_dynamic_modifier_group_v2\":false}", + "max_selections_per_item": 1, + "parent_visiblity": 1, + "show_customisation": true, + "items": [ + { + "item": { + "id": "pv_100061391", + "name": "Half", + "price": 70, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524281", + "parent_menu_id": "ctg_22246914", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "half", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "is_default": 1, + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Momos", + "fb_slug": "", + "item_metadata": "{\"v\":424524281,\"s_ctg\":33446740,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"fried-momos\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + }, + { + "item": { + "id": "pv_100061392", + "name": "Full", + "price": 110, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524282", + "parent_menu_id": "ctg_22246914", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "full", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Momos", + "fb_slug": "", + "item_metadata": "{\"v\":424524282,\"s_ctg\":33446740,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"fried-momos\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + } + ], + "reference_items": [] + } + } + ], + "enable_customisation_on_cart": false, + "customisation_tracking_metadata": "{\"exp_no\":1,\"default_v_price\":70,\"lowest_v_price\":70,\"highest_v_price\":110,\"total_mgs\":-1,\"total_v_count\":2,\"properties\":1}" + } + }, + { + "item": { + "id": "ctl_339496651", + "name": "Paneer Momos", + "price": 0, + "desc": "", + "min_price": 95, + "max_price": 160, + "default_price": 95, + "display_price": 95, + "variant_id": "", + "parent_menu_id": "ctg_22246914", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 3, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "paneermomos", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "sf-not-vegan", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services", + "sf-not-vegan" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Momos", + "fb_slug": "paneermomos-momos-19713383", + "item_metadata": "{\"v\":0,\"s_ctg\":33446740,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"paneer-momos\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1, + "groups": [ + { + "group": { + "id": "p_36442542", + "name": "Quantity", + "label": "Quantity", + "min": 1, + "max": 1, + "parent_menu_id": 0, + "hasFocus": 0, + "entity_type": "property", + "name_slug": "quantity", + "tracking_metadata": "{\"has_discounted_addon\":false,\"is_dynamic_modifier_group\":false,\"is_dynamic_modifier_group_v2\":false}", + "max_selections_per_item": 1, + "parent_visiblity": 1, + "show_customisation": true, + "items": [ + { + "item": { + "id": "pv_100061393", + "name": "Half", + "price": 95, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524283", + "parent_menu_id": "ctg_22246914", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "half", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "is_default": 1, + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Momos", + "fb_slug": "", + "item_metadata": "{\"v\":424524283,\"s_ctg\":33446740,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"paneer-momos\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + }, + { + "item": { + "id": "pv_100061394", + "name": "Full", + "price": 160, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524284", + "parent_menu_id": "ctg_22246914", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "full", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Momos", + "fb_slug": "", + "item_metadata": "{\"v\":424524284,\"s_ctg\":33446740,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"paneer-momos\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + } + ], + "reference_items": [] + } + } + ], + "enable_customisation_on_cart": false, + "customisation_tracking_metadata": "{\"exp_no\":1,\"default_v_price\":95,\"lowest_v_price\":95,\"highest_v_price\":160,\"total_mgs\":-1,\"total_v_count\":2,\"properties\":1}" + } + }, + { + "item": { + "id": "ctl_339496652", + "name": "Kurkure Momos", + "price": 0, + "desc": "", + "min_price": 110, + "max_price": 180, + "default_price": 110, + "display_price": 110, + "variant_id": "", + "parent_menu_id": "ctg_22246914", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 4, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "kurkuremomos", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Momos", + "fb_slug": "kurkuremomos-momos-19713383", + "item_metadata": "{\"v\":0,\"s_ctg\":33446740,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"kurkure-momos\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1, + "groups": [ + { + "group": { + "id": "p_36442543", + "name": "Quantity", + "label": "Quantity", + "min": 1, + "max": 1, + "parent_menu_id": 0, + "hasFocus": 0, + "entity_type": "property", + "name_slug": "quantity", + "tracking_metadata": "{\"has_discounted_addon\":false,\"is_dynamic_modifier_group\":false,\"is_dynamic_modifier_group_v2\":false}", + "max_selections_per_item": 1, + "parent_visiblity": 1, + "show_customisation": true, + "items": [ + { + "item": { + "id": "pv_100061395", + "name": "Half", + "price": 110, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524285", + "parent_menu_id": "ctg_22246914", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "half", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "is_default": 1, + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Momos", + "fb_slug": "", + "item_metadata": "{\"v\":424524285,\"s_ctg\":33446740,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"kurkure-momos\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + }, + { + "item": { + "id": "pv_100061396", + "name": "Full", + "price": 180, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524286", + "parent_menu_id": "ctg_22246914", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "full", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Momos", + "fb_slug": "", + "item_metadata": "{\"v\":424524286,\"s_ctg\":33446740,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"kurkure-momos\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + } + ], + "reference_items": [] + } + } + ], + "enable_customisation_on_cart": false, + "customisation_tracking_metadata": "{\"exp_no\":1,\"default_v_price\":110,\"lowest_v_price\":110,\"highest_v_price\":180,\"total_mgs\":-1,\"total_v_count\":2,\"properties\":1}" + } + } + ], + "reference_items": [], + "tag_ids": "1", + "is_expanded": 0 + } + } + ], + "is_expanded": 0, + "should_disable_items": false, + "subtitle": null, + "stepper_disabled_text": "", + "is_hidden": false + } + }, + { + "menu": { + "id": "ctg_22246911", + "name": "Snacks", + "cart_category_id": 0, + "categories": [ + { + "category": { + "id": "s_ctg_33446737", + "name": "", + "items": [ + { + "item": { + "id": "ctl_339496646", + "name": "Spring Roll", + "price": 0, + "desc": "", + "min_price": 40, + "max_price": 70, + "default_price": 40, + "display_price": 40, + "variant_id": "", + "parent_menu_id": "ctg_22246911", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 3, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "springroll", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Snacks", + "fb_slug": "springroll-snacks-19713383", + "item_metadata": "{\"v\":0,\"s_ctg\":33446737,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"spring-roll\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1, + "groups": [ + { + "group": { + "id": "p_36442539", + "name": "Quantity", + "label": "Quantity", + "min": 1, + "max": 1, + "parent_menu_id": 0, + "hasFocus": 0, + "entity_type": "property", + "name_slug": "quantity", + "tracking_metadata": "{\"has_discounted_addon\":false,\"is_dynamic_modifier_group\":false,\"is_dynamic_modifier_group_v2\":false}", + "max_selections_per_item": 1, + "parent_visiblity": 1, + "show_customisation": true, + "items": [ + { + "item": { + "id": "pv_100061387", + "name": "Half", + "price": 40, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524275", + "parent_menu_id": "ctg_22246911", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "half", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "is_default": 1, + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Snacks", + "fb_slug": "", + "item_metadata": "{\"v\":424524275,\"s_ctg\":33446737,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"spring-roll\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + }, + { + "item": { + "id": "pv_100061388", + "name": "Full", + "price": 70, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_424524276", + "parent_menu_id": "ctg_22246911", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "full", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Snacks", + "fb_slug": "", + "item_metadata": "{\"v\":424524276,\"s_ctg\":33446737,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"spring-roll\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + } + ], + "reference_items": [] + } + } + ], + "enable_customisation_on_cart": false, + "customisation_tracking_metadata": "{\"exp_no\":1,\"default_v_price\":40,\"lowest_v_price\":40,\"highest_v_price\":70,\"total_mgs\":-1,\"total_v_count\":2,\"properties\":1}" + } + }, + { + "item": { + "id": "ctl_432129811", + "name": "French Fries", + "price": 0, + "desc": "", + "min_price": 55, + "max_price": 95, + "default_price": 55, + "display_price": 55, + "variant_id": "", + "parent_menu_id": "ctg_22246911", + "item_image_url": "https://b.zmtcdn.com/data/dish_photos/78d/ff2e39ee70bc2b00197ff5810f73078d.jpeg", + "item_image_thumb_url": "https://b.zmtcdn.com/data/dish_photos/78d/ff2e39ee70bc2b00197ff5810f73078d.jpeg?fit=around%7C200%3A200&crop=200%3A200%3B%2A%2C%2A", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 4, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": true, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "frenchfries", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [ + { + "mediaType": "image", + "image": { + "url": "https://b.zmtcdn.com/data/dish_photos/78d/ff2e39ee70bc2b00197ff5810f73078d.jpeg?fit=around%7C200%3A200&crop=200%3A200%3B%2A%2C%2A", + "data": null, + "bgColor": null, + "borderColor": null, + "bgColorHex": null, + "animation": null, + "animate": null, + "aspectRatio": null, + "type": null, + "height": null, + "width": null, + "clickAction": null, + "border": null, + "shouldIgnoreResizing": null, + "tracking": null, + "placeholderColor": null, + "heightRatio": null, + "scaleMode": null, + "filter": null, + "overlayTextData": null, + "overlayIcon": null, + "containerAnimations": null, + "overlayAnimation": null, + "offsetRatio": null, + "shouldExpand": null, + "cornerRadius": null, + "trackingData": null, + "gravity": null, + "repeatCount": null, + "themeConfig": null + }, + "video": null, + "audio": null, + "lottie": null + } + ], + "info_tags": [], + "tag_slugs": [ + "veg", + "services", + "delivery-enabled" + ], + "service_slugs": [ + "delivery-enabled" + ], + "dietary_slugs": [ + "veg" + ], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [ + "services" + ], + "disclaimer_tag_slugs": [], + "entity_type": "catalogue", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Snacks", + "fb_slug": "frenchfries-snacks-19713383", + "item_metadata": "{\"v\":0,\"s_ctg\":33446737,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"french-fries\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "tag_ids": "1", + "item_tag_image": "https://b.zmtcdn.com/data/o2_assets/958e4cb4a8b255659daae2c430f976ea1608028693.png?output-format=webp", + "tag_images": [], + "tag_texts": [], + "tag_objects": [], + "hide_in_meals_menu": 1, + "groups": [ + { + "group": { + "id": "p_43856070", + "name": "Quantity", + "label": "Quantity", + "min": 1, + "max": 1, + "parent_menu_id": 0, + "hasFocus": 0, + "entity_type": "property", + "name_slug": "quantity", + "tracking_metadata": "{\"has_discounted_addon\":false,\"is_dynamic_modifier_group\":false,\"is_dynamic_modifier_group_v2\":false}", + "max_selections_per_item": 1, + "parent_visiblity": 1, + "show_customisation": true, + "items": [ + { + "item": { + "id": "pv_120454633", + "name": "Half", + "price": 55, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_532968858", + "parent_menu_id": "ctg_22246911", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 1, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "half", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "is_default": 1, + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Snacks", + "fb_slug": "", + "item_metadata": "{\"v\":532968858,\"s_ctg\":33446737,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"french-fries\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + }, + { + "item": { + "id": "pv_120454634", + "name": "Full", + "price": 95, + "desc": "", + "min_price": 0, + "max_price": 0, + "default_price": 0, + "display_price": 0, + "variant_id": "v_532968859", + "parent_menu_id": "ctg_22246911", + "always_show_on_checkout": 0, + "is_bogo_active": false, + "order": 2, + "auto_add": 0, + "auto_add_quantity": 0, + "show_item_image": false, + "visible": false, + "item_state": "available", + "rating": null, + "name_slug": "full", + "free_dish_quantity": 0, + "item_type": "dish", + "added_bottom_text_field": null, + "modifier_group_ids": [], + "not_added_bottom_text_field": null, + "tracking_dish_type": "dish", + "disabled_text": null, + "media": [], + "info_tags": [], + "tag_slugs": [], + "service_slugs": [], + "dietary_slugs": [], + "inapplicable_filter_tag_slugs": [], + "secondary_tag_slugs": [], + "disclaimer_tag_slugs": [], + "entity_type": "property_value", + "primary_tag_slug": "", + "food_legends": null, + "search_alias": "Snacks", + "fb_slug": "", + "item_metadata": "{\"v\":532968859,\"s_ctg\":33446737,\"cmb_sel\":0,\"cmb\":0,\"ff\":false,\"ctl_n\":\"french-fries\"}", + "added_bottom_text": "", + "not_added_bottom_text": "", + "not_added_bottom_text_color": "", + "remove_popup_text": "", + "item_auto_add_toast_message": "", + "item_auto_already_added_toast_message": "", + "remove_popup_title": "", + "show_customisation": false + } + } + ], + "reference_items": [] + } + } + ], + "enable_customisation_on_cart": false, + "customisation_tracking_metadata": "{\"exp_no\":1,\"default_v_price\":55,\"lowest_v_price\":55,\"highest_v_price\":95,\"total_mgs\":-1,\"total_v_count\":2,\"properties\":1}" + } + } + ], + "reference_items": [], + "tag_ids": "1", + "is_expanded": 0 + } + } + ], + "is_expanded": 0, + "should_disable_items": false, + "subtitle": null, + "stepper_disabled_text": "", + "is_hidden": false + } + } + ], + "modifierGroups": {}, + "promosOnMenu": { + "promos": [] + }, + "offerSnackbar": [], + "postbackParams": "logistics_partner_id%3D0%26vendor_serviceability_flow%3D0%26delivery_mode%3Ddelivery", + "onlinePaymentsFlag": 1, + "showItemsFilter": 1, + "address": [], + "fssaiInfo": { + "text": "Lic. No. 22721879000010", + "image": "https://b.zmtcdn.com/data/o2_banners/54de14cdc3793dfce39a46c989f3e5c1.jpg?output-format=webp" + }, + "tags": [ + { + "slug": "bestseller", + "title": { + "text": "BESTSELLER", + "font": null, + "color": { + "tint": "500", + "type": "white", + "alpha": null, + "transparency": null, + "darkTint": null, + "darkType": null, + "themeBucket": null + }, + "bgColor": null, + "prefixIcon": null, + "suffixIcon": null, + "prefixImage": null, + "prefix_image": null, + "suffixImage": null, + "deeplink": null, + "strike": null, + "prefixText": null, + "suffixText": null, + "numberOfLines": null, + "alignment": null, + "disableTitleLetterSpacing": null, + "kerning": null, + "gradient": null, + "isMarkdown": null, + "is_markdown": null, + "shouldRoundForHeight": null, + "suffixButton": null, + "clickAction": null, + "markdownVersion": null, + "maxCharLimit": null, + "type": null, + "size": null, + "prefixTextHexCode": null, + "replacementText": null, + "hexCode": null, + "isClickable": null, + "expandedText": null, + "collapsedText": null, + "trackingData": null, + "underline": null, + "shouldNotAdjustFontSize": null, + "maxLines": null, + "animation": null, + "leftIcon": null, + "icon": null, + "rightIcon": null, + "maxTruncationLineLimit": null, + "shouldShowReadMore": null, + "themeBucket": null, + "spacing_config": null, + "time": null, + "id": null, + "rightTagText": null, + "containerAnimations": null, + "margin": null + }, + "bg_color": { + "tint": "300", + "type": "orange", + "alpha": null, + "transparency": null, + "darkTint": null, + "darkType": null, + "themeBucket": null + } + }, + { + "slug": "veg", + "title": { + "text": "Veg", + "font": null, + "color": null, + "bgColor": null, + "prefixIcon": null, + "suffixIcon": null, + "prefixImage": null, + "prefix_image": null, + "suffixImage": null, + "deeplink": null, + "strike": null, + "prefixText": null, + "suffixText": null, + "numberOfLines": null, + "alignment": null, + "disableTitleLetterSpacing": null, + "kerning": null, + "gradient": null, + "isMarkdown": null, + "is_markdown": null, + "shouldRoundForHeight": null, + "suffixButton": null, + "clickAction": null, + "markdownVersion": null, + "maxCharLimit": null, + "type": null, + "size": null, + "prefixTextHexCode": null, + "replacementText": null, + "hexCode": null, + "isClickable": null, + "expandedText": null, + "collapsedText": null, + "trackingData": null, + "underline": null, + "shouldNotAdjustFontSize": null, + "maxLines": null, + "animation": null, + "leftIcon": null, + "icon": null, + "rightIcon": null, + "maxTruncationLineLimit": null, + "shouldShowReadMore": null, + "themeBucket": null, + "spacing_config": null, + "time": null, + "id": null, + "rightTagText": null, + "containerAnimations": null, + "margin": null + }, + "image": { + "url": "https://b.zmtcdn.com/data/o2_assets/54539a4514498a9b2d80c537c35126ca1670328399.png?output-format=webp", + "data": null, + "bgColor": null, + "borderColor": null, + "bgColorHex": null, + "animation": null, + "animate": null, + "aspectRatio": null, + "type": null, + "height": null, + "width": null, + "clickAction": null, + "border": null, + "shouldIgnoreResizing": null, + "tracking": null, + "placeholderColor": null, + "heightRatio": null, + "scaleMode": null, + "filter": null, + "overlayTextData": null, + "overlayIcon": null, + "containerAnimations": null, + "overlayAnimation": null, + "offsetRatio": null, + "shouldExpand": null, + "cornerRadius": null, + "trackingData": null, + "gravity": null, + "repeatCount": null, + "themeConfig": null + } + }, + { + "slug": "non-veg", + "title": { + "text": "Non-Veg", + "font": null, + "color": null, + "bgColor": null, + "prefixIcon": null, + "suffixIcon": null, + "prefixImage": null, + "prefix_image": null, + "suffixImage": null, + "deeplink": null, + "strike": null, + "prefixText": null, + "suffixText": null, + "numberOfLines": null, + "alignment": null, + "disableTitleLetterSpacing": null, + "kerning": null, + "gradient": null, + "isMarkdown": null, + "is_markdown": null, + "shouldRoundForHeight": null, + "suffixButton": null, + "clickAction": null, + "markdownVersion": null, + "maxCharLimit": null, + "type": null, + "size": null, + "prefixTextHexCode": null, + "replacementText": null, + "hexCode": null, + "isClickable": null, + "expandedText": null, + "collapsedText": null, + "trackingData": null, + "underline": null, + "shouldNotAdjustFontSize": null, + "maxLines": null, + "animation": null, + "leftIcon": null, + "icon": null, + "rightIcon": null, + "maxTruncationLineLimit": null, + "shouldShowReadMore": null, + "themeBucket": null, + "spacing_config": null, + "time": null, + "id": null, + "rightTagText": null, + "containerAnimations": null, + "margin": null + }, + "image": { + "url": "https://b.zmtcdn.com/data/o2_assets/5d4de18a3d402878965275e9f24656951635253564.png?output-format=webp", + "data": null, + "bgColor": null, + "borderColor": null, + "bgColorHex": null, + "animation": null, + "animate": null, + "aspectRatio": null, + "type": null, + "height": null, + "width": null, + "clickAction": null, + "border": null, + "shouldIgnoreResizing": null, + "tracking": null, + "placeholderColor": null, + "heightRatio": null, + "scaleMode": null, + "filter": null, + "overlayTextData": null, + "overlayIcon": null, + "containerAnimations": null, + "overlayAnimation": null, + "offsetRatio": null, + "shouldExpand": null, + "cornerRadius": null, + "trackingData": null, + "gravity": null, + "repeatCount": null, + "themeConfig": null + } + }, + { + "slug": "egg", + "title": { + "text": "Egg", + "font": null, + "color": null, + "bgColor": null, + "prefixIcon": null, + "suffixIcon": null, + "prefixImage": null, + "prefix_image": null, + "suffixImage": null, + "deeplink": null, + "strike": null, + "prefixText": null, + "suffixText": null, + "numberOfLines": null, + "alignment": null, + "disableTitleLetterSpacing": null, + "kerning": null, + "gradient": null, + "isMarkdown": null, + "is_markdown": null, + "shouldRoundForHeight": null, + "suffixButton": null, + "clickAction": null, + "markdownVersion": null, + "maxCharLimit": null, + "type": null, + "size": null, + "prefixTextHexCode": null, + "replacementText": null, + "hexCode": null, + "isClickable": null, + "expandedText": null, + "collapsedText": null, + "trackingData": null, + "underline": null, + "shouldNotAdjustFontSize": null, + "maxLines": null, + "animation": null, + "leftIcon": null, + "icon": null, + "rightIcon": null, + "maxTruncationLineLimit": null, + "shouldShowReadMore": null, + "themeBucket": null, + "spacing_config": null, + "time": null, + "id": null, + "rightTagText": null, + "containerAnimations": null, + "margin": null + }, + "image": { + "url": "https://b.zmtcdn.com/data/o2_assets/5d4de18a3d402878965275e9f24656951635253564.png?output-format=webp", + "data": null, + "bgColor": null, + "borderColor": null, + "bgColorHex": null, + "animation": null, + "animate": null, + "aspectRatio": null, + "type": null, + "height": null, + "width": null, + "clickAction": null, + "border": null, + "shouldIgnoreResizing": null, + "tracking": null, + "placeholderColor": null, + "heightRatio": null, + "scaleMode": null, + "filter": null, + "overlayTextData": null, + "overlayIcon": null, + "containerAnimations": null, + "overlayAnimation": null, + "offsetRatio": null, + "shouldExpand": null, + "cornerRadius": null, + "trackingData": null, + "gravity": null, + "repeatCount": null, + "themeConfig": null + } + }, + { + "slug": "services", + "title": { + "text": "Services", + "font": null, + "color": null, + "bgColor": null, + "prefixIcon": null, + "suffixIcon": null, + "prefixImage": null, + "prefix_image": null, + "suffixImage": null, + "deeplink": null, + "strike": null, + "prefixText": null, + "suffixText": null, + "numberOfLines": null, + "alignment": null, + "disableTitleLetterSpacing": null, + "kerning": null, + "gradient": null, + "isMarkdown": null, + "is_markdown": null, + "shouldRoundForHeight": null, + "suffixButton": null, + "clickAction": null, + "markdownVersion": null, + "maxCharLimit": null, + "type": null, + "size": null, + "prefixTextHexCode": null, + "replacementText": null, + "hexCode": null, + "isClickable": null, + "expandedText": null, + "collapsedText": null, + "trackingData": null, + "underline": null, + "shouldNotAdjustFontSize": null, + "maxLines": null, + "animation": null, + "leftIcon": null, + "icon": null, + "rightIcon": null, + "maxTruncationLineLimit": null, + "shouldShowReadMore": null, + "themeBucket": null, + "spacing_config": null, + "time": null, + "id": null, + "rightTagText": null, + "containerAnimations": null, + "margin": null + } + }, + { + "slug": "sf-not-vegan", + "title": { + "text": "Not Vegan", + "font": null, + "color": null, + "bgColor": null, + "prefixIcon": null, + "suffixIcon": null, + "prefixImage": null, + "prefix_image": null, + "suffixImage": null, + "deeplink": null, + "strike": null, + "prefixText": null, + "suffixText": null, + "numberOfLines": null, + "alignment": null, + "disableTitleLetterSpacing": null, + "kerning": null, + "gradient": null, + "isMarkdown": null, + "is_markdown": null, + "shouldRoundForHeight": null, + "suffixButton": null, + "clickAction": null, + "markdownVersion": null, + "maxCharLimit": null, + "type": null, + "size": null, + "prefixTextHexCode": null, + "replacementText": null, + "hexCode": null, + "isClickable": null, + "expandedText": null, + "collapsedText": null, + "trackingData": null, + "underline": null, + "shouldNotAdjustFontSize": null, + "maxLines": null, + "animation": null, + "leftIcon": null, + "icon": null, + "rightIcon": null, + "maxTruncationLineLimit": null, + "shouldShowReadMore": null, + "themeBucket": null, + "spacing_config": null, + "time": null, + "id": null, + "rightTagText": null, + "containerAnimations": null, + "margin": null + } + }, + { + "slug": "sf-spicy", + "title": { + "text": "SF Spicy", + "font": null, + "color": null, + "bgColor": null, + "prefixIcon": null, + "suffixIcon": null, + "prefixImage": null, + "prefix_image": null, + "suffixImage": null, + "deeplink": null, + "strike": null, + "prefixText": null, + "suffixText": null, + "numberOfLines": null, + "alignment": null, + "disableTitleLetterSpacing": null, + "kerning": null, + "gradient": null, + "isMarkdown": null, + "is_markdown": null, + "shouldRoundForHeight": null, + "suffixButton": null, + "clickAction": null, + "markdownVersion": null, + "maxCharLimit": null, + "type": null, + "size": null, + "prefixTextHexCode": null, + "replacementText": null, + "hexCode": null, + "isClickable": null, + "expandedText": null, + "collapsedText": null, + "trackingData": null, + "underline": null, + "shouldNotAdjustFontSize": null, + "maxLines": null, + "animation": null, + "leftIcon": null, + "icon": null, + "rightIcon": null, + "maxTruncationLineLimit": null, + "shouldShowReadMore": null, + "themeBucket": null, + "spacing_config": null, + "time": null, + "id": null, + "rightTagText": null, + "containerAnimations": null, + "margin": null + } + }, + { + "slug": "non-promo", + "title": { + "text": "Not eligible for coupons", + "font": null, + "color": { + "tint": "500", + "type": "grey", + "alpha": null, + "transparency": null, + "darkTint": null, + "darkType": null, + "themeBucket": null + }, + "bgColor": null, + "prefixIcon": null, + "suffixIcon": null, + "prefixImage": null, + "prefix_image": null, + "suffixImage": null, + "deeplink": null, + "strike": null, + "prefixText": null, + "suffixText": null, + "numberOfLines": null, + "alignment": null, + "disableTitleLetterSpacing": null, + "kerning": null, + "gradient": null, + "isMarkdown": null, + "is_markdown": null, + "shouldRoundForHeight": null, + "suffixButton": null, + "clickAction": null, + "markdownVersion": null, + "maxCharLimit": null, + "type": null, + "size": null, + "prefixTextHexCode": null, + "replacementText": null, + "hexCode": null, + "isClickable": null, + "expandedText": null, + "collapsedText": null, + "trackingData": null, + "underline": null, + "shouldNotAdjustFontSize": null, + "maxLines": null, + "animation": null, + "leftIcon": null, + "icon": null, + "rightIcon": null, + "maxTruncationLineLimit": null, + "shouldShowReadMore": null, + "themeBucket": null, + "spacing_config": null, + "time": null, + "id": null, + "rightTagText": null, + "containerAnimations": null, + "margin": null + }, + "bg_color": { + "tint": "100", + "type": "grey", + "alpha": null, + "transparency": null, + "darkTint": null, + "darkType": null, + "themeBucket": null + } + }, + { + "slug": "new", + "title": { + "text": "NEW", + "font": null, + "color": { + "tint": "500", + "type": "white", + "alpha": null, + "transparency": null, + "darkTint": null, + "darkType": null, + "themeBucket": null + }, + "bgColor": null, + "prefixIcon": null, + "suffixIcon": null, + "prefixImage": null, + "prefix_image": null, + "suffixImage": null, + "deeplink": null, + "strike": null, + "prefixText": null, + "suffixText": null, + "numberOfLines": null, + "alignment": null, + "disableTitleLetterSpacing": null, + "kerning": null, + "gradient": null, + "isMarkdown": null, + "is_markdown": null, + "shouldRoundForHeight": null, + "suffixButton": null, + "clickAction": null, + "markdownVersion": null, + "maxCharLimit": null, + "type": null, + "size": null, + "prefixTextHexCode": null, + "replacementText": null, + "hexCode": null, + "isClickable": null, + "expandedText": null, + "collapsedText": null, + "trackingData": null, + "underline": null, + "shouldNotAdjustFontSize": null, + "maxLines": null, + "animation": null, + "leftIcon": null, + "icon": null, + "rightIcon": null, + "maxTruncationLineLimit": null, + "shouldShowReadMore": null, + "themeBucket": null, + "spacing_config": null, + "time": null, + "id": null, + "rightTagText": null, + "containerAnimations": null, + "margin": null + }, + "bg_color": { + "tint": "400", + "type": "red", + "alpha": null, + "transparency": null, + "darkTint": null, + "darkType": null, + "themeBucket": null + } + }, + { + "slug": "must-try", + "title": { + "text": "MUST TRY", + "font": null, + "color": { + "tint": "500", + "type": "white", + "alpha": null, + "transparency": null, + "darkTint": null, + "darkType": null, + "themeBucket": null + }, + "bgColor": null, + "prefixIcon": null, + "suffixIcon": null, + "prefixImage": null, + "prefix_image": null, + "suffixImage": null, + "deeplink": null, + "strike": null, + "prefixText": null, + "suffixText": null, + "numberOfLines": null, + "alignment": null, + "disableTitleLetterSpacing": null, + "kerning": null, + "gradient": null, + "isMarkdown": null, + "is_markdown": null, + "shouldRoundForHeight": null, + "suffixButton": null, + "clickAction": null, + "markdownVersion": null, + "maxCharLimit": null, + "type": null, + "size": null, + "prefixTextHexCode": null, + "replacementText": null, + "hexCode": null, + "isClickable": null, + "expandedText": null, + "collapsedText": null, + "trackingData": null, + "underline": null, + "shouldNotAdjustFontSize": null, + "maxLines": null, + "animation": null, + "leftIcon": null, + "icon": null, + "rightIcon": null, + "maxTruncationLineLimit": null, + "shouldShowReadMore": null, + "themeBucket": null, + "spacing_config": null, + "time": null, + "id": null, + "rightTagText": null, + "containerAnimations": null, + "margin": null + }, + "bg_color": { + "tint": "400", + "type": "blue", + "alpha": null, + "transparency": null, + "darkTint": null, + "darkType": null, + "themeBucket": null + } + } + ] + }, + "proBranchDeeplink": "", + "liveTrackingDeeplink": "" + }, + "trackingDataLogin": { + "googleAdsPayload": { + "mobileProfileIconClick": { + "eventName": "conversion", + "payload": { + "send_to": "AW-958674130/gEmzCLCZwfABENLpkMkD" + } + }, + "loginClick": { + "eventName": "conversion", + "payload": { + "send_to": "AW-958674130/MOW8CIO6-uwBENLpkMkD" + } + }, + "signupClick": { + "eventName": "conversion", + "payload": { + "send_to": "AW-958674130/M4gKCJi9-uwBENLpkMkD" + } + }, + "signupSuccess": { + "eventName": "conversion", + "payload": { + "send_to": "AW-958674130/NWiMCPrDne0BENLpkMkD" + } + } + } + }, + "cartData": {} + } + }, + "awards": { + "cities": [], + "cityWinners": [], + "currentCity": {}, + "loader": false + }, + "user": {}, + "userSettings": {}, + "sauceBlog": {}, + "Kitchen": { + "kitchenApiKey": "", + "defaultLat": 0, + "defaultLng": 0 + }, + "celebrations": { + "setFormRequirement": false + }, + "cdng": {}, + "postOrder": { + "orderData": {}, + "orderSupportData": {}, + "deliveryAddressDetails": {}, + "riderDetails": { + "riderName": "", + "riderPhone": "" + }, + "restaurantDetails": {}, + "creatorDetails": {}, + "deliveryStatus": false, + "deliveryMessage": "", + "deliveryLabel": "", + "status": "", + "deliveryTimeStr": "", + "pollingStatus": 0, + "orderCreationTime": 0, + "orderDeliveryTime": 0, + "currentStatus": "", + "deliveryMode": "", + "crystalData": { + "items": {} + } + }, + "zomaland": { + "currentCityId": 0, + "pageType": "HOME", + "userData": { + "tickets": [] + }, + "uiStates": { + "ticketsModalVisible": false, + "showLoader": false, + "ticketView": "FULL_TICKET", + "galleryIndex": 0, + "showTicket": false + }, + "cityLevelData": {}, + "zomalandPages": {}, + "zomalandDeepLink": { + "link": [] + } + }, + "orderOnline": { + "paymentDetails": { + "payment_method_type": "", + "isZcreditsSelected": false + }, + "paymentState": { + "checkoutStatus": "IDLE", + "errorState": { + "code": 0, + "errorMessage": "" + } + }, + "kitValidationStatus": false, + "makePaymentParams": null, + "isPlaceOrderSuccess": false, + "sectionVerifyPhone": { + "allCountryCode": [], + "selectedCountryCode": {} + }, + "sectionAutoApplyPromoCodes": { + "autoApplyPromoCodes": [] + }, + "trackingData": { + "googleAdsPayload": { + "addToCart": { + "eventName": "conversion", + "payload": { + "send_to": "AW-958674130/1QC4COu6ne0BENLpkMkD" + } + } + } + }, + "trackingDataLogin": { + "googleAdsPayload": { + "mobileProfileIconClick": { + "eventName": "conversion", + "payload": { + "send_to": "AW-958674130/gEmzCLCZwfABENLpkMkD" + } + }, + "loginClick": { + "eventName": "conversion", + "payload": { + "send_to": "AW-958674130/MOW8CIO6-uwBENLpkMkD" + } + }, + "signupClick": { + "eventName": "conversion", + "payload": { + "send_to": "AW-958674130/M4gKCJi9-uwBENLpkMkD" + } + }, + "signupSuccess": { + "eventName": "conversion", + "payload": { + "send_to": "AW-958674130/NWiMCPrDne0BENLpkMkD" + } + } + } + } + }, + "deliverycities": { + "allO2Cities": [] + }, + "zomatoForWork": {}, + "pageNotFound": {}, + "collections": {}, + "collectionDetails": {}, + "appDownload": {}, + "contests": {}, + "search": {}, + "singleJob": {}, + "goldSubscriptionAgreement": { + "pageTitle": "" + }, + "zoomBackgrounds": {}, + "country": {}, + "tablePostBooking": {}, + "city": {}, + "gold": { + "plans": [], + "customSectionsForCity": [], + "orderingRestaurants": [], + "dineoutRestaurants": [], + "faqs": [], + "constants": { + "planSectionHeading": "", + "faqHeading": "", + "aboutGoldText": "", + "goldLogoSrc": "" + } + }, + "feedingPhilippines": {}, + "feedingIndonesia": {}, + "talentHub": {}, + "dining": { + "cartUi": { + "isCheckboxClicked": false, + "isUserClicked": false + } + }, + "scanner": {}, + "cupcake": {}, + "partnershipInit": {}, + "paymentStatus": {}, + "planPage": { + "benefitsData": [] + }, + "dotePdp": {}, + "doteHome": {}, + "familyPlanPage": { + "familyPlanData": [] + }, + "orderCartProgress": { + "currentState": "CART_IDLE" + }, + "financialInformation": {}, + "investorRelations": {}, + "investorRelationsV2": {}, + "goldMarketingPage": { + "mainText": "", + "bottomText": "", + "headerText": "", + "oneLink": "" + }, + "agentSearch": { + "selectedRes": null, + "selectedDishes": null, + "disableResSelection": false, + "disableDishSelection": false, + "isMultiSelectOn": true + }, + "agentRestaurant": { + "resItems": null, + "disabledMenuItemSelection": false, + "lastAddedItemData": null, + "orderData": null + }, + "diningPay": {}, + "bloggers": { + "bannerData": [], + "snippetData": [], + "formData": [] + }, + "neighbourhoods": {}, + "resAdminToolkit": {}, + "individualPhotoPage": {}, + "openGiftCard": {}, + "proPage": {}, + "zopayStoryUploader": {}, + "orderShare": { + "riderDetails": {}, + "resDetails": {}, + "userDetails": {}, + "deliveryState": "", + "orderDeliveredSnippet": {}, + "orderRejectedSnippet": {}, + "mapSection": {}, + "resName": "", + "hashed_tab_id": "", + "resId": 0, + "headerData": {}, + "mapSectionStaticData": {}, + "orderId": 0, + "linkExpiredSnippet": {} + }, + "giftCard": {}, + "zLiveHomePageReducer": {}, + "zLiveCartReducer": { + "packagesData": [], + "paymentSdkData": {}, + "paymentKitStates": {} + }, + "zLiveModalReducer": { + "type": null, + "data": null + }, + "zLiveV2PageReducer": {}, + "zLiveV2CartReducer": { + "packagesData": [], + "seatsIoData": null + }, + "zLiveV2ModalReducer": { + "type": null, + "data": null + }, + "zLiveV2ZpaykitReducer": { + "paymentSdkData": {}, + "paymentKitStates": {}, + "buildCartApiResponse": {} + }, + "zLiveV2ErrorReducer": { + "isError": false + }, + "zLiveV2CustomerDetailsReducer": { + "customerDetailsForm": { + "fullName": { + "value": "", + "isError": false, + "isTriggerError": false + }, + "mobileNum": { + "value": "", + "isError": false, + "isTriggerError": false + }, + "email": { + "value": "", + "isError": false, + "isTriggerError": false + }, + "addressLine1": { + "value": "", + "isError": false, + "isTriggerError": false + }, + "addressLine2": { + "value": "", + "isError": false, + "isTriggerError": false + }, + "cityPincode": { + "value": "", + "isError": false, + "isTriggerError": false + }, + "cityName": { + "value": "", + "isError": false, + "isTriggerError": false + }, + "stateId": { + "value": "", + "isTriggerError": false + }, + "stateName": { + "value": "", + "isTriggerError": false + }, + "withPet": { + "value": true, + "isTriggerError": false + }, + "petName": { + "value": "", + "isError": false, + "isTriggerError": false + }, + "petBreed": { + "value": "", + "isError": false, + "isTriggerError": false + }, + "petBirthDate": { + "value": "", + "isError": false, + "isTriggerError": false + }, + "formType": { + "value": "" + } + } + }, + "irctcPartnership": {}, + "campaigns": {} + }, + "blogData": { + "blogs": [], + "error": null, + "isfetching": null + }, + "pageUrlMappings": { + "/modinagar/schezwan-spicy-food-modinagar-locality/order": { + "name": "restaurant", + "pageTitle": "Schezwan Spicy Food, Modinagar Locality order online - Zomato", + "pageDescription": "Order food online from Schezwan Spicy Food, Modinagar Locality, Modinagar. Get great offers and super fast food delivery when you order food online from Schezwan Spicy Food on Zomato.", + "resId": 19713383, + "pageUrl": "/modinagar/schezwan-spicy-food-modinagar-locality/order", + "canonicalUrl": "https://www.zomato.com/modinagar/schezwan-spicy-food-modinagar-locality/order", + "title": "Order Online", + "subType": "order", + "key": "order", + "ogTitle": "Schezwan Spicy Food, Modinagar Locality order online - Zomato", + "ogDescription": "Order food online from Schezwan Spicy Food, Modinagar Locality, Modinagar. Get great offers and super fast food delivery when you order food online from Schezwan Spicy Food on Zomato.", + "ogUrl": "/modinagar/schezwan-spicy-food-modinagar-locality/order", + "ampHtmlUrl": "", + "isFloodReliefRes": false, + "isNoIndex": false, + "checkoutUrl": "/modinagar/schezwan-spicy-food-modinagar-locality/order/verify", + "show_rating_v15": true, + "showBookingV2": true, + "isRestaurantPageV2": true, + "isMobile": 0, + "isOAuthV2Enabled": false, + "useAuthSdkForLogin": true, + "useAuthSdkForLogout": false, + "gaPageType": "Restaurant" + } + }, + "careers": { + "departments": [] + }, + "allJobs": { + "openings": [], + "filters": [] + }, + "department": {}, + "aboutus": { + "leadershipData": [] + }, + "sneakpeek": {}, + "apiState": {}, + "entities": { + "REVIEWS": {}, + "IMAGES": { + "r_YwMjUyMTA4OTIw": { + "photoId": "r_YwMjUyMTA4OTIw", + "url": "https://b.zmtcdn.com/data/pictures/3/19713383/71da879caaa34072035375454ccdc2cf.png?output-format=webp", + "thumbUrl": "https://b.zmtcdn.com/data/pictures/3/19713383/71da879caaa34072035375454ccdc2cf.png?fit=around%7C200%3A200&crop=200%3A200%3B%2A%2C%2A", + "uploaderName": "Zomato", + "uploaderProfilePic": "https://b.zmtcdn.com/images/square_zomato_logo_new.svg", + "uploaderProfileUrl": "", + "timestamp": "Apr 28, 2021", + "likeCount": 0, + "commentCount": 0, + "comments": [], + "isLiked": 0, + "hash": "" + } + }, + "VIDEOS": {}, + "REVIEW_COMMENTS": {}, + "REVIEW_REPLIES": {}, + "PHOTO_COMMENTS": {}, + "POSITIVE_TAGS": {}, + "NEGATIVE_TAGS": {}, + "RATING": {}, + "EVENTS": {}, + "AD_BANNERS": {}, + "RESTAURANTS": {}, + "COLLECTIONS": {}, + "ORDER": {}, + "ADDRESSES": {}, + "USER": {}, + "PENDING_REVIEW": {}, + "CDNG_ORDER": {}, + "DOTE_ORDER": {}, + "ZLIVE_EVENTS": {} + }, + "user": { + "currentAddress": {}, + "is_admin_user": false, + "admin_access": [], + "admin_links": [] + }, + "uiLogic": { + "isPreciseLocationBannerOpen": true, + "searchPageMounted": false, + "isUniversalLocationWithBannerModalOpen": false, + "isUniversalLocationModalWithDishCardOpen": false, + "mountPartnershipPreciseLocationModal": true, + "universalLMDishCard": {}, + "promoBlockerOnPageLoadAllowed": false + }, + "location": { + "currentLocation": { + "addressId": 0, + "entityId": 11595, + "entityType": "city", + "locationType": "", + "isOrderLocation": 1, + "cityId": 11595, + "latitude": "28.8310000000000000", + "longitude": "77.5780000000000000", + "userDefinedLatitude": 28.831, + "userDefinedLongitude": 77.578, + "entityName": "Modinagar", + "orderLocationName": "Modinagar", + "cityName": "Modinagar", + "countryId": 1, + "countryName": "India", + "displayTitle": "Modinagar", + "o2Serviceable": true, + "placeId": "52540", + "cellId": "4110813000082915328", + "deliverySubzoneId": 52540, + "placeType": "DSZ", + "placeName": "Modinagar", + "isO2City": true, + "fetchFromGoogle": false, + "fetchedFromCookie": false, + "locationPrompt": [], + "isO2OnlyCity": true, + "addressBlocker": 0, + "address_template": [], + "otherRestaurantsUrl": "" + } + }, + "gAds": [], + "footer": { + "languages": [ + { + "name": "English", + "value": "en" + }, + { + "name": "T\u00fcrk\u00e7e", + "value": "tr" + }, + { + "name": "\u0939\u093f\u0902\u0926\u0940", + "value": "hi" + }, + { + "name": "Portugu\u00eas (BR)", + "value": "pt_br" + }, + { + "name": "Indonesian", + "value": "id" + }, + { + "name": "Portugu\u00eas (PT)", + "value": "pt" + }, + { + "name": "Espa\u00f1ol", + "value": "es" + }, + { + "name": "\u010ce\u0161tina", + "value": "cs" + }, + { + "name": "Sloven\u010dina", + "value": "sk" + }, + { + "name": "Polish", + "value": "pl" + }, + { + "name": "Italian", + "value": "it" + }, + { + "name": "Vietnamese", + "value": "vi" + } + ], + "selectedLanguage": { + "name": "English", + "value": "en" + }, + "linksData": { + "aboutusContent": [ + { + "label": "Who We Are", + "link": "https://www.zomato.com/who-we-are" + }, + { + "label": "Blog", + "link": "https://blog.zomato.com/" + }, + { + "label": "Work With Us", + "link": "https://www.zomato.com/careers" + }, + { + "label": "Investor Relations", + "link": "https://www.zomato.com/investor-relations" + }, + { + "label": "Report Fraud", + "link": "https://www.zomato.com/report-fraud" + }, + { + "label": "Press Kit", + "link": "https://blog.zomato.com/press-kit" + }, + { + "label": "Contact Us", + "link": "https://www.zomato.com/contact" + } + ], + "learnMoreContent": [ + { + "label": "Privacy", + "link": "https://www.zomato.com/privacy" + }, + { + "label": "Security", + "link": "https://www.zomato.com/security" + }, + { + "label": "Terms", + "link": "https://www.zomato.com/conditions" + } + ], + "restaurantsContent": [ + { + "label": "Partner With Us", + "link": "https://www.zomato.com/partner_with_us" + }, + { + "label": "Apps For You", + "link": "https://play.google.com/store/apps/details?id=com.application.services.partner&hl=en_IN&gl=US" + } + ], + "countries": [ + { + "id": 1, + "name": "India", + "value": "india" + }, + { + "id": 14, + "name": "Australia", + "value": "australia" + }, + { + "id": 30, + "name": "Brazil", + "value": "brazil" + }, + { + "id": 37, + "name": "Canada", + "value": "canada" + }, + { + "id": 42, + "name": "Chile", + "value": "chile" + }, + { + "id": 54, + "name": "Czech Republic", + "value": "czechrepublic" + }, + { + "id": 94, + "name": "Indonesia", + "value": "indonesia" + }, + { + "id": 97, + "name": "Ireland", + "value": "ireland" + }, + { + "id": 99, + "name": "Italy", + "value": "italy" + }, + { + "id": 112, + "name": "Lebanon", + "value": "lebanon" + }, + { + "id": 123, + "name": "Malaysia", + "value": "malaysia" + }, + { + "id": 148, + "name": "New Zealand", + "value": "newzealand" + }, + { + "id": 162, + "name": "Philippines", + "value": "philippines" + }, + { + "id": 163, + "name": "Poland", + "value": "poland" + }, + { + "id": 164, + "name": "Portugal", + "value": "portugal" + }, + { + "id": 166, + "name": "Qatar", + "value": "qatar" + }, + { + "id": 184, + "name": "Singapore", + "value": "singapore" + }, + { + "id": 185, + "name": "Slovakia", + "value": "slovakia" + }, + { + "id": 189, + "name": "South Africa", + "value": "southafrica" + }, + { + "id": 191, + "name": "Sri Lanka", + "value": "srilanka" + }, + { + "id": 208, + "name": "Turkey", + "value": "turkey" + }, + { + "id": 214, + "name": "UAE", + "value": "uae" + }, + { + "id": 215, + "name": "United Kingdom", + "value": "uk" + }, + { + "id": 216, + "name": "USA", + "value": "usa" + } + ], + "enterpriseContent": [ + { + "label": "Zomato For Enterprise", + "link": "https://www.zomato.com/enterprise-solutions" + } + ], + "zomaverseContent": [ + { + "label": "Zomato", + "link": "https://www.zomato.com/" + }, + { + "label": "Blinkit", + "link": "https://www.blinkit.com/" + }, + { + "label": "District", + "link": "https://www.district.in/" + }, + { + "label": "Feeding India", + "link": "https://www.feedingindia.org/" + }, + { + "label": "Hyperpure", + "link": "https://www.hyperpure.com/" + }, + { + "label": "Zomato Live", + "link": "https://www.zomato.com/live" + }, + { + "label": "Zomaland", + "link": "https://www.zomato.com/zomaland" + }, + { + "label": "Weather Union", + "link": "https://www.weatherunion.com/" + } + ] + }, + "footerDataArray": { + "ABOUT_US": "About Zomato", + "ZOMAVERSE": "Zomaverse", + "FOR_RESTAURANTS": "For Restaurants", + "LEARN_MORE": "Learn More", + "SOCIAL_LINKS": "Social links" + }, + "disclaimerText": "By continuing past this page, you agree to our Terms of Service, Cookie Policy, Privacy Policy and Content Policies. All trademarks are properties of their respective owners. 2008-2025 \u00a9 Zomato\u2122 Ltd. All rights reserved." + }, + "langKeys": { + "RES_INFO_TITLE": "About this place", + "ADDRESS_TITLE": "Address", + "FEATURED_IN_TITLE": "Featured in Collections", + "RATE_EXPERIENCE_TITLE": "Tap to rate your experience", + "WRITE_REVIEW_TITLE": "Write a Review", + "CONTACT_TITLE": "Tap a number to call", + "REVIEW_INPUT_LABEL": "Write your Review", + "REVIEW_ERROR_TOAST_MSG": "Minimum {0} tag(s) required", + "IMAGE_UPLOADER_DRAG_DROP_LABEL": "Drag & drop to upload or", + "IMAGE_UPLOADER_BROWSE_BUTTON_LABEL": "Browse", + "CLEAR_TEXT": "clear", + "VIEW_GALLERY": "View Gallery", + "OUR_SPONSORS_TITLE": "Explore other restaurants", + "SIMILAR_RES_TITLE": "Similar restaurants", + "HISTOGRAM_TITLE": "Trustworthy Reviews", + "HISTOGRAM_DESC": "100% genuine reviews. We weed out all the fake reviews", + "HISTOGRAM_TRENDS_TITLE": "Recent ratings trend", + "MOST_RECENT": "most recent", + "PEOPLE_SAY": "Review Highlights", + "ADD_REVIEW_TITLE": "Add Review", + "ORDER_TITLE": "Order Online", + "REVIEWS_TITLE": "Reviews", + "EVENT_TITLE": "Events", + "SEE_ALL_EVENTS_TITLE": "See all events", + "OPENING_HOURS": "Opening Hours", + "HAPPY_HOURS": "Happy Hours", + "CHEF_DETAILS_TITLE": "CHEF DETAILS", + "NO_MENU_TEXT": "We don't have a menu for this restaurant yet", + "EMPTY_MENU_LIST_STRING": "It's empty here!", + "NO_REVIEW_TEXT": "Your ratings and reviews go a long way towards helping people decide where to eat", + "VIEW_ALL_REVIEWS_TITLE": "View all reviews", + "CALCULATE_COST_TEXT": "How do we calculate cost for two?", + "FILTER_ALL_REVIEWS": "All Reviews", + "FILTER_FOLLOWING": "Following", + "FILTER_POPULAR": "Popular", + "FILTER_MY_REVIEWS": "My Reviews", + "FILTER_BLOGGERS_REVIEWS": "Bloggers", + "FILTER_ORDER_REVIEWS": "Order Reviews", + "SORT_NEWEST_FIRST": "Newest First", + "SORT_OLDEST_FIRST": "Oldest First", + "SORT_HIGHEST_RATED": "Highest Rated", + "SORT_LOWEST_RATED": "Lowest Rated", + "POSITIVE_TAGS": "POSITIVE", + "NEGATIVE_TAGS": "NEGATIVE", + "DAILY_MENU_TITLE": "Daily menu", + "DELETE_REVIEW_TEXT": "Are you sure you want to delete this review? This action cannot be undone.", + "DIRECTION_TITLE": "Direction", + "BOOKMARK_TITLE": "Bookmark", + "SHARE_TITLE": "Share", + "CALL_TEXT": "Call", + "COPY_TEXT": "Copy", + "PROMO_COPIED_TEXT": "Copied to clipboard", + "FEATURED_IN_TEXT": "Featured in", + "REVIEWS_TEXT": "reviews", + "FOLLOWERS_TEXT": "Followers", + "FOLLOW_TEXT": "Follow", + "LIKES_TEXT": "Votes for helpful", + "COMMENTS_TEXT": "Comments", + "LIKE_TEXT": "Helpful vote", + "COMMENT_TEXT": "Comment", + "LIKED_TEXT": "Helpful", + "SEE_ALL_MENUS_TEXT": "See all menus", + "SEE_FULL_MENU_TEXT": "See full menu", + "UGC_DISABLED_ERROR": "Sorry! You can only post content for operational restaurants", + "NUMBER_UNAVAILABLE": "Number not available", + "DONATE_NOW": "Donate Now", + "TAKEAWAY_TITLE": "Online Takeaway", + "TAKEAWAY_PICKUP_ADDRESS_PREFIX": "You will need to pick up this order from", + "ORDER_ADDRESS_PREFIX": "Delivering to: ", + "ORDER_ADDRESS_PREFIX_NOT_DELIVERING": "Does not deliver to: ", + "REPEAT_CUST_ADD_NEW_CAPTION": "Add new", + "REPEAT_CUST_REPEAT_LAST_CAPTION": "Repeat Last", + "REPEAT_CUST_MODAL_TITLE": "Repeat last used customisation", + "REMOVE_CUST_MODAL_TITLE": "Remove your items", + "MIN_MAX_QTY_ERROR_MSG": "Please select a minimum of {0} and maximum of {1} {2}", + "CUST_MENU_RADIO_SELECT_MESSAGE": "You can choose any 1 option", + "CUST_MENU_MULTI_SELECT_MIN_MAX_MESSAGE": "You can choose a minimum of {0} and maximum of {1} options", + "CUST_MENU_MULTI_SELECT_MAX_MESSAGE": "You can choose upto {0} options", + "CUST_MENU_ADD_TO_ORDER_BUTTON_CAPTION": "Add to order", + "VEG_ONLY_FILTER_LABEL": "veg only", + "NON_VEG_ONLY_FILTER_LABEL": "non veg only", + "CONTAINS_EGG_FILTER_LABEL": "contains egg", + "MENU_SEARCH_PLACEHOLDER_TEXT": "Search within menu", + "OFFER_DETAILS_MODAL_TITLE": "Offer Details", + "CLOSED_FOR_ONLINE_ORDERGING_TEXT": "Currently closed for online ordering", + "AVAILABLE_ON_THE_APP": "Available on the App", + "LIVE_TRACKING_OPEN_APP_MODAL_TITLE": "Live order tracking on the Zomato app", + "LIVE_TRACKING_OPEN_APP_MODAL_DESCRIPTION": "Switch to the app for the latest offers, better experience and more.", + "OPEN_APP_MODAL_CLICK_ACTION_TEXT": "Use the App", + "OPEN_APP_MODAL_CLOSE_ACTION_TEXT": "Not Now", + "MORE_INFO_TEXT": "More Info", + "FIND_OTHER_RESTAURANTS_TEXT": "Find restaurants delivering to your location", + "LOCATION_MODAL_WITH_DISH_CARD_DESC": "Enter delivery location to add this to your cart", + "RES_DOES_NOT_DELIVER": "This restaurant doesn't deliver to your location", + "DOES_NOT_DELIVER_DESCRIPTION": "To continue please view other restaurants or change location", + "VIEW_OTHER_RES_TEXT": "View Other Restaurants", + "DELIVERING_TO_TEXT": "Delivering to", + "CHANGE_LOCATION_TEXT": "Change Location", + "RES_NOT_SERVICEABLE_TEXT": "Currently not accepting orders", + "FIND_OTHER_RESTAURANTS": "Find other restaurants", + "ORDER_ONLINE_NOT_AVAILABLE_TITLE": "Online ordering is only supported on the Zomato mobile app", + "ORDER_ONLINE_NOT_AVAILABLE_BUTTON": "Open this page in the App", + "DOWNLOAD_THE_APP": "Download the App", + "ORDER_ONLINE_NOT_AVAILABLE_DESKTOP": "Online ordering is only supported on the mobile app", + "ORDER_SUSHI_EDIT_ADDRESS_TITLE": "Edit address", + "ORDER_SUSHI_DELIVERY_AREA_LABEL": "DELIVERY AREA", + "ORDER_SUSHI_ADDRESS_INPUT_PLACEHOLDER": "House no. / Flat no. / Floor / Building", + "ORDER_SUSHI_INSTRUCTIONS_INPUT_PLACEHOLDER": "Delivery instruction if any:", + "ORDER_SUSHI_BACK_TO_ADDRESS_BUTTON_CAPTION": "Back to select an address", + "ORDER_SUSHI_CANCEL_BUTTON_CAPTION": "CANCEL", + "ORDER_SUSHI_CHANGE_BUTTON_CAPTION": "CHANGE", + "ORDER_SUSHI_SAVE_AND_PROCEED_BUTTON_CAPTION": "Save and proceed", + "ORDER_SUSHI_ADD_OTHER_TAG_PLACEHOLDER": "Add tag", + "ORDER_SUSHI_SEARCH_INPUT_PLACEHOLDER": "Start typing to search", + "ORDER_SUSHI_RECENT_LOCATION_HEADER_TEXT": "recent locations", + "ORDER_SUSHI_BACK_TO_ADD_ADDRESS_BUTTON_CAPTION": "Back to add address", + "ORDER_SUSHI_ADDRESS_TYPE_LABEL_WORK": "Work", + "ORDER_SUSHI_ADDRESS_TYPE_LABEL_HOME": "Home", + "ORDER_SUSHI_ADDRESS_TYPE_LABEL_HOTEL": "Hotel", + "ORDER_SUSHI_ADDRESS_TYPE_LABEL_OTHER": "Other", + "ORDER_SUSHI_SEARCH_LOCATION_MODAL_TITLE": "Search Location", + "ORDER_SUSHI_SELECT_ADDRESS_MODAL_TITLE": "Select an address", + "ORDER_SUSHI_SAVED_ADDRESS_SEARCH_PLACEHOLDER": "Search in saved addresses", + "ORDER_SUSHI_ADD_ADDRESS_SEARCH_PLACEHOLDER": "Add new address", + "ORDER_SUSHI_SAVED_ADDRESSES_TITLE": "SAVED ADDRESSES", + "ORDER_SUSHI_DELIVERS_HERE_TEXT": "Delivers here", + "ORDER_SUSHI_NOT_DELIVER_HERE_TEXT": "Doesn't deliver here", + "ORDER_SUSHI_EDIT_BUTTON_CAPTION": "Edit", + "ORDER_SUSHI_SET_DELIVERY_LOCATION_TITLE": "Set your delivery location", + "ORDER_SUSHI_CONFIRM_AND_PROCEED_BUTTON_CAPTION": "Confirm and Proceed", + "ORDER_SUSHI_MOVE_THE_PIN": "Move the pin", + "ORDER_SUSHI_ADDRESS_BLOCKER_TEXT": "Please choose a more specific location below or move the pin on the map to the intended location.", + "ORDER_SUSHI_GOOGLE_MAP_PROMPT_LINE1": "Your order will be delivered here", + "ORDER_SUSHI_GOOGLE_MAP_PROMPT_LINE2": "Move pin to your exact location", + "ORDER_SUSHI_ADD_LABEL": "ADD", + "ORDER_SUSHI_CUSTOMIZE_LABEL": "customizable", + "ORDER_SUSHI_OUT_OF_STOCK_LABEL": "Out of stock", + "ORDER_SUSHI_MENU_BUTTON_CAPTION": "Menu", + "ORDER_SUSHI_ITEMS_SUFFIX_TEXT": "ITEMS", + "ORDER_SUSHI_SINGLE_ITEM_SUFFIX_TEXT": "ITEM", + "ORDER_SUSHI_CONTINUE_BUTTTON_CAPTION": "Continue", + "ORDER_SUSHI_CLEAR_BUTTON_CAPTION": "Clear Cart", + "ORDER_SUSHI_AMOUNT_SUFFIX_PLUS_TAXES_TEXT": "plus taxes", + "ORDER_SUSHI_CART_HEADER_TEXT": "Your Orders", + "ORDER_SUSHI_DEKTOP_TOGGLE_BUTTON_SUFFIX_TEXT": "Your Order", + "ORDER_SUSHI_SUBTOTAL_TEXT": "Subtotal", + "NO_SEARCH_RESULT_FOUND": "We could not understand what you mean, try rephrasing the query.", + "TRENDING_SEARCHES": "Trending Searches", + "NO_TRENDING_SEARCH": "No results found for Trending Searches", + "TOP_RESTAURANTS": "Top Restaurants", + "SEARCH_PLACEHOLDER": "Search for restaurant, cuisine or a dish", + "DETECT_LOCATION": "Detect current location", + "DETECT_LOCATION_SUBTITLE": "Using GPS", + "ADD_ADDRESS": "Add address", + "SAVED_ADDRESSES": "Saved Addresses", + "POPULAR_LOCATIONS": "Popular Locations", + "LOCATION_NO_RESULT_SUB": "Check for spelling errors or search for a nearby location", + "LOCATION_NO_RESULT": "No results found", + "SEARCH_MODAL_MOBILE_VIEW_TITLE": "Select location", + "GEO_LOCATION_NO_BROWSER_SUPPORT": "Seems like, Your browser does not support Geolocation.", + "GEO_LOCATION_PERMISSION_DENIED": "Please enable location permission from settings and try again!", + "GEO_LOCATION_POSITION_UNAVAILABLE": "We can't locate your position, please try again!", + "GEO_LOCATION_TIMEOUT": "Request for location has timed out!", + "GEO_LOCATION_UNKNOWN_ERROR": "An unknown error occurred, Please try again!", + "GEO_LOCATION_DEFAULT_ERROR": "An unknown error occurred, Please try again!", + "PROFILE_LINK_NAME": "Profile", + "REVIEWS_LINK_NAME": "Reviews", + "SETTINGS_LINK_NAME": "Settings", + "LOGOUT_LINK_NAME": "Log out", + "LOGIN_FAILED_TITLE": "Login Failed", + "SIGNUP_FAILED_TITLE": "Signup Failed", + "OTP_VERIFICATION_TITLE": "Enter OTP", + "LOGIN_TITLE": "Log in", + "SIGNUP_TITLE": "Sign up", + "SIGNUP_NAME_ERROR_MESSAGE": "Please enter a valid name", + "SIGNUP_EMPTY_EMAIL_ERROR_MESSAGE": "Please enter an email", + "SIGNUP_INVALID_EMAIL_ERROR_MESSAGE": "Invalid Email id", + "SIGNUP_PHONE_ERROR_MESSAGE": "Please enter phone number", + "SIGNUP_FULL_NAME_LABEL": "Full Name", + "SIGNUP_EMAIL_LABEL": "Email", + "SIGNUP_PHONE_LABEL": "Phone number", + "TERMS_OF_SERVICE_TEXT": "Terms of Service", + "PRIVACY_POLICY_TEXT": "Privacy Policy", + "CONTENT_POLICIES": "Content Policies", + "AGREE_TO_ZOMATO_POLICY_TEXT": "I agree to Zomato's {0}, {1} and {2}", + "CREATE_ACCOUNT_BUTTON_TEXT": "Create account", + "ALREADY_HAVE_AN_ACCOUNT_TEXT": "Already have an account? {0}", + "LOGIN_WITH_PHONE_ERROR": "Login with Phone number is not currently available", + "NEW_TO_ZOMATO_TEXT": "New to Zomato?", + "SEND_OTP_TEXT": "Send OTP", + "ERROR_MESSAAGE_BOX_TRY_OTHER_METHODS_TEXT": "Try using other methods", + "ERROR_MESSAAGE_BOX_SKIP_FOR_NOW_TEXT": "Skip for now", + "NEW_OTP_HAS_BEEN_SENT_TEXT": "A new OTP has been sent", + "NOT_RECEIVED_OTP_TEXT": "Didn't receive OTP?", + "RESEND_NOW_TEXT": "Resend Now", + "TERMINATE_VERIFICATION_TEXT": "Are you sure you want to terminate the verification?", + "YES_BUTTON_TEXT": "Yes", + "NO_BUTTON_TEXT": "No", + "CONTINUE_WITH_GOOGLE_BUTTON_TEXT": "Continue with Google", + "ERROR_OCCURED_TEXT": "Error occurred", + "OR_TEXT": "or", + "OTP_TEXT_BOX_LABEL": "OTP", + "OTP_TEXT_BOX_PROCEED_BUTTON": "Proceed", + "OTP_NOT_RECEIVED_TEXT": "Not received OTP? ", + "COOKIE_BANNER_TEXT": "By using this site you agree to Zomato's use of cookies to give you a personalised experience. Please read the cookie policy for more information or to delete/block them." + }, + "deviceSpecificInfo": { + "browser": { + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "name": "Unknown", + "version": "?", + "platform": "windows" + } + }, + "pageBlockerInfo": {}, + "fullPageAds": { + "pageViews": [], + "adVisible": false + }, + "networkState": { + "isOnline": -1 + }, + "fetchConfigs": { + "headers": {} + }, + "hrefLangInfo": [ + { + "link": "https://www.zomato.com/modinagar/schezwan-spicy-food-modinagar-locality/order", + "hreflang": "en-in", + "isSelected": true + }, + { + "link": "https://www.zomato.com/hi/modinagar/schezwan-spicy-food-modinagar-locality/order", + "hreflang": "hi-in", + "isSelected": false + }, + { + "link": "https://www.zomato.com/bn/modinagar/schezwan-spicy-food-modinagar-locality/order", + "hreflang": "bn-in", + "isSelected": false + }, + { + "link": "https://www.zomato.com/te/modinagar/schezwan-spicy-food-modinagar-locality/order", + "hreflang": "te-in", + "isSelected": false + }, + { + "link": "https://www.zomato.com/ta/modinagar/schezwan-spicy-food-modinagar-locality/order", + "hreflang": "ta-in", + "isSelected": false + }, + { + "link": "https://www.zomato.com/kn/modinagar/schezwan-spicy-food-modinagar-locality/order", + "hreflang": "kn-in", + "isSelected": false + }, + { + "link": "https://www.zomato.com/mr/modinagar/schezwan-spicy-food-modinagar-locality/order", + "hreflang": "mr-in", + "isSelected": false + }, + { + "link": "https://www.zomato.com/gu/modinagar/schezwan-spicy-food-modinagar-locality/order", + "hreflang": "gu-in", + "isSelected": false + }, + { + "link": "https://www.zomato.com/pa/modinagar/schezwan-spicy-food-modinagar-locality/order", + "hreflang": "pa-in", + "isSelected": false + }, + { + "link": "https://www.zomato.com/ml/modinagar/schezwan-spicy-food-modinagar-locality/order", + "hreflang": "ml-in", + "isSelected": false + }, + { + "link": "https://www.zomato.com/or/modinagar/schezwan-spicy-food-modinagar-locality/order", + "hreflang": "or-in", + "isSelected": false + }, + { + "link": "https://www.zomato.com/hi-en/modinagar/schezwan-spicy-food-modinagar-locality/order", + "hreflang": "hi-en-in", + "isSelected": false + }, + { + "link": "https://www.zomato.com/modinagar/schezwan-spicy-food-modinagar-locality/order", + "hreflang": "x-default", + "isSelected": false + } + ], + "pageConfig": { + "showLocationBannerAutoPopup": false, + "openAppPill": [], + "isLocationPopupFlowAllowed": false, + "cacheMeta": { + "cacheable": false, + "max-age": 172800, + "trackingData": { + "identifier": "19713383" + } + }, + "orderPageBlocker": { + "showO2": false, + "desktopDeeplinkUrl": "" + }, + "hideCookieBanner": true, + "showRatingV2": true + }, + "partnershipLoginModal": { + "isVisible": false + }, + "partnershipLoginOptionModal": { + "isVisible": false + }, + "doesNotDeliverModal": { + "isVisible": false + }, + "backButton": { + "showLoadingState": false + }, + "renderingStrategy": { + "isSSG": false + } +} \ No newline at end of file diff --git a/backend/app/tests/api/__init__.py b/backend/menu_scrapper/demo.py similarity index 100% rename from backend/app/tests/api/__init__.py rename to backend/menu_scrapper/demo.py diff --git a/backend/menu_scrapper/test.py b/backend/menu_scrapper/test.py new file mode 100644 index 0000000000..5ed1ebb595 --- /dev/null +++ b/backend/menu_scrapper/test.py @@ -0,0 +1,200 @@ +import json +import requests +from bs4 import BeautifulSoup +import logging +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, HttpUrl +from typing import Dict, Any + +# Configure logging +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +app = FastAPI() + +class ZomatoUrl(BaseModel): + url: HttpUrl + +def extract_menu_data(json_data: dict) -> dict: + try: + logger.debug("Starting menu data extraction...") + restaurant_data = json_data.get('pages', {}).get('current', {}) + restaurant_details = json_data.get('pages', {}).get('restaurant', {}) + + # Debug log the structure + logger.debug(f"Available keys in json_data: {list(json_data.keys())}") + logger.debug(f"Restaurant details keys: {list(restaurant_details.keys())}") + + restaurant_id = next(iter(restaurant_details)) if restaurant_details else None + + if not restaurant_id: + logger.error("No restaurant ID found") + return {} + + res_info = restaurant_details[restaurant_id].get('sections', {}) + logger.debug(f"Available sections: {list(res_info.keys())}") + + # Restaurant Info + basic_info = res_info.get('SECTION_BASIC_INFO', {}) + restaurant_info = { + 'restaurant_id': basic_info.get('res_id'), + 'name': basic_info.get('name'), + 'cuisines': basic_info.get('cuisine_string'), + 'rating': { + 'aggregate_rating': basic_info.get('rating', {}).get('aggregate_rating'), + 'votes': basic_info.get('rating', {}).get('votes'), + 'rating_text': basic_info.get('rating', {}).get('rating_text') + }, + 'location': { + 'locality': restaurant_data.get('pageDescription', ''), + 'url': basic_info.get('resUrl') + }, + 'timing': { + 'description': basic_info.get('timing', {}).get('timing_desc'), + 'hours': basic_info.get('timing', {}).get('customised_timings', {}).get('opening_hours', []) + } + } + + # Menu Items + menu_data = res_info.get('SECTION_MENU_WIDGET', {}) + logger.debug(f"Menu widget data keys: {list(menu_data.keys())}") + + menu_categories = [] + for category in menu_data.get('categories', []): + category_items = { + 'category': category.get('name', ''), + 'items': [] + } + + for item in category.get('items', []): + menu_item = { + 'id': str(item.get('id')), + 'name': item.get('name'), + 'description': item.get('description', ''), + 'price': float(item.get('price', 0)), + 'image_url': item.get('imageUrl', ''), + 'is_veg': item.get('isVeg', True), + 'spice_level': item.get('spiceLevel', 'None') + } + category_items['items'].append(menu_item) + + if category_items['items']: + menu_categories.append(category_items) + + return { + 'restaurant_info': restaurant_info, + 'menu': menu_categories + } + + except Exception as e: + logger.error(f"Error extracting menu data: {str(e)}") + return {} + +def fetch_zomato_data(url: str) -> str: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Connection': 'keep-alive', + } + + try: + logger.info(f"Fetching data from URL: {url}") + response = requests.get(url, headers=headers) + response.raise_for_status() + logger.debug(f"Response status code: {response.status_code}") + return response.text + except Exception as e: + logger.error(f"Error fetching data: {str(e)}") + raise + +def parse_zomato_page(html_content: str) -> dict: + try: + logger.debug("Starting page parsing...") + soup = BeautifulSoup(html_content, 'html.parser') + + # Find script with PRELOADED_STATE + scripts = soup.find_all('script') + target_script = None + + for script in scripts: + if script.string and 'window.__PRELOADED_STATE__' in script.string: + target_script = script + break + + if not target_script: + raise ValueError("Could not find PRELOADED_STATE in page") + + # Extract and clean JSON string + json_str = target_script.string.split('window.__PRELOADED_STATE__ = JSON.parse(')[1] + json_str = json_str.split(');')[0].strip() + + # Clean the JSON string + json_str = json_str.strip('"') + json_str = json_str.replace('\\"', '"') + json_str = json_str.replace('\\\\', '\\') + json_str = json_str.replace('\\n', '') + + # Debug log + logger.debug(f"Extracted JSON string (first 200 chars): {json_str[:200]}...") + + # Parse JSON + parsed_data = json.loads(json_str) + + # Save raw data for debugging + with open('raw_data.json', 'w', encoding='utf-8') as f: + json.dump(parsed_data, f, indent=2, ensure_ascii=False) + logger.info("Raw data saved to raw_data.json") + + return parsed_data + + except Exception as e: + logger.error(f"Error parsing page: {str(e)}") + logger.error(f"Script content: {target_script.string[:200] if target_script else 'No script found'}") + raise + +@app.get("/") +async def root(): + return {"message": "Welcome to Zomato Menu Scraper"} + +@app.post("/scrape-menu") +async def scrape_menu(request: ZomatoUrl) -> Dict[Any, Any]: + try: + if "zomato.com" not in str(request.url): + raise HTTPException( + status_code=400, + detail="Invalid URL. Please provide a valid Zomato restaurant URL" + ) + + logger.info("Starting scraping process...") + html_content = fetch_zomato_data(str(request.url)) + json_data = parse_zomato_page(html_content) + formatted_data = extract_menu_data(json_data) + + if not formatted_data: + raise HTTPException( + status_code=500, + detail="Failed to extract menu data" + ) + + # Save formatted data for debugging + with open('formatted_data.json', 'w', encoding='utf-8') as f: + json.dump(formatted_data, f, indent=2, ensure_ascii=False) + logger.info("Formatted data saved to formatted_data.json") + + return formatted_data + + except Exception as e: + logger.error(f"Scraping failed: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Failed to scrape menu: {str(e)}" + ) + +if __name__ == "__main__": + import uvicorn + logger.info("Starting Zomato Menu Scraper...") + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/backend/poetry.lock b/backend/poetry.lock index f56ce480f0..e9a85279f5 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1,5 +1,182 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +[[package]] +name = "aiofiles" +version = "24.1.0" +description = "File support for asyncio." +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, + {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.3" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572"}, + {file = "aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586"}, +] + +[[package]] +name = "aiohttp" +version = "3.10.10" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohttp-3.10.10-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:be7443669ae9c016b71f402e43208e13ddf00912f47f623ee5994e12fc7d4b3f"}, + {file = "aiohttp-3.10.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b06b7843929e41a94ea09eb1ce3927865387e3e23ebe108e0d0d09b08d25be9"}, + {file = "aiohttp-3.10.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:333cf6cf8e65f6a1e06e9eb3e643a0c515bb850d470902274239fea02033e9a8"}, + {file = "aiohttp-3.10.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:274cfa632350225ce3fdeb318c23b4a10ec25c0e2c880eff951a3842cf358ac1"}, + {file = "aiohttp-3.10.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9e5e4a85bdb56d224f412d9c98ae4cbd032cc4f3161818f692cd81766eee65a"}, + {file = "aiohttp-3.10.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b606353da03edcc71130b52388d25f9a30a126e04caef1fd637e31683033abd"}, + {file = "aiohttp-3.10.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab5a5a0c7a7991d90446a198689c0535be89bbd6b410a1f9a66688f0880ec026"}, + {file = "aiohttp-3.10.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:578a4b875af3e0daaf1ac6fa983d93e0bbfec3ead753b6d6f33d467100cdc67b"}, + {file = "aiohttp-3.10.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8105fd8a890df77b76dd3054cddf01a879fc13e8af576805d667e0fa0224c35d"}, + {file = "aiohttp-3.10.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3bcd391d083f636c06a68715e69467963d1f9600f85ef556ea82e9ef25f043f7"}, + {file = "aiohttp-3.10.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fbc6264158392bad9df19537e872d476f7c57adf718944cc1e4495cbabf38e2a"}, + {file = "aiohttp-3.10.10-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e48d5021a84d341bcaf95c8460b152cfbad770d28e5fe14a768988c461b821bc"}, + {file = "aiohttp-3.10.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2609e9ab08474702cc67b7702dbb8a80e392c54613ebe80db7e8dbdb79837c68"}, + {file = "aiohttp-3.10.10-cp310-cp310-win32.whl", hash = "sha256:84afcdea18eda514c25bc68b9af2a2b1adea7c08899175a51fe7c4fb6d551257"}, + {file = "aiohttp-3.10.10-cp310-cp310-win_amd64.whl", hash = "sha256:9c72109213eb9d3874f7ac8c0c5fa90e072d678e117d9061c06e30c85b4cf0e6"}, + {file = "aiohttp-3.10.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c30a0eafc89d28e7f959281b58198a9fa5e99405f716c0289b7892ca345fe45f"}, + {file = "aiohttp-3.10.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:258c5dd01afc10015866114e210fb7365f0d02d9d059c3c3415382ab633fcbcb"}, + {file = "aiohttp-3.10.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:15ecd889a709b0080f02721255b3f80bb261c2293d3c748151274dfea93ac871"}, + {file = "aiohttp-3.10.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3935f82f6f4a3820270842e90456ebad3af15810cf65932bd24da4463bc0a4c"}, + {file = "aiohttp-3.10.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:413251f6fcf552a33c981c4709a6bba37b12710982fec8e558ae944bfb2abd38"}, + {file = "aiohttp-3.10.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1720b4f14c78a3089562b8875b53e36b51c97c51adc53325a69b79b4b48ebcb"}, + {file = "aiohttp-3.10.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:679abe5d3858b33c2cf74faec299fda60ea9de62916e8b67e625d65bf069a3b7"}, + {file = "aiohttp-3.10.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79019094f87c9fb44f8d769e41dbb664d6e8fcfd62f665ccce36762deaa0e911"}, + {file = "aiohttp-3.10.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe2fb38c2ed905a2582948e2de560675e9dfbee94c6d5ccdb1301c6d0a5bf092"}, + {file = "aiohttp-3.10.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a3f00003de6eba42d6e94fabb4125600d6e484846dbf90ea8e48a800430cc142"}, + {file = "aiohttp-3.10.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1bbb122c557a16fafc10354b9d99ebf2f2808a660d78202f10ba9d50786384b9"}, + {file = "aiohttp-3.10.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:30ca7c3b94708a9d7ae76ff281b2f47d8eaf2579cd05971b5dc681db8caac6e1"}, + {file = "aiohttp-3.10.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df9270660711670e68803107d55c2b5949c2e0f2e4896da176e1ecfc068b974a"}, + {file = "aiohttp-3.10.10-cp311-cp311-win32.whl", hash = "sha256:aafc8ee9b742ce75044ae9a4d3e60e3d918d15a4c2e08a6c3c3e38fa59b92d94"}, + {file = "aiohttp-3.10.10-cp311-cp311-win_amd64.whl", hash = "sha256:362f641f9071e5f3ee6f8e7d37d5ed0d95aae656adf4ef578313ee585b585959"}, + {file = "aiohttp-3.10.10-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9294bbb581f92770e6ed5c19559e1e99255e4ca604a22c5c6397b2f9dd3ee42c"}, + {file = "aiohttp-3.10.10-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a8fa23fe62c436ccf23ff930149c047f060c7126eae3ccea005f0483f27b2e28"}, + {file = "aiohttp-3.10.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c6a5b8c7926ba5d8545c7dd22961a107526562da31a7a32fa2456baf040939f"}, + {file = "aiohttp-3.10.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:007ec22fbc573e5eb2fb7dec4198ef8f6bf2fe4ce20020798b2eb5d0abda6138"}, + {file = "aiohttp-3.10.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9627cc1a10c8c409b5822a92d57a77f383b554463d1884008e051c32ab1b3742"}, + {file = "aiohttp-3.10.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:50edbcad60d8f0e3eccc68da67f37268b5144ecc34d59f27a02f9611c1d4eec7"}, + {file = "aiohttp-3.10.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a45d85cf20b5e0d0aa5a8dca27cce8eddef3292bc29d72dcad1641f4ed50aa16"}, + {file = "aiohttp-3.10.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b00807e2605f16e1e198f33a53ce3c4523114059b0c09c337209ae55e3823a8"}, + {file = "aiohttp-3.10.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f2d4324a98062be0525d16f768a03e0bbb3b9fe301ceee99611dc9a7953124e6"}, + {file = "aiohttp-3.10.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:438cd072f75bb6612f2aca29f8bd7cdf6e35e8f160bc312e49fbecab77c99e3a"}, + {file = "aiohttp-3.10.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:baa42524a82f75303f714108fea528ccacf0386af429b69fff141ffef1c534f9"}, + {file = "aiohttp-3.10.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a7d8d14fe962153fc681f6366bdec33d4356f98a3e3567782aac1b6e0e40109a"}, + {file = "aiohttp-3.10.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1277cd707c465cd09572a774559a3cc7c7a28802eb3a2a9472588f062097205"}, + {file = "aiohttp-3.10.10-cp312-cp312-win32.whl", hash = "sha256:59bb3c54aa420521dc4ce3cc2c3fe2ad82adf7b09403fa1f48ae45c0cbde6628"}, + {file = "aiohttp-3.10.10-cp312-cp312-win_amd64.whl", hash = "sha256:0e1b370d8007c4ae31ee6db7f9a2fe801a42b146cec80a86766e7ad5c4a259cf"}, + {file = "aiohttp-3.10.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ad7593bb24b2ab09e65e8a1d385606f0f47c65b5a2ae6c551db67d6653e78c28"}, + {file = "aiohttp-3.10.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1eb89d3d29adaf533588f209768a9c02e44e4baf832b08118749c5fad191781d"}, + {file = "aiohttp-3.10.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3fe407bf93533a6fa82dece0e74dbcaaf5d684e5a51862887f9eaebe6372cd79"}, + {file = "aiohttp-3.10.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50aed5155f819873d23520919e16703fc8925e509abbb1a1491b0087d1cd969e"}, + {file = "aiohttp-3.10.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f05e9727ce409358baa615dbeb9b969db94324a79b5a5cea45d39bdb01d82e6"}, + {file = "aiohttp-3.10.10-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dffb610a30d643983aeb185ce134f97f290f8935f0abccdd32c77bed9388b42"}, + {file = "aiohttp-3.10.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa6658732517ddabe22c9036479eabce6036655ba87a0224c612e1ae6af2087e"}, + {file = "aiohttp-3.10.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:741a46d58677d8c733175d7e5aa618d277cd9d880301a380fd296975a9cdd7bc"}, + {file = "aiohttp-3.10.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e00e3505cd80440f6c98c6d69269dcc2a119f86ad0a9fd70bccc59504bebd68a"}, + {file = "aiohttp-3.10.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ffe595f10566f8276b76dc3a11ae4bb7eba1aac8ddd75811736a15b0d5311414"}, + {file = "aiohttp-3.10.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdfcf6443637c148c4e1a20c48c566aa694fa5e288d34b20fcdc58507882fed3"}, + {file = "aiohttp-3.10.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d183cf9c797a5291e8301790ed6d053480ed94070637bfaad914dd38b0981f67"}, + {file = "aiohttp-3.10.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:77abf6665ae54000b98b3c742bc6ea1d1fb31c394bcabf8b5d2c1ac3ebfe7f3b"}, + {file = "aiohttp-3.10.10-cp313-cp313-win32.whl", hash = "sha256:4470c73c12cd9109db8277287d11f9dd98f77fc54155fc71a7738a83ffcc8ea8"}, + {file = "aiohttp-3.10.10-cp313-cp313-win_amd64.whl", hash = "sha256:486f7aabfa292719a2753c016cc3a8f8172965cabb3ea2e7f7436c7f5a22a151"}, + {file = "aiohttp-3.10.10-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1b66ccafef7336a1e1f0e389901f60c1d920102315a56df85e49552308fc0486"}, + {file = "aiohttp-3.10.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:acd48d5b80ee80f9432a165c0ac8cbf9253eaddb6113269a5e18699b33958dbb"}, + {file = "aiohttp-3.10.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3455522392fb15ff549d92fbf4b73b559d5e43dc522588f7eb3e54c3f38beee7"}, + {file = "aiohttp-3.10.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45c3b868724137f713a38376fef8120c166d1eadd50da1855c112fe97954aed8"}, + {file = "aiohttp-3.10.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:da1dee8948d2137bb51fbb8a53cce6b1bcc86003c6b42565f008438b806cccd8"}, + {file = "aiohttp-3.10.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c5ce2ce7c997e1971b7184ee37deb6ea9922ef5163c6ee5aa3c274b05f9e12fa"}, + {file = "aiohttp-3.10.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28529e08fde6f12eba8677f5a8608500ed33c086f974de68cc65ab218713a59d"}, + {file = "aiohttp-3.10.10-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7db54c7914cc99d901d93a34704833568d86c20925b2762f9fa779f9cd2e70f"}, + {file = "aiohttp-3.10.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:03a42ac7895406220124c88911ebee31ba8b2d24c98507f4a8bf826b2937c7f2"}, + {file = "aiohttp-3.10.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:7e338c0523d024fad378b376a79faff37fafb3c001872a618cde1d322400a572"}, + {file = "aiohttp-3.10.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:038f514fe39e235e9fef6717fbf944057bfa24f9b3db9ee551a7ecf584b5b480"}, + {file = "aiohttp-3.10.10-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:64f6c17757251e2b8d885d728b6433d9d970573586a78b78ba8929b0f41d045a"}, + {file = "aiohttp-3.10.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:93429602396f3383a797a2a70e5f1de5df8e35535d7806c9f91df06f297e109b"}, + {file = "aiohttp-3.10.10-cp38-cp38-win32.whl", hash = "sha256:c823bc3971c44ab93e611ab1a46b1eafeae474c0c844aff4b7474287b75fe49c"}, + {file = "aiohttp-3.10.10-cp38-cp38-win_amd64.whl", hash = "sha256:54ca74df1be3c7ca1cf7f4c971c79c2daf48d9aa65dea1a662ae18926f5bc8ce"}, + {file = "aiohttp-3.10.10-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:01948b1d570f83ee7bbf5a60ea2375a89dfb09fd419170e7f5af029510033d24"}, + {file = "aiohttp-3.10.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9fc1500fd2a952c5c8e3b29aaf7e3cc6e27e9cfc0a8819b3bce48cc1b849e4cc"}, + {file = "aiohttp-3.10.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f614ab0c76397661b90b6851a030004dac502e48260ea10f2441abd2207fbcc7"}, + {file = "aiohttp-3.10.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00819de9e45d42584bed046314c40ea7e9aea95411b38971082cad449392b08c"}, + {file = "aiohttp-3.10.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05646ebe6b94cc93407b3bf34b9eb26c20722384d068eb7339de802154d61bc5"}, + {file = "aiohttp-3.10.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:998f3bd3cfc95e9424a6acd7840cbdd39e45bc09ef87533c006f94ac47296090"}, + {file = "aiohttp-3.10.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9010c31cd6fa59438da4e58a7f19e4753f7f264300cd152e7f90d4602449762"}, + {file = "aiohttp-3.10.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ea7ffc6d6d6f8a11e6f40091a1040995cdff02cfc9ba4c2f30a516cb2633554"}, + {file = "aiohttp-3.10.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ef9c33cc5cbca35808f6c74be11eb7f5f6b14d2311be84a15b594bd3e58b5527"}, + {file = "aiohttp-3.10.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ce0cdc074d540265bfeb31336e678b4e37316849d13b308607efa527e981f5c2"}, + {file = "aiohttp-3.10.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:597a079284b7ee65ee102bc3a6ea226a37d2b96d0418cc9047490f231dc09fe8"}, + {file = "aiohttp-3.10.10-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:7789050d9e5d0c309c706953e5e8876e38662d57d45f936902e176d19f1c58ab"}, + {file = "aiohttp-3.10.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e7f8b04d83483577fd9200461b057c9f14ced334dcb053090cea1da9c8321a91"}, + {file = "aiohttp-3.10.10-cp39-cp39-win32.whl", hash = "sha256:c02a30b904282777d872266b87b20ed8cc0d1501855e27f831320f471d54d983"}, + {file = "aiohttp-3.10.10-cp39-cp39-win_amd64.whl", hash = "sha256:edfe3341033a6b53a5c522c802deb2079eee5cbfbb0af032a55064bd65c73a23"}, + {file = "aiohttp-3.10.10.tar.gz", hash = "sha256:0631dd7c9f0822cc61c88586ca76d5b5ada26538097d0f1df510b082bad3411a"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.3.0" +aiosignal = ">=1.1.2" +async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.12.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] + +[[package]] +name = "aioredis" +version = "1.3.1" +description = "asyncio (PEP 3156) Redis support" +optional = false +python-versions = "*" +files = [ + {file = "aioredis-1.3.1-py3-none-any.whl", hash = "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3"}, + {file = "aioredis-1.3.1.tar.gz", hash = "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a"}, +] + +[package.dependencies] +async-timeout = "*" +hiredis = "*" + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "aiosqlite" +version = "0.17.0" +description = "asyncio bridge to the standard sqlite3 module" +optional = false +python-versions = ">=3.6" +files = [ + {file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"}, + {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"}, +] + +[package.dependencies] +typing_extensions = ">=3.7.2" + [[package]] name = "alembic" version = "1.13.2" @@ -52,40 +229,255 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] +[[package]] +name = "argon2-cffi" +version = "23.1.0" +description = "Argon2 for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, + {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, +] + +[package.dependencies] +argon2-cffi-bindings = "*" + +[package.extras] +dev = ["argon2-cffi[tests,typing]", "tox (>4)"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-copybutton", "sphinx-notfound-page"] +tests = ["hypothesis", "pytest"] +typing = ["mypy"] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +description = "Low-level CFFI bindings for Argon2" +optional = false +python-versions = ">=3.6" +files = [ + {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, + {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, +] + +[package.dependencies] +cffi = ">=1.0.1" + +[package.extras] +dev = ["cogapp", "pre-commit", "pytest", "wheel"] +tests = ["pytest"] + +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[package]] +name = "asyncpg" +version = "0.29.0" +description = "An asyncio PostgreSQL driver" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "asyncpg-0.29.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72fd0ef9f00aeed37179c62282a3d14262dbbafb74ec0ba16e1b1864d8a12169"}, + {file = "asyncpg-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52e8f8f9ff6e21f9b39ca9f8e3e33a5fcdceaf5667a8c5c32bee158e313be385"}, + {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e6823a7012be8b68301342ba33b4740e5a166f6bbda0aee32bc01638491a22"}, + {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746e80d83ad5d5464cfbf94315eb6744222ab00aa4e522b704322fb182b83610"}, + {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ff8e8109cd6a46ff852a5e6bab8b0a047d7ea42fcb7ca5ae6eaae97d8eacf397"}, + {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97eb024685b1d7e72b1972863de527c11ff87960837919dac6e34754768098eb"}, + {file = "asyncpg-0.29.0-cp310-cp310-win32.whl", hash = "sha256:5bbb7f2cafd8d1fa3e65431833de2642f4b2124be61a449fa064e1a08d27e449"}, + {file = "asyncpg-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:76c3ac6530904838a4b650b2880f8e7af938ee049e769ec2fba7cd66469d7772"}, + {file = "asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4"}, + {file = "asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b"}, + {file = "asyncpg-0.29.0-cp311-cp311-win32.whl", hash = "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675"}, + {file = "asyncpg-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175"}, + {file = "asyncpg-0.29.0-cp312-cp312-win32.whl", hash = "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02"}, + {file = "asyncpg-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe"}, + {file = "asyncpg-0.29.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0009a300cae37b8c525e5b449233d59cd9868fd35431abc470a3e364d2b85cb9"}, + {file = "asyncpg-0.29.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cad1324dbb33f3ca0cd2074d5114354ed3be2b94d48ddfd88af75ebda7c43cc"}, + {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:012d01df61e009015944ac7543d6ee30c2dc1eb2f6b10b62a3f598beb6531548"}, + {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000c996c53c04770798053e1730d34e30cb645ad95a63265aec82da9093d88e7"}, + {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e0bfe9c4d3429706cf70d3249089de14d6a01192d617e9093a8e941fea8ee775"}, + {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:642a36eb41b6313ffa328e8a5c5c2b5bea6ee138546c9c3cf1bffaad8ee36dd9"}, + {file = "asyncpg-0.29.0-cp38-cp38-win32.whl", hash = "sha256:a921372bbd0aa3a5822dd0409da61b4cd50df89ae85150149f8c119f23e8c408"}, + {file = "asyncpg-0.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:103aad2b92d1506700cbf51cd8bb5441e7e72e87a7b3a2ca4e32c840f051a6a3"}, + {file = "asyncpg-0.29.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5340dd515d7e52f4c11ada32171d87c05570479dc01dc66d03ee3e150fb695da"}, + {file = "asyncpg-0.29.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e17b52c6cf83e170d3d865571ba574577ab8e533e7361a2b8ce6157d02c665d3"}, + {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f100d23f273555f4b19b74a96840aa27b85e99ba4b1f18d4ebff0734e78dc090"}, + {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48e7c58b516057126b363cec8ca02b804644fd012ef8e6c7e23386b7d5e6ce83"}, + {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9ea3f24eb4c49a615573724d88a48bd1b7821c890c2effe04f05382ed9e8810"}, + {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8d36c7f14a22ec9e928f15f92a48207546ffe68bc412f3be718eedccdf10dc5c"}, + {file = "asyncpg-0.29.0-cp39-cp39-win32.whl", hash = "sha256:797ab8123ebaed304a1fad4d7576d5376c3a006a4100380fb9d517f0b59c1ab2"}, + {file = "asyncpg-0.29.0-cp39-cp39-win_amd64.whl", hash = "sha256:cce08a178858b426ae1aa8409b5cc171def45d4293626e7aa6510696d46decd8"}, + {file = "asyncpg-0.29.0.tar.gz", hash = "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.12.0\""} + +[package.extras] +docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"] + +[[package]] +name = "attrs" +version = "24.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "autoscraper" +version = "1.1.14" +description = "A Smart, Automatic, Fast and Lightweight Web Scraper for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "autoscraper-1.1.14-py3-none-any.whl", hash = "sha256:fc0265723f6bcc80ee908d7be8d5318129f3388d2830d049fc0bda6c25695cf9"}, + {file = "autoscraper-1.1.14.tar.gz", hash = "sha256:281901477fb69aa09aa235abbd15bb38c46df1682c2cad504d0ac1ee0b6b81d0"}, +] + +[package.dependencies] +bs4 = "*" +lxml = "*" +requests = "*" + +[[package]] +name = "babel" +version = "2.16.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, + {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + [[package]] name = "bcrypt" -version = "4.0.1" +version = "4.1.2" description = "Modern password hashing for your software and your servers" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"}, - {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"}, - {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"}, - {file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"}, - {file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"}, - {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"}, - {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"}, - {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"}, - {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"}, - {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"}, - {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"}, - {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"}, - {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"}, - {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"}, - {file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"}, + {file = "bcrypt-4.1.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ac621c093edb28200728a9cca214d7e838529e557027ef0581685909acd28b5e"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea505c97a5c465ab8c3ba75c0805a102ce526695cd6818c6de3b1a38f6f60da1"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57fa9442758da926ed33a91644649d3e340a71e2d0a5a8de064fb621fd5a3326"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb3bd3321517916696233b5e0c67fd7d6281f0ef48e66812db35fc963a422a1c"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6cad43d8c63f34b26aef462b6f5e44fdcf9860b723d2453b5d391258c4c8e966"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:44290ccc827d3a24604f2c8bcd00d0da349e336e6503656cb8192133e27335e2"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:732b3920a08eacf12f93e6b04ea276c489f1c8fb49344f564cca2adb663b3e4c"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c28973decf4e0e69cee78c68e30a523be441972c826703bb93099868a8ff5b5"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b8df79979c5bae07f1db22dcc49cc5bccf08a0380ca5c6f391cbb5790355c0b0"}, + {file = "bcrypt-4.1.2-cp37-abi3-win32.whl", hash = "sha256:fbe188b878313d01b7718390f31528be4010fed1faa798c5a1d0469c9c48c369"}, + {file = "bcrypt-4.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:9800ae5bd5077b13725e2e3934aa3c9c37e49d3ea3d06318010aa40f54c63551"}, + {file = "bcrypt-4.1.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:71b8be82bc46cedd61a9f4ccb6c1a493211d031415a34adde3669ee1b0afbb63"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e3c6642077b0c8092580c819c1684161262b2e30c4f45deb000c38947bf483"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:387e7e1af9a4dd636b9505a465032f2f5cb8e61ba1120e79a0e1cd0b512f3dfc"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f70d9c61f9c4ca7d57f3bfe88a5ccf62546ffbadf3681bb1e268d9d2e41c91a7"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2a298db2a8ab20056120b45e86c00a0a5eb50ec4075b6142db35f593b97cb3fb"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ba55e40de38a24e2d78d34c2d36d6e864f93e0d79d0b6ce915e4335aa81d01b1"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3566a88234e8de2ccae31968127b0ecccbb4cddb629da744165db72b58d88ca4"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b90e216dc36864ae7132cb151ffe95155a37a14e0de3a8f64b49655dd959ff9c"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:69057b9fc5093ea1ab00dd24ede891f3e5e65bee040395fb1e66ee196f9c9b4a"}, + {file = "bcrypt-4.1.2-cp39-abi3-win32.whl", hash = "sha256:02d9ef8915f72dd6daaef40e0baeef8a017ce624369f09754baf32bb32dba25f"}, + {file = "bcrypt-4.1.2-cp39-abi3-win_amd64.whl", hash = "sha256:be3ab1071662f6065899fe08428e45c16aa36e28bc42921c4901a191fda6ee42"}, + {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d75fc8cd0ba23f97bae88a6ec04e9e5351ff3c6ad06f38fe32ba50cbd0d11946"}, + {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a97e07e83e3262599434816f631cc4c7ca2aa8e9c072c1b1a7fec2ae809a1d2d"}, + {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e51c42750b7585cee7892c2614be0d14107fad9581d1738d954a262556dd1aab"}, + {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba4e4cc26610581a6329b3937e02d319f5ad4b85b074846bf4fef8a8cf51e7bb"}, + {file = "bcrypt-4.1.2.tar.gz", hash = "sha256:33313a1200a3ae90b75587ceac502b048b840fc69e7f7a0905b5f87fac7a1258"}, ] [package.extras] tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "bs4" +version = "0.0.2" +description = "Dummy package for Beautiful Soup (beautifulsoup4)" +optional = false +python-versions = "*" +files = [ + {file = "bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc"}, + {file = "bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925"}, +] + +[package.dependencies] +beautifulsoup4 = "*" + [[package]] name = "cachetools" version = "5.4.0" @@ -108,6 +500,85 @@ files = [ {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "cfgv" version = "3.4.0" @@ -318,6 +789,55 @@ files = [ [package.extras] toml = ["tomli"] +[[package]] +name = "cryptography" +version = "43.0.1" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"}, + {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"}, + {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"}, + {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"}, + {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"}, + {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"}, + {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"}, + {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"}, + {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"}, + {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"}, + {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "cssselect" version = "1.2.0" @@ -380,13 +900,13 @@ wmi = ["wmi (>=1.5.1)"] [[package]] name = "email-validator" -version = "2.2.0" +version = "2.1.2" description = "A robust email address syntax and deliverability validation library." optional = false python-versions = ">=3.8" files = [ - {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, - {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, + {file = "email_validator-2.1.2-py3-none-any.whl", hash = "sha256:d89f6324e13b1e39889eab7f9ca2f91dc9aebb6fa50a6d8bd4329ab50f251115"}, + {file = "email_validator-2.1.2.tar.gz", hash = "sha256:14c0f3d343c4beda37400421b39fa411bbe33a75df20825df73ad53e06a9f04c"}, ] [package.dependencies] @@ -445,6 +965,68 @@ typing-extensions = ">=4.8.0" [package.extras] all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +[[package]] +name = "fastapi-admin" +version = "1.0.4" +description = "A fast admin dashboard based on FastAPI and TortoiseORM with tabler ui, inspired by Django admin." +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "fastapi-admin-1.0.4.tar.gz", hash = "sha256:d45cb23a23fc2bbffb0e8adfb1362350a1fe808f47912253c1f067f7d7745d88"}, + {file = "fastapi_admin-1.0.4-py3-none-any.whl", hash = "sha256:0b00dd2d72bbb5af50847cd4c49261aab999eeba7d8371786b822dce4b97ef0d"}, +] + +[package.dependencies] +aiofiles = "*" +aioredis = "*" +Babel = "*" +bcrypt = "*" +fastapi = "*" +jinja2 = "*" +pendulum = "*" +python-multipart = "*" +tortoise-orm = "*" +uvicorn = {version = "*", extras = ["standard"]} + +[[package]] +name = "fastapi-cache" +version = "0.1.0" +description = "FastAPI simple cache" +optional = false +python-versions = "*" +files = [ + {file = "fastapi-cache-0.1.0.tar.gz", hash = "sha256:1f57e6e666672c84e3dd5d4141ec808d5339d158e10c87a87eb9ce11ff8b1735"}, +] + +[package.dependencies] +aioredis = "1.3.1" + +[[package]] +name = "fastapi-users" +version = "13.0.0" +description = "Ready-to-use and customizable users management for FastAPI" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi_users-13.0.0-py3-none-any.whl", hash = "sha256:e6246529e3080a5b50e5afeed1e996663b661f1dc791a1ac478925cb5bfc0fa0"}, + {file = "fastapi_users-13.0.0.tar.gz", hash = "sha256:b397c815b7051c8fd4b560fbeee707acd28e00bd3e8f25c292ad158a1e47e884"}, +] + +[package.dependencies] +email-validator = ">=1.1.0,<2.2" +fastapi = ">=0.65.2" +httpx-oauth = {version = ">=0.13", optional = true, markers = "extra == \"oauth\""} +makefun = ">=1.11.2,<2.0.0" +pwdlib = {version = "0.2.0", extras = ["argon2", "bcrypt"]} +pyjwt = {version = "2.8.0", extras = ["crypto"]} +python-multipart = "0.0.9" + +[package.extras] +beanie = ["fastapi-users-db-beanie (>=3.0.0)"] +oauth = ["httpx-oauth (>=0.13)"] +redis = ["redis (>=4.3.3,<6.0.0)"] +sqlalchemy = ["fastapi-users-db-sqlalchemy (>=6.0.0)"] + [[package]] name = "filelock" version = "3.15.4" @@ -461,6 +1043,92 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] typing = ["typing-extensions (>=4.8)"] +[[package]] +name = "frozenlist" +version = "1.4.1" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, + {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, + {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, + {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, + {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, + {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, + {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, + {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, + {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, + {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, + {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, + {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, + {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, +] + [[package]] name = "greenlet" version = "3.0.3" @@ -564,6 +1232,163 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] +[[package]] +name = "h3" +version = "3.7.7" +description = "Hierarchical hexagonal geospatial indexing system" +optional = false +python-versions = "*" +files = [ + {file = "h3-3.7.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:951ecc9da0bcd5091670b13636928747bc98bc76891da0fa725524ec017cd9de"}, + {file = "h3-3.7.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:26b9dd605541223ef927cc913deccb236cee024b16032f4a3e4387e2791479f2"}, + {file = "h3-3.7.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:996ebb32dc26dd607af7493149f94ce316117be6f42971f7b33bbd326ec695d2"}, + {file = "h3-3.7.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa2a4aa888cd9476788b874b4e11e178293f5b86e8461c36596bf183c242d417"}, + {file = "h3-3.7.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0256e42687470c6f0044ca78fe375fe32a654be8b5a8313b4a68f52f513389c6"}, + {file = "h3-3.7.7-cp310-cp310-win_amd64.whl", hash = "sha256:a3e2bc125490f900e0513c30480722f129bab1415f23040b6cd3a3f8d5a39336"}, + {file = "h3-3.7.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7d59018a50cd3b6d0ff0b18a54fdfcbaf2f79c13c831842f54fd2780c4b561ea"}, + {file = "h3-3.7.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e74526d941c1656fe162cc63b459b61aa83a15e257e9477b1570f26c544b51a"}, + {file = "h3-3.7.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c7398dbab685fcf3fe92f7c4c5901ab258bc66f7fa05fd1da8693375a10a549"}, + {file = "h3-3.7.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d22ea488ab5fe01c94070e9a6b3222916905a4d3f7a9d33cb2298c93fa0ffd3"}, + {file = "h3-3.7.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c94836155e8169be393980fc059f06481a14dd1913bd9cba609f6f1e8864c171"}, + {file = "h3-3.7.7-cp311-cp311-win_amd64.whl", hash = "sha256:836e74313ff55324485cd7e07783bc67df3191ec08a318035d7cd8ee0b0badab"}, + {file = "h3-3.7.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:51c2f63ef5a57e4b18ebc9c0eb56656433e280ec45ab487a514127bb6e7d6a1f"}, + {file = "h3-3.7.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d6e38dea47c220d9802af8e8bebc806f9f39358aee07b736191ff21e2c9921d"}, + {file = "h3-3.7.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e408342e94f558802a97bfcbe1baae2af8b1fd926ad9041d970ff9dbd0502099"}, + {file = "h3-3.7.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:644c3c84585aa4df62e81bc54fd305c4d6686324731de230b0ddbd7036ed172c"}, + {file = "h3-3.7.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bb4a3d5e82d0c89512dc71b4eac17976a29be29da250ba76bc94bc5b9e824f0e"}, + {file = "h3-3.7.7-cp312-cp312-win_amd64.whl", hash = "sha256:2ccff5f02589e80202597ed0b9f61ebd114e262e7dd0fe88059298602898192f"}, + {file = "h3-3.7.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ef2e71b619f984e71c4bd9d128152e2c7e3e788e2d2ec571b32cef1d295ddf38"}, + {file = "h3-3.7.7-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cb13f0213ed6da80e739355e5b62cfc81b7b1469af997be3384a6cbc3a1a750"}, + {file = "h3-3.7.7-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:701f72f703d892fb17e66b9fd7b6b2ad125e135b091eb7dd0ec11858b84d84d2"}, + {file = "h3-3.7.7-cp36-cp36m-win_amd64.whl", hash = "sha256:796622be7cb052690404c0ac03768183e51ae22505ce4a424b4537b2b7609fba"}, + {file = "h3-3.7.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bcd88a72d6aa97d0f3b3b87b7bfd9725a8909501e6cb9d0057d5b690b6bb37b0"}, + {file = "h3-3.7.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7358ba3f91193a2551c4a8d7ad7fd348e567b3a3581c9c161630029dfb23e07"}, + {file = "h3-3.7.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8f34b204edc2e8f7d99a6db4ed1b5d202b7ea3ec6817d373ec432dee14efe04"}, + {file = "h3-3.7.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aa0f8ce89b5e694815ee7a5172a782d58f2652267329de7008354b110b53955"}, + {file = "h3-3.7.7-cp37-cp37m-win_amd64.whl", hash = "sha256:4c851baa1c2d4f29b01157ce2a4cdb1f3879fff5c36ff7861dad1526963a17a7"}, + {file = "h3-3.7.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6f3a9da5472820b0a4add342f96fe52f65fbb8f46984383885738517b38af69e"}, + {file = "h3-3.7.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1c57da776a3c1a01e2986b1f6a31d497ee0be8fcdbaaf9b23bb90f5a90eb8f0b"}, + {file = "h3-3.7.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a5c0c0ddd9c57694ecc3b9ba99cbef2842882f8943d6edc676a365e139dbc6d"}, + {file = "h3-3.7.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c1b5a0a652719b645387231bf6d7d4dd85150e4440a4ce72a804a10e86592ae"}, + {file = "h3-3.7.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:64f76dc827fef94e9f43f95a1daea2e11f2ad2e8c55deac072f3d59bd62412d4"}, + {file = "h3-3.7.7-cp38-cp38-win_amd64.whl", hash = "sha256:c993a36120d7f5607f24ba9e39caf715eaf9cd9d44f5d5660fd85e3f4e0c6bf7"}, + {file = "h3-3.7.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eb154d2af699870b888e10476e327c895078009d2d2a6ef2d053d7dcf0e2c270"}, + {file = "h3-3.7.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c96ad74e246bb7638d413efa8199dd4c58ee929424a4dcaadb16365195f77f87"}, + {file = "h3-3.7.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52901f14f8b6e2c82075fd52c0e70176b868f621d47b5dc93f468c510e963722"}, + {file = "h3-3.7.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9d82a0fcc647e7bab36ab2e7a7392d141edc95d113ccf972e0fb7b0ddf80a0"}, + {file = "h3-3.7.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f4417d09acb36f0452346052f576923d6e4334bff3459f217d6278d40397424"}, + {file = "h3-3.7.7-cp39-cp39-win_amd64.whl", hash = "sha256:7ae774cd43b057f68dc10c99e4522fa40ed6b32ab90b2df0025595ffa15e77a0"}, + {file = "h3-3.7.7.tar.gz", hash = "sha256:33d141c3cef0725a881771fd8cb80c06a0db84a6e4ca5c647ce095ae07c61e94"}, +] + +[package.extras] +all = ["flake8", "numpy", "pylint", "pytest", "pytest-cov"] +numpy = ["numpy"] +test = ["flake8", "pylint", "pytest", "pytest-cov"] + +[[package]] +name = "hiredis" +version = "3.0.0" +description = "Python wrapper for hiredis" +optional = false +python-versions = ">=3.8" +files = [ + {file = "hiredis-3.0.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:4b182791c41c5eb1d9ed736f0ff81694b06937ca14b0d4dadde5dadba7ff6dae"}, + {file = "hiredis-3.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:13c275b483a052dd645eb2cb60d6380f1f5215e4c22d6207e17b86be6dd87ffa"}, + {file = "hiredis-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1018cc7f12824506f165027eabb302735b49e63af73eb4d5450c66c88f47026"}, + {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83a29cc7b21b746cb6a480189e49f49b2072812c445e66a9e38d2004d496b81c"}, + {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e241fab6332e8fb5f14af00a4a9c6aefa22f19a336c069b7ddbf28ef8341e8d6"}, + {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fb8de899f0145d6c4d5d4bd0ee88a78eb980a7ffabd51e9889251b8f58f1785"}, + {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b23291951959141173eec10f8573538e9349fa27f47a0c34323d1970bf891ee5"}, + {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e421ac9e4b5efc11705a0d5149e641d4defdc07077f748667f359e60dc904420"}, + {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77c8006c12154c37691b24ff293c077300c22944018c3ff70094a33e10c1d795"}, + {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:41afc0d3c18b59eb50970479a9c0e5544fb4b95e3a79cf2fbaece6ddefb926fe"}, + {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:04ccae6dcd9647eae6025425ab64edb4d79fde8b9e6e115ebfabc6830170e3b2"}, + {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fe91d62b0594db5ea7d23fc2192182b1a7b6973f628a9b8b2e0a42a2be721ac6"}, + {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:99516d99316062824a24d145d694f5b0d030c80da693ea6f8c4ecf71a251d8bb"}, + {file = "hiredis-3.0.0-cp310-cp310-win32.whl", hash = "sha256:562eaf820de045eb487afaa37e6293fe7eceb5b25e158b5a1974b7e40bf04543"}, + {file = "hiredis-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a1c81c89ed765198da27412aa21478f30d54ef69bf5e4480089d9c3f77b8f882"}, + {file = "hiredis-3.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:4664dedcd5933364756d7251a7ea86d60246ccf73a2e00912872dacbfcef8978"}, + {file = "hiredis-3.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:47de0bbccf4c8a9f99d82d225f7672b9dd690d8fd872007b933ef51a302c9fa6"}, + {file = "hiredis-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e43679eca508ba8240d016d8cca9d27342d70184773c15bea78a23c87a1922f1"}, + {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13c345e7278c210317e77e1934b27b61394fee0dec2e8bd47e71570900f75823"}, + {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00018f22f38530768b73ea86c11f47e8d4df65facd4e562bd78773bd1baef35e"}, + {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ea3a86405baa8eb0d3639ced6926ad03e07113de54cb00fd7510cb0db76a89d"}, + {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c073848d2b1d5561f3903879ccf4e1a70c9b1e7566c7bdcc98d082fa3e7f0a1d"}, + {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a8dffb5f5b3415a4669d25de48b617fd9d44b0bccfc4c2ab24b06406ecc9ecb"}, + {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:22c17c96143c2a62dfd61b13803bc5de2ac526b8768d2141c018b965d0333b66"}, + {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3ece960008dab66c6b8bb3a1350764677ee7c74ccd6270aaf1b1caf9ccebb46"}, + {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f75999ae00a920f7dce6ecae76fa5e8674a3110e5a75f12c7a2c75ae1af53396"}, + {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e069967cbd5e1900aafc4b5943888f6d34937fc59bf8918a1a546cb729b4b1e4"}, + {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0aacc0a78e1d94d843a6d191f224a35893e6bdfeb77a4a89264155015c65f126"}, + {file = "hiredis-3.0.0-cp311-cp311-win32.whl", hash = "sha256:719c32147ba29528cb451f037bf837dcdda4ff3ddb6cdb12c4216b0973174718"}, + {file = "hiredis-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:bdc144d56333c52c853c31b4e2e52cfbdb22d3da4374c00f5f3d67c42158970f"}, + {file = "hiredis-3.0.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:484025d2eb8f6348f7876fc5a2ee742f568915039fcb31b478fd5c242bb0fe3a"}, + {file = "hiredis-3.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:fcdb552ffd97151dab8e7bc3ab556dfa1512556b48a367db94b5c20253a35ee1"}, + {file = "hiredis-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bb6f9fd92f147ba11d338ef5c68af4fd2908739c09e51f186e1d90958c68cc1"}, + {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa86bf9a0ed339ec9e8a9a9d0ae4dccd8671625c83f9f9f2640729b15e07fbfd"}, + {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e194a0d5df9456995d8f510eab9f529213e7326af6b94770abf8f8b7952ddcaa"}, + {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a1df39d74ec507d79c7a82c8063eee60bf80537cdeee652f576059b9cdd15c"}, + {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f91456507427ba36fd81b2ca11053a8e112c775325acc74e993201ea912d63e9"}, + {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9862db92ef67a8a02e0d5370f07d380e14577ecb281b79720e0d7a89aedb9ee5"}, + {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d10fcd9e0eeab835f492832b2a6edb5940e2f1230155f33006a8dfd3bd2c94e4"}, + {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:48727d7d405d03977d01885f317328dc21d639096308de126c2c4e9950cbd3c9"}, + {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e0bb6102ebe2efecf8a3292c6660a0e6fac98176af6de67f020bea1c2343717"}, + {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:df274e3abb4df40f4c7274dd3e587dfbb25691826c948bc98d5fead019dfb001"}, + {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:034925b5fb514f7b11aac38cd55b3fd7e9d3af23bd6497f3f20aa5b8ba58e232"}, + {file = "hiredis-3.0.0-cp312-cp312-win32.whl", hash = "sha256:120f2dda469b28d12ccff7c2230225162e174657b49cf4cd119db525414ae281"}, + {file = "hiredis-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:e584fe5f4e6681d8762982be055f1534e0170f6308a7a90f58d737bab12ff6a8"}, + {file = "hiredis-3.0.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:122171ff47d96ed8dd4bba6c0e41d8afaba3e8194949f7720431a62aa29d8895"}, + {file = "hiredis-3.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:ba9fc605ac558f0de67463fb588722878641e6fa1dabcda979e8e69ff581d0bd"}, + {file = "hiredis-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a631e2990b8be23178f655cae8ac6c7422af478c420dd54e25f2e26c29e766f1"}, + {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63482db3fadebadc1d01ad33afa6045ebe2ea528eb77ccaabd33ee7d9c2bad48"}, + {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f669212c390eebfbe03c4e20181f5970b82c5d0a0ad1df1785f7ffbe7d61150"}, + {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a49ef161739f8018c69b371528bdb47d7342edfdee9ddc75a4d8caddf45a6e"}, + {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98a152052b8878e5e43a2e3a14075218adafc759547c98668a21e9485882696c"}, + {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50a196af0ce657fcde9bf8a0bbe1032e22c64d8fcec2bc926a35e7ff68b3a166"}, + {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f2f312eef8aafc2255e3585dcf94d5da116c43ef837db91db9ecdc1bc930072d"}, + {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:6ca41fa40fa019cde42c21add74aadd775e71458051a15a352eabeb12eb4d084"}, + {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:6eecb343c70629f5af55a8b3e53264e44fa04e155ef7989de13668a0cb102a90"}, + {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:c3fdad75e7837a475900a1d3a5cc09aa024293c3b0605155da2d42f41bc0e482"}, + {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8854969e7480e8d61ed7549eb232d95082a743e94138d98d7222ba4e9f7ecacd"}, + {file = "hiredis-3.0.0-cp38-cp38-win32.whl", hash = "sha256:f114a6c86edbf17554672b050cce72abf489fe58d583c7921904d5f1c9691605"}, + {file = "hiredis-3.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:7d99b91e42217d7b4b63354b15b41ce960e27d216783e04c4a350224d55842a4"}, + {file = "hiredis-3.0.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:4c6efcbb5687cf8d2aedcc2c3ed4ac6feae90b8547427d417111194873b66b06"}, + {file = "hiredis-3.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5b5cff42a522a0d81c2ae7eae5e56d0ee7365e0c4ad50c4de467d8957aff4414"}, + {file = "hiredis-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:82f794d564f4bc76b80c50b03267fe5d6589e93f08e66b7a2f674faa2fa76ebc"}, + {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7a4c1791d7aa7e192f60fe028ae409f18ccdd540f8b1e6aeb0df7816c77e4a4"}, + {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2537b2cd98192323fce4244c8edbf11f3cac548a9d633dbbb12b48702f379f4"}, + {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fed69bbaa307040c62195a269f82fc3edf46b510a17abb6b30a15d7dab548df"}, + {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869f6d5537d243080f44253491bb30aa1ec3c21754003b3bddeadedeb65842b0"}, + {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d435ae89073d7cd51e6b6bf78369c412216261c9c01662e7008ff00978153729"}, + {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:204b79b30a0e6be0dc2301a4d385bb61472809f09c49f400497f1cdd5a165c66"}, + {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ea635101b739c12effd189cc19b2671c268abb03013fd1f6321ca29df3ca625"}, + {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f359175197fd833c8dd7a8c288f1516be45415bb5c939862ab60c2918e1e1943"}, + {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac6d929cb33dd12ad3424b75725975f0a54b5b12dbff95f2a2d660c510aa106d"}, + {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:100431e04d25a522ef2c3b94f294c4219c4de3bfc7d557b6253296145a144c11"}, + {file = "hiredis-3.0.0-cp39-cp39-win32.whl", hash = "sha256:e1a9c14ae9573d172dc050a6f63a644457df5d01ec4d35a6a0f097f812930f83"}, + {file = "hiredis-3.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:54a6dd7b478e6eb01ce15b3bb5bf771e108c6c148315bf194eb2ab776a3cac4d"}, + {file = "hiredis-3.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50da7a9edf371441dfcc56288d790985ee9840d982750580710a9789b8f4a290"}, + {file = "hiredis-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9b285ef6bf1581310b0d5e8f6ce64f790a1c40e89c660e1320b35f7515433672"}, + {file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dcfa684966f25b335072115de2f920228a3c2caf79d4bfa2b30f6e4f674a948"}, + {file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a41be8af1fd78ca97bc948d789a09b730d1e7587d07ca53af05758f31f4b985d"}, + {file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:038756db735e417ab36ee6fd7725ce412385ed2bd0767e8179a4755ea11b804f"}, + {file = "hiredis-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fcecbd39bd42cef905c0b51c9689c39d0cc8b88b1671e7f40d4fb213423aef3a"}, + {file = "hiredis-3.0.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a131377493a59fb0f5eaeb2afd49c6540cafcfba5b0b3752bed707be9e7c4eaf"}, + {file = "hiredis-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d22c53f0ec5c18ecb3d92aa9420563b1c5d657d53f01356114978107b00b860"}, + {file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a91e9520fbc65a799943e5c970ffbcd67905744d8becf2e75f9f0a5e8414f0"}, + {file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dc8043959b50141df58ab4f398e8ae84c6f9e673a2c9407be65fc789138f4a6"}, + {file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51b99cfac514173d7b8abdfe10338193e8a0eccdfe1870b646009d2fb7cbe4b5"}, + {file = "hiredis-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:fa1fcad89d8a41d8dc10b1e54951ec1e161deabd84ed5a2c95c3c7213bdb3514"}, + {file = "hiredis-3.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:898636a06d9bf575d2c594129085ad6b713414038276a4bfc5db7646b8a5be78"}, + {file = "hiredis-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:466f836dbcf86de3f9692097a7a01533dc9926986022c6617dc364a402b265c5"}, + {file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23142a8af92a13fc1e3f2ca1d940df3dcf2af1d176be41fe8d89e30a837a0b60"}, + {file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:793c80a3d6b0b0e8196a2d5de37a08330125668c8012922685e17aa9108c33ac"}, + {file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:467d28112c7faa29b7db743f40803d927c8591e9da02b6ce3d5fadc170a542a2"}, + {file = "hiredis-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:dc384874a719c767b50a30750f937af18842ee5e288afba95a5a3ed703b1515a"}, + {file = "hiredis-3.0.0.tar.gz", hash = "sha256:fed8581ae26345dea1f1e0d1a96e05041a727a45e7d8d459164583e23c6ac441"}, +] + [[package]] name = "httpcore" version = "1.0.5" @@ -657,6 +1482,20 @@ cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +[[package]] +name = "httpx-oauth" +version = "0.15.1" +description = "Async OAuth client using HTTPX" +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx_oauth-0.15.1-py3-none-any.whl", hash = "sha256:89b45f250e93e42bbe9631adf349cab0e3d3ced958c07e06651735198d1bdf00"}, + {file = "httpx_oauth-0.15.1.tar.gz", hash = "sha256:4094cf0938fc7252b5f5dfd62cd1ab5aee2fcb6734e621942ee17d1af4806b74"}, +] + +[package.dependencies] +httpx = ">=0.18,<1.0.0" + [[package]] name = "identify" version = "2.6.0" @@ -693,6 +1532,28 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "iso8601" +version = "1.1.0" +description = "Simple module to parse ISO 8601 dates" +optional = false +python-versions = ">=3.6.2,<4.0" +files = [ + {file = "iso8601-1.1.0-py3-none-any.whl", hash = "sha256:8400e90141bf792bce2634df533dc57e3bee19ea120a87bebcd3da89a58ad73f"}, + {file = "iso8601-1.1.0.tar.gz", hash = "sha256:32811e7b81deee2063ea6d2e94f8819a86d1f3811e49d23623a41fa832bef03f"}, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + [[package]] name = "jinja2" version = "3.1.4" @@ -868,6 +1729,17 @@ html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] source = ["Cython (>=3.0.10)"] +[[package]] +name = "makefun" +version = "1.15.4" +description = "Small library to dynamically create python functions." +optional = false +python-versions = "*" +files = [ + {file = "makefun-1.15.4-py2.py3-none-any.whl", hash = "sha256:945d078a7e01a903f2cbef738b33e0ebc52b8d35fb7e20c528ed87b5c80db5b7"}, + {file = "makefun-1.15.4.tar.gz", hash = "sha256:9f9b9904e7c397759374a88f4c57781fbab2a458dec78df4b3ee6272cd9fb010"}, +] + [[package]] name = "mako" version = "1.3.5" @@ -967,6 +1839,110 @@ files = [ {file = "more_itertools-10.3.0-py3-none-any.whl", hash = "sha256:ea6a02e24a9161e51faad17a8782b92a0df82c12c1c8886fec7f0c3fa1a1b320"}, ] +[[package]] +name = "multidict" +version = "6.1.0" +description = "multidict implementation" +optional = false +python-versions = ">=3.8" +files = [ + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, + {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, + {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, + {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, + {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, + {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, + {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, + {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, + {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, + {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, + {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, + {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, + {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, + {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, + {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + [[package]] name = "mypy" version = "1.11.1" @@ -1036,6 +2012,37 @@ files = [ {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] +[[package]] +name = "otplessauthsdk" +version = "0.3.3" +description = "otpless-auth-sdk" +optional = false +python-versions = ">=3" +files = [ + {file = "OTPLessAuthSDK-0.3.3-py3-none-any.whl", hash = "sha256:7f698e19a7af3501d69b1b8ca8cf952bcaaa6d3a783a7cfc390c7e0c65dfdd7f"}, + {file = "OTPLessAuthSDK-0.3.3.tar.gz", hash = "sha256:46126c5f6a5009d44b7f7adaf7e67567930de5be530a2f0edb49d8f764cfae22"}, +] + +[package.dependencies] +cryptography = "*" +PyJWT = "*" +requests = "*" +rsa = "*" + +[[package]] +name = "outcome" +version = "1.3.0.post0" +description = "Capture the outcome of Python function calls." +optional = false +python-versions = ">=3.7" +files = [ + {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, + {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, +] + +[package.dependencies] +attrs = ">=19.2.0" + [[package]] name = "packaging" version = "24.1" @@ -1067,6 +2074,105 @@ bcrypt = ["bcrypt (>=3.1.0)"] build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] totp = ["cryptography"] +[[package]] +name = "pendulum" +version = "3.0.0" +description = "Python datetimes made easy" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pendulum-3.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2cf9e53ef11668e07f73190c805dbdf07a1939c3298b78d5a9203a86775d1bfd"}, + {file = "pendulum-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb551b9b5e6059377889d2d878d940fd0bbb80ae4810543db18e6f77b02c5ef6"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c58227ac260d5b01fc1025176d7b31858c9f62595737f350d22124a9a3ad82d"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60fb6f415fea93a11c52578eaa10594568a6716602be8430b167eb0d730f3332"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b69f6b4dbcb86f2c2fe696ba991e67347bcf87fe601362a1aba6431454b46bde"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138afa9c373ee450ede206db5a5e9004fd3011b3c6bbe1e57015395cd076a09f"}, + {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:83d9031f39c6da9677164241fd0d37fbfc9dc8ade7043b5d6d62f56e81af8ad2"}, + {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c2308af4033fa534f089595bcd40a95a39988ce4059ccd3dc6acb9ef14ca44a"}, + {file = "pendulum-3.0.0-cp310-none-win_amd64.whl", hash = "sha256:9a59637cdb8462bdf2dbcb9d389518c0263799189d773ad5c11db6b13064fa79"}, + {file = "pendulum-3.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3725245c0352c95d6ca297193192020d1b0c0f83d5ee6bb09964edc2b5a2d508"}, + {file = "pendulum-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c035f03a3e565ed132927e2c1b691de0dbf4eb53b02a5a3c5a97e1a64e17bec"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597e66e63cbd68dd6d58ac46cb7a92363d2088d37ccde2dae4332ef23e95cd00"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99a0f8172e19f3f0c0e4ace0ad1595134d5243cf75985dc2233e8f9e8de263ca"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77d8839e20f54706aed425bec82a83b4aec74db07f26acd039905d1237a5e1d4"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afde30e8146292b059020fbc8b6f8fd4a60ae7c5e6f0afef937bbb24880bdf01"}, + {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:660434a6fcf6303c4efd36713ca9212c753140107ee169a3fc6c49c4711c2a05"}, + {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dee9e5a48c6999dc1106eb7eea3e3a50e98a50651b72c08a87ee2154e544b33e"}, + {file = "pendulum-3.0.0-cp311-none-win_amd64.whl", hash = "sha256:d4cdecde90aec2d67cebe4042fd2a87a4441cc02152ed7ed8fb3ebb110b94ec4"}, + {file = "pendulum-3.0.0-cp311-none-win_arm64.whl", hash = "sha256:773c3bc4ddda2dda9f1b9d51fe06762f9200f3293d75c4660c19b2614b991d83"}, + {file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"}, + {file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"}, + {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"}, + {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"}, + {file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"}, + {file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"}, + {file = "pendulum-3.0.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d4e2512f4e1a4670284a153b214db9719eb5d14ac55ada5b76cbdb8c5c00399d"}, + {file = "pendulum-3.0.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:3d897eb50883cc58d9b92f6405245f84b9286cd2de6e8694cb9ea5cb15195a32"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e169cc2ca419517f397811bbe4589cf3cd13fca6dc38bb352ba15ea90739ebb"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17c3084a4524ebefd9255513692f7e7360e23c8853dc6f10c64cc184e1217ab"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:826d6e258052715f64d05ae0fc9040c0151e6a87aae7c109ba9a0ed930ce4000"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2aae97087872ef152a0c40e06100b3665d8cb86b59bc8471ca7c26132fccd0f"}, + {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ac65eeec2250d03106b5e81284ad47f0d417ca299a45e89ccc69e36130ca8bc7"}, + {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a5346d08f3f4a6e9e672187faa179c7bf9227897081d7121866358af369f44f9"}, + {file = "pendulum-3.0.0-cp37-none-win_amd64.whl", hash = "sha256:235d64e87946d8f95c796af34818c76e0f88c94d624c268693c85b723b698aa9"}, + {file = "pendulum-3.0.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:6a881d9c2a7f85bc9adafcfe671df5207f51f5715ae61f5d838b77a1356e8b7b"}, + {file = "pendulum-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d7762d2076b9b1cb718a6631ad6c16c23fc3fac76cbb8c454e81e80be98daa34"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e8e36a8130819d97a479a0e7bf379b66b3b1b520e5dc46bd7eb14634338df8c"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7dc843253ac373358ffc0711960e2dd5b94ab67530a3e204d85c6e8cb2c5fa10"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a78ad3635d609ceb1e97d6aedef6a6a6f93433ddb2312888e668365908c7120"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a137e9e0d1f751e60e67d11fc67781a572db76b2296f7b4d44554761049d6"}, + {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c95984037987f4a457bb760455d9ca80467be792236b69d0084f228a8ada0162"}, + {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d29c6e578fe0f893766c0d286adbf0b3c726a4e2341eba0917ec79c50274ec16"}, + {file = "pendulum-3.0.0-cp38-none-win_amd64.whl", hash = "sha256:deaba8e16dbfcb3d7a6b5fabdd5a38b7c982809567479987b9c89572df62e027"}, + {file = "pendulum-3.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b11aceea5b20b4b5382962b321dbc354af0defe35daa84e9ff3aae3c230df694"}, + {file = "pendulum-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a90d4d504e82ad236afac9adca4d6a19e4865f717034fc69bafb112c320dcc8f"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:825799c6b66e3734227756fa746cc34b3549c48693325b8b9f823cb7d21b19ac"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad769e98dc07972e24afe0cff8d365cb6f0ebc7e65620aa1976fcfbcadc4c6f3"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6fc26907eb5fb8cc6188cc620bc2075a6c534d981a2f045daa5f79dfe50d512"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c717eab1b6d898c00a3e0fa7781d615b5c5136bbd40abe82be100bb06df7a56"}, + {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3ddd1d66d1a714ce43acfe337190be055cdc221d911fc886d5a3aae28e14b76d"}, + {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:822172853d7a9cf6da95d7b66a16c7160cb99ae6df55d44373888181d7a06edc"}, + {file = "pendulum-3.0.0-cp39-none-win_amd64.whl", hash = "sha256:840de1b49cf1ec54c225a2a6f4f0784d50bd47f68e41dc005b7f67c7d5b5f3ae"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b1f74d1e6ffe5d01d6023870e2ce5c2191486928823196f8575dcc786e107b1"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:729e9f93756a2cdfa77d0fc82068346e9731c7e884097160603872686e570f07"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e586acc0b450cd21cbf0db6bae386237011b75260a3adceddc4be15334689a9a"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e7944ffc1f0099a79ff468ee9630c73f8c7835cd76fdb57ef7320e6a409df4"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fa30af36bd8e50686846bdace37cf6707bdd044e5cb6e1109acbad3277232e04"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:440215347b11914ae707981b9a57ab9c7b6983ab0babde07063c6ee75c0dc6e7"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:314c4038dc5e6a52991570f50edb2f08c339debdf8cea68ac355b32c4174e820"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5acb1d386337415f74f4d1955c4ce8d0201978c162927d07df8eb0692b2d8533"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a789e12fbdefaffb7b8ac67f9d8f22ba17a3050ceaaa635cd1cc4645773a4b1e"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:860aa9b8a888e5913bd70d819306749e5eb488e6b99cd6c47beb701b22bdecf5"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5ebc65ea033ef0281368217fbf59f5cb05b338ac4dd23d60959c7afcd79a60a0"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d9fef18ab0386ef6a9ac7bad7e43ded42c83ff7ad412f950633854f90d59afa8"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c134ba2f0571d0b68b83f6972e2307a55a5a849e7dac8505c715c531d2a8795"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:385680812e7e18af200bb9b4a49777418c32422d05ad5a8eb85144c4a285907b"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eec91cd87c59fb32ec49eb722f375bd58f4be790cae11c1b70fac3ee4f00da0"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4386bffeca23c4b69ad50a36211f75b35a4deb6210bdca112ac3043deb7e494a"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dfbcf1661d7146d7698da4b86e7f04814221081e9fe154183e34f4c5f5fa3bf8"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:04a1094a5aa1daa34a6b57c865b25f691848c61583fb22722a4df5699f6bf74c"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5b0ec85b9045bd49dd3a3493a5e7ddfd31c36a2a60da387c419fa04abcaecb23"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0a15b90129765b705eb2039062a6daf4d22c4e28d1a54fa260892e8c3ae6e157"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:bb8f6d7acd67a67d6fedd361ad2958ff0539445ef51cbe8cd288db4306503cd0"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd69b15374bef7e4b4440612915315cc42e8575fcda2a3d7586a0d88192d0c88"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc00f8110db6898360c53c812872662e077eaf9c75515d53ecc65d886eec209a"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:83a44e8b40655d0ba565a5c3d1365d27e3e6778ae2a05b69124db9e471255c4a"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1a3604e9fbc06b788041b2a8b78f75c243021e0f512447806a6d37ee5214905d"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:92c307ae7accebd06cbae4729f0ba9fa724df5f7d91a0964b1b972a22baa482b"}, + {file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"}, +] + +[package.dependencies] +python-dateutil = ">=2.6" +tzdata = ">=2020.1" + +[package.extras] +test = ["time-machine (>=2.6.0)"] + [[package]] name = "platformdirs" version = "4.2.2" @@ -1138,6 +2244,113 @@ requests = "*" dev = ["black", "flake8", "therapist", "tox", "twine", "wheel"] test = ["mock", "nose"] +[[package]] +name = "propcache" +version = "0.2.0" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.8" +files = [ + {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58"}, + {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b"}, + {file = "propcache-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33ac8f098df0585c0b53009f039dfd913b38c1d2edafed0cedcc0c32a05aa110"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e48e8875e6c13909c800fa344cd54cc4b2b0db1d5f911f840458a500fde2c2"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388f3217649d6d59292b722d940d4d2e1e6a7003259eb835724092a1cca0203a"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f571aea50ba5623c308aa146eb650eebf7dbe0fd8c5d946e28343cb3b5aad577"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dfafb44f7bb35c0c06eda6b2ab4bfd58f02729e7c4045e179f9a861b07c9850"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3ebe9a75be7ab0b7da2464a77bb27febcb4fab46a34f9288f39d74833db7f61"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2f0d0f976985f85dfb5f3d685697ef769faa6b71993b46b295cdbbd6be8cc37"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a3dc1a4b165283bd865e8f8cb5f0c64c05001e0718ed06250d8cac9bec115b48"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e0f07b42d2a50c7dd2d8675d50f7343d998c64008f1da5fef888396b7f84630"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e63e3e1e0271f374ed489ff5ee73d4b6e7c60710e1f76af5f0e1a6117cd26394"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:56bb5c98f058a41bb58eead194b4db8c05b088c93d94d5161728515bd52b052b"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7665f04d0c7f26ff8bb534e1c65068409bf4687aa2534faf7104d7182debb336"}, + {file = "propcache-0.2.0-cp310-cp310-win32.whl", hash = "sha256:7cf18abf9764746b9c8704774d8b06714bcb0a63641518a3a89c7f85cc02c2ad"}, + {file = "propcache-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:cfac69017ef97db2438efb854edf24f5a29fd09a536ff3a992b75990720cdc99"}, + {file = "propcache-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354"}, + {file = "propcache-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de"}, + {file = "propcache-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b"}, + {file = "propcache-0.2.0-cp311-cp311-win32.whl", hash = "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1"}, + {file = "propcache-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71"}, + {file = "propcache-0.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2"}, + {file = "propcache-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7"}, + {file = "propcache-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348"}, + {file = "propcache-0.2.0-cp312-cp312-win32.whl", hash = "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5"}, + {file = "propcache-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3"}, + {file = "propcache-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7"}, + {file = "propcache-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763"}, + {file = "propcache-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544"}, + {file = "propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032"}, + {file = "propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e"}, + {file = "propcache-0.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:53d1bd3f979ed529f0805dd35ddaca330f80a9a6d90bc0121d2ff398f8ed8861"}, + {file = "propcache-0.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:83928404adf8fb3d26793665633ea79b7361efa0287dfbd372a7e74311d51ee6"}, + {file = "propcache-0.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:77a86c261679ea5f3896ec060be9dc8e365788248cc1e049632a1be682442063"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:218db2a3c297a3768c11a34812e63b3ac1c3234c3a086def9c0fee50d35add1f"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7735e82e3498c27bcb2d17cb65d62c14f1100b71723b68362872bca7d0913d90"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:20a617c776f520c3875cf4511e0d1db847a076d720714ae35ffe0df3e440be68"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67b69535c870670c9f9b14a75d28baa32221d06f6b6fa6f77a0a13c5a7b0a5b9"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4569158070180c3855e9c0791c56be3ceeb192defa2cdf6a3f39e54319e56b89"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:db47514ffdbd91ccdc7e6f8407aac4ee94cc871b15b577c1c324236b013ddd04"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:2a60ad3e2553a74168d275a0ef35e8c0a965448ffbc3b300ab3a5bb9956c2162"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:662dd62358bdeaca0aee5761de8727cfd6861432e3bb828dc2a693aa0471a563"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:25a1f88b471b3bc911d18b935ecb7115dff3a192b6fef46f0bfaf71ff4f12418"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:f60f0ac7005b9f5a6091009b09a419ace1610e163fa5deaba5ce3484341840e7"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:74acd6e291f885678631b7ebc85d2d4aec458dd849b8c841b57ef04047833bed"}, + {file = "propcache-0.2.0-cp38-cp38-win32.whl", hash = "sha256:d9b6ddac6408194e934002a69bcaadbc88c10b5f38fb9307779d1c629181815d"}, + {file = "propcache-0.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:676135dcf3262c9c5081cc8f19ad55c8a64e3f7282a21266d05544450bffc3a5"}, + {file = "propcache-0.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6"}, + {file = "propcache-0.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638"}, + {file = "propcache-0.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798"}, + {file = "propcache-0.2.0-cp39-cp39-win32.whl", hash = "sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9"}, + {file = "propcache-0.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df"}, + {file = "propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036"}, + {file = "propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70"}, +] + [[package]] name = "psycopg" version = "3.2.1" @@ -1224,20 +2437,83 @@ files = [ {file = "psycopg_binary-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:921f0c7f39590763d64a619de84d1b142587acc70fd11cbb5ba8fa39786f3073"}, ] +[[package]] +name = "psycopg2" +version = "2.9.9" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "psycopg2-2.9.9-cp310-cp310-win32.whl", hash = "sha256:38a8dcc6856f569068b47de286b472b7c473ac7977243593a288ebce0dc89516"}, + {file = "psycopg2-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3"}, + {file = "psycopg2-2.9.9-cp311-cp311-win32.whl", hash = "sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372"}, + {file = "psycopg2-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981"}, + {file = "psycopg2-2.9.9-cp312-cp312-win32.whl", hash = "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024"}, + {file = "psycopg2-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693"}, + {file = "psycopg2-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa"}, + {file = "psycopg2-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a"}, + {file = "psycopg2-2.9.9-cp38-cp38-win32.whl", hash = "sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c"}, + {file = "psycopg2-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:bac58c024c9922c23550af2a581998624d6e02350f4ae9c5f0bc642c633a2d5e"}, + {file = "psycopg2-2.9.9-cp39-cp39-win32.whl", hash = "sha256:c92811b2d4c9b6ea0285942b2e7cac98a59e166d59c588fe5cfe1eda58e72d59"}, + {file = "psycopg2-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:de80739447af31525feddeb8effd640782cf5998e1a4e9192ebdf829717e3913"}, + {file = "psycopg2-2.9.9.tar.gz", hash = "sha256:d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156"}, +] + +[[package]] +name = "pwdlib" +version = "0.2.0" +description = "Modern password hashing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pwdlib-0.2.0-py3-none-any.whl", hash = "sha256:be53812012ab66795a57ac9393a59716ae7c2b60841ed453eb1262017fdec144"}, + {file = "pwdlib-0.2.0.tar.gz", hash = "sha256:b1bdafc064310eb6d3d07144a210267063ab4f45ac73a97be948e6589f74e861"}, +] + +[package.dependencies] +argon2-cffi = {version = "23.1.0", optional = true, markers = "extra == \"argon2\""} +bcrypt = {version = "4.1.2", optional = true, markers = "extra == \"bcrypt\""} + +[package.extras] +argon2 = ["argon2-cffi (==23.1.0)"] +bcrypt = ["bcrypt (==4.1.2)"] + +[[package]] +name = "pyasn1" +version = "0.6.1" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, +] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + [[package]] name = "pydantic" -version = "2.8.2" +version = "2.9.2" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, - {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, ] [package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.20.1" +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" typing-extensions = [ {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, {version = ">=4.6.1", markers = "python_version < \"3.13\""}, @@ -1245,103 +2521,104 @@ typing-extensions = [ [package.extras] email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.20.1" +version = "2.23.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, - {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, - {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, - {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, - {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, - {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, - {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, - {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, - {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, - {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, - {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, - {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, - {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, - {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, ] [package.dependencies] @@ -1378,12 +2655,52 @@ files = [ {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, ] +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} + [package.extras] crypto = ["cryptography (>=3.4.0)"] dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] +[[package]] +name = "pyotp" +version = "2.9.0" +description = "Python One Time Password Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyotp-2.9.0-py3-none-any.whl", hash = "sha256:81c2e5865b8ac55e825b0358e496e1d9387c811e85bb40e71a3b29b288963612"}, + {file = "pyotp-2.9.0.tar.gz", hash = "sha256:346b6642e0dbdde3b4ff5a930b664ca82abfa116356ed48cc42c7d6590d36f63"}, +] + +[package.extras] +test = ["coverage", "mypy", "ruff", "wheel"] + +[[package]] +name = "pypika-tortoise" +version = "0.1.6" +description = "Forked from pypika and streamline just for tortoise-orm" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "pypika-tortoise-0.1.6.tar.gz", hash = "sha256:d802868f479a708e3263724c7b5719a26ad79399b2a70cea065f4a4cadbebf36"}, + {file = "pypika_tortoise-0.1.6-py3-none-any.whl", hash = "sha256:2d68bbb7e377673743cff42aa1059f3a80228d411fbcae591e4465e173109fd8"}, +] + +[[package]] +name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + [[package]] name = "pytest" version = "7.4.4" @@ -1436,17 +2753,28 @@ cli = ["click (>=5.0)"] [[package]] name = "python-multipart" -version = "0.0.7" +version = "0.0.9" description = "A streaming multipart parser for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "python_multipart-0.0.7-py3-none-any.whl", hash = "sha256:b1fef9a53b74c795e2347daac8c54b252d9e0df9c619712691c1cc8021bd3c49"}, - {file = "python_multipart-0.0.7.tar.gz", hash = "sha256:288a6c39b06596c1b988bb6794c6fbc80e6c369e35e5062637df256bee0c9af9"}, + {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, + {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, ] [package.extras] -dev = ["atomicwrites (==1.2.1)", "attrs (==19.2.0)", "coverage (==6.5.0)", "hatch", "invoke (==2.2.0)", "more-itertools (==4.3.0)", "pbr (==4.3.0)", "pluggy (==1.0.0)", "py (==1.11.0)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-timeout (==2.1.0)", "pyyaml (==5.1)"] +dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"] + +[[package]] +name = "pytz" +version = "2024.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, +] [[package]] name = "pyyaml" @@ -1508,6 +2836,24 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "redis" +version = "5.0.8" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.7" +files = [ + {file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"}, + {file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} + +[package.extras] +hiredis = ["hiredis (>1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] + [[package]] name = "requests" version = "2.32.3" @@ -1529,6 +2875,20 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "rsa" +version = "4.9" +description = "Pure-Python RSA implementation" +optional = false +python-versions = ">=3.6,<4" +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + [[package]] name = "ruff" version = "0.2.2" @@ -1555,6 +2915,25 @@ files = [ {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, ] +[[package]] +name = "selenium" +version = "4.25.0" +description = "Official Python bindings for Selenium WebDriver" +optional = false +python-versions = ">=3.8" +files = [ + {file = "selenium-4.25.0-py3-none-any.whl", hash = "sha256:3798d2d12b4a570bc5790163ba57fef10b2afee958bf1d80f2a3cf07c4141f33"}, + {file = "selenium-4.25.0.tar.gz", hash = "sha256:95d08d3b82fb353f3c474895154516604c7f0e6a9a565ae6498ef36c9bac6921"}, +] + +[package.dependencies] +certifi = ">=2021.10.8" +trio = ">=0.17,<1.0" +trio-websocket = ">=0.9,<1.0" +typing_extensions = ">=4.9,<5.0" +urllib3 = {version = ">=1.26,<3", extras = ["socks"]} +websocket-client = ">=1.8,<2.0" + [[package]] name = "sentry-sdk" version = "1.45.1" @@ -1625,6 +3004,50 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = false +python-versions = "*" +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + +[[package]] +name = "soupsieve" +version = "2.6" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, +] + +[[package]] +name = "sqladmin" +version = "0.19.0" +description = "SQLAlchemy admin for FastAPI and Starlette" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sqladmin-0.19.0-py3-none-any.whl", hash = "sha256:7549df159c1a9e65d2a9bf66ddca321e4c2aafef824faa23b881fa0aa08b88bf"}, + {file = "sqladmin-0.19.0.tar.gz", hash = "sha256:edd7d1a16e61fc4edb428dc92a99e9f5b41252127a9d93637ce1d9b3eaa20877"}, +] + +[package.dependencies] +itsdangerous = {version = "*", optional = true, markers = "extra == \"full\""} +jinja2 = "*" +python-multipart = "*" +sqlalchemy = ">=1.4" +starlette = "*" +wtforms = ">=3.1,<3.2" + +[package.extras] +full = ["itsdangerous"] + [[package]] name = "sqlalchemy" version = "2.0.31" @@ -1770,6 +3193,68 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "tortoise-orm" +version = "0.21.6" +description = "Easy async ORM for python, built with relations in mind" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "tortoise_orm-0.21.6-py3-none-any.whl", hash = "sha256:98fcf07dce3396075eac36b0d2b14d2267ff875d32455e03ee15e38de2f138df"}, + {file = "tortoise_orm-0.21.6.tar.gz", hash = "sha256:0fbc718001647bf282c01eaaa360f94f1432c9281701244180703d48d58a88ec"}, +] + +[package.dependencies] +aiosqlite = ">=0.16.0,<0.18.0" +iso8601 = ">=1.0.2,<2.0.0" +pydantic = ">=2.0,<2.7.0 || >2.7.0,<3.0" +pypika-tortoise = ">=0.1.6,<0.2.0" +pytz = "*" + +[package.extras] +accel = ["ciso8601", "orjson", "uvloop"] +aiomysql = ["aiomysql"] +asyncmy = ["asyncmy (>=0.2.8,<0.3.0)"] +asyncodbc = ["asyncodbc (>=0.1.1,<0.2.0)"] +asyncpg = ["asyncpg"] +psycopg = ["psycopg[binary,pool] (>=3.0.12,<4.0.0)"] + +[[package]] +name = "trio" +version = "0.27.0" +description = "A friendly Python library for async concurrency and I/O" +optional = false +python-versions = ">=3.8" +files = [ + {file = "trio-0.27.0-py3-none-any.whl", hash = "sha256:68eabbcf8f457d925df62da780eff15ff5dc68fd6b367e2dde59f7aaf2a0b884"}, + {file = "trio-0.27.0.tar.gz", hash = "sha256:1dcc95ab1726b2da054afea8fd761af74bad79bd52381b84eae408e983c76831"}, +] + +[package.dependencies] +attrs = ">=23.2.0" +cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +idna = "*" +outcome = "*" +sniffio = ">=1.3.0" +sortedcontainers = "*" + +[[package]] +name = "trio-websocket" +version = "0.11.1" +description = "WebSocket library for Trio" +optional = false +python-versions = ">=3.7" +files = [ + {file = "trio-websocket-0.11.1.tar.gz", hash = "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f"}, + {file = "trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638"}, +] + +[package.dependencies] +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +trio = ">=0.11" +wsproto = ">=0.14" + [[package]] name = "types-passlib" version = "1.7.7.20240327" @@ -1803,6 +3288,21 @@ files = [ {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, ] +[[package]] +name = "unshortenit" +version = "0.4.0" +description = "Unshortens ad-based shorteners and any 301 redirected urls." +optional = false +python-versions = "*" +files = [ + {file = "unshortenit-0.4.0.tar.gz", hash = "sha256:ffe218acb22cd743b152e90ca3ac547a99d642333824048b6567a3e1688cf703"}, +] + +[package.dependencies] +click = ">=6.7" +lxml = ">=4.1.1" +requests = ">=2.18.4" + [[package]] name = "urllib3" version = "2.2.2" @@ -1814,6 +3314,9 @@ files = [ {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] +[package.dependencies] +pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} + [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] @@ -1997,6 +3500,38 @@ files = [ [package.dependencies] anyio = ">=3.0.0" +[[package]] +name = "webdriver-manager" +version = "4.0.2" +description = "Library provides the way to automatically manage drivers for different browsers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "webdriver_manager-4.0.2-py2.py3-none-any.whl", hash = "sha256:75908d92ecc45ff2b9953614459c633db8f9aa1ff30181cefe8696e312908129"}, + {file = "webdriver_manager-4.0.2.tar.gz", hash = "sha256:efedf428f92fd6d5c924a0d054e6d1322dd77aab790e834ee767af392b35590f"}, +] + +[package.dependencies] +packaging = "*" +python-dotenv = "*" +requests = "*" + +[[package]] +name = "websocket-client" +version = "1.8.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, + {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + [[package]] name = "websockets" version = "12.0" @@ -2078,7 +3613,134 @@ files = [ {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, ] +[[package]] +name = "wsproto" +version = "1.2.0" +description = "WebSockets state-machine based protocol implementation" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, +] + +[package.dependencies] +h11 = ">=0.9.0,<1" + +[[package]] +name = "wtforms" +version = "3.1.2" +description = "Form validation and rendering for Python web development." +optional = false +python-versions = ">=3.8" +files = [ + {file = "wtforms-3.1.2-py3-none-any.whl", hash = "sha256:bf831c042829c8cdbad74c27575098d541d039b1faa74c771545ecac916f2c07"}, + {file = "wtforms-3.1.2.tar.gz", hash = "sha256:f8d76180d7239c94c6322f7990ae1216dae3659b7aa1cee94b6318bdffb474b9"}, +] + +[package.dependencies] +markupsafe = "*" + +[package.extras] +email = ["email-validator"] + +[[package]] +name = "yarl" +version = "1.15.5" +description = "Yet another URL library" +optional = false +python-versions = ">=3.9" +files = [ + {file = "yarl-1.15.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6c57972a406ea0f61e3f28f2b3a780fb71fbe1d82d267afe5a2f889a83ee7e7"}, + {file = "yarl-1.15.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c3ac5bdcc1375c8ee52784adf94edbce37c471dd2100a117cfef56fe8dbc2b4"}, + {file = "yarl-1.15.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:68d21d0563d82aaf46163eac529adac301b20be3181b8a2811f7bd5615466055"}, + {file = "yarl-1.15.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7d317fb80bc17ed4b34a9aad8b80cef34bea0993654f3e8566daf323def7ef9"}, + {file = "yarl-1.15.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed9c72d5361cfd5af5ccadffa8f8077f4929640e1f938aa0f4b92c5a24996ac5"}, + {file = "yarl-1.15.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bb707859218e8335447b210f41a755e7b1367c33e87add884128bba144694a7f"}, + {file = "yarl-1.15.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6563394492c96cb57f4dff0c69c63d2b28b5469c59c66f35a1e6451583cd0ab4"}, + {file = "yarl-1.15.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c2d1109c8d92059314cc34dd8f0a31f74b720dc140744923ed7ca228bf9b491"}, + {file = "yarl-1.15.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8fc727f0fb388debc771eaa7091c092bd2e8b6b4741b73354b8efadcf96d6031"}, + {file = "yarl-1.15.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:94189746c5ad62e1014a16298130e696fe593d031d442ef135fb7787b7a1f820"}, + {file = "yarl-1.15.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b06d8b05d0fafef204d635a4711283ddbf19c7c0facdc61b4b775f6e47e2d4be"}, + {file = "yarl-1.15.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:de6917946dc6bc237d4b354e38aa13a232e0c7948fdbdb160edee3862e9d735f"}, + {file = "yarl-1.15.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:34816f1d833433a16c4832562a050b0a60eac53dcb71b2032e6ebff82d74b6a7"}, + {file = "yarl-1.15.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:19e2a4b2935f95fad0949f420514c5d862f5f18058fbbfd8854f496a97d9fd87"}, + {file = "yarl-1.15.5-cp310-cp310-win32.whl", hash = "sha256:30ca64521f1a96b72886dd9e8652f16eab11891b4572dcfcfc1ad6d6ccb27abd"}, + {file = "yarl-1.15.5-cp310-cp310-win_amd64.whl", hash = "sha256:86648c53b10c53db8b967a75fb41e0c89dbec7398f6525e34af2b6c456bb0ac0"}, + {file = "yarl-1.15.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e652aa9f8dfa808bc5b2da4d1f4e286cf1d640570fdfa72ffc0c1d16ba114651"}, + {file = "yarl-1.15.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:21050b6cd569980fe20ceeab4baeb900d3f7247270475e42bafe117416a5496c"}, + {file = "yarl-1.15.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18940191ec9a83bbfe63eea61c3e9d12474bb910d5613bce8fa46e84a80b75b2"}, + {file = "yarl-1.15.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a082dc948045606f62dca0228ab24f13737180b253378d6443f5b2b9ef8beefe"}, + {file = "yarl-1.15.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a843e692f9d5402b3455653f4607dc521de2385f01c5cad7ba4a87c46e2ea8d"}, + {file = "yarl-1.15.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5093a453176a4fad4f9c3006f507cf300546190bb3e27944275a37cfd6323a65"}, + {file = "yarl-1.15.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2597a589859b94d0a5e2f5d30fee95081867926e57cb751f8b44a7dd92da4e79"}, + {file = "yarl-1.15.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f5a1ca6eaabfe62718b87eac06d9a47b30cf92ffa065fee9196d3ecd24a3cf1"}, + {file = "yarl-1.15.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ac83b307cc4b8907345b52994055c6c3c2601ceb6fcb94c5ed6a93c6b4e8257"}, + {file = "yarl-1.15.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:325e2beb2cd8654b276e7686a3cd203628dd3fe32d5c616e632bc35a2901fb16"}, + {file = "yarl-1.15.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:75d04ba8ed335042328086e643e01165e0c24598216f72da709b375930ae3bdb"}, + {file = "yarl-1.15.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7abd7d15aedb3961a967cc65f8144dbbca42e3626a21c5f4f29919cf43eeafb9"}, + {file = "yarl-1.15.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:294c742a273f44511f14b03a9e06b66094dcdf4bbb75a5e23fead548fd5310ae"}, + {file = "yarl-1.15.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63d46606b20f80a6476f1044bab78e1a69c2e0747f174583e2f12fc70bad2170"}, + {file = "yarl-1.15.5-cp311-cp311-win32.whl", hash = "sha256:b1217102a455e3ac9ac293081093f21f0183e978c7692171ff669fee5296fa28"}, + {file = "yarl-1.15.5-cp311-cp311-win_amd64.whl", hash = "sha256:5848500b6a01497560969e8c3a7eb1b2570853c74a0ca6f67ebaf6064106c49b"}, + {file = "yarl-1.15.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d3309ee667f2d9c7ac9ecf44620d6b274bfdd8065b8c5019ff6795dd887b8fed"}, + {file = "yarl-1.15.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:96ce879799fee124d241ea3b84448378f638e290c49493d00b706f3fd57ec22b"}, + {file = "yarl-1.15.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c884dfa56b050f718ea3cbbfd972e29a6f07f63a7449b10d9a20d64f7eec92e2"}, + {file = "yarl-1.15.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0327081978fe186c3390dd4f73f95f825d0bb9c74967e22c2a1a87735974d8f5"}, + {file = "yarl-1.15.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:524b3bb7dff320e305bc979c65eddc0342548c56ea9241502f907853fe53c408"}, + {file = "yarl-1.15.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd56de8b645421ff09c993fdb0ee9c5a3b50d290a8f55793b500d99b34d0c1ce"}, + {file = "yarl-1.15.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c166ad987265bb343be58cdf4fbc4478cc1d81f2246d2be9a15f94393b269faa"}, + {file = "yarl-1.15.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d56980374a10c74255fcea6ebcfb0aeca7166d212ee9fd7e823ddef35fb62ad0"}, + {file = "yarl-1.15.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cbf36099a9b407e1456dbf55844743a98603fcba32d2a46fb3a698d926facf1b"}, + {file = "yarl-1.15.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d7fa4b033e2f267e37aabcc36949fa89f9f1716a723395912147f9cf3fb437c7"}, + {file = "yarl-1.15.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb129f77ddaea2d8e6e00417b8d907448de3407af4eddacca0a515574ad71493"}, + {file = "yarl-1.15.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:68e837b3edfcd037f9706157e7cb8efda832de6248c7d9e893e2638356dfae5d"}, + {file = "yarl-1.15.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5b8af4165e097ff84d9bbb97bb4f4d7f71b9c1c9565a2d0e27d93e5f92dae220"}, + {file = "yarl-1.15.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:70d074d5a96e0954fe6db81ff356f4361397da1cda3f7c127fc0902f671a087e"}, + {file = "yarl-1.15.5-cp312-cp312-win32.whl", hash = "sha256:362da97ad4360e4ef1dd24ccdd3bceb18332da7f40026a42f49b7edd686e31c3"}, + {file = "yarl-1.15.5-cp312-cp312-win_amd64.whl", hash = "sha256:9aa054d97033beac9cb9b19b7c0b8784b85b12cd17879087ca6bffba57884e02"}, + {file = "yarl-1.15.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5fadcf532fd9f6cbad71485ef8c2462dd9a91d3efc72ca01eb0970792c92552a"}, + {file = "yarl-1.15.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8b7dd6983c81523f9de0ae6334c3b7a3cb33283936e0525f80c4f713f54a9bb6"}, + {file = "yarl-1.15.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fcfd663dc88465ebe41c7c938bdc91c4b01cda96a0d64bf38fd66c1877323771"}, + {file = "yarl-1.15.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd529e637cd23204bd82072f6637cff7af2516ad2c132e8f3342cbc84871f7d1"}, + {file = "yarl-1.15.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b30f13fac56598474071a4f1ecd66c78fdaf2f8619042d7ca135f72dbb348cf"}, + {file = "yarl-1.15.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:44088ec0be82fba118ed29b6b429f80bf295297727adae4c257ac297e01e8bcd"}, + {file = "yarl-1.15.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:607683991bab8607e5158cd290dd8fdaa613442aeab802fe1c237d3a3eee7358"}, + {file = "yarl-1.15.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da48cdff56b01ea4282a6d04b83b07a2088351a4a3ff7aacc1e7e9b6b04b90b9"}, + {file = "yarl-1.15.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9162ea117ce8bad8ebc95b7376b4135988acd888d2cf4702f8281e3c11f8b81f"}, + {file = "yarl-1.15.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:e8aa19c39cb20bfb16f0266df175a6004943122cf20707fbf0cacc21f6468a25"}, + {file = "yarl-1.15.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5d6be369488d503c8edc14e2f63d71ab2a607041ad216a8ad444fa18e8dea792"}, + {file = "yarl-1.15.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6e2c674cfe4c03ad7a4d536b1f808221f0d11a360486b4b032d2557c0bd633ad"}, + {file = "yarl-1.15.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:041bafaa82b77fd4ec2826d42a55461ec86d999adf7ed9644eef7e8a9febb366"}, + {file = "yarl-1.15.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2eeb9ba53c055740cd282ae9d34eb7970d65e73a46f15adec4b0c1b0f2e55cc2"}, + {file = "yarl-1.15.5-cp313-cp313-win32.whl", hash = "sha256:73143dd279e641543da52c55652ad7b4c7c5f79e797f124f58f04cc060f14271"}, + {file = "yarl-1.15.5-cp313-cp313-win_amd64.whl", hash = "sha256:94ab1185900f43760d5487c8e49f5f1a66f864e36092f282f1813597479b9dfa"}, + {file = "yarl-1.15.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6b3d2767bd64c62909ea33525b954ba05c8f9726bfdf2141d175da4e344f19ae"}, + {file = "yarl-1.15.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:44359c52af9c383e5107f3b6301446fc8269599721fa42fafb2afb5f31a42dcb"}, + {file = "yarl-1.15.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6493da9ba5c551978c679ab04856c2cf8f79c316e8ec8c503460a135705edc3b"}, + {file = "yarl-1.15.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a6b6e95bc621c11cf9ff21012173337e789f2461ebc3b4e5bf65c74ef69adb8"}, + {file = "yarl-1.15.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7983290ede3aaa2c9620879530849532529b4dcbf5b12a0b6a91163a773eadb9"}, + {file = "yarl-1.15.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07a4b53abe85813c538b9cdbb02909ebe3734e3af466a587df516e960d500cc8"}, + {file = "yarl-1.15.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5882faa2a6e684f65ee44f18c701768749a950cbd5e72db452fc07805f6bdec0"}, + {file = "yarl-1.15.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e27861251d9c094f641d39a8a78dd2371fb9a252ea2f689d1ad353a31d46a0bc"}, + {file = "yarl-1.15.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8669a110f655c9eb22f16fb68a7d4942020aeaa09f1def584a80183e3e89953c"}, + {file = "yarl-1.15.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:10bfe0bef4cf5ea0383886beda004071faadedf2647048b9f876664284c5b60d"}, + {file = "yarl-1.15.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f7de0d4b6b4d8a77e422eb54d765255c0ec6883ee03b8fd537101633948619d7"}, + {file = "yarl-1.15.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:00bb3a559d7bd006a5302ecd7e409916939106a8cdbe31f4eb5e5b9ffcca57ea"}, + {file = "yarl-1.15.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:06ec070a2d71415f90dbe9d70af3158e7da97a128519dba2d1581156ee27fb92"}, + {file = "yarl-1.15.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b997a806846c00d1f41d6a251803732837771b2091bead7566f68820e317bfe7"}, + {file = "yarl-1.15.5-cp39-cp39-win32.whl", hash = "sha256:7825506fbee4055265528ec3532a8197ff26fc53d4978917a4c8ddbb4c1667d7"}, + {file = "yarl-1.15.5-cp39-cp39-win_amd64.whl", hash = "sha256:71730658be0b5de7c570a9795d7404c577b2313c1db370407092c66f70e04ccb"}, + {file = "yarl-1.15.5-py3-none-any.whl", hash = "sha256:625f31d6650829fba4030b4e7bdb2d69e41510dddfa29a1da27076c199521757"}, + {file = "yarl-1.15.5.tar.gz", hash = "sha256:8249147ee81c1cf4d1dc6f26ba28a1b9d92751529f83c308ad02164bb93abd0d"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.0" + [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "7ec220bee66b5bc207f9a8b2f4ca9100da0213bb9d0a407b51cac3dc8201e97c" +content-hash = "d337d313741d3aa3b36d4dd7d2761cdc6165ff5b0ce547bf479e9fc76aac03d4" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 671a864645..d8724b02eb 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -8,12 +8,14 @@ authors = ["Admin "] python = "^3.10" uvicorn = {extras = ["standard"], version = "^0.24.0.post1"} fastapi = "^0.109.1" -python-multipart = "^0.0.7" +python-multipart = "0.0.9" email-validator = "^2.1.0.post1" passlib = {extras = ["bcrypt"], version = "^1.7.4"} tenacity = "^8.2.3" -pydantic = ">2.0" +pydantic = "^2.9.2" emails = "^0.6" +psycopg2 = "^2.9.6" +h3 = "^3.7.7" gunicorn = "^22.0.0" jinja2 = "^3.1.4" @@ -22,10 +24,24 @@ httpx = "^0.25.1" psycopg = {extras = ["binary"], version = "^3.1.13"} sqlmodel = "^0.0.21" # Pin bcrypt until passlib supports the latest -bcrypt = "4.0.1" +bcrypt = "4.1.2" pydantic-settings = "^2.2.1" sentry-sdk = {extras = ["fastapi"], version = "^1.40.6"} pyjwt = "^2.8.0" +fastapi-cache = {extras = ["redis"], version = "^0.1.0"} +pyotp = "^2.9.0" +redis = "^5.0.8" +fastapi-users = {extras = ["oauth"], version = "^13.0.0"} +asyncpg = "^0.29.0" +otplessauthsdk = "^0.3.3" +fastapi-admin = "^1.0.4" +sqladmin = {extras = ["full"], version = "^0.19.0"} +unshortenit = "^0.4.0" +aiohttp = "^3.10.10" +bs4 = "^0.0.2" +selenium = "^4.25.0" +webdriver-manager = "^4.0.2" +autoscraper = "^1.1.14" [tool.poetry.group.dev.dependencies] pytest = "^7.4.3" diff --git a/copier.yml b/copier.yml index 5db3891c8d..f53c66bb2f 100644 --- a/copier.yml +++ b/copier.yml @@ -74,7 +74,6 @@ _exclude: - poetry.lock - .cache - .venv - # Frontend # Logs - logs - "*.log" diff --git a/deployment.md b/deployment.md index 6bcbe40259..a0b8b0db17 100644 --- a/deployment.md +++ b/deployment.md @@ -284,8 +284,6 @@ Traefik UI: `https://traefik.fastapi-project.example.com` ### Production -Frontend: `https://fastapi-project.example.com` - Backend API docs: `https://fastapi-project.example.com/docs` Backend API base URL: `https://fastapi-project.example.com/api/` @@ -294,8 +292,6 @@ Adminer: `https://adminer.fastapi-project.example.com` ### Staging -Frontend: `https://staging.fastapi-project.example.com` - Backend API docs: `https://staging.fastapi-project.example.com/docs` Backend API base URL: `https://staging.fastapi-project.example.com/api/` diff --git a/development.md b/development.md index 857a4e0a38..b4753b3902 100644 --- a/development.md +++ b/development.md @@ -146,13 +146,9 @@ The production or staging URLs would use these same paths, but with your own dom Development URLs, for local development. -Frontend: http://localhost - -Backend: http://localhost/api/ - Automatic Interactive Docs (Swagger UI): http://localhost/docs -Automatic Alternative Docs (ReDoc): http://localhost/redoc +Automatic Alternative Docs (ReDoc): http://localhost/redoc Adminer: http://localhost:8080 @@ -162,8 +158,6 @@ Traefik UI: http://localhost:8090 Development URLs, for local development. -Frontend: http://localhost.tiangolo.com - Backend: http://localhost.tiangolo.com/api/ Automatic Interactive Docs (Swagger UI): http://localhost.tiangolo.com/docs diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 418b535ab6..15f669bb68 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -74,14 +74,6 @@ services: - "1080:1080" - "1025:1025" - frontend: - restart: "no" - build: - context: ./frontend - args: - - VITE_API_URL=http://${DOMAIN?Variable not set} - - NODE_ENV=development - networks: traefik-public: # For local dev, don't expect an external Traefik network diff --git a/docker-compose.yml b/docker-compose.yml index d614942cbd..42a4754b23 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,7 @@ services: - SMTP_USER=${SMTP_USER} - SMTP_PASSWORD=${SMTP_PASSWORD} - EMAILS_FROM_EMAIL=${EMAILS_FROM_EMAIL} - - POSTGRES_SERVER=db + - POSTGRES_SERVER=${POSTGRES_SERVER} - POSTGRES_PORT=${POSTGRES_PORT} - POSTGRES_DB=${POSTGRES_DB} - POSTGRES_USER=${POSTGRES_USER?Variable not set} @@ -72,59 +72,21 @@ services: - traefik.enable=true - traefik.docker.network=traefik-public - traefik.constraint-label=traefik-public - - traefik.http.services.${STACK_NAME?Variable not set}-backend.loadbalancer.server.port=80 - - traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.rule=(Host(`${DOMAIN?Variable not set}`) || Host(`www.${DOMAIN?Variable not set}`)) && (PathPrefix(`/api`) || PathPrefix(`/docs`) || PathPrefix(`/redoc`)) - traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.entrypoints=http - - traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.rule=(Host(`${DOMAIN?Variable not set}`) || Host(`www.${DOMAIN?Variable not set}`)) && (PathPrefix(`/api`) || PathPrefix(`/docs`) || PathPrefix(`/redoc`)) - traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.entrypoints=https - traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls=true - traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls.certresolver=le - - # Define Traefik Middleware to handle domain with and without "www" to redirect to only one - traefik.http.middlewares.${STACK_NAME?Variable not set}-www-redirect.redirectregex.regex=^http(s)?://www.(${DOMAIN?Variable not set})/(.*) - # Redirect a domain with www to non-www - traefik.http.middlewares.${STACK_NAME?Variable not set}-www-redirect.redirectregex.replacement=http$${1}://${DOMAIN?Variable not set}/$${3} - - # Enable www redirection for HTTP and HTTPS - traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.middlewares=https-redirect,${STACK_NAME?Variable not set}-www-redirect - traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.middlewares=${STACK_NAME?Variable not set}-www-redirect - frontend: - image: '${DOCKER_IMAGE_FRONTEND?Variable not set}:${TAG-latest}' - restart: always - networks: - - traefik-public - - default - build: - context: ./frontend - args: - - VITE_API_URL=https://${DOMAIN?Variable not set} - - NODE_ENV=production - labels: - - traefik.enable=true - - traefik.docker.network=traefik-public - - traefik.constraint-label=traefik-public - - - traefik.http.services.${STACK_NAME?Variable not set}-frontend.loadbalancer.server.port=80 - - - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=Host(`${DOMAIN?Variable not set}`) || Host(`www.${DOMAIN?Variable not set}`) - - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.entrypoints=http - - - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.rule=Host(`${DOMAIN?Variable not set}`) || Host(`www.${DOMAIN?Variable not set}`) - - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.entrypoints=https - - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.tls=true - - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.tls.certresolver=le - - # Enable www redirection for HTTP and HTTPS - - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.middlewares=${STACK_NAME?Variable not set}-www-redirect - - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.middlewares=https-redirect,${STACK_NAME?Variable not set}-www-redirect volumes: app-db-data: networks: traefik-public: - # Allow setting it to false for testing - external: true + external: true \ No newline at end of file diff --git a/frontend/.dockerignore b/frontend/.dockerignore deleted file mode 100644 index f06235c460..0000000000 --- a/frontend/.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -dist diff --git a/frontend/.env b/frontend/.env deleted file mode 100644 index f829bd1979..0000000000 --- a/frontend/.env +++ /dev/null @@ -1 +0,0 @@ -VITE_API_URL=http://localhost diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index dfc4015cce..0000000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,29 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local -openapi.json - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ diff --git a/frontend/.nvmrc b/frontend/.nvmrc deleted file mode 100644 index 209e3ef4b6..0000000000 --- a/frontend/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -20 diff --git a/frontend/Dockerfile b/frontend/Dockerfile deleted file mode 100644 index 8728c7b029..0000000000 --- a/frontend/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -# Stage 0, "build-stage", based on Node.js, to build and compile the frontend -FROM node:20 AS build-stage - -WORKDIR /app - -COPY package*.json /app/ - -RUN npm install - -COPY ./ /app/ - -ARG VITE_API_URL=${VITE_API_URL} - -RUN npm run build - - -# Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx -FROM nginx:1 - -COPY --from=build-stage /app/dist/ /usr/share/nginx/html - -COPY ./nginx.conf /etc/nginx/conf.d/default.conf -COPY ./nginx-backend-not-found.conf /etc/nginx/extra-conf.d/backend-not-found.conf diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 9a01970fda..0000000000 --- a/frontend/README.md +++ /dev/null @@ -1,160 +0,0 @@ -# FastAPI Project - Frontend - -The frontend is built with [Vite](https://vitejs.dev/), [React](https://reactjs.org/), [TypeScript](https://www.typescriptlang.org/), [TanStack Query](https://tanstack.com/query), [TanStack Router](https://tanstack.com/router) and [Chakra UI](https://chakra-ui.com/). - -## Frontend development - -Before you begin, ensure that you have either the Node Version Manager (nvm) or Fast Node Manager (fnm) installed on your system. - -* To install fnm follow the [official fnm guide](https://github.com/Schniz/fnm#installation). If you prefer nvm, you can install it using the [official nvm guide](https://github.com/nvm-sh/nvm#installing-and-updating). - -* After installing either nvm or fnm, proceed to the `frontend` directory: - -```bash -cd frontend -``` -* If the Node.js version specified in the `.nvmrc` file isn't installed on your system, you can install it using the appropriate command: - -```bash -# If using fnm -fnm install - -# If using nvm -nvm install -``` - -* Once the installation is complete, switch to the installed version: - -```bash -# If using fnm -fnm use - -# If using nvm -nvm use -``` - -* Within the `frontend` directory, install the necessary NPM packages: - -```bash -npm install -``` - -* And start the live server with the following `npm` script: - -```bash -npm run dev -``` - -* Then open your browser at http://localhost:5173/. - -Notice that this live server is not running inside Docker, it's for local development, and that is the recommended workflow. Once you are happy with your frontend, you can build the frontend Docker image and start it, to test it in a production-like environment. But building the image at every change will not be as productive as running the local development server with live reload. - -Check the file `package.json` to see other available options. - -### Removing the frontend - -If you are developing an API-only app and want to remove the frontend, you can do it easily: - -* Remove the `./frontend` directory. - -* In the `docker-compose.yml` file, remove the whole service / section `frontend`. - -* In the `docker-compose.override.yml` file, remove the whole service / section `frontend`. - -Done, you have a frontend-less (api-only) app. ๐Ÿค“ - ---- - -If you want, you can also remove the `FRONTEND` environment variables from: - -* `.env` -* `./scripts/*.sh` - -But it would be only to clean them up, leaving them won't really have any effect either way. - -## Generate Client - -### Automatically - -* Activate the backend virtual environment. -* From the top level project directory, run the script: - -```bash -./scripts/generate-frontend-client.sh -``` - -* Commit the changes. - -### Manually - -* Start the Docker Compose stack. - -* Download the OpenAPI JSON file from `http://localhost/api/v1/openapi.json` and copy it to a new file `openapi.json` at the root of the `frontend` directory. - -* To simplify the names in the generated frontend client code, modify the `openapi.json` file by running the following script: - -```bash -node modify-openapi-operationids.js -``` - -* To generate the frontend client, run: - -```bash -npm run generate-client -``` - -* Commit the changes. - -Notice that everytime the backend changes (changing the OpenAPI schema), you should follow these steps again to update the frontend client. - -## Using a Remote API - -If you want to use a remote API, you can set the environment variable `VITE_API_URL` to the URL of the remote API. For example, you can set it in the `frontend/.env` file: - -```env -VITE_API_URL=https://my-remote-api.example.com -``` - -Then, when you run the frontend, it will use that URL as the base URL for the API. - -## Code Structure - -The frontend code is structured as follows: - -* `frontend/src` - The main frontend code. -* `frontend/src/assets` - Static assets. -* `frontend/src/client` - The generated OpenAPI client. -* `frontend/src/components` - The different components of the frontend. -* `frontend/src/hooks` - Custom hooks. -* `frontend/src/routes` - The different routes of the frontend which include the pages. -* `theme.tsx` - The Chakra UI custom theme. - -## End-to-End Testing with Playwright - -The frontend includes initial end-to-end tests using Playwright. To run the tests, you need to have the Docker Compose stack running. Start the stack with the following command: - -```bash -docker compose up -d -``` - -Then, you can run the tests with the following command: - -```bash -npx playwright test -``` - -You can also run your tests in UI mode to see the browser and interact with it running: - -```bash -npx playwright test --ui -``` - -To stop and remove the Docker Compose stack and clean the data created in tests, use the following command: - -```bash -docker compose down -v -``` - -To update the tests, navigate to the tests directory and modify the existing test files or add new ones as needed. - -For more information on writing and running Playwright tests, refer to the official [Playwright documentation](https://playwright.dev/docs/intro). \ No newline at end of file diff --git a/frontend/biome.json b/frontend/biome.json deleted file mode 100644 index a06315dc2a..0000000000 --- a/frontend/biome.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/1.6.1/schema.json", - "organizeImports": { - "enabled": true - }, - "files": { - "ignore": [ - "node_modules", - "src/routeTree.gen.ts", - "playwright.config.ts", - "playwright-report" - ] - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "suspicious": { - "noExplicitAny": "off", - "noArrayIndexKey": "off" - }, - "style": { - "noNonNullAssertion": "off" - } - } - }, - "formatter": { - "indentStyle": "space" - }, - "javascript": { - "formatter": { - "quoteStyle": "double", - "semicolons": "asNeeded" - } - } -} diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index 57621a268b..0000000000 --- a/frontend/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - Full Stack FastAPI Project - - - -
- - - diff --git a/frontend/modify-openapi-operationids.js b/frontend/modify-openapi-operationids.js deleted file mode 100644 index b22fd17f9e..0000000000 --- a/frontend/modify-openapi-operationids.js +++ /dev/null @@ -1,36 +0,0 @@ -import * as fs from "node:fs" - -async function modifyOpenAPIFile(filePath) { - try { - const data = await fs.promises.readFile(filePath) - const openapiContent = JSON.parse(data) - - const paths = openapiContent.paths - for (const pathKey of Object.keys(paths)) { - const pathData = paths[pathKey] - for (const method of Object.keys(pathData)) { - const operation = pathData[method] - if (operation.tags && operation.tags.length > 0) { - const tag = operation.tags[0] - const operationId = operation.operationId - const toRemove = `${tag}-` - if (operationId.startsWith(toRemove)) { - const newOperationId = operationId.substring(toRemove.length) - operation.operationId = newOperationId - } - } - } - } - - await fs.promises.writeFile( - filePath, - JSON.stringify(openapiContent, null, 2), - ) - console.log("File successfully modified") - } catch (err) { - console.error("Error:", err) - } -} - -const filePath = "./openapi.json" -modifyOpenAPIFile(filePath) diff --git a/frontend/nginx-backend-not-found.conf b/frontend/nginx-backend-not-found.conf deleted file mode 100644 index f6fea66358..0000000000 --- a/frontend/nginx-backend-not-found.conf +++ /dev/null @@ -1,9 +0,0 @@ -location /api { - return 404; -} -location /docs { - return 404; -} -location /redoc { - return 404; -} diff --git a/frontend/nginx.conf b/frontend/nginx.conf deleted file mode 100644 index ba4d9aad6c..0000000000 --- a/frontend/nginx.conf +++ /dev/null @@ -1,11 +0,0 @@ -server { - listen 80; - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - try_files $uri /index.html =404; - } - - include /etc/nginx/extra-conf.d/*.conf; -} diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index 9560ff9f69..0000000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,6478 +0,0 @@ -{ - "name": "frontend", - "version": "0.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "frontend", - "version": "0.0.0", - "dependencies": { - "@chakra-ui/icons": "2.1.1", - "@chakra-ui/react": "2.8.2", - "@emotion/react": "11.11.3", - "@emotion/styled": "11.11.0", - "@tanstack/react-query": "^5.28.14", - "@tanstack/react-query-devtools": "^5.28.14", - "@tanstack/react-router": "1.19.1", - "axios": "1.7.4", - "form-data": "4.0.0", - "framer-motion": "10.16.16", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-error-boundary": "^4.0.13", - "react-hook-form": "7.49.3", - "react-icons": "5.0.1" - }, - "devDependencies": { - "@biomejs/biome": "1.6.1", - "@hey-api/openapi-ts": "^0.34.1", - "@playwright/test": "^1.45.2", - "@tanstack/router-devtools": "1.19.1", - "@tanstack/router-vite-plugin": "1.19.0", - "@types/node": "^20.10.5", - "@types/react": "^18.2.37", - "@types/react-dom": "^18.2.15", - "@vitejs/plugin-react-swc": "^3.5.0", - "dotenv": "^16.4.5", - "typescript": "^5.2.2", - "vite": "^5.0.13" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", - "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", - "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@biomejs/biome": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.6.1.tgz", - "integrity": "sha512-SILQvA2S0XeaOuu1bivv6fQmMo7zMfr2xqDEN+Sz78pGbAKZnGmg0emsXjQWoBY/RVm9kPCgX+aGEpZZTYaM7w==", - "dev": true, - "hasInstallScript": true, - "bin": { - "biome": "bin/biome" - }, - "engines": { - "node": ">=14.*" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/biome" - }, - "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "1.6.1", - "@biomejs/cli-darwin-x64": "1.6.1", - "@biomejs/cli-linux-arm64": "1.6.1", - "@biomejs/cli-linux-arm64-musl": "1.6.1", - "@biomejs/cli-linux-x64": "1.6.1", - "@biomejs/cli-linux-x64-musl": "1.6.1", - "@biomejs/cli-win32-arm64": "1.6.1", - "@biomejs/cli-win32-x64": "1.6.1" - } - }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.6.1.tgz", - "integrity": "sha512-KlvY00iB9T/vFi4m/GXxEyYkYnYy6aw06uapzUIIdiMMj7I/pmZu7CsZlzWdekVD0j+SsQbxdZMsb0wPhnRSsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.*" - } - }, - "node_modules/@biomejs/cli-darwin-x64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.6.1.tgz", - "integrity": "sha512-jP4E8TXaQX5e3nvRJSzB+qicZrdIDCrjR0sSb1DaDTx4JPZH5WXq/BlTqAyWi3IijM+IYMjWqAAK4kOHsSCzxw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.*" - } - }, - "node_modules/@biomejs/cli-linux-arm64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.6.1.tgz", - "integrity": "sha512-nxD1UyX3bWSl/RSKlib/JsOmt+652/9yieogdSC/UTLgVCZYOF7u8L/LK7kAa0Y4nA8zSPavAQTgko7mHC2ObA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.*" - } - }, - "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.6.1.tgz", - "integrity": "sha512-YdkDgFecdHJg7PJxAMaZIixVWGB6St4yH08BHagO0fEhNNiY8cAKEVo2mcXlsnEiTMpeSEAY9VxLUrVT3IVxpw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.*" - } - }, - "node_modules/@biomejs/cli-linux-x64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.6.1.tgz", - "integrity": "sha512-BYAzenlMF3QdngjNFw9QVBXKGNzeecqwF3pwDgUGEvU7OJpn1/lyVkJVxYPtVGRNdjQ9e6l/s8NjKuBpW/ZR4Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.*" - } - }, - "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.6.1.tgz", - "integrity": "sha512-aSISIDmxq04NNy7tm4x9rBk2vH0ub2VDIE4outEmdC2LBtEJoINiphlZagx/FvjbsqUfygent9QUSn0oREnAXg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.*" - } - }, - "node_modules/@biomejs/cli-win32-arm64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.6.1.tgz", - "integrity": "sha512-/eCHQKZ1kEawUpkSuXq4urtxMsD1P1678OPG3zNKt3ru16AqqspLdO3jzBe3k74xCPYnQ36e9Yqc97Mo0qgPtg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.*" - } - }, - "node_modules/@biomejs/cli-win32-x64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.6.1.tgz", - "integrity": "sha512-5TUZbzBwnDLFxLVGEPsorNi6eC2Gt+z4Oei9Qvq0M/4c4/mjZ96ABgwao/tMxf4ZBr/qyy2YdvF+gX9Rc+xC0A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.*" - } - }, - "node_modules/@chakra-ui/accordion": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/accordion/-/accordion-2.3.1.tgz", - "integrity": "sha512-FSXRm8iClFyU+gVaXisOSEw0/4Q+qZbFRiuhIAkVU6Boj0FxAMrlo9a8AV5TuF77rgaHytCdHk0Ng+cyUijrag==", - "dependencies": { - "@chakra-ui/descendant": "3.1.0", - "@chakra-ui/icon": "3.2.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-use-controllable-state": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5", - "@chakra-ui/transition": "2.1.0" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "framer-motion": ">=4.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/alert": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/alert/-/alert-2.2.2.tgz", - "integrity": "sha512-jHg4LYMRNOJH830ViLuicjb3F+v6iriE/2G5T+Sd0Hna04nukNJ1MxUmBPE+vI22me2dIflfelu2v9wdB6Pojw==", - "dependencies": { - "@chakra-ui/icon": "3.2.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5", - "@chakra-ui/spinner": "2.1.0" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/anatomy": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/anatomy/-/anatomy-2.2.2.tgz", - "integrity": "sha512-MV6D4VLRIHr4PkW4zMyqfrNS1mPlCTiCXwvYGtDFQYr+xHFfonhAuf9WjsSc0nyp2m0OdkSLnzmVKkZFLo25Tg==" - }, - "node_modules/@chakra-ui/avatar": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/avatar/-/avatar-2.3.0.tgz", - "integrity": "sha512-8gKSyLfygnaotbJbDMHDiJoF38OHXUYVme4gGxZ1fLnQEdPVEaIWfH+NndIjOM0z8S+YEFnT9KyGMUtvPrBk3g==", - "dependencies": { - "@chakra-ui/image": "2.1.0", - "@chakra-ui/react-children-utils": "2.0.6", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/breadcrumb": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/breadcrumb/-/breadcrumb-2.2.0.tgz", - "integrity": "sha512-4cWCG24flYBxjruRi4RJREWTGF74L/KzI2CognAW/d/zWR0CjiScuJhf37Am3LFbCySP6WSoyBOtTIoTA4yLEA==", - "dependencies": { - "@chakra-ui/react-children-utils": "2.0.6", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/breakpoint-utils": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@chakra-ui/breakpoint-utils/-/breakpoint-utils-2.0.8.tgz", - "integrity": "sha512-Pq32MlEX9fwb5j5xx8s18zJMARNHlQZH2VH1RZgfgRDpp7DcEgtRW5AInfN5CfqdHLO1dGxA7I3MqEuL5JnIsA==", - "dependencies": { - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "node_modules/@chakra-ui/button": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/button/-/button-2.1.0.tgz", - "integrity": "sha512-95CplwlRKmmUXkdEp/21VkEWgnwcx2TOBG6NfYlsuLBDHSLlo5FKIiE2oSi4zXc4TLcopGcWPNcm/NDaSC5pvA==", - "dependencies": { - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5", - "@chakra-ui/spinner": "2.1.0" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/card": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/card/-/card-2.2.0.tgz", - "integrity": "sha512-xUB/k5MURj4CtPAhdSoXZidUbm8j3hci9vnc+eZJVDqhDOShNlD6QeniQNRPRys4lWAQLCbFcrwL29C8naDi6g==", - "dependencies": { - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/checkbox": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/checkbox/-/checkbox-2.3.2.tgz", - "integrity": "sha512-85g38JIXMEv6M+AcyIGLh7igNtfpAN6KGQFYxY9tBj0eWvWk4NKQxvqqyVta0bSAyIl1rixNIIezNpNWk2iO4g==", - "dependencies": { - "@chakra-ui/form-control": "2.2.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-callback-ref": "2.1.0", - "@chakra-ui/react-use-controllable-state": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/react-use-safe-layout-effect": "2.1.0", - "@chakra-ui/react-use-update-effect": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5", - "@chakra-ui/visually-hidden": "2.2.0", - "@zag-js/focus-visible": "0.16.0" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/clickable": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/clickable/-/clickable-2.1.0.tgz", - "integrity": "sha512-flRA/ClPUGPYabu+/GLREZVZr9j2uyyazCAUHAdrTUEdDYCr31SVGhgh7dgKdtq23bOvAQJpIJjw/0Bs0WvbXw==", - "dependencies": { - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/close-button": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/close-button/-/close-button-2.1.1.tgz", - "integrity": "sha512-gnpENKOanKexswSVpVz7ojZEALl2x5qjLYNqSQGbxz+aP9sOXPfUS56ebyBrre7T7exuWGiFeRwnM0oVeGPaiw==", - "dependencies": { - "@chakra-ui/icon": "3.2.0" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/color-mode": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/color-mode/-/color-mode-2.2.0.tgz", - "integrity": "sha512-niTEA8PALtMWRI9wJ4LL0CSBDo8NBfLNp4GD6/0hstcm3IlbBHTVKxN6HwSaoNYfphDQLxCjT4yG+0BJA5tFpg==", - "dependencies": { - "@chakra-ui/react-use-safe-layout-effect": "2.1.0" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/control-box": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/control-box/-/control-box-2.1.0.tgz", - "integrity": "sha512-gVrRDyXFdMd8E7rulL0SKeoljkLQiPITFnsyMO8EFHNZ+AHt5wK4LIguYVEq88APqAGZGfHFWXr79RYrNiE3Mg==", - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/counter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/counter/-/counter-2.1.0.tgz", - "integrity": "sha512-s6hZAEcWT5zzjNz2JIWUBzRubo9la/oof1W7EKZVVfPYHERnl5e16FmBC79Yfq8p09LQ+aqFKm/etYoJMMgghw==", - "dependencies": { - "@chakra-ui/number-utils": "2.0.7", - "@chakra-ui/react-use-callback-ref": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/css-reset": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/css-reset/-/css-reset-2.3.0.tgz", - "integrity": "sha512-cQwwBy5O0jzvl0K7PLTLgp8ijqLPKyuEMiDXwYzl95seD3AoeuoCLyzZcJtVqaUZ573PiBdAbY/IlZcwDOItWg==", - "peerDependencies": { - "@emotion/react": ">=10.0.35", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/descendant": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/descendant/-/descendant-3.1.0.tgz", - "integrity": "sha512-VxCIAir08g5w27klLyi7PVo8BxhW4tgU/lxQyujkmi4zx7hT9ZdrcQLAted/dAa+aSIZ14S1oV0Q9lGjsAdxUQ==", - "dependencies": { - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/dom-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/dom-utils/-/dom-utils-2.1.0.tgz", - "integrity": "sha512-ZmF2qRa1QZ0CMLU8M1zCfmw29DmPNtfjR9iTo74U5FPr3i1aoAh7fbJ4qAlZ197Xw9eAW28tvzQuoVWeL5C7fQ==" - }, - "node_modules/@chakra-ui/editable": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/editable/-/editable-3.1.0.tgz", - "integrity": "sha512-j2JLrUL9wgg4YA6jLlbU88370eCRyor7DZQD9lzpY95tSOXpTljeg3uF9eOmDnCs6fxp3zDWIfkgMm/ExhcGTg==", - "dependencies": { - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-callback-ref": "2.1.0", - "@chakra-ui/react-use-controllable-state": "2.1.0", - "@chakra-ui/react-use-focus-on-pointer-down": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/react-use-safe-layout-effect": "2.1.0", - "@chakra-ui/react-use-update-effect": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/event-utils": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@chakra-ui/event-utils/-/event-utils-2.0.8.tgz", - "integrity": "sha512-IGM/yGUHS+8TOQrZGpAKOJl/xGBrmRYJrmbHfUE7zrG3PpQyXvbLDP1M+RggkCFVgHlJi2wpYIf0QtQlU0XZfw==" - }, - "node_modules/@chakra-ui/focus-lock": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/focus-lock/-/focus-lock-2.1.0.tgz", - "integrity": "sha512-EmGx4PhWGjm4dpjRqM4Aa+rCWBxP+Rq8Uc/nAVnD4YVqkEhBkrPTpui2lnjsuxqNaZ24fIAZ10cF1hlpemte/w==", - "dependencies": { - "@chakra-ui/dom-utils": "2.1.0", - "react-focus-lock": "^2.9.4" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/form-control": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/form-control/-/form-control-2.2.0.tgz", - "integrity": "sha512-wehLC1t4fafCVJ2RvJQT2jyqsAwX7KymmiGqBu7nQoQz8ApTkGABWpo/QwDh3F/dBLrouHDoOvGmYTqft3Mirw==", - "dependencies": { - "@chakra-ui/icon": "3.2.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/hooks": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/hooks/-/hooks-2.2.1.tgz", - "integrity": "sha512-RQbTnzl6b1tBjbDPf9zGRo9rf/pQMholsOudTxjy4i9GfTfz6kgp5ValGjQm2z7ng6Z31N1cnjZ1AlSzQ//ZfQ==", - "dependencies": { - "@chakra-ui/react-utils": "2.0.12", - "@chakra-ui/utils": "2.0.15", - "compute-scroll-into-view": "3.0.3", - "copy-to-clipboard": "3.3.3" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/icon": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/icon/-/icon-3.2.0.tgz", - "integrity": "sha512-xxjGLvlX2Ys4H0iHrI16t74rG9EBcpFvJ3Y3B7KMQTrnW34Kf7Da/UC8J67Gtx85mTHW020ml85SVPKORWNNKQ==", - "dependencies": { - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/icons": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/icons/-/icons-2.1.1.tgz", - "integrity": "sha512-3p30hdo4LlRZTT5CwoAJq3G9fHI0wDc0pBaMHj4SUn0yomO+RcDRlzhdXqdr5cVnzax44sqXJVnf3oQG0eI+4g==", - "dependencies": { - "@chakra-ui/icon": "3.2.0" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/image": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/image/-/image-2.1.0.tgz", - "integrity": "sha512-bskumBYKLiLMySIWDGcz0+D9Th0jPvmX6xnRMs4o92tT3Od/bW26lahmV2a2Op2ItXeCmRMY+XxJH5Gy1i46VA==", - "dependencies": { - "@chakra-ui/react-use-safe-layout-effect": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/input": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/input/-/input-2.1.2.tgz", - "integrity": "sha512-GiBbb3EqAA8Ph43yGa6Mc+kUPjh4Spmxp1Pkelr8qtudpc3p2PJOOebLpd90mcqw8UePPa+l6YhhPtp6o0irhw==", - "dependencies": { - "@chakra-ui/form-control": "2.2.0", - "@chakra-ui/object-utils": "2.1.0", - "@chakra-ui/react-children-utils": "2.0.6", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/layout": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/layout/-/layout-2.3.1.tgz", - "integrity": "sha512-nXuZ6WRbq0WdgnRgLw+QuxWAHuhDtVX8ElWqcTK+cSMFg/52eVP47czYBE5F35YhnoW2XBwfNoNgZ7+e8Z01Rg==", - "dependencies": { - "@chakra-ui/breakpoint-utils": "2.0.8", - "@chakra-ui/icon": "3.2.0", - "@chakra-ui/object-utils": "2.1.0", - "@chakra-ui/react-children-utils": "2.0.6", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/lazy-utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@chakra-ui/lazy-utils/-/lazy-utils-2.0.5.tgz", - "integrity": "sha512-UULqw7FBvcckQk2n3iPO56TMJvDsNv0FKZI6PlUNJVaGsPbsYxK/8IQ60vZgaTVPtVcjY6BE+y6zg8u9HOqpyg==" - }, - "node_modules/@chakra-ui/live-region": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/live-region/-/live-region-2.1.0.tgz", - "integrity": "sha512-ZOxFXwtaLIsXjqnszYYrVuswBhnIHHP+XIgK1vC6DePKtyK590Wg+0J0slDwThUAd4MSSIUa/nNX84x1GMphWw==", - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/media-query": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/media-query/-/media-query-3.3.0.tgz", - "integrity": "sha512-IsTGgFLoICVoPRp9ykOgqmdMotJG0CnPsKvGQeSFOB/dZfIujdVb14TYxDU4+MURXry1MhJ7LzZhv+Ml7cr8/g==", - "dependencies": { - "@chakra-ui/breakpoint-utils": "2.0.8", - "@chakra-ui/react-env": "3.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/menu": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/menu/-/menu-2.2.1.tgz", - "integrity": "sha512-lJS7XEObzJxsOwWQh7yfG4H8FzFPRP5hVPN/CL+JzytEINCSBvsCDHrYPQGp7jzpCi8vnTqQQGQe0f8dwnXd2g==", - "dependencies": { - "@chakra-ui/clickable": "2.1.0", - "@chakra-ui/descendant": "3.1.0", - "@chakra-ui/lazy-utils": "2.0.5", - "@chakra-ui/popper": "3.1.0", - "@chakra-ui/react-children-utils": "2.0.6", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-use-animation-state": "2.1.0", - "@chakra-ui/react-use-controllable-state": "2.1.0", - "@chakra-ui/react-use-disclosure": "2.1.0", - "@chakra-ui/react-use-focus-effect": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/react-use-outside-click": "2.2.0", - "@chakra-ui/react-use-update-effect": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5", - "@chakra-ui/transition": "2.1.0" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "framer-motion": ">=4.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/modal": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/modal/-/modal-2.3.1.tgz", - "integrity": "sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==", - "dependencies": { - "@chakra-ui/close-button": "2.1.1", - "@chakra-ui/focus-lock": "2.1.0", - "@chakra-ui/portal": "2.1.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5", - "@chakra-ui/transition": "2.1.0", - "aria-hidden": "^1.2.3", - "react-remove-scroll": "^2.5.6" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "framer-motion": ">=4.0.0", - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/@chakra-ui/number-input": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/number-input/-/number-input-2.1.2.tgz", - "integrity": "sha512-pfOdX02sqUN0qC2ysuvgVDiws7xZ20XDIlcNhva55Jgm095xjm8eVdIBfNm3SFbSUNxyXvLTW/YQanX74tKmuA==", - "dependencies": { - "@chakra-ui/counter": "2.1.0", - "@chakra-ui/form-control": "2.2.0", - "@chakra-ui/icon": "3.2.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-callback-ref": "2.1.0", - "@chakra-ui/react-use-event-listener": "2.1.0", - "@chakra-ui/react-use-interval": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/react-use-safe-layout-effect": "2.1.0", - "@chakra-ui/react-use-update-effect": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/number-utils": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@chakra-ui/number-utils/-/number-utils-2.0.7.tgz", - "integrity": "sha512-yOGxBjXNvLTBvQyhMDqGU0Oj26s91mbAlqKHiuw737AXHt0aPllOthVUqQMeaYLwLCjGMg0jtI7JReRzyi94Dg==" - }, - "node_modules/@chakra-ui/object-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/object-utils/-/object-utils-2.1.0.tgz", - "integrity": "sha512-tgIZOgLHaoti5PYGPTwK3t/cqtcycW0owaiOXoZOcpwwX/vlVb+H1jFsQyWiiwQVPt9RkoSLtxzXamx+aHH+bQ==" - }, - "node_modules/@chakra-ui/pin-input": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/pin-input/-/pin-input-2.1.0.tgz", - "integrity": "sha512-x4vBqLStDxJFMt+jdAHHS8jbh294O53CPQJoL4g228P513rHylV/uPscYUHrVJXRxsHfRztQO9k45jjTYaPRMw==", - "dependencies": { - "@chakra-ui/descendant": "3.1.0", - "@chakra-ui/react-children-utils": "2.0.6", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-use-controllable-state": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/popover": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/popover/-/popover-2.2.1.tgz", - "integrity": "sha512-K+2ai2dD0ljvJnlrzesCDT9mNzLifE3noGKZ3QwLqd/K34Ym1W/0aL1ERSynrcG78NKoXS54SdEzkhCZ4Gn/Zg==", - "dependencies": { - "@chakra-ui/close-button": "2.1.1", - "@chakra-ui/lazy-utils": "2.0.5", - "@chakra-ui/popper": "3.1.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-animation-state": "2.1.0", - "@chakra-ui/react-use-disclosure": "2.1.0", - "@chakra-ui/react-use-focus-effect": "2.1.0", - "@chakra-ui/react-use-focus-on-pointer-down": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "framer-motion": ">=4.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/popper": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/popper/-/popper-3.1.0.tgz", - "integrity": "sha512-ciDdpdYbeFG7og6/6J8lkTFxsSvwTdMLFkpVylAF6VNC22jssiWfquj2eyD4rJnzkRFPvIWJq8hvbfhsm+AjSg==", - "dependencies": { - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@popperjs/core": "^2.9.3" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/portal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/portal/-/portal-2.1.0.tgz", - "integrity": "sha512-9q9KWf6SArEcIq1gGofNcFPSWEyl+MfJjEUg/un1SMlQjaROOh3zYr+6JAwvcORiX7tyHosnmWC3d3wI2aPSQg==", - "dependencies": { - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-use-safe-layout-effect": "2.1.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/@chakra-ui/progress": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/progress/-/progress-2.2.0.tgz", - "integrity": "sha512-qUXuKbuhN60EzDD9mHR7B67D7p/ZqNS2Aze4Pbl1qGGZfulPW0PY8Rof32qDtttDQBkzQIzFGE8d9QpAemToIQ==", - "dependencies": { - "@chakra-ui/react-context": "2.1.0" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/provider": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/provider/-/provider-2.4.2.tgz", - "integrity": "sha512-w0Tef5ZCJK1mlJorcSjItCSbyvVuqpvyWdxZiVQmE6fvSJR83wZof42ux0+sfWD+I7rHSfj+f9nzhNaEWClysw==", - "dependencies": { - "@chakra-ui/css-reset": "2.3.0", - "@chakra-ui/portal": "2.1.0", - "@chakra-ui/react-env": "3.1.0", - "@chakra-ui/system": "2.6.2", - "@chakra-ui/utils": "2.0.15" - }, - "peerDependencies": { - "@emotion/react": "^11.0.0", - "@emotion/styled": "^11.0.0", - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/@chakra-ui/radio": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/radio/-/radio-2.1.2.tgz", - "integrity": "sha512-n10M46wJrMGbonaghvSRnZ9ToTv/q76Szz284gv4QUWvyljQACcGrXIONUnQ3BIwbOfkRqSk7Xl/JgZtVfll+w==", - "dependencies": { - "@chakra-ui/form-control": "2.2.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5", - "@zag-js/focus-visible": "0.16.0" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-2.8.2.tgz", - "integrity": "sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==", - "dependencies": { - "@chakra-ui/accordion": "2.3.1", - "@chakra-ui/alert": "2.2.2", - "@chakra-ui/avatar": "2.3.0", - "@chakra-ui/breadcrumb": "2.2.0", - "@chakra-ui/button": "2.1.0", - "@chakra-ui/card": "2.2.0", - "@chakra-ui/checkbox": "2.3.2", - "@chakra-ui/close-button": "2.1.1", - "@chakra-ui/control-box": "2.1.0", - "@chakra-ui/counter": "2.1.0", - "@chakra-ui/css-reset": "2.3.0", - "@chakra-ui/editable": "3.1.0", - "@chakra-ui/focus-lock": "2.1.0", - "@chakra-ui/form-control": "2.2.0", - "@chakra-ui/hooks": "2.2.1", - "@chakra-ui/icon": "3.2.0", - "@chakra-ui/image": "2.1.0", - "@chakra-ui/input": "2.1.2", - "@chakra-ui/layout": "2.3.1", - "@chakra-ui/live-region": "2.1.0", - "@chakra-ui/media-query": "3.3.0", - "@chakra-ui/menu": "2.2.1", - "@chakra-ui/modal": "2.3.1", - "@chakra-ui/number-input": "2.1.2", - "@chakra-ui/pin-input": "2.1.0", - "@chakra-ui/popover": "2.2.1", - "@chakra-ui/popper": "3.1.0", - "@chakra-ui/portal": "2.1.0", - "@chakra-ui/progress": "2.2.0", - "@chakra-ui/provider": "2.4.2", - "@chakra-ui/radio": "2.1.2", - "@chakra-ui/react-env": "3.1.0", - "@chakra-ui/select": "2.1.2", - "@chakra-ui/skeleton": "2.1.0", - "@chakra-ui/skip-nav": "2.1.0", - "@chakra-ui/slider": "2.1.0", - "@chakra-ui/spinner": "2.1.0", - "@chakra-ui/stat": "2.1.1", - "@chakra-ui/stepper": "2.3.1", - "@chakra-ui/styled-system": "2.9.2", - "@chakra-ui/switch": "2.1.2", - "@chakra-ui/system": "2.6.2", - "@chakra-ui/table": "2.1.0", - "@chakra-ui/tabs": "3.0.0", - "@chakra-ui/tag": "3.1.1", - "@chakra-ui/textarea": "2.1.2", - "@chakra-ui/theme": "3.3.1", - "@chakra-ui/theme-utils": "2.0.21", - "@chakra-ui/toast": "7.0.2", - "@chakra-ui/tooltip": "2.3.1", - "@chakra-ui/transition": "2.1.0", - "@chakra-ui/utils": "2.0.15", - "@chakra-ui/visually-hidden": "2.2.0" - }, - "peerDependencies": { - "@emotion/react": "^11.0.0", - "@emotion/styled": "^11.0.0", - "framer-motion": ">=4.0.0", - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/@chakra-ui/react-children-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-children-utils/-/react-children-utils-2.0.6.tgz", - "integrity": "sha512-QVR2RC7QsOsbWwEnq9YduhpqSFnZGvjjGREV8ygKi8ADhXh93C8azLECCUVgRJF2Wc+So1fgxmjLcbZfY2VmBA==", - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-context": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-context/-/react-context-2.1.0.tgz", - "integrity": "sha512-iahyStvzQ4AOwKwdPReLGfDesGG+vWJfEsn0X/NoGph/SkN+HXtv2sCfYFFR9k7bb+Kvc6YfpLlSuLvKMHi2+w==", - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-env": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-env/-/react-env-3.1.0.tgz", - "integrity": "sha512-Vr96GV2LNBth3+IKzr/rq1IcnkXv+MLmwjQH6C8BRtn3sNskgDFD5vLkVXcEhagzZMCh8FR3V/bzZPojBOyNhw==", - "dependencies": { - "@chakra-ui/react-use-safe-layout-effect": "2.1.0" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-types/-/react-types-2.0.7.tgz", - "integrity": "sha512-12zv2qIZ8EHwiytggtGvo4iLT0APris7T0qaAWqzpUGS0cdUtR8W+V1BJ5Ocq+7tA6dzQ/7+w5hmXih61TuhWQ==", - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-use-animation-state": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-animation-state/-/react-use-animation-state-2.1.0.tgz", - "integrity": "sha512-CFZkQU3gmDBwhqy0vC1ryf90BVHxVN8cTLpSyCpdmExUEtSEInSCGMydj2fvn7QXsz/za8JNdO2xxgJwxpLMtg==", - "dependencies": { - "@chakra-ui/dom-utils": "2.1.0", - "@chakra-ui/react-use-event-listener": "2.1.0" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-use-callback-ref": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-callback-ref/-/react-use-callback-ref-2.1.0.tgz", - "integrity": "sha512-efnJrBtGDa4YaxDzDE90EnKD3Vkh5a1t3w7PhnRQmsphLy3g2UieasoKTlT2Hn118TwDjIv5ZjHJW6HbzXA9wQ==", - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-use-controllable-state": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-controllable-state/-/react-use-controllable-state-2.1.0.tgz", - "integrity": "sha512-QR/8fKNokxZUs4PfxjXuwl0fj/d71WPrmLJvEpCTkHjnzu7LnYvzoe2wB867IdooQJL0G1zBxl0Dq+6W1P3jpg==", - "dependencies": { - "@chakra-ui/react-use-callback-ref": "2.1.0" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-use-disclosure": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-disclosure/-/react-use-disclosure-2.1.0.tgz", - "integrity": "sha512-Ax4pmxA9LBGMyEZJhhUZobg9C0t3qFE4jVF1tGBsrLDcdBeLR9fwOogIPY9Hf0/wqSlAryAimICbr5hkpa5GSw==", - "dependencies": { - "@chakra-ui/react-use-callback-ref": "2.1.0" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-use-event-listener": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-event-listener/-/react-use-event-listener-2.1.0.tgz", - "integrity": "sha512-U5greryDLS8ISP69DKDsYcsXRtAdnTQT+jjIlRYZ49K/XhUR/AqVZCK5BkR1spTDmO9H8SPhgeNKI70ODuDU/Q==", - "dependencies": { - "@chakra-ui/react-use-callback-ref": "2.1.0" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-use-focus-effect": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-focus-effect/-/react-use-focus-effect-2.1.0.tgz", - "integrity": "sha512-xzVboNy7J64xveLcxTIJ3jv+lUJKDwRM7Szwn9tNzUIPD94O3qwjV7DDCUzN2490nSYDF4OBMt/wuDBtaR3kUQ==", - "dependencies": { - "@chakra-ui/dom-utils": "2.1.0", - "@chakra-ui/react-use-event-listener": "2.1.0", - "@chakra-ui/react-use-safe-layout-effect": "2.1.0", - "@chakra-ui/react-use-update-effect": "2.1.0" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-use-focus-on-pointer-down": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-focus-on-pointer-down/-/react-use-focus-on-pointer-down-2.1.0.tgz", - "integrity": "sha512-2jzrUZ+aiCG/cfanrolsnSMDykCAbv9EK/4iUyZno6BYb3vziucmvgKuoXbMPAzWNtwUwtuMhkby8rc61Ue+Lg==", - "dependencies": { - "@chakra-ui/react-use-event-listener": "2.1.0" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-use-interval": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-interval/-/react-use-interval-2.1.0.tgz", - "integrity": "sha512-8iWj+I/+A0J08pgEXP1J1flcvhLBHkk0ln7ZvGIyXiEyM6XagOTJpwNhiu+Bmk59t3HoV/VyvyJTa+44sEApuw==", - "dependencies": { - "@chakra-ui/react-use-callback-ref": "2.1.0" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-use-latest-ref": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-latest-ref/-/react-use-latest-ref-2.1.0.tgz", - "integrity": "sha512-m0kxuIYqoYB0va9Z2aW4xP/5b7BzlDeWwyXCH6QpT2PpW3/281L3hLCm1G0eOUcdVlayqrQqOeD6Mglq+5/xoQ==", - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-use-merge-refs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-merge-refs/-/react-use-merge-refs-2.1.0.tgz", - "integrity": "sha512-lERa6AWF1cjEtWSGjxWTaSMvneccnAVH4V4ozh8SYiN9fSPZLlSG3kNxfNzdFvMEhM7dnP60vynF7WjGdTgQbQ==", - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-use-outside-click": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-outside-click/-/react-use-outside-click-2.2.0.tgz", - "integrity": "sha512-PNX+s/JEaMneijbgAM4iFL+f3m1ga9+6QK0E5Yh4s8KZJQ/bLwZzdhMz8J/+mL+XEXQ5J0N8ivZN28B82N1kNw==", - "dependencies": { - "@chakra-ui/react-use-callback-ref": "2.1.0" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-use-pan-event": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-pan-event/-/react-use-pan-event-2.1.0.tgz", - "integrity": "sha512-xmL2qOHiXqfcj0q7ZK5s9UjTh4Gz0/gL9jcWPA6GVf+A0Od5imEDa/Vz+533yQKWiNSm1QGrIj0eJAokc7O4fg==", - "dependencies": { - "@chakra-ui/event-utils": "2.0.8", - "@chakra-ui/react-use-latest-ref": "2.1.0", - "framesync": "6.1.2" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-use-previous": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-previous/-/react-use-previous-2.1.0.tgz", - "integrity": "sha512-pjxGwue1hX8AFcmjZ2XfrQtIJgqbTF3Qs1Dy3d1krC77dEsiCUbQ9GzOBfDc8pfd60DrB5N2tg5JyHbypqh0Sg==", - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-use-safe-layout-effect": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-safe-layout-effect/-/react-use-safe-layout-effect-2.1.0.tgz", - "integrity": "sha512-Knbrrx/bcPwVS1TorFdzrK/zWA8yuU/eaXDkNj24IrKoRlQrSBFarcgAEzlCHtzuhufP3OULPkELTzz91b0tCw==", - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-use-size": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-size/-/react-use-size-2.1.0.tgz", - "integrity": "sha512-tbLqrQhbnqOjzTaMlYytp7wY8BW1JpL78iG7Ru1DlV4EWGiAmXFGvtnEt9HftU0NJ0aJyjgymkxfVGI55/1Z4A==", - "dependencies": { - "@zag-js/element-size": "0.10.5" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-use-timeout": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-timeout/-/react-use-timeout-2.1.0.tgz", - "integrity": "sha512-cFN0sobKMM9hXUhyCofx3/Mjlzah6ADaEl/AXl5Y+GawB5rgedgAcu2ErAgarEkwvsKdP6c68CKjQ9dmTQlJxQ==", - "dependencies": { - "@chakra-ui/react-use-callback-ref": "2.1.0" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-use-update-effect": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-update-effect/-/react-use-update-effect-2.1.0.tgz", - "integrity": "sha512-ND4Q23tETaR2Qd3zwCKYOOS1dfssojPLJMLvUtUbW5M9uW1ejYWgGUobeAiOVfSplownG8QYMmHTP86p/v0lbA==", - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/react-utils": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-utils/-/react-utils-2.0.12.tgz", - "integrity": "sha512-GbSfVb283+YA3kA8w8xWmzbjNWk14uhNpntnipHCftBibl0lxtQ9YqMFQLwuFOO0U2gYVocszqqDWX+XNKq9hw==", - "dependencies": { - "@chakra-ui/utils": "2.0.15" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@chakra-ui/select": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/select/-/select-2.1.2.tgz", - "integrity": "sha512-ZwCb7LqKCVLJhru3DXvKXpZ7Pbu1TDZ7N0PdQ0Zj1oyVLJyrpef1u9HR5u0amOpqcH++Ugt0f5JSmirjNlctjA==", - "dependencies": { - "@chakra-ui/form-control": "2.2.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/shared-utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@chakra-ui/shared-utils/-/shared-utils-2.0.5.tgz", - "integrity": "sha512-4/Wur0FqDov7Y0nCXl7HbHzCg4aq86h+SXdoUeuCMD3dSj7dpsVnStLYhng1vxvlbUnLpdF4oz5Myt3i/a7N3Q==" - }, - "node_modules/@chakra-ui/skeleton": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/skeleton/-/skeleton-2.1.0.tgz", - "integrity": "sha512-JNRuMPpdZGd6zFVKjVQ0iusu3tXAdI29n4ZENYwAJEMf/fN0l12sVeirOxkJ7oEL0yOx2AgEYFSKdbcAgfUsAQ==", - "dependencies": { - "@chakra-ui/media-query": "3.3.0", - "@chakra-ui/react-use-previous": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/skip-nav": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/skip-nav/-/skip-nav-2.1.0.tgz", - "integrity": "sha512-Hk+FG+vadBSH0/7hwp9LJnLjkO0RPGnx7gBJWI4/SpoJf3e4tZlWYtwGj0toYY4aGKl93jVghuwGbDBEMoHDug==", - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/slider": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/slider/-/slider-2.1.0.tgz", - "integrity": "sha512-lUOBcLMCnFZiA/s2NONXhELJh6sY5WtbRykPtclGfynqqOo47lwWJx+VP7xaeuhDOPcWSSecWc9Y1BfPOCz9cQ==", - "dependencies": { - "@chakra-ui/number-utils": "2.0.7", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-callback-ref": "2.1.0", - "@chakra-ui/react-use-controllable-state": "2.1.0", - "@chakra-ui/react-use-latest-ref": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/react-use-pan-event": "2.1.0", - "@chakra-ui/react-use-size": "2.1.0", - "@chakra-ui/react-use-update-effect": "2.1.0" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/spinner": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/spinner/-/spinner-2.1.0.tgz", - "integrity": "sha512-hczbnoXt+MMv/d3gE+hjQhmkzLiKuoTo42YhUG7Bs9OSv2lg1fZHW1fGNRFP3wTi6OIbD044U1P9HK+AOgFH3g==", - "dependencies": { - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/stat": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/stat/-/stat-2.1.1.tgz", - "integrity": "sha512-LDn0d/LXQNbAn2KaR3F1zivsZCewY4Jsy1qShmfBMKwn6rI8yVlbvu6SiA3OpHS0FhxbsZxQI6HefEoIgtqY6Q==", - "dependencies": { - "@chakra-ui/icon": "3.2.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/stepper": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/stepper/-/stepper-2.3.1.tgz", - "integrity": "sha512-ky77lZbW60zYkSXhYz7kbItUpAQfEdycT0Q4bkHLxfqbuiGMf8OmgZOQkOB9uM4v0zPwy2HXhe0vq4Dd0xa55Q==", - "dependencies": { - "@chakra-ui/icon": "3.2.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/styled-system": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/styled-system/-/styled-system-2.9.2.tgz", - "integrity": "sha512-To/Z92oHpIE+4nk11uVMWqo2GGRS86coeMmjxtpnErmWRdLcp1WVCVRAvn+ZwpLiNR+reWFr2FFqJRsREuZdAg==", - "dependencies": { - "@chakra-ui/shared-utils": "2.0.5", - "csstype": "^3.1.2", - "lodash.mergewith": "4.6.2" - } - }, - "node_modules/@chakra-ui/switch": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/switch/-/switch-2.1.2.tgz", - "integrity": "sha512-pgmi/CC+E1v31FcnQhsSGjJnOE2OcND4cKPyTE+0F+bmGm48Q/b5UmKD9Y+CmZsrt/7V3h8KNczowupfuBfIHA==", - "dependencies": { - "@chakra-ui/checkbox": "2.3.2", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "framer-motion": ">=4.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/system": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/system/-/system-2.6.2.tgz", - "integrity": "sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==", - "dependencies": { - "@chakra-ui/color-mode": "2.2.0", - "@chakra-ui/object-utils": "2.1.0", - "@chakra-ui/react-utils": "2.0.12", - "@chakra-ui/styled-system": "2.9.2", - "@chakra-ui/theme-utils": "2.0.21", - "@chakra-ui/utils": "2.0.15", - "react-fast-compare": "3.2.2" - }, - "peerDependencies": { - "@emotion/react": "^11.0.0", - "@emotion/styled": "^11.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/table": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/table/-/table-2.1.0.tgz", - "integrity": "sha512-o5OrjoHCh5uCLdiUb0Oc0vq9rIAeHSIRScc2ExTC9Qg/uVZl2ygLrjToCaKfaaKl1oQexIeAcZDKvPG8tVkHyQ==", - "dependencies": { - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/tabs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/tabs/-/tabs-3.0.0.tgz", - "integrity": "sha512-6Mlclp8L9lqXmsGWF5q5gmemZXOiOYuh0SGT/7PgJVNPz3LXREXlXg2an4MBUD8W5oTkduCX+3KTMCwRrVrDYw==", - "dependencies": { - "@chakra-ui/clickable": "2.1.0", - "@chakra-ui/descendant": "3.1.0", - "@chakra-ui/lazy-utils": "2.0.5", - "@chakra-ui/react-children-utils": "2.0.6", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-use-controllable-state": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/react-use-safe-layout-effect": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/tag": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/tag/-/tag-3.1.1.tgz", - "integrity": "sha512-Bdel79Dv86Hnge2PKOU+t8H28nm/7Y3cKd4Kfk9k3lOpUh4+nkSGe58dhRzht59lEqa4N9waCgQiBdkydjvBXQ==", - "dependencies": { - "@chakra-ui/icon": "3.2.0", - "@chakra-ui/react-context": "2.1.0" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/textarea": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/textarea/-/textarea-2.1.2.tgz", - "integrity": "sha512-ip7tvklVCZUb2fOHDb23qPy/Fr2mzDOGdkrpbNi50hDCiV4hFX02jdQJdi3ydHZUyVgZVBKPOJ+lT9i7sKA2wA==", - "dependencies": { - "@chakra-ui/form-control": "2.2.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/theme": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/theme/-/theme-3.3.1.tgz", - "integrity": "sha512-Hft/VaT8GYnItGCBbgWd75ICrIrIFrR7lVOhV/dQnqtfGqsVDlrztbSErvMkoPKt0UgAkd9/o44jmZ6X4U2nZQ==", - "dependencies": { - "@chakra-ui/anatomy": "2.2.2", - "@chakra-ui/shared-utils": "2.0.5", - "@chakra-ui/theme-tools": "2.1.2" - }, - "peerDependencies": { - "@chakra-ui/styled-system": ">=2.8.0" - } - }, - "node_modules/@chakra-ui/theme-tools": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/theme-tools/-/theme-tools-2.1.2.tgz", - "integrity": "sha512-Qdj8ajF9kxY4gLrq7gA+Azp8CtFHGO9tWMN2wfF9aQNgG9AuMhPrUzMq9AMQ0MXiYcgNq/FD3eegB43nHVmXVA==", - "dependencies": { - "@chakra-ui/anatomy": "2.2.2", - "@chakra-ui/shared-utils": "2.0.5", - "color2k": "^2.0.2" - }, - "peerDependencies": { - "@chakra-ui/styled-system": ">=2.0.0" - } - }, - "node_modules/@chakra-ui/theme-utils": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/@chakra-ui/theme-utils/-/theme-utils-2.0.21.tgz", - "integrity": "sha512-FjH5LJbT794r0+VSCXB3lT4aubI24bLLRWB+CuRKHijRvsOg717bRdUN/N1fEmEpFnRVrbewttWh/OQs0EWpWw==", - "dependencies": { - "@chakra-ui/shared-utils": "2.0.5", - "@chakra-ui/styled-system": "2.9.2", - "@chakra-ui/theme": "3.3.1", - "lodash.mergewith": "4.6.2" - } - }, - "node_modules/@chakra-ui/toast": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/toast/-/toast-7.0.2.tgz", - "integrity": "sha512-yvRP8jFKRs/YnkuE41BVTq9nB2v/KDRmje9u6dgDmE5+1bFt3bwjdf9gVbif4u5Ve7F7BGk5E093ARRVtvLvXA==", - "dependencies": { - "@chakra-ui/alert": "2.2.2", - "@chakra-ui/close-button": "2.1.1", - "@chakra-ui/portal": "2.1.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-use-timeout": "2.1.0", - "@chakra-ui/react-use-update-effect": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5", - "@chakra-ui/styled-system": "2.9.2", - "@chakra-ui/theme": "3.3.1" - }, - "peerDependencies": { - "@chakra-ui/system": "2.6.2", - "framer-motion": ">=4.0.0", - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/@chakra-ui/tooltip": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/tooltip/-/tooltip-2.3.1.tgz", - "integrity": "sha512-Rh39GBn/bL4kZpuEMPPRwYNnccRCL+w9OqamWHIB3Qboxs6h8cOyXfIdGxjo72lvhu1QI/a4KFqkM3St+WfC0A==", - "dependencies": { - "@chakra-ui/dom-utils": "2.1.0", - "@chakra-ui/popper": "3.1.0", - "@chakra-ui/portal": "2.1.0", - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-disclosure": "2.1.0", - "@chakra-ui/react-use-event-listener": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "framer-motion": ">=4.0.0", - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/@chakra-ui/transition": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/transition/-/transition-2.1.0.tgz", - "integrity": "sha512-orkT6T/Dt+/+kVwJNy7zwJ+U2xAZ3EU7M3XCs45RBvUnZDr/u9vdmaM/3D/rOpmQJWgQBwKPJleUXrYWUagEDQ==", - "dependencies": { - "@chakra-ui/shared-utils": "2.0.5" - }, - "peerDependencies": { - "framer-motion": ">=4.0.0", - "react": ">=18" - } - }, - "node_modules/@chakra-ui/utils": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/@chakra-ui/utils/-/utils-2.0.15.tgz", - "integrity": "sha512-El4+jL0WSaYYs+rJbuYFDbjmfCcfGDmRY95GO4xwzit6YAPZBLcR65rOEwLps+XWluZTy1xdMrusg/hW0c1aAA==", - "dependencies": { - "@types/lodash.mergewith": "4.6.7", - "css-box-model": "1.2.1", - "framesync": "6.1.2", - "lodash.mergewith": "4.6.2" - } - }, - "node_modules/@chakra-ui/visually-hidden": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/visually-hidden/-/visually-hidden-2.2.0.tgz", - "integrity": "sha512-KmKDg01SrQ7VbTD3+cPWf/UfpF5MSwm3v7MWi0n5t8HnnadT13MF0MJCDSXbBWnzLv1ZKJ6zlyAOeARWX+DpjQ==", - "peerDependencies": { - "@chakra-ui/system": ">=2.0.0", - "react": ">=18" - } - }, - "node_modules/@emotion/babel-plugin": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", - "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.1", - "@emotion/memoize": "^0.8.1", - "@emotion/serialize": "^1.1.2", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/babel-plugin/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@emotion/cache": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", - "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", - "dependencies": { - "@emotion/memoize": "^0.8.1", - "@emotion/sheet": "^1.2.2", - "@emotion/utils": "^1.2.1", - "@emotion/weak-memoize": "^0.3.1", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", - "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", - "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", - "dependencies": { - "@emotion/memoize": "^0.8.1" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" - }, - "node_modules/@emotion/react": { - "version": "11.11.3", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.3.tgz", - "integrity": "sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.11.0", - "@emotion/cache": "^11.11.0", - "@emotion/serialize": "^1.1.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", - "@emotion/utils": "^1.2.1", - "@emotion/weak-memoize": "^0.3.1", - "hoist-non-react-statics": "^3.3.1" - }, - "peerDependencies": { - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/serialize": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz", - "integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==", - "dependencies": { - "@emotion/hash": "^0.9.1", - "@emotion/memoize": "^0.8.1", - "@emotion/unitless": "^0.8.1", - "@emotion/utils": "^1.2.1", - "csstype": "^3.0.2" - } - }, - "node_modules/@emotion/sheet": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", - "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" - }, - "node_modules/@emotion/styled": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", - "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.11.0", - "@emotion/is-prop-valid": "^1.2.1", - "@emotion/serialize": "^1.1.2", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", - "@emotion/utils": "^1.2.1" - }, - "peerDependencies": { - "@emotion/react": "^11.0.0-rc.0", - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/unitless": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" - }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", - "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@emotion/utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", - "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", - "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" - }, - "node_modules/@esbuild/android-arm": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.8.tgz", - "integrity": "sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.8.tgz", - "integrity": "sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.8.tgz", - "integrity": "sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.8.tgz", - "integrity": "sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.8.tgz", - "integrity": "sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.8.tgz", - "integrity": "sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.8.tgz", - "integrity": "sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.8.tgz", - "integrity": "sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.8.tgz", - "integrity": "sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.8.tgz", - "integrity": "sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.8.tgz", - "integrity": "sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.8.tgz", - "integrity": "sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.8.tgz", - "integrity": "sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.8.tgz", - "integrity": "sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.8.tgz", - "integrity": "sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.8.tgz", - "integrity": "sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.8.tgz", - "integrity": "sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.8.tgz", - "integrity": "sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.8.tgz", - "integrity": "sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.8.tgz", - "integrity": "sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.8.tgz", - "integrity": "sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.8.tgz", - "integrity": "sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@hey-api/openapi-ts": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.34.1.tgz", - "integrity": "sha512-7Ak+0nvf4Nhzk04tXGg6h4eM7lnWRgfjCPmMl2MyXrhS5urxd3Bg/PhtpB84u18wnwcM4rIeCUlTwDDQ/OB3NQ==", - "dev": true, - "dependencies": { - "@apidevtools/json-schema-ref-parser": "11.5.4", - "camelcase": "8.0.0", - "commander": "12.0.0", - "handlebars": "4.7.8" - }, - "bin": { - "openapi-ts": "bin/index.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/@hey-api/openapi-ts/node_modules/@apidevtools/json-schema-ref-parser": { - "version": "11.5.4", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.5.4.tgz", - "integrity": "sha512-o2fsypTGU0WxRxbax8zQoHiIB4dyrkwYfcm8TxZ+bx9pCzcWZbQtiMqpgBvWA/nJ2TrGjK5adCLfTH8wUeU/Wg==", - "dev": true, - "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.15", - "js-yaml": "^4.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/philsturgeon" - } - }, - "node_modules/@hey-api/openapi-ts/node_modules/camelcase": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", - "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@hey-api/openapi-ts/node_modules/commander": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", - "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "dev": true - }, - "node_modules/@playwright/test": { - "version": "1.45.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.2.tgz", - "integrity": "sha512-JxG9eq92ET75EbVi3s+4sYbcG7q72ECeZNbdBlaMkGcNbiDQ4cAi8U2QP5oKkOx+1gpaiL1LDStmzCaEM1Z6fQ==", - "dev": true, - "dependencies": { - "playwright": "1.45.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.6.1.tgz", - "integrity": "sha512-0WQ0ouLejaUCRsL93GD4uft3rOmB8qoQMU05Kb8CmMtMBe7XUDLAltxVZI1q6byNqEtU7N1ZX1Vw5lIpgulLQA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.6.1.tgz", - "integrity": "sha512-1TKm25Rn20vr5aTGGZqo6E4mzPicCUD79k17EgTLAsXc1zysyi4xXKACfUbwyANEPAEIxkzwue6JZ+stYzWUTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.6.1.tgz", - "integrity": "sha512-cEXJQY/ZqMACb+nxzDeX9IPLAg7S94xouJJCNVE5BJM8JUEP4HeTF+ti3cmxWeSJo+5D+o8Tc0UAWUkfENdeyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.6.1.tgz", - "integrity": "sha512-LoSU9Xu56isrkV2jLldcKspJ7sSXmZWkAxg7sW/RfF7GS4F5/v4EiqKSMCFbZtDu2Nc1gxxFdQdKwkKS4rwxNg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.6.1.tgz", - "integrity": "sha512-EfI3hzYAy5vFNDqpXsNxXcgRDcFHUWSx5nnRSCKwXuQlI5J9dD84g2Usw81n3FLBNsGCegKGwwTVsSKK9cooSQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.6.1.tgz", - "integrity": "sha512-9lhc4UZstsegbNLhH0Zu6TqvDfmhGzuCWtcTFXY10VjLLUe4Mr0Ye2L3rrtHaDd/J5+tFMEuo5LTCSCMXWfUKw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.6.1.tgz", - "integrity": "sha512-FfoOK1yP5ksX3wwZ4Zk1NgyGHZyuRhf99j64I5oEmirV8EFT7+OhUZEnP+x17lcP/QHJNWGsoJwrz4PJ9fBEXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.6.1.tgz", - "integrity": "sha512-DNGZvZDO5YF7jN5fX8ZqmGLjZEXIJRdJEdTFMhiyXqyXubBa0WVLDWSNlQ5JR2PNgDbEV1VQowhVRUh+74D+RA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.6.1.tgz", - "integrity": "sha512-RkJVNVRM+piYy87HrKmhbexCHg3A6Z6MU0W9GHnJwBQNBeyhCJG9KDce4SAMdicQnpURggSvtbGo9xAWOfSvIQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.6.1.tgz", - "integrity": "sha512-v2FVT6xfnnmTe3W9bJXl6r5KwJglMK/iRlkKiIFfO6ysKs0rDgz7Cwwf3tjldxQUrHL9INT/1r4VA0n9L/F1vQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.6.1.tgz", - "integrity": "sha512-YEeOjxRyEjqcWphH9dyLbzgkF8wZSKAKUkldRY6dgNR5oKs2LZazqGB41cWJ4Iqqcy9/zqYgmzBkRoVz3Q9MLw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.6.1.tgz", - "integrity": "sha512-0zfTlFAIhgz8V2G8STq8toAjsYYA6eci1hnXuyOTUFnymrtJwnS6uGKiv3v5UrPZkBlamLvrLV2iiaeqCKzb0A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@swc/core": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.100.tgz", - "integrity": "sha512-7dKgTyxJjlrMwFZYb1auj3Xq0D8ZBe+5oeIgfMlRU05doXZypYJe0LAk0yjj3WdbwYzpF+T1PLxwTWizI0pckw==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "@swc/counter": "^0.1.1", - "@swc/types": "^0.1.5" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.3.100", - "@swc/core-darwin-x64": "1.3.100", - "@swc/core-linux-arm64-gnu": "1.3.100", - "@swc/core-linux-arm64-musl": "1.3.100", - "@swc/core-linux-x64-gnu": "1.3.100", - "@swc/core-linux-x64-musl": "1.3.100", - "@swc/core-win32-arm64-msvc": "1.3.100", - "@swc/core-win32-ia32-msvc": "1.3.100", - "@swc/core-win32-x64-msvc": "1.3.100" - }, - "peerDependencies": { - "@swc/helpers": "^0.5.0" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.100.tgz", - "integrity": "sha512-XVWFsKe6ei+SsDbwmsuRkYck1SXRpO60Hioa4hoLwR8fxbA9eVp6enZtMxzVVMBi8ej5seZ4HZQeAWepbukiBw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.100.tgz", - "integrity": "sha512-KF/MXrnH1nakm1wbt4XV8FS7kvqD9TGmVxeJ0U4bbvxXMvzeYUurzg3AJUTXYmXDhH/VXOYJE5N5RkwZZPs5iA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.100.tgz", - "integrity": "sha512-p8hikNnAEJrw5vHCtKiFT4hdlQxk1V7vqPmvUDgL/qe2menQDK/i12tbz7/3BEQ4UqUPnvwpmVn2d19RdEMNxw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.100.tgz", - "integrity": "sha512-BWx/0EeY89WC4q3AaIaBSGfQxkYxIlS3mX19dwy2FWJs/O+fMvF9oLk/CyJPOZzbp+1DjGeeoGFuDYpiNO91JA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.100.tgz", - "integrity": "sha512-XUdGu3dxAkjsahLYnm8WijPfKebo+jHgHphDxaW0ovI6sTdmEGFDew7QzKZRlbYL2jRkUuuKuDGvD6lO5frmhA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.100.tgz", - "integrity": "sha512-PhoXKf+f0OaNW/GCuXjJ0/KfK9EJX7z2gko+7nVnEA0p3aaPtbP6cq1Ubbl6CMoPL+Ci3gZ7nYumDqXNc3CtLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.100.tgz", - "integrity": "sha512-PwLADZN6F9cXn4Jw52FeP/MCLVHm8vwouZZSOoOScDtihjY495SSjdPnlosMaRSR4wJQssGwiD/4MbpgQPqbAw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.100.tgz", - "integrity": "sha512-0f6nicKSLlDKlyPRl2JEmkpBV4aeDfRQg6n8mPqgL7bliZIcDahG0ej+HxgNjZfS3e0yjDxsNRa6sAqWU2Z60A==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.100.tgz", - "integrity": "sha512-b7J0rPoMkRTa3XyUGt8PwCaIBuYWsL2DqbirrQKRESzgCvif5iNpqaM6kjIjI/5y5q1Ycv564CB51YDpiS8EtQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", - "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==", - "dev": true - }, - "node_modules/@swc/types": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", - "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", - "dev": true - }, - "node_modules/@tanstack/history": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.15.13.tgz", - "integrity": "sha512-ToaeMtK5S4YaxCywAlYexc7KPFN0esjyTZ4vXzJhXEWAkro9iHgh7m/4ozPJb7oTo65WkHWX0W9GjcZbInSD8w==", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/query-core": { - "version": "5.28.13", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.28.13.tgz", - "integrity": "sha512-C3+CCOcza+mrZ7LglQbjeYEOTEC3LV0VN0eYaIN6GvqAZ8Foegdgch7n6QYPtT4FuLae5ALy+m+ZMEKpD6tMCQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/query-devtools": { - "version": "5.28.10", - "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.28.10.tgz", - "integrity": "sha512-5UN629fKa5/1K/2Pd26gaU7epxRrYiT1gy+V+pW5K6hnf1DeUKK3pANSb2eHKlecjIKIhTwyF7k9XdyE2gREvQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.28.14", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.28.14.tgz", - "integrity": "sha512-cZqt03Igb3I9tM72qNX5TAAmeYl75Z+k4Mv92VkXIXc2hCrv0fIywd7GN3JV1BBJl4mr7Cc+OOKKOPy8sNVOkA==", - "dependencies": { - "@tanstack/query-core": "5.28.13" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18.0.0" - } - }, - "node_modules/@tanstack/react-query-devtools": { - "version": "5.28.14", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.28.14.tgz", - "integrity": "sha512-4CrFBI1O5wibV1ZdGAnBMmTuc7SiShhxWubxRMyIloeEioxs3DQkFbouGBea5nexuwIxAkvhUB8khpPnNjhxMw==", - "dependencies": { - "@tanstack/query-devtools": "5.28.10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "@tanstack/react-query": "^5.28.14", - "react": "^18.0.0" - } - }, - "node_modules/@tanstack/react-router": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.19.1.tgz", - "integrity": "sha512-a4Xf074qo2fQLmSi8PTncEFn8XakaH3+DT7Dted4OPClzQFS+c6yU3HONVNAsuYWZ7lDK1HMKoHPDFbnHPEWvA==", - "dependencies": { - "@tanstack/history": "1.15.13", - "@tanstack/react-store": "^0.2.1", - "tiny-invariant": "^1.3.1", - "tiny-warning": "^1.0.3" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": ">=16", - "react-dom": ">=16" - } - }, - "node_modules/@tanstack/react-store": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.2.1.tgz", - "integrity": "sha512-tEbMCQjbeVw9KOP/202LfqZMSNAVi6zYkkp1kBom8nFuMx/965Hzes3+6G6b/comCwVxoJU8Gg9IrcF8yRPthw==", - "dependencies": { - "@tanstack/store": "0.1.3", - "use-sync-external-store": "^1.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": ">=16", - "react-dom": ">=16" - } - }, - "node_modules/@tanstack/router-devtools": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@tanstack/router-devtools/-/router-devtools-1.19.1.tgz", - "integrity": "sha512-l560JHnffcDccSTo/sOtB+gKvtgaWYpOKOu9MyvswN9XB2pt752UFFIN1Yt/Gsp2Iooq/FcYlYnEPHb4GFzalg==", - "dev": true, - "dependencies": { - "@tanstack/react-router": "1.19.1", - "clsx": "^2.1.0", - "date-fns": "^2.29.1", - "goober": "^2.1.14" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": ">=16", - "react-dom": ">=16" - } - }, - "node_modules/@tanstack/router-generator": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.19.0.tgz", - "integrity": "sha512-vFF8Q7SdyygiYC7lfJ83GRif0vcxjak9SAcgtX/w7TLR0O+qdxRXFPvhKTQQXH6vVezy5Au9bSaSI2EgDD1ubA==", - "dev": true, - "dependencies": { - "prettier": "^3.1.1", - "zod": "^3.22.4" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/router-vite-plugin": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@tanstack/router-vite-plugin/-/router-vite-plugin-1.19.0.tgz", - "integrity": "sha512-yvvQnJ7JvqsnxAFqwiHhNTV2n1jKkidjc+XbgS2aNnEHC0aHnYH2ygPlmmfiVD7PMO7x64PdI5e12TzY/aKoFA==", - "dev": true, - "dependencies": { - "@tanstack/router-generator": "1.19.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/store": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.1.3.tgz", - "integrity": "sha512-GnolmC8Fr4mvsHE1fGQmR3Nm0eBO3KnZjDU0a+P3TeQNM/dDscFGxtA7p31NplQNW3KwBw4t1RVFmz0VeKLxcw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "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 - }, - "node_modules/@types/lodash": { - "version": "4.14.202", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", - "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" - }, - "node_modules/@types/lodash.mergewith": { - "version": "4.6.7", - "resolved": "https://registry.npmjs.org/@types/lodash.mergewith/-/lodash.mergewith-4.6.7.tgz", - "integrity": "sha512-3m+lkO5CLRRYU0fhGRp7zbsGi6+BZj0uTVSwvcKU+nSlhjA9/QRNfuSGnD2mX6hQA7ZbmcCkzk5h4ZYGOtk14A==", - "dependencies": { - "@types/lodash": "*" - } - }, - "node_modules/@types/node": { - "version": "20.10.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", - "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", - "dev": true, - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" - }, - "node_modules/@types/prop-types": { - "version": "15.7.11", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", - "devOptional": true - }, - "node_modules/@types/react": { - "version": "18.2.39", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz", - "integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==", - "devOptional": true, - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.2.17", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz", - "integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "devOptional": true - }, - "node_modules/@vitejs/plugin-react-swc": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.5.0.tgz", - "integrity": "sha512-1PrOvAaDpqlCV+Up8RkAh9qaiUjoDUcjtttyhXDKw53XA6Ve16SOp6cCOpRs8Dj8DqUQs6eTW5YkLcLJjrXAig==", - "dev": true, - "dependencies": { - "@swc/core": "^1.3.96" - }, - "peerDependencies": { - "vite": "^4 || ^5" - } - }, - "node_modules/@zag-js/dom-query": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@zag-js/dom-query/-/dom-query-0.16.0.tgz", - "integrity": "sha512-Oqhd6+biWyKnhKwFFuZrrf6lxBz2tX2pRQe6grUnYwO6HJ8BcbqZomy2lpOdr+3itlaUqx+Ywj5E5ZZDr/LBfQ==" - }, - "node_modules/@zag-js/element-size": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/@zag-js/element-size/-/element-size-0.10.5.tgz", - "integrity": "sha512-uQre5IidULANvVkNOBQ1tfgwTQcGl4hliPSe69Fct1VfYb2Fd0jdAcGzqQgPhfrXFpR62MxLPB7erxJ/ngtL8w==" - }, - "node_modules/@zag-js/focus-visible": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@zag-js/focus-visible/-/focus-visible-0.16.0.tgz", - "integrity": "sha512-a7U/HSopvQbrDU4GLerpqiMcHKEkQkNPeDZJWz38cw/6Upunh41GjHetq5TB84hxyCaDzJ6q2nEdNoBQfC0FKA==", - "dependencies": { - "@zag-js/dom-query": "0.16.0" - } - }, - "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 - }, - "node_modules/aria-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", - "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/color2k": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/color2k/-/color2k-2.0.3.tgz", - "integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/compute-scroll-into-view": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.0.3.tgz", - "integrity": "sha512-nadqwNxghAGTamwIqQSG433W6OADZx2vCo3UXHNrzTRHK/htu+7+L0zhjEoaeaQVNAi3YgqWDv8+tzf0hRfR+A==" - }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, - "node_modules/copy-to-clipboard": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", - "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", - "dependencies": { - "toggle-selection": "^1.0.6" - } - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/css-box-model": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", - "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", - "dependencies": { - "tiny-invariant": "^1.0.6" - } - }, - "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" - }, - "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" - }, - "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/esbuild": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.8.tgz", - "integrity": "sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.19.8", - "@esbuild/android-arm64": "0.19.8", - "@esbuild/android-x64": "0.19.8", - "@esbuild/darwin-arm64": "0.19.8", - "@esbuild/darwin-x64": "0.19.8", - "@esbuild/freebsd-arm64": "0.19.8", - "@esbuild/freebsd-x64": "0.19.8", - "@esbuild/linux-arm": "0.19.8", - "@esbuild/linux-arm64": "0.19.8", - "@esbuild/linux-ia32": "0.19.8", - "@esbuild/linux-loong64": "0.19.8", - "@esbuild/linux-mips64el": "0.19.8", - "@esbuild/linux-ppc64": "0.19.8", - "@esbuild/linux-riscv64": "0.19.8", - "@esbuild/linux-s390x": "0.19.8", - "@esbuild/linux-x64": "0.19.8", - "@esbuild/netbsd-x64": "0.19.8", - "@esbuild/openbsd-x64": "0.19.8", - "@esbuild/sunos-x64": "0.19.8", - "@esbuild/win32-arm64": "0.19.8", - "@esbuild/win32-ia32": "0.19.8", - "@esbuild/win32-x64": "0.19.8" - } - }, - "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==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" - }, - "node_modules/focus-lock": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-1.3.3.tgz", - "integrity": "sha512-hfXkZha7Xt4RQtrL1HBfspAuIj89Y0fb6GX0dfJilb8S2G/lvL4akPAcHq6xoD2NuZnDMCnZL/zQesMyeu6Psg==", - "dependencies": { - "tslib": "^2.0.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/framer-motion": { - "version": "10.16.16", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.16.16.tgz", - "integrity": "sha512-je6j91rd7NmUX7L1XHouwJ4v3R+SO4umso2LUcgOct3rHZ0PajZ80ETYZTajzEXEl9DlKyzjyt4AvGQ+lrebOw==", - "dependencies": { - "tslib": "^2.4.0" - }, - "optionalDependencies": { - "@emotion/is-prop-valid": "^0.8.2" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/framer-motion/node_modules/@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "optional": true, - "dependencies": { - "@emotion/memoize": "0.7.4" - } - }, - "node_modules/framer-motion/node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "optional": true - }, - "node_modules/framesync": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.1.2.tgz", - "integrity": "sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==", - "dependencies": { - "tslib": "2.4.0" - } - }, - "node_modules/framesync/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, - "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, - "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==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "engines": { - "node": ">=6" - } - }, - "node_modules/goober": { - "version": "2.1.14", - "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz", - "integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==", - "dev": true, - "peerDependencies": { - "csstype": "^3.0.10" - } - }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/hasown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "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, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "node_modules/lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "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==", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/playwright": { - "version": "1.45.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.2.tgz", - "integrity": "sha512-ReywF2t/0teRvNBpfIgh5e4wnrI/8Su8ssdo5XsQKpjxJj+jspm00jSoz9BTg91TT0c9HRjXO7LBNVrgYj9X0g==", - "dev": true, - "dependencies": { - "playwright-core": "1.45.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.45.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.2.tgz", - "integrity": "sha512-ha175tAWb0dTK0X4orvBIqi3jGEt701SMxMhyujxNrgd8K0Uy5wMSwwcQHtyB4om7INUkfndx02XnQ2p6dvLDw==", - "dev": true, - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/playwright/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, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", - "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" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-clientside-effect": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz", - "integrity": "sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==", - "dependencies": { - "@babel/runtime": "^7.12.13" - }, - "peerDependencies": { - "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, - "node_modules/react-error-boundary": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz", - "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "peerDependencies": { - "react": ">=16.13.1" - } - }, - "node_modules/react-fast-compare": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", - "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" - }, - "node_modules/react-focus-lock": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.11.1.tgz", - "integrity": "sha512-IXLwnTBrLTlKTpASZXqqXJ8oymWrgAlOfuuDYN4XCuN1YJ72dwX198UCaF1QqGUk5C3QOnlMik//n3ufcfe8Ig==", - "dependencies": { - "@babel/runtime": "^7.0.0", - "focus-lock": "^1.3.2", - "prop-types": "^15.6.2", - "react-clientside-effect": "^1.2.6", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-hook-form": { - "version": "7.49.3", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.3.tgz", - "integrity": "sha512-foD6r3juidAT1cOZzpmD/gOKt7fRsDhXXZ0y28+Al1CHgX+AY1qIN9VSIIItXRq1dN68QrRwl1ORFlwjBaAqeQ==", - "engines": { - "node": ">=18", - "pnpm": "8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-hook-form" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18" - } - }, - "node_modules/react-icons": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz", - "integrity": "sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==", - "peerDependencies": { - "react": "*" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/react-remove-scroll": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", - "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", - "dependencies": { - "react-remove-scroll-bar": "^2.3.4", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.5.tgz", - "integrity": "sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==", - "dependencies": { - "react-style-singleton": "^2.2.1", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", - "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", - "dependencies": { - "get-nonce": "^1.0.0", - "invariant": "^2.2.4", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "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==", - "engines": { - "node": ">=4" - } - }, - "node_modules/rollup": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.6.1.tgz", - "integrity": "sha512-jZHaZotEHQaHLgKr8JnQiDT1rmatjgKlMekyksz+yk9jt/8z9quNjnKNRoaM0wd9DC2QKXjmWWuDYtM3jfF8pQ==", - "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.6.1", - "@rollup/rollup-android-arm64": "4.6.1", - "@rollup/rollup-darwin-arm64": "4.6.1", - "@rollup/rollup-darwin-x64": "4.6.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.6.1", - "@rollup/rollup-linux-arm64-gnu": "4.6.1", - "@rollup/rollup-linux-arm64-musl": "4.6.1", - "@rollup/rollup-linux-x64-gnu": "4.6.1", - "@rollup/rollup-linux-x64-musl": "4.6.1", - "@rollup/rollup-win32-arm64-msvc": "4.6.1", - "@rollup/rollup-win32-ia32-msvc": "4.6.1", - "@rollup/rollup-win32-x64-msvc": "4.6.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" - }, - "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==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" - }, - "node_modules/tiny-warning": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "engines": { - "node": ">=4" - } - }, - "node_modules/toggle-selection": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" - }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, - "node_modules/typescript": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", - "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", - "dev": true, - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true - }, - "node_modules/use-callback-ref": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.1.tgz", - "integrity": "sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", - "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/vite": { - "version": "5.0.13", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.13.tgz", - "integrity": "sha512-/9ovhv2M2dGTuA+dY93B9trfyWMDRQw2jdVBhHNP6wr0oF34wG2i/N55801iZIpgUpnHDm4F/FabGQLyc+eOgg==", - "dev": true, - "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.32", - "rollup": "^4.2.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.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", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", - "requires": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "requires": { - "@babel/types": "^7.22.15" - } - }, - "@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==" - }, - "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" - }, - "@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", - "requires": { - "regenerator-runtime": "^0.14.0" - } - }, - "@babel/types": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", - "requires": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - } - }, - "@biomejs/biome": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.6.1.tgz", - "integrity": "sha512-SILQvA2S0XeaOuu1bivv6fQmMo7zMfr2xqDEN+Sz78pGbAKZnGmg0emsXjQWoBY/RVm9kPCgX+aGEpZZTYaM7w==", - "dev": true, - "requires": { - "@biomejs/cli-darwin-arm64": "1.6.1", - "@biomejs/cli-darwin-x64": "1.6.1", - "@biomejs/cli-linux-arm64": "1.6.1", - "@biomejs/cli-linux-arm64-musl": "1.6.1", - "@biomejs/cli-linux-x64": "1.6.1", - "@biomejs/cli-linux-x64-musl": "1.6.1", - "@biomejs/cli-win32-arm64": "1.6.1", - "@biomejs/cli-win32-x64": "1.6.1" - } - }, - "@biomejs/cli-darwin-arm64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.6.1.tgz", - "integrity": "sha512-KlvY00iB9T/vFi4m/GXxEyYkYnYy6aw06uapzUIIdiMMj7I/pmZu7CsZlzWdekVD0j+SsQbxdZMsb0wPhnRSsg==", - "dev": true, - "optional": true - }, - "@biomejs/cli-darwin-x64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.6.1.tgz", - "integrity": "sha512-jP4E8TXaQX5e3nvRJSzB+qicZrdIDCrjR0sSb1DaDTx4JPZH5WXq/BlTqAyWi3IijM+IYMjWqAAK4kOHsSCzxw==", - "dev": true, - "optional": true - }, - "@biomejs/cli-linux-arm64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.6.1.tgz", - "integrity": "sha512-nxD1UyX3bWSl/RSKlib/JsOmt+652/9yieogdSC/UTLgVCZYOF7u8L/LK7kAa0Y4nA8zSPavAQTgko7mHC2ObA==", - "dev": true, - "optional": true - }, - "@biomejs/cli-linux-arm64-musl": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.6.1.tgz", - "integrity": "sha512-YdkDgFecdHJg7PJxAMaZIixVWGB6St4yH08BHagO0fEhNNiY8cAKEVo2mcXlsnEiTMpeSEAY9VxLUrVT3IVxpw==", - "dev": true, - "optional": true - }, - "@biomejs/cli-linux-x64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.6.1.tgz", - "integrity": "sha512-BYAzenlMF3QdngjNFw9QVBXKGNzeecqwF3pwDgUGEvU7OJpn1/lyVkJVxYPtVGRNdjQ9e6l/s8NjKuBpW/ZR4Q==", - "dev": true, - "optional": true - }, - "@biomejs/cli-linux-x64-musl": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.6.1.tgz", - "integrity": "sha512-aSISIDmxq04NNy7tm4x9rBk2vH0ub2VDIE4outEmdC2LBtEJoINiphlZagx/FvjbsqUfygent9QUSn0oREnAXg==", - "dev": true, - "optional": true - }, - "@biomejs/cli-win32-arm64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.6.1.tgz", - "integrity": "sha512-/eCHQKZ1kEawUpkSuXq4urtxMsD1P1678OPG3zNKt3ru16AqqspLdO3jzBe3k74xCPYnQ36e9Yqc97Mo0qgPtg==", - "dev": true, - "optional": true - }, - "@biomejs/cli-win32-x64": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.6.1.tgz", - "integrity": "sha512-5TUZbzBwnDLFxLVGEPsorNi6eC2Gt+z4Oei9Qvq0M/4c4/mjZ96ABgwao/tMxf4ZBr/qyy2YdvF+gX9Rc+xC0A==", - "dev": true, - "optional": true - }, - "@chakra-ui/accordion": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/accordion/-/accordion-2.3.1.tgz", - "integrity": "sha512-FSXRm8iClFyU+gVaXisOSEw0/4Q+qZbFRiuhIAkVU6Boj0FxAMrlo9a8AV5TuF77rgaHytCdHk0Ng+cyUijrag==", - "requires": { - "@chakra-ui/descendant": "3.1.0", - "@chakra-ui/icon": "3.2.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-use-controllable-state": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5", - "@chakra-ui/transition": "2.1.0" - } - }, - "@chakra-ui/alert": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/alert/-/alert-2.2.2.tgz", - "integrity": "sha512-jHg4LYMRNOJH830ViLuicjb3F+v6iriE/2G5T+Sd0Hna04nukNJ1MxUmBPE+vI22me2dIflfelu2v9wdB6Pojw==", - "requires": { - "@chakra-ui/icon": "3.2.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5", - "@chakra-ui/spinner": "2.1.0" - } - }, - "@chakra-ui/anatomy": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/anatomy/-/anatomy-2.2.2.tgz", - "integrity": "sha512-MV6D4VLRIHr4PkW4zMyqfrNS1mPlCTiCXwvYGtDFQYr+xHFfonhAuf9WjsSc0nyp2m0OdkSLnzmVKkZFLo25Tg==" - }, - "@chakra-ui/avatar": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/avatar/-/avatar-2.3.0.tgz", - "integrity": "sha512-8gKSyLfygnaotbJbDMHDiJoF38OHXUYVme4gGxZ1fLnQEdPVEaIWfH+NndIjOM0z8S+YEFnT9KyGMUtvPrBk3g==", - "requires": { - "@chakra-ui/image": "2.1.0", - "@chakra-ui/react-children-utils": "2.0.6", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/breadcrumb": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/breadcrumb/-/breadcrumb-2.2.0.tgz", - "integrity": "sha512-4cWCG24flYBxjruRi4RJREWTGF74L/KzI2CognAW/d/zWR0CjiScuJhf37Am3LFbCySP6WSoyBOtTIoTA4yLEA==", - "requires": { - "@chakra-ui/react-children-utils": "2.0.6", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/breakpoint-utils": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@chakra-ui/breakpoint-utils/-/breakpoint-utils-2.0.8.tgz", - "integrity": "sha512-Pq32MlEX9fwb5j5xx8s18zJMARNHlQZH2VH1RZgfgRDpp7DcEgtRW5AInfN5CfqdHLO1dGxA7I3MqEuL5JnIsA==", - "requires": { - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/button": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/button/-/button-2.1.0.tgz", - "integrity": "sha512-95CplwlRKmmUXkdEp/21VkEWgnwcx2TOBG6NfYlsuLBDHSLlo5FKIiE2oSi4zXc4TLcopGcWPNcm/NDaSC5pvA==", - "requires": { - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5", - "@chakra-ui/spinner": "2.1.0" - } - }, - "@chakra-ui/card": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/card/-/card-2.2.0.tgz", - "integrity": "sha512-xUB/k5MURj4CtPAhdSoXZidUbm8j3hci9vnc+eZJVDqhDOShNlD6QeniQNRPRys4lWAQLCbFcrwL29C8naDi6g==", - "requires": { - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/checkbox": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/checkbox/-/checkbox-2.3.2.tgz", - "integrity": "sha512-85g38JIXMEv6M+AcyIGLh7igNtfpAN6KGQFYxY9tBj0eWvWk4NKQxvqqyVta0bSAyIl1rixNIIezNpNWk2iO4g==", - "requires": { - "@chakra-ui/form-control": "2.2.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-callback-ref": "2.1.0", - "@chakra-ui/react-use-controllable-state": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/react-use-safe-layout-effect": "2.1.0", - "@chakra-ui/react-use-update-effect": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5", - "@chakra-ui/visually-hidden": "2.2.0", - "@zag-js/focus-visible": "0.16.0" - } - }, - "@chakra-ui/clickable": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/clickable/-/clickable-2.1.0.tgz", - "integrity": "sha512-flRA/ClPUGPYabu+/GLREZVZr9j2uyyazCAUHAdrTUEdDYCr31SVGhgh7dgKdtq23bOvAQJpIJjw/0Bs0WvbXw==", - "requires": { - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/close-button": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/close-button/-/close-button-2.1.1.tgz", - "integrity": "sha512-gnpENKOanKexswSVpVz7ojZEALl2x5qjLYNqSQGbxz+aP9sOXPfUS56ebyBrre7T7exuWGiFeRwnM0oVeGPaiw==", - "requires": { - "@chakra-ui/icon": "3.2.0" - } - }, - "@chakra-ui/color-mode": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/color-mode/-/color-mode-2.2.0.tgz", - "integrity": "sha512-niTEA8PALtMWRI9wJ4LL0CSBDo8NBfLNp4GD6/0hstcm3IlbBHTVKxN6HwSaoNYfphDQLxCjT4yG+0BJA5tFpg==", - "requires": { - "@chakra-ui/react-use-safe-layout-effect": "2.1.0" - } - }, - "@chakra-ui/control-box": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/control-box/-/control-box-2.1.0.tgz", - "integrity": "sha512-gVrRDyXFdMd8E7rulL0SKeoljkLQiPITFnsyMO8EFHNZ+AHt5wK4LIguYVEq88APqAGZGfHFWXr79RYrNiE3Mg==", - "requires": {} - }, - "@chakra-ui/counter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/counter/-/counter-2.1.0.tgz", - "integrity": "sha512-s6hZAEcWT5zzjNz2JIWUBzRubo9la/oof1W7EKZVVfPYHERnl5e16FmBC79Yfq8p09LQ+aqFKm/etYoJMMgghw==", - "requires": { - "@chakra-ui/number-utils": "2.0.7", - "@chakra-ui/react-use-callback-ref": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/css-reset": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/css-reset/-/css-reset-2.3.0.tgz", - "integrity": "sha512-cQwwBy5O0jzvl0K7PLTLgp8ijqLPKyuEMiDXwYzl95seD3AoeuoCLyzZcJtVqaUZ573PiBdAbY/IlZcwDOItWg==", - "requires": {} - }, - "@chakra-ui/descendant": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/descendant/-/descendant-3.1.0.tgz", - "integrity": "sha512-VxCIAir08g5w27klLyi7PVo8BxhW4tgU/lxQyujkmi4zx7hT9ZdrcQLAted/dAa+aSIZ14S1oV0Q9lGjsAdxUQ==", - "requires": { - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0" - } - }, - "@chakra-ui/dom-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/dom-utils/-/dom-utils-2.1.0.tgz", - "integrity": "sha512-ZmF2qRa1QZ0CMLU8M1zCfmw29DmPNtfjR9iTo74U5FPr3i1aoAh7fbJ4qAlZ197Xw9eAW28tvzQuoVWeL5C7fQ==" - }, - "@chakra-ui/editable": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/editable/-/editable-3.1.0.tgz", - "integrity": "sha512-j2JLrUL9wgg4YA6jLlbU88370eCRyor7DZQD9lzpY95tSOXpTljeg3uF9eOmDnCs6fxp3zDWIfkgMm/ExhcGTg==", - "requires": { - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-callback-ref": "2.1.0", - "@chakra-ui/react-use-controllable-state": "2.1.0", - "@chakra-ui/react-use-focus-on-pointer-down": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/react-use-safe-layout-effect": "2.1.0", - "@chakra-ui/react-use-update-effect": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/event-utils": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@chakra-ui/event-utils/-/event-utils-2.0.8.tgz", - "integrity": "sha512-IGM/yGUHS+8TOQrZGpAKOJl/xGBrmRYJrmbHfUE7zrG3PpQyXvbLDP1M+RggkCFVgHlJi2wpYIf0QtQlU0XZfw==" - }, - "@chakra-ui/focus-lock": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/focus-lock/-/focus-lock-2.1.0.tgz", - "integrity": "sha512-EmGx4PhWGjm4dpjRqM4Aa+rCWBxP+Rq8Uc/nAVnD4YVqkEhBkrPTpui2lnjsuxqNaZ24fIAZ10cF1hlpemte/w==", - "requires": { - "@chakra-ui/dom-utils": "2.1.0", - "react-focus-lock": "^2.9.4" - } - }, - "@chakra-ui/form-control": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/form-control/-/form-control-2.2.0.tgz", - "integrity": "sha512-wehLC1t4fafCVJ2RvJQT2jyqsAwX7KymmiGqBu7nQoQz8ApTkGABWpo/QwDh3F/dBLrouHDoOvGmYTqft3Mirw==", - "requires": { - "@chakra-ui/icon": "3.2.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/hooks": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/hooks/-/hooks-2.2.1.tgz", - "integrity": "sha512-RQbTnzl6b1tBjbDPf9zGRo9rf/pQMholsOudTxjy4i9GfTfz6kgp5ValGjQm2z7ng6Z31N1cnjZ1AlSzQ//ZfQ==", - "requires": { - "@chakra-ui/react-utils": "2.0.12", - "@chakra-ui/utils": "2.0.15", - "compute-scroll-into-view": "3.0.3", - "copy-to-clipboard": "3.3.3" - } - }, - "@chakra-ui/icon": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/icon/-/icon-3.2.0.tgz", - "integrity": "sha512-xxjGLvlX2Ys4H0iHrI16t74rG9EBcpFvJ3Y3B7KMQTrnW34Kf7Da/UC8J67Gtx85mTHW020ml85SVPKORWNNKQ==", - "requires": { - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/icons": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/icons/-/icons-2.1.1.tgz", - "integrity": "sha512-3p30hdo4LlRZTT5CwoAJq3G9fHI0wDc0pBaMHj4SUn0yomO+RcDRlzhdXqdr5cVnzax44sqXJVnf3oQG0eI+4g==", - "requires": { - "@chakra-ui/icon": "3.2.0" - } - }, - "@chakra-ui/image": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/image/-/image-2.1.0.tgz", - "integrity": "sha512-bskumBYKLiLMySIWDGcz0+D9Th0jPvmX6xnRMs4o92tT3Od/bW26lahmV2a2Op2ItXeCmRMY+XxJH5Gy1i46VA==", - "requires": { - "@chakra-ui/react-use-safe-layout-effect": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/input": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/input/-/input-2.1.2.tgz", - "integrity": "sha512-GiBbb3EqAA8Ph43yGa6Mc+kUPjh4Spmxp1Pkelr8qtudpc3p2PJOOebLpd90mcqw8UePPa+l6YhhPtp6o0irhw==", - "requires": { - "@chakra-ui/form-control": "2.2.0", - "@chakra-ui/object-utils": "2.1.0", - "@chakra-ui/react-children-utils": "2.0.6", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/layout": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/layout/-/layout-2.3.1.tgz", - "integrity": "sha512-nXuZ6WRbq0WdgnRgLw+QuxWAHuhDtVX8ElWqcTK+cSMFg/52eVP47czYBE5F35YhnoW2XBwfNoNgZ7+e8Z01Rg==", - "requires": { - "@chakra-ui/breakpoint-utils": "2.0.8", - "@chakra-ui/icon": "3.2.0", - "@chakra-ui/object-utils": "2.1.0", - "@chakra-ui/react-children-utils": "2.0.6", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/lazy-utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@chakra-ui/lazy-utils/-/lazy-utils-2.0.5.tgz", - "integrity": "sha512-UULqw7FBvcckQk2n3iPO56TMJvDsNv0FKZI6PlUNJVaGsPbsYxK/8IQ60vZgaTVPtVcjY6BE+y6zg8u9HOqpyg==" - }, - "@chakra-ui/live-region": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/live-region/-/live-region-2.1.0.tgz", - "integrity": "sha512-ZOxFXwtaLIsXjqnszYYrVuswBhnIHHP+XIgK1vC6DePKtyK590Wg+0J0slDwThUAd4MSSIUa/nNX84x1GMphWw==", - "requires": {} - }, - "@chakra-ui/media-query": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/media-query/-/media-query-3.3.0.tgz", - "integrity": "sha512-IsTGgFLoICVoPRp9ykOgqmdMotJG0CnPsKvGQeSFOB/dZfIujdVb14TYxDU4+MURXry1MhJ7LzZhv+Ml7cr8/g==", - "requires": { - "@chakra-ui/breakpoint-utils": "2.0.8", - "@chakra-ui/react-env": "3.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/menu": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/menu/-/menu-2.2.1.tgz", - "integrity": "sha512-lJS7XEObzJxsOwWQh7yfG4H8FzFPRP5hVPN/CL+JzytEINCSBvsCDHrYPQGp7jzpCi8vnTqQQGQe0f8dwnXd2g==", - "requires": { - "@chakra-ui/clickable": "2.1.0", - "@chakra-ui/descendant": "3.1.0", - "@chakra-ui/lazy-utils": "2.0.5", - "@chakra-ui/popper": "3.1.0", - "@chakra-ui/react-children-utils": "2.0.6", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-use-animation-state": "2.1.0", - "@chakra-ui/react-use-controllable-state": "2.1.0", - "@chakra-ui/react-use-disclosure": "2.1.0", - "@chakra-ui/react-use-focus-effect": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/react-use-outside-click": "2.2.0", - "@chakra-ui/react-use-update-effect": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5", - "@chakra-ui/transition": "2.1.0" - } - }, - "@chakra-ui/modal": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/modal/-/modal-2.3.1.tgz", - "integrity": "sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==", - "requires": { - "@chakra-ui/close-button": "2.1.1", - "@chakra-ui/focus-lock": "2.1.0", - "@chakra-ui/portal": "2.1.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5", - "@chakra-ui/transition": "2.1.0", - "aria-hidden": "^1.2.3", - "react-remove-scroll": "^2.5.6" - } - }, - "@chakra-ui/number-input": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/number-input/-/number-input-2.1.2.tgz", - "integrity": "sha512-pfOdX02sqUN0qC2ysuvgVDiws7xZ20XDIlcNhva55Jgm095xjm8eVdIBfNm3SFbSUNxyXvLTW/YQanX74tKmuA==", - "requires": { - "@chakra-ui/counter": "2.1.0", - "@chakra-ui/form-control": "2.2.0", - "@chakra-ui/icon": "3.2.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-callback-ref": "2.1.0", - "@chakra-ui/react-use-event-listener": "2.1.0", - "@chakra-ui/react-use-interval": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/react-use-safe-layout-effect": "2.1.0", - "@chakra-ui/react-use-update-effect": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/number-utils": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@chakra-ui/number-utils/-/number-utils-2.0.7.tgz", - "integrity": "sha512-yOGxBjXNvLTBvQyhMDqGU0Oj26s91mbAlqKHiuw737AXHt0aPllOthVUqQMeaYLwLCjGMg0jtI7JReRzyi94Dg==" - }, - "@chakra-ui/object-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/object-utils/-/object-utils-2.1.0.tgz", - "integrity": "sha512-tgIZOgLHaoti5PYGPTwK3t/cqtcycW0owaiOXoZOcpwwX/vlVb+H1jFsQyWiiwQVPt9RkoSLtxzXamx+aHH+bQ==" - }, - "@chakra-ui/pin-input": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/pin-input/-/pin-input-2.1.0.tgz", - "integrity": "sha512-x4vBqLStDxJFMt+jdAHHS8jbh294O53CPQJoL4g228P513rHylV/uPscYUHrVJXRxsHfRztQO9k45jjTYaPRMw==", - "requires": { - "@chakra-ui/descendant": "3.1.0", - "@chakra-ui/react-children-utils": "2.0.6", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-use-controllable-state": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/popover": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/popover/-/popover-2.2.1.tgz", - "integrity": "sha512-K+2ai2dD0ljvJnlrzesCDT9mNzLifE3noGKZ3QwLqd/K34Ym1W/0aL1ERSynrcG78NKoXS54SdEzkhCZ4Gn/Zg==", - "requires": { - "@chakra-ui/close-button": "2.1.1", - "@chakra-ui/lazy-utils": "2.0.5", - "@chakra-ui/popper": "3.1.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-animation-state": "2.1.0", - "@chakra-ui/react-use-disclosure": "2.1.0", - "@chakra-ui/react-use-focus-effect": "2.1.0", - "@chakra-ui/react-use-focus-on-pointer-down": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/popper": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/popper/-/popper-3.1.0.tgz", - "integrity": "sha512-ciDdpdYbeFG7og6/6J8lkTFxsSvwTdMLFkpVylAF6VNC22jssiWfquj2eyD4rJnzkRFPvIWJq8hvbfhsm+AjSg==", - "requires": { - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@popperjs/core": "^2.9.3" - } - }, - "@chakra-ui/portal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/portal/-/portal-2.1.0.tgz", - "integrity": "sha512-9q9KWf6SArEcIq1gGofNcFPSWEyl+MfJjEUg/un1SMlQjaROOh3zYr+6JAwvcORiX7tyHosnmWC3d3wI2aPSQg==", - "requires": { - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-use-safe-layout-effect": "2.1.0" - } - }, - "@chakra-ui/progress": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/progress/-/progress-2.2.0.tgz", - "integrity": "sha512-qUXuKbuhN60EzDD9mHR7B67D7p/ZqNS2Aze4Pbl1qGGZfulPW0PY8Rof32qDtttDQBkzQIzFGE8d9QpAemToIQ==", - "requires": { - "@chakra-ui/react-context": "2.1.0" - } - }, - "@chakra-ui/provider": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/provider/-/provider-2.4.2.tgz", - "integrity": "sha512-w0Tef5ZCJK1mlJorcSjItCSbyvVuqpvyWdxZiVQmE6fvSJR83wZof42ux0+sfWD+I7rHSfj+f9nzhNaEWClysw==", - "requires": { - "@chakra-ui/css-reset": "2.3.0", - "@chakra-ui/portal": "2.1.0", - "@chakra-ui/react-env": "3.1.0", - "@chakra-ui/system": "2.6.2", - "@chakra-ui/utils": "2.0.15" - } - }, - "@chakra-ui/radio": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/radio/-/radio-2.1.2.tgz", - "integrity": "sha512-n10M46wJrMGbonaghvSRnZ9ToTv/q76Szz284gv4QUWvyljQACcGrXIONUnQ3BIwbOfkRqSk7Xl/JgZtVfll+w==", - "requires": { - "@chakra-ui/form-control": "2.2.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5", - "@zag-js/focus-visible": "0.16.0" - } - }, - "@chakra-ui/react": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-2.8.2.tgz", - "integrity": "sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==", - "requires": { - "@chakra-ui/accordion": "2.3.1", - "@chakra-ui/alert": "2.2.2", - "@chakra-ui/avatar": "2.3.0", - "@chakra-ui/breadcrumb": "2.2.0", - "@chakra-ui/button": "2.1.0", - "@chakra-ui/card": "2.2.0", - "@chakra-ui/checkbox": "2.3.2", - "@chakra-ui/close-button": "2.1.1", - "@chakra-ui/control-box": "2.1.0", - "@chakra-ui/counter": "2.1.0", - "@chakra-ui/css-reset": "2.3.0", - "@chakra-ui/editable": "3.1.0", - "@chakra-ui/focus-lock": "2.1.0", - "@chakra-ui/form-control": "2.2.0", - "@chakra-ui/hooks": "2.2.1", - "@chakra-ui/icon": "3.2.0", - "@chakra-ui/image": "2.1.0", - "@chakra-ui/input": "2.1.2", - "@chakra-ui/layout": "2.3.1", - "@chakra-ui/live-region": "2.1.0", - "@chakra-ui/media-query": "3.3.0", - "@chakra-ui/menu": "2.2.1", - "@chakra-ui/modal": "2.3.1", - "@chakra-ui/number-input": "2.1.2", - "@chakra-ui/pin-input": "2.1.0", - "@chakra-ui/popover": "2.2.1", - "@chakra-ui/popper": "3.1.0", - "@chakra-ui/portal": "2.1.0", - "@chakra-ui/progress": "2.2.0", - "@chakra-ui/provider": "2.4.2", - "@chakra-ui/radio": "2.1.2", - "@chakra-ui/react-env": "3.1.0", - "@chakra-ui/select": "2.1.2", - "@chakra-ui/skeleton": "2.1.0", - "@chakra-ui/skip-nav": "2.1.0", - "@chakra-ui/slider": "2.1.0", - "@chakra-ui/spinner": "2.1.0", - "@chakra-ui/stat": "2.1.1", - "@chakra-ui/stepper": "2.3.1", - "@chakra-ui/styled-system": "2.9.2", - "@chakra-ui/switch": "2.1.2", - "@chakra-ui/system": "2.6.2", - "@chakra-ui/table": "2.1.0", - "@chakra-ui/tabs": "3.0.0", - "@chakra-ui/tag": "3.1.1", - "@chakra-ui/textarea": "2.1.2", - "@chakra-ui/theme": "3.3.1", - "@chakra-ui/theme-utils": "2.0.21", - "@chakra-ui/toast": "7.0.2", - "@chakra-ui/tooltip": "2.3.1", - "@chakra-ui/transition": "2.1.0", - "@chakra-ui/utils": "2.0.15", - "@chakra-ui/visually-hidden": "2.2.0" - } - }, - "@chakra-ui/react-children-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-children-utils/-/react-children-utils-2.0.6.tgz", - "integrity": "sha512-QVR2RC7QsOsbWwEnq9YduhpqSFnZGvjjGREV8ygKi8ADhXh93C8azLECCUVgRJF2Wc+So1fgxmjLcbZfY2VmBA==", - "requires": {} - }, - "@chakra-ui/react-context": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-context/-/react-context-2.1.0.tgz", - "integrity": "sha512-iahyStvzQ4AOwKwdPReLGfDesGG+vWJfEsn0X/NoGph/SkN+HXtv2sCfYFFR9k7bb+Kvc6YfpLlSuLvKMHi2+w==", - "requires": {} - }, - "@chakra-ui/react-env": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-env/-/react-env-3.1.0.tgz", - "integrity": "sha512-Vr96GV2LNBth3+IKzr/rq1IcnkXv+MLmwjQH6C8BRtn3sNskgDFD5vLkVXcEhagzZMCh8FR3V/bzZPojBOyNhw==", - "requires": { - "@chakra-ui/react-use-safe-layout-effect": "2.1.0" - } - }, - "@chakra-ui/react-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-types/-/react-types-2.0.7.tgz", - "integrity": "sha512-12zv2qIZ8EHwiytggtGvo4iLT0APris7T0qaAWqzpUGS0cdUtR8W+V1BJ5Ocq+7tA6dzQ/7+w5hmXih61TuhWQ==", - "requires": {} - }, - "@chakra-ui/react-use-animation-state": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-animation-state/-/react-use-animation-state-2.1.0.tgz", - "integrity": "sha512-CFZkQU3gmDBwhqy0vC1ryf90BVHxVN8cTLpSyCpdmExUEtSEInSCGMydj2fvn7QXsz/za8JNdO2xxgJwxpLMtg==", - "requires": { - "@chakra-ui/dom-utils": "2.1.0", - "@chakra-ui/react-use-event-listener": "2.1.0" - } - }, - "@chakra-ui/react-use-callback-ref": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-callback-ref/-/react-use-callback-ref-2.1.0.tgz", - "integrity": "sha512-efnJrBtGDa4YaxDzDE90EnKD3Vkh5a1t3w7PhnRQmsphLy3g2UieasoKTlT2Hn118TwDjIv5ZjHJW6HbzXA9wQ==", - "requires": {} - }, - "@chakra-ui/react-use-controllable-state": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-controllable-state/-/react-use-controllable-state-2.1.0.tgz", - "integrity": "sha512-QR/8fKNokxZUs4PfxjXuwl0fj/d71WPrmLJvEpCTkHjnzu7LnYvzoe2wB867IdooQJL0G1zBxl0Dq+6W1P3jpg==", - "requires": { - "@chakra-ui/react-use-callback-ref": "2.1.0" - } - }, - "@chakra-ui/react-use-disclosure": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-disclosure/-/react-use-disclosure-2.1.0.tgz", - "integrity": "sha512-Ax4pmxA9LBGMyEZJhhUZobg9C0t3qFE4jVF1tGBsrLDcdBeLR9fwOogIPY9Hf0/wqSlAryAimICbr5hkpa5GSw==", - "requires": { - "@chakra-ui/react-use-callback-ref": "2.1.0" - } - }, - "@chakra-ui/react-use-event-listener": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-event-listener/-/react-use-event-listener-2.1.0.tgz", - "integrity": "sha512-U5greryDLS8ISP69DKDsYcsXRtAdnTQT+jjIlRYZ49K/XhUR/AqVZCK5BkR1spTDmO9H8SPhgeNKI70ODuDU/Q==", - "requires": { - "@chakra-ui/react-use-callback-ref": "2.1.0" - } - }, - "@chakra-ui/react-use-focus-effect": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-focus-effect/-/react-use-focus-effect-2.1.0.tgz", - "integrity": "sha512-xzVboNy7J64xveLcxTIJ3jv+lUJKDwRM7Szwn9tNzUIPD94O3qwjV7DDCUzN2490nSYDF4OBMt/wuDBtaR3kUQ==", - "requires": { - "@chakra-ui/dom-utils": "2.1.0", - "@chakra-ui/react-use-event-listener": "2.1.0", - "@chakra-ui/react-use-safe-layout-effect": "2.1.0", - "@chakra-ui/react-use-update-effect": "2.1.0" - } - }, - "@chakra-ui/react-use-focus-on-pointer-down": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-focus-on-pointer-down/-/react-use-focus-on-pointer-down-2.1.0.tgz", - "integrity": "sha512-2jzrUZ+aiCG/cfanrolsnSMDykCAbv9EK/4iUyZno6BYb3vziucmvgKuoXbMPAzWNtwUwtuMhkby8rc61Ue+Lg==", - "requires": { - "@chakra-ui/react-use-event-listener": "2.1.0" - } - }, - "@chakra-ui/react-use-interval": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-interval/-/react-use-interval-2.1.0.tgz", - "integrity": "sha512-8iWj+I/+A0J08pgEXP1J1flcvhLBHkk0ln7ZvGIyXiEyM6XagOTJpwNhiu+Bmk59t3HoV/VyvyJTa+44sEApuw==", - "requires": { - "@chakra-ui/react-use-callback-ref": "2.1.0" - } - }, - "@chakra-ui/react-use-latest-ref": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-latest-ref/-/react-use-latest-ref-2.1.0.tgz", - "integrity": "sha512-m0kxuIYqoYB0va9Z2aW4xP/5b7BzlDeWwyXCH6QpT2PpW3/281L3hLCm1G0eOUcdVlayqrQqOeD6Mglq+5/xoQ==", - "requires": {} - }, - "@chakra-ui/react-use-merge-refs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-merge-refs/-/react-use-merge-refs-2.1.0.tgz", - "integrity": "sha512-lERa6AWF1cjEtWSGjxWTaSMvneccnAVH4V4ozh8SYiN9fSPZLlSG3kNxfNzdFvMEhM7dnP60vynF7WjGdTgQbQ==", - "requires": {} - }, - "@chakra-ui/react-use-outside-click": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-outside-click/-/react-use-outside-click-2.2.0.tgz", - "integrity": "sha512-PNX+s/JEaMneijbgAM4iFL+f3m1ga9+6QK0E5Yh4s8KZJQ/bLwZzdhMz8J/+mL+XEXQ5J0N8ivZN28B82N1kNw==", - "requires": { - "@chakra-ui/react-use-callback-ref": "2.1.0" - } - }, - "@chakra-ui/react-use-pan-event": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-pan-event/-/react-use-pan-event-2.1.0.tgz", - "integrity": "sha512-xmL2qOHiXqfcj0q7ZK5s9UjTh4Gz0/gL9jcWPA6GVf+A0Od5imEDa/Vz+533yQKWiNSm1QGrIj0eJAokc7O4fg==", - "requires": { - "@chakra-ui/event-utils": "2.0.8", - "@chakra-ui/react-use-latest-ref": "2.1.0", - "framesync": "6.1.2" - } - }, - "@chakra-ui/react-use-previous": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-previous/-/react-use-previous-2.1.0.tgz", - "integrity": "sha512-pjxGwue1hX8AFcmjZ2XfrQtIJgqbTF3Qs1Dy3d1krC77dEsiCUbQ9GzOBfDc8pfd60DrB5N2tg5JyHbypqh0Sg==", - "requires": {} - }, - "@chakra-ui/react-use-safe-layout-effect": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-safe-layout-effect/-/react-use-safe-layout-effect-2.1.0.tgz", - "integrity": "sha512-Knbrrx/bcPwVS1TorFdzrK/zWA8yuU/eaXDkNj24IrKoRlQrSBFarcgAEzlCHtzuhufP3OULPkELTzz91b0tCw==", - "requires": {} - }, - "@chakra-ui/react-use-size": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-size/-/react-use-size-2.1.0.tgz", - "integrity": "sha512-tbLqrQhbnqOjzTaMlYytp7wY8BW1JpL78iG7Ru1DlV4EWGiAmXFGvtnEt9HftU0NJ0aJyjgymkxfVGI55/1Z4A==", - "requires": { - "@zag-js/element-size": "0.10.5" - } - }, - "@chakra-ui/react-use-timeout": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-timeout/-/react-use-timeout-2.1.0.tgz", - "integrity": "sha512-cFN0sobKMM9hXUhyCofx3/Mjlzah6ADaEl/AXl5Y+GawB5rgedgAcu2ErAgarEkwvsKdP6c68CKjQ9dmTQlJxQ==", - "requires": { - "@chakra-ui/react-use-callback-ref": "2.1.0" - } - }, - "@chakra-ui/react-use-update-effect": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-update-effect/-/react-use-update-effect-2.1.0.tgz", - "integrity": "sha512-ND4Q23tETaR2Qd3zwCKYOOS1dfssojPLJMLvUtUbW5M9uW1ejYWgGUobeAiOVfSplownG8QYMmHTP86p/v0lbA==", - "requires": {} - }, - "@chakra-ui/react-utils": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@chakra-ui/react-utils/-/react-utils-2.0.12.tgz", - "integrity": "sha512-GbSfVb283+YA3kA8w8xWmzbjNWk14uhNpntnipHCftBibl0lxtQ9YqMFQLwuFOO0U2gYVocszqqDWX+XNKq9hw==", - "requires": { - "@chakra-ui/utils": "2.0.15" - } - }, - "@chakra-ui/select": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/select/-/select-2.1.2.tgz", - "integrity": "sha512-ZwCb7LqKCVLJhru3DXvKXpZ7Pbu1TDZ7N0PdQ0Zj1oyVLJyrpef1u9HR5u0amOpqcH++Ugt0f5JSmirjNlctjA==", - "requires": { - "@chakra-ui/form-control": "2.2.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/shared-utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@chakra-ui/shared-utils/-/shared-utils-2.0.5.tgz", - "integrity": "sha512-4/Wur0FqDov7Y0nCXl7HbHzCg4aq86h+SXdoUeuCMD3dSj7dpsVnStLYhng1vxvlbUnLpdF4oz5Myt3i/a7N3Q==" - }, - "@chakra-ui/skeleton": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/skeleton/-/skeleton-2.1.0.tgz", - "integrity": "sha512-JNRuMPpdZGd6zFVKjVQ0iusu3tXAdI29n4ZENYwAJEMf/fN0l12sVeirOxkJ7oEL0yOx2AgEYFSKdbcAgfUsAQ==", - "requires": { - "@chakra-ui/media-query": "3.3.0", - "@chakra-ui/react-use-previous": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/skip-nav": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/skip-nav/-/skip-nav-2.1.0.tgz", - "integrity": "sha512-Hk+FG+vadBSH0/7hwp9LJnLjkO0RPGnx7gBJWI4/SpoJf3e4tZlWYtwGj0toYY4aGKl93jVghuwGbDBEMoHDug==", - "requires": {} - }, - "@chakra-ui/slider": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/slider/-/slider-2.1.0.tgz", - "integrity": "sha512-lUOBcLMCnFZiA/s2NONXhELJh6sY5WtbRykPtclGfynqqOo47lwWJx+VP7xaeuhDOPcWSSecWc9Y1BfPOCz9cQ==", - "requires": { - "@chakra-ui/number-utils": "2.0.7", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-callback-ref": "2.1.0", - "@chakra-ui/react-use-controllable-state": "2.1.0", - "@chakra-ui/react-use-latest-ref": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/react-use-pan-event": "2.1.0", - "@chakra-ui/react-use-size": "2.1.0", - "@chakra-ui/react-use-update-effect": "2.1.0" - } - }, - "@chakra-ui/spinner": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/spinner/-/spinner-2.1.0.tgz", - "integrity": "sha512-hczbnoXt+MMv/d3gE+hjQhmkzLiKuoTo42YhUG7Bs9OSv2lg1fZHW1fGNRFP3wTi6OIbD044U1P9HK+AOgFH3g==", - "requires": { - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/stat": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/stat/-/stat-2.1.1.tgz", - "integrity": "sha512-LDn0d/LXQNbAn2KaR3F1zivsZCewY4Jsy1qShmfBMKwn6rI8yVlbvu6SiA3OpHS0FhxbsZxQI6HefEoIgtqY6Q==", - "requires": { - "@chakra-ui/icon": "3.2.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/stepper": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/stepper/-/stepper-2.3.1.tgz", - "integrity": "sha512-ky77lZbW60zYkSXhYz7kbItUpAQfEdycT0Q4bkHLxfqbuiGMf8OmgZOQkOB9uM4v0zPwy2HXhe0vq4Dd0xa55Q==", - "requires": { - "@chakra-ui/icon": "3.2.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/styled-system": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/styled-system/-/styled-system-2.9.2.tgz", - "integrity": "sha512-To/Z92oHpIE+4nk11uVMWqo2GGRS86coeMmjxtpnErmWRdLcp1WVCVRAvn+ZwpLiNR+reWFr2FFqJRsREuZdAg==", - "requires": { - "@chakra-ui/shared-utils": "2.0.5", - "csstype": "^3.1.2", - "lodash.mergewith": "4.6.2" - } - }, - "@chakra-ui/switch": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/switch/-/switch-2.1.2.tgz", - "integrity": "sha512-pgmi/CC+E1v31FcnQhsSGjJnOE2OcND4cKPyTE+0F+bmGm48Q/b5UmKD9Y+CmZsrt/7V3h8KNczowupfuBfIHA==", - "requires": { - "@chakra-ui/checkbox": "2.3.2", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/system": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/system/-/system-2.6.2.tgz", - "integrity": "sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==", - "requires": { - "@chakra-ui/color-mode": "2.2.0", - "@chakra-ui/object-utils": "2.1.0", - "@chakra-ui/react-utils": "2.0.12", - "@chakra-ui/styled-system": "2.9.2", - "@chakra-ui/theme-utils": "2.0.21", - "@chakra-ui/utils": "2.0.15", - "react-fast-compare": "3.2.2" - } - }, - "@chakra-ui/table": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/table/-/table-2.1.0.tgz", - "integrity": "sha512-o5OrjoHCh5uCLdiUb0Oc0vq9rIAeHSIRScc2ExTC9Qg/uVZl2ygLrjToCaKfaaKl1oQexIeAcZDKvPG8tVkHyQ==", - "requires": { - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/tabs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/tabs/-/tabs-3.0.0.tgz", - "integrity": "sha512-6Mlclp8L9lqXmsGWF5q5gmemZXOiOYuh0SGT/7PgJVNPz3LXREXlXg2an4MBUD8W5oTkduCX+3KTMCwRrVrDYw==", - "requires": { - "@chakra-ui/clickable": "2.1.0", - "@chakra-ui/descendant": "3.1.0", - "@chakra-ui/lazy-utils": "2.0.5", - "@chakra-ui/react-children-utils": "2.0.6", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-use-controllable-state": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/react-use-safe-layout-effect": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/tag": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/tag/-/tag-3.1.1.tgz", - "integrity": "sha512-Bdel79Dv86Hnge2PKOU+t8H28nm/7Y3cKd4Kfk9k3lOpUh4+nkSGe58dhRzht59lEqa4N9waCgQiBdkydjvBXQ==", - "requires": { - "@chakra-ui/icon": "3.2.0", - "@chakra-ui/react-context": "2.1.0" - } - }, - "@chakra-ui/textarea": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/textarea/-/textarea-2.1.2.tgz", - "integrity": "sha512-ip7tvklVCZUb2fOHDb23qPy/Fr2mzDOGdkrpbNi50hDCiV4hFX02jdQJdi3ydHZUyVgZVBKPOJ+lT9i7sKA2wA==", - "requires": { - "@chakra-ui/form-control": "2.2.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/theme": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/theme/-/theme-3.3.1.tgz", - "integrity": "sha512-Hft/VaT8GYnItGCBbgWd75ICrIrIFrR7lVOhV/dQnqtfGqsVDlrztbSErvMkoPKt0UgAkd9/o44jmZ6X4U2nZQ==", - "requires": { - "@chakra-ui/anatomy": "2.2.2", - "@chakra-ui/shared-utils": "2.0.5", - "@chakra-ui/theme-tools": "2.1.2" - } - }, - "@chakra-ui/theme-tools": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/theme-tools/-/theme-tools-2.1.2.tgz", - "integrity": "sha512-Qdj8ajF9kxY4gLrq7gA+Azp8CtFHGO9tWMN2wfF9aQNgG9AuMhPrUzMq9AMQ0MXiYcgNq/FD3eegB43nHVmXVA==", - "requires": { - "@chakra-ui/anatomy": "2.2.2", - "@chakra-ui/shared-utils": "2.0.5", - "color2k": "^2.0.2" - } - }, - "@chakra-ui/theme-utils": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/@chakra-ui/theme-utils/-/theme-utils-2.0.21.tgz", - "integrity": "sha512-FjH5LJbT794r0+VSCXB3lT4aubI24bLLRWB+CuRKHijRvsOg717bRdUN/N1fEmEpFnRVrbewttWh/OQs0EWpWw==", - "requires": { - "@chakra-ui/shared-utils": "2.0.5", - "@chakra-ui/styled-system": "2.9.2", - "@chakra-ui/theme": "3.3.1", - "lodash.mergewith": "4.6.2" - } - }, - "@chakra-ui/toast": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@chakra-ui/toast/-/toast-7.0.2.tgz", - "integrity": "sha512-yvRP8jFKRs/YnkuE41BVTq9nB2v/KDRmje9u6dgDmE5+1bFt3bwjdf9gVbif4u5Ve7F7BGk5E093ARRVtvLvXA==", - "requires": { - "@chakra-ui/alert": "2.2.2", - "@chakra-ui/close-button": "2.1.1", - "@chakra-ui/portal": "2.1.0", - "@chakra-ui/react-context": "2.1.0", - "@chakra-ui/react-use-timeout": "2.1.0", - "@chakra-ui/react-use-update-effect": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5", - "@chakra-ui/styled-system": "2.9.2", - "@chakra-ui/theme": "3.3.1" - } - }, - "@chakra-ui/tooltip": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@chakra-ui/tooltip/-/tooltip-2.3.1.tgz", - "integrity": "sha512-Rh39GBn/bL4kZpuEMPPRwYNnccRCL+w9OqamWHIB3Qboxs6h8cOyXfIdGxjo72lvhu1QI/a4KFqkM3St+WfC0A==", - "requires": { - "@chakra-ui/dom-utils": "2.1.0", - "@chakra-ui/popper": "3.1.0", - "@chakra-ui/portal": "2.1.0", - "@chakra-ui/react-types": "2.0.7", - "@chakra-ui/react-use-disclosure": "2.1.0", - "@chakra-ui/react-use-event-listener": "2.1.0", - "@chakra-ui/react-use-merge-refs": "2.1.0", - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/transition": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/transition/-/transition-2.1.0.tgz", - "integrity": "sha512-orkT6T/Dt+/+kVwJNy7zwJ+U2xAZ3EU7M3XCs45RBvUnZDr/u9vdmaM/3D/rOpmQJWgQBwKPJleUXrYWUagEDQ==", - "requires": { - "@chakra-ui/shared-utils": "2.0.5" - } - }, - "@chakra-ui/utils": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/@chakra-ui/utils/-/utils-2.0.15.tgz", - "integrity": "sha512-El4+jL0WSaYYs+rJbuYFDbjmfCcfGDmRY95GO4xwzit6YAPZBLcR65rOEwLps+XWluZTy1xdMrusg/hW0c1aAA==", - "requires": { - "@types/lodash.mergewith": "4.6.7", - "css-box-model": "1.2.1", - "framesync": "6.1.2", - "lodash.mergewith": "4.6.2" - } - }, - "@chakra-ui/visually-hidden": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@chakra-ui/visually-hidden/-/visually-hidden-2.2.0.tgz", - "integrity": "sha512-KmKDg01SrQ7VbTD3+cPWf/UfpF5MSwm3v7MWi0n5t8HnnadT13MF0MJCDSXbBWnzLv1ZKJ6zlyAOeARWX+DpjQ==", - "requires": {} - }, - "@emotion/babel-plugin": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", - "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", - "requires": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.1", - "@emotion/memoize": "^0.8.1", - "@emotion/serialize": "^1.1.2", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.2.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==" - } - } - }, - "@emotion/cache": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", - "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", - "requires": { - "@emotion/memoize": "^0.8.1", - "@emotion/sheet": "^1.2.2", - "@emotion/utils": "^1.2.1", - "@emotion/weak-memoize": "^0.3.1", - "stylis": "4.2.0" - } - }, - "@emotion/hash": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", - "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" - }, - "@emotion/is-prop-valid": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", - "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", - "requires": { - "@emotion/memoize": "^0.8.1" - } - }, - "@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" - }, - "@emotion/react": { - "version": "11.11.3", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.3.tgz", - "integrity": "sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==", - "requires": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.11.0", - "@emotion/cache": "^11.11.0", - "@emotion/serialize": "^1.1.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", - "@emotion/utils": "^1.2.1", - "@emotion/weak-memoize": "^0.3.1", - "hoist-non-react-statics": "^3.3.1" - } - }, - "@emotion/serialize": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz", - "integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==", - "requires": { - "@emotion/hash": "^0.9.1", - "@emotion/memoize": "^0.8.1", - "@emotion/unitless": "^0.8.1", - "@emotion/utils": "^1.2.1", - "csstype": "^3.0.2" - } - }, - "@emotion/sheet": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", - "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" - }, - "@emotion/styled": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", - "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", - "requires": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.11.0", - "@emotion/is-prop-valid": "^1.2.1", - "@emotion/serialize": "^1.1.2", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", - "@emotion/utils": "^1.2.1" - } - }, - "@emotion/unitless": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" - }, - "@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", - "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", - "requires": {} - }, - "@emotion/utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", - "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" - }, - "@emotion/weak-memoize": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", - "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" - }, - "@esbuild/android-arm": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.8.tgz", - "integrity": "sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.8.tgz", - "integrity": "sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.8.tgz", - "integrity": "sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.8.tgz", - "integrity": "sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.8.tgz", - "integrity": "sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.8.tgz", - "integrity": "sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.8.tgz", - "integrity": "sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.8.tgz", - "integrity": "sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.8.tgz", - "integrity": "sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.8.tgz", - "integrity": "sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.8.tgz", - "integrity": "sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.8.tgz", - "integrity": "sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.8.tgz", - "integrity": "sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.8.tgz", - "integrity": "sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.8.tgz", - "integrity": "sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.8.tgz", - "integrity": "sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.8.tgz", - "integrity": "sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.8.tgz", - "integrity": "sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.8.tgz", - "integrity": "sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.8.tgz", - "integrity": "sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.8.tgz", - "integrity": "sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.8.tgz", - "integrity": "sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==", - "dev": true, - "optional": true - }, - "@hey-api/openapi-ts": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.34.1.tgz", - "integrity": "sha512-7Ak+0nvf4Nhzk04tXGg6h4eM7lnWRgfjCPmMl2MyXrhS5urxd3Bg/PhtpB84u18wnwcM4rIeCUlTwDDQ/OB3NQ==", - "dev": true, - "requires": { - "@apidevtools/json-schema-ref-parser": "11.5.4", - "camelcase": "8.0.0", - "commander": "12.0.0", - "handlebars": "4.7.8" - }, - "dependencies": { - "@apidevtools/json-schema-ref-parser": { - "version": "11.5.4", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.5.4.tgz", - "integrity": "sha512-o2fsypTGU0WxRxbax8zQoHiIB4dyrkwYfcm8TxZ+bx9pCzcWZbQtiMqpgBvWA/nJ2TrGjK5adCLfTH8wUeU/Wg==", - "dev": true, - "requires": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.15", - "js-yaml": "^4.1.0" - } - }, - "camelcase": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", - "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", - "dev": true - }, - "commander": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", - "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", - "dev": true - } - } - }, - "@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "dev": true - }, - "@playwright/test": { - "version": "1.45.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.2.tgz", - "integrity": "sha512-JxG9eq92ET75EbVi3s+4sYbcG7q72ECeZNbdBlaMkGcNbiDQ4cAi8U2QP5oKkOx+1gpaiL1LDStmzCaEM1Z6fQ==", - "dev": true, - "requires": { - "playwright": "1.45.2" - } - }, - "@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" - }, - "@rollup/rollup-android-arm-eabi": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.6.1.tgz", - "integrity": "sha512-0WQ0ouLejaUCRsL93GD4uft3rOmB8qoQMU05Kb8CmMtMBe7XUDLAltxVZI1q6byNqEtU7N1ZX1Vw5lIpgulLQA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-android-arm64": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.6.1.tgz", - "integrity": "sha512-1TKm25Rn20vr5aTGGZqo6E4mzPicCUD79k17EgTLAsXc1zysyi4xXKACfUbwyANEPAEIxkzwue6JZ+stYzWUTA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-darwin-arm64": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.6.1.tgz", - "integrity": "sha512-cEXJQY/ZqMACb+nxzDeX9IPLAg7S94xouJJCNVE5BJM8JUEP4HeTF+ti3cmxWeSJo+5D+o8Tc0UAWUkfENdeyw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-darwin-x64": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.6.1.tgz", - "integrity": "sha512-LoSU9Xu56isrkV2jLldcKspJ7sSXmZWkAxg7sW/RfF7GS4F5/v4EiqKSMCFbZtDu2Nc1gxxFdQdKwkKS4rwxNg==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.6.1.tgz", - "integrity": "sha512-EfI3hzYAy5vFNDqpXsNxXcgRDcFHUWSx5nnRSCKwXuQlI5J9dD84g2Usw81n3FLBNsGCegKGwwTVsSKK9cooSQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm64-gnu": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.6.1.tgz", - "integrity": "sha512-9lhc4UZstsegbNLhH0Zu6TqvDfmhGzuCWtcTFXY10VjLLUe4Mr0Ye2L3rrtHaDd/J5+tFMEuo5LTCSCMXWfUKw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm64-musl": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.6.1.tgz", - "integrity": "sha512-FfoOK1yP5ksX3wwZ4Zk1NgyGHZyuRhf99j64I5oEmirV8EFT7+OhUZEnP+x17lcP/QHJNWGsoJwrz4PJ9fBEXw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-x64-gnu": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.6.1.tgz", - "integrity": "sha512-DNGZvZDO5YF7jN5fX8ZqmGLjZEXIJRdJEdTFMhiyXqyXubBa0WVLDWSNlQ5JR2PNgDbEV1VQowhVRUh+74D+RA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-x64-musl": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.6.1.tgz", - "integrity": "sha512-RkJVNVRM+piYy87HrKmhbexCHg3A6Z6MU0W9GHnJwBQNBeyhCJG9KDce4SAMdicQnpURggSvtbGo9xAWOfSvIQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-arm64-msvc": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.6.1.tgz", - "integrity": "sha512-v2FVT6xfnnmTe3W9bJXl6r5KwJglMK/iRlkKiIFfO6ysKs0rDgz7Cwwf3tjldxQUrHL9INT/1r4VA0n9L/F1vQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-ia32-msvc": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.6.1.tgz", - "integrity": "sha512-YEeOjxRyEjqcWphH9dyLbzgkF8wZSKAKUkldRY6dgNR5oKs2LZazqGB41cWJ4Iqqcy9/zqYgmzBkRoVz3Q9MLw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-x64-msvc": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.6.1.tgz", - "integrity": "sha512-0zfTlFAIhgz8V2G8STq8toAjsYYA6eci1hnXuyOTUFnymrtJwnS6uGKiv3v5UrPZkBlamLvrLV2iiaeqCKzb0A==", - "dev": true, - "optional": true - }, - "@swc/core": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.100.tgz", - "integrity": "sha512-7dKgTyxJjlrMwFZYb1auj3Xq0D8ZBe+5oeIgfMlRU05doXZypYJe0LAk0yjj3WdbwYzpF+T1PLxwTWizI0pckw==", - "dev": true, - "requires": { - "@swc/core-darwin-arm64": "1.3.100", - "@swc/core-darwin-x64": "1.3.100", - "@swc/core-linux-arm64-gnu": "1.3.100", - "@swc/core-linux-arm64-musl": "1.3.100", - "@swc/core-linux-x64-gnu": "1.3.100", - "@swc/core-linux-x64-musl": "1.3.100", - "@swc/core-win32-arm64-msvc": "1.3.100", - "@swc/core-win32-ia32-msvc": "1.3.100", - "@swc/core-win32-x64-msvc": "1.3.100", - "@swc/counter": "^0.1.1", - "@swc/types": "^0.1.5" - } - }, - "@swc/core-darwin-arm64": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.100.tgz", - "integrity": "sha512-XVWFsKe6ei+SsDbwmsuRkYck1SXRpO60Hioa4hoLwR8fxbA9eVp6enZtMxzVVMBi8ej5seZ4HZQeAWepbukiBw==", - "dev": true, - "optional": true - }, - "@swc/core-darwin-x64": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.100.tgz", - "integrity": "sha512-KF/MXrnH1nakm1wbt4XV8FS7kvqD9TGmVxeJ0U4bbvxXMvzeYUurzg3AJUTXYmXDhH/VXOYJE5N5RkwZZPs5iA==", - "dev": true, - "optional": true - }, - "@swc/core-linux-arm64-gnu": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.100.tgz", - "integrity": "sha512-p8hikNnAEJrw5vHCtKiFT4hdlQxk1V7vqPmvUDgL/qe2menQDK/i12tbz7/3BEQ4UqUPnvwpmVn2d19RdEMNxw==", - "dev": true, - "optional": true - }, - "@swc/core-linux-arm64-musl": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.100.tgz", - "integrity": "sha512-BWx/0EeY89WC4q3AaIaBSGfQxkYxIlS3mX19dwy2FWJs/O+fMvF9oLk/CyJPOZzbp+1DjGeeoGFuDYpiNO91JA==", - "dev": true, - "optional": true - }, - "@swc/core-linux-x64-gnu": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.100.tgz", - "integrity": "sha512-XUdGu3dxAkjsahLYnm8WijPfKebo+jHgHphDxaW0ovI6sTdmEGFDew7QzKZRlbYL2jRkUuuKuDGvD6lO5frmhA==", - "dev": true, - "optional": true - }, - "@swc/core-linux-x64-musl": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.100.tgz", - "integrity": "sha512-PhoXKf+f0OaNW/GCuXjJ0/KfK9EJX7z2gko+7nVnEA0p3aaPtbP6cq1Ubbl6CMoPL+Ci3gZ7nYumDqXNc3CtLQ==", - "dev": true, - "optional": true - }, - "@swc/core-win32-arm64-msvc": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.100.tgz", - "integrity": "sha512-PwLADZN6F9cXn4Jw52FeP/MCLVHm8vwouZZSOoOScDtihjY495SSjdPnlosMaRSR4wJQssGwiD/4MbpgQPqbAw==", - "dev": true, - "optional": true - }, - "@swc/core-win32-ia32-msvc": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.100.tgz", - "integrity": "sha512-0f6nicKSLlDKlyPRl2JEmkpBV4aeDfRQg6n8mPqgL7bliZIcDahG0ej+HxgNjZfS3e0yjDxsNRa6sAqWU2Z60A==", - "dev": true, - "optional": true - }, - "@swc/core-win32-x64-msvc": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.100.tgz", - "integrity": "sha512-b7J0rPoMkRTa3XyUGt8PwCaIBuYWsL2DqbirrQKRESzgCvif5iNpqaM6kjIjI/5y5q1Ycv564CB51YDpiS8EtQ==", - "dev": true, - "optional": true - }, - "@swc/counter": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", - "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==", - "dev": true - }, - "@swc/types": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", - "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", - "dev": true - }, - "@tanstack/history": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.15.13.tgz", - "integrity": "sha512-ToaeMtK5S4YaxCywAlYexc7KPFN0esjyTZ4vXzJhXEWAkro9iHgh7m/4ozPJb7oTo65WkHWX0W9GjcZbInSD8w==" - }, - "@tanstack/query-core": { - "version": "5.28.13", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.28.13.tgz", - "integrity": "sha512-C3+CCOcza+mrZ7LglQbjeYEOTEC3LV0VN0eYaIN6GvqAZ8Foegdgch7n6QYPtT4FuLae5ALy+m+ZMEKpD6tMCQ==" - }, - "@tanstack/query-devtools": { - "version": "5.28.10", - "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.28.10.tgz", - "integrity": "sha512-5UN629fKa5/1K/2Pd26gaU7epxRrYiT1gy+V+pW5K6hnf1DeUKK3pANSb2eHKlecjIKIhTwyF7k9XdyE2gREvQ==" - }, - "@tanstack/react-query": { - "version": "5.28.14", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.28.14.tgz", - "integrity": "sha512-cZqt03Igb3I9tM72qNX5TAAmeYl75Z+k4Mv92VkXIXc2hCrv0fIywd7GN3JV1BBJl4mr7Cc+OOKKOPy8sNVOkA==", - "requires": { - "@tanstack/query-core": "5.28.13" - } - }, - "@tanstack/react-query-devtools": { - "version": "5.28.14", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.28.14.tgz", - "integrity": "sha512-4CrFBI1O5wibV1ZdGAnBMmTuc7SiShhxWubxRMyIloeEioxs3DQkFbouGBea5nexuwIxAkvhUB8khpPnNjhxMw==", - "requires": { - "@tanstack/query-devtools": "5.28.10" - } - }, - "@tanstack/react-router": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.19.1.tgz", - "integrity": "sha512-a4Xf074qo2fQLmSi8PTncEFn8XakaH3+DT7Dted4OPClzQFS+c6yU3HONVNAsuYWZ7lDK1HMKoHPDFbnHPEWvA==", - "requires": { - "@tanstack/history": "1.15.13", - "@tanstack/react-store": "^0.2.1", - "tiny-invariant": "^1.3.1", - "tiny-warning": "^1.0.3" - } - }, - "@tanstack/react-store": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.2.1.tgz", - "integrity": "sha512-tEbMCQjbeVw9KOP/202LfqZMSNAVi6zYkkp1kBom8nFuMx/965Hzes3+6G6b/comCwVxoJU8Gg9IrcF8yRPthw==", - "requires": { - "@tanstack/store": "0.1.3", - "use-sync-external-store": "^1.2.0" - } - }, - "@tanstack/router-devtools": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@tanstack/router-devtools/-/router-devtools-1.19.1.tgz", - "integrity": "sha512-l560JHnffcDccSTo/sOtB+gKvtgaWYpOKOu9MyvswN9XB2pt752UFFIN1Yt/Gsp2Iooq/FcYlYnEPHb4GFzalg==", - "dev": true, - "requires": { - "@tanstack/react-router": "1.19.1", - "clsx": "^2.1.0", - "date-fns": "^2.29.1", - "goober": "^2.1.14" - } - }, - "@tanstack/router-generator": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.19.0.tgz", - "integrity": "sha512-vFF8Q7SdyygiYC7lfJ83GRif0vcxjak9SAcgtX/w7TLR0O+qdxRXFPvhKTQQXH6vVezy5Au9bSaSI2EgDD1ubA==", - "dev": true, - "requires": { - "prettier": "^3.1.1", - "zod": "^3.22.4" - } - }, - "@tanstack/router-vite-plugin": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@tanstack/router-vite-plugin/-/router-vite-plugin-1.19.0.tgz", - "integrity": "sha512-yvvQnJ7JvqsnxAFqwiHhNTV2n1jKkidjc+XbgS2aNnEHC0aHnYH2ygPlmmfiVD7PMO7x64PdI5e12TzY/aKoFA==", - "dev": true, - "requires": { - "@tanstack/router-generator": "1.19.0" - } - }, - "@tanstack/store": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.1.3.tgz", - "integrity": "sha512-GnolmC8Fr4mvsHE1fGQmR3Nm0eBO3KnZjDU0a+P3TeQNM/dDscFGxtA7p31NplQNW3KwBw4t1RVFmz0VeKLxcw==" - }, - "@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 - }, - "@types/lodash": { - "version": "4.14.202", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", - "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" - }, - "@types/lodash.mergewith": { - "version": "4.6.7", - "resolved": "https://registry.npmjs.org/@types/lodash.mergewith/-/lodash.mergewith-4.6.7.tgz", - "integrity": "sha512-3m+lkO5CLRRYU0fhGRp7zbsGi6+BZj0uTVSwvcKU+nSlhjA9/QRNfuSGnD2mX6hQA7ZbmcCkzk5h4ZYGOtk14A==", - "requires": { - "@types/lodash": "*" - } - }, - "@types/node": { - "version": "20.10.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", - "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", - "dev": true, - "requires": { - "undici-types": "~5.26.4" - } - }, - "@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" - }, - "@types/prop-types": { - "version": "15.7.11", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", - "devOptional": true - }, - "@types/react": { - "version": "18.2.39", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz", - "integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==", - "devOptional": true, - "requires": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "@types/react-dom": { - "version": "18.2.17", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz", - "integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==", - "dev": true, - "requires": { - "@types/react": "*" - } - }, - "@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "devOptional": true - }, - "@vitejs/plugin-react-swc": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.5.0.tgz", - "integrity": "sha512-1PrOvAaDpqlCV+Up8RkAh9qaiUjoDUcjtttyhXDKw53XA6Ve16SOp6cCOpRs8Dj8DqUQs6eTW5YkLcLJjrXAig==", - "dev": true, - "requires": { - "@swc/core": "^1.3.96" - } - }, - "@zag-js/dom-query": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@zag-js/dom-query/-/dom-query-0.16.0.tgz", - "integrity": "sha512-Oqhd6+biWyKnhKwFFuZrrf6lxBz2tX2pRQe6grUnYwO6HJ8BcbqZomy2lpOdr+3itlaUqx+Ywj5E5ZZDr/LBfQ==" - }, - "@zag-js/element-size": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/@zag-js/element-size/-/element-size-0.10.5.tgz", - "integrity": "sha512-uQre5IidULANvVkNOBQ1tfgwTQcGl4hliPSe69Fct1VfYb2Fd0jdAcGzqQgPhfrXFpR62MxLPB7erxJ/ngtL8w==" - }, - "@zag-js/focus-visible": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@zag-js/focus-visible/-/focus-visible-0.16.0.tgz", - "integrity": "sha512-a7U/HSopvQbrDU4GLerpqiMcHKEkQkNPeDZJWz38cw/6Upunh41GjHetq5TB84hxyCaDzJ6q2nEdNoBQfC0FKA==", - "requires": { - "@zag-js/dom-query": "0.16.0" - } - }, - "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 - }, - "aria-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", - "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", - "requires": { - "tslib": "^2.0.0" - } - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", - "requires": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "requires": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" - }, - "clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", - "dev": true - }, - "color2k": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/color2k/-/color2k-2.0.3.tgz", - "integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "compute-scroll-into-view": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.0.3.tgz", - "integrity": "sha512-nadqwNxghAGTamwIqQSG433W6OADZx2vCo3UXHNrzTRHK/htu+7+L0zhjEoaeaQVNAi3YgqWDv8+tzf0hRfR+A==" - }, - "convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, - "copy-to-clipboard": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", - "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", - "requires": { - "toggle-selection": "^1.0.6" - } - }, - "cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "css-box-model": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", - "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", - "requires": { - "tiny-invariant": "^1.0.6" - } - }, - "csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" - }, - "date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.21.0" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" - }, - "detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" - }, - "dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "dev": true - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "esbuild": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.8.tgz", - "integrity": "sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==", - "dev": true, - "requires": { - "@esbuild/android-arm": "0.19.8", - "@esbuild/android-arm64": "0.19.8", - "@esbuild/android-x64": "0.19.8", - "@esbuild/darwin-arm64": "0.19.8", - "@esbuild/darwin-x64": "0.19.8", - "@esbuild/freebsd-arm64": "0.19.8", - "@esbuild/freebsd-x64": "0.19.8", - "@esbuild/linux-arm": "0.19.8", - "@esbuild/linux-arm64": "0.19.8", - "@esbuild/linux-ia32": "0.19.8", - "@esbuild/linux-loong64": "0.19.8", - "@esbuild/linux-mips64el": "0.19.8", - "@esbuild/linux-ppc64": "0.19.8", - "@esbuild/linux-riscv64": "0.19.8", - "@esbuild/linux-s390x": "0.19.8", - "@esbuild/linux-x64": "0.19.8", - "@esbuild/netbsd-x64": "0.19.8", - "@esbuild/openbsd-x64": "0.19.8", - "@esbuild/sunos-x64": "0.19.8", - "@esbuild/win32-arm64": "0.19.8", - "@esbuild/win32-ia32": "0.19.8", - "@esbuild/win32-x64": "0.19.8" - } - }, - "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==" - }, - "find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" - }, - "focus-lock": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-1.3.3.tgz", - "integrity": "sha512-hfXkZha7Xt4RQtrL1HBfspAuIj89Y0fb6GX0dfJilb8S2G/lvL4akPAcHq6xoD2NuZnDMCnZL/zQesMyeu6Psg==", - "requires": { - "tslib": "^2.0.3" - } - }, - "follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "framer-motion": { - "version": "10.16.16", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.16.16.tgz", - "integrity": "sha512-je6j91rd7NmUX7L1XHouwJ4v3R+SO4umso2LUcgOct3rHZ0PajZ80ETYZTajzEXEl9DlKyzjyt4AvGQ+lrebOw==", - "requires": { - "@emotion/is-prop-valid": "^0.8.2", - "tslib": "^2.4.0" - }, - "dependencies": { - "@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "optional": true, - "requires": { - "@emotion/memoize": "0.7.4" - } - }, - "@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "optional": true - } - } - }, - "framesync": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.1.2.tgz", - "integrity": "sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==", - "requires": { - "tslib": "2.4.0" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, - "fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==" - }, - "goober": { - "version": "2.1.14", - "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz", - "integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==", - "dev": true, - "requires": {} - }, - "handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4", - "wordwrap": "^1.0.0" - } - }, - "hasown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", - "requires": { - "function-bind": "^1.1.2" - } - }, - "hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "requires": { - "react-is": "^16.7.0" - } - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "requires": { - "loose-envify": "^1.0.0" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, - "is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "requires": { - "hasown": "^2.0.0" - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "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, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true - }, - "nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true - }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" - }, - "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==", - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "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==" - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "playwright": { - "version": "1.45.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.2.tgz", - "integrity": "sha512-ReywF2t/0teRvNBpfIgh5e4wnrI/8Su8ssdo5XsQKpjxJj+jspm00jSoz9BTg91TT0c9HRjXO7LBNVrgYj9X0g==", - "dev": true, - "requires": { - "fsevents": "2.3.2", - "playwright-core": "1.45.2" - }, - "dependencies": { - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - } - } - }, - "playwright-core": { - "version": "1.45.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.2.tgz", - "integrity": "sha512-ha175tAWb0dTK0X4orvBIqi3jGEt701SMxMhyujxNrgd8K0Uy5wMSwwcQHtyB4om7INUkfndx02XnQ2p6dvLDw==", - "dev": true - }, - "postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", - "dev": true, - "requires": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", - "dev": true - }, - "prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "requires": { - "loose-envify": "^1.1.0" - } - }, - "react-clientside-effect": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz", - "integrity": "sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==", - "requires": { - "@babel/runtime": "^7.12.13" - } - }, - "react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "requires": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - } - }, - "react-error-boundary": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz", - "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", - "requires": { - "@babel/runtime": "^7.12.5" - } - }, - "react-fast-compare": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", - "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" - }, - "react-focus-lock": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.11.1.tgz", - "integrity": "sha512-IXLwnTBrLTlKTpASZXqqXJ8oymWrgAlOfuuDYN4XCuN1YJ72dwX198UCaF1QqGUk5C3QOnlMik//n3ufcfe8Ig==", - "requires": { - "@babel/runtime": "^7.0.0", - "focus-lock": "^1.3.2", - "prop-types": "^15.6.2", - "react-clientside-effect": "^1.2.6", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - } - }, - "react-hook-form": { - "version": "7.49.3", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.3.tgz", - "integrity": "sha512-foD6r3juidAT1cOZzpmD/gOKt7fRsDhXXZ0y28+Al1CHgX+AY1qIN9VSIIItXRq1dN68QrRwl1ORFlwjBaAqeQ==", - "requires": {} - }, - "react-icons": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz", - "integrity": "sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==", - "requires": {} - }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "react-remove-scroll": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", - "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", - "requires": { - "react-remove-scroll-bar": "^2.3.4", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - } - }, - "react-remove-scroll-bar": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.5.tgz", - "integrity": "sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==", - "requires": { - "react-style-singleton": "^2.2.1", - "tslib": "^2.0.0" - } - }, - "react-style-singleton": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", - "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", - "requires": { - "get-nonce": "^1.0.0", - "invariant": "^2.2.4", - "tslib": "^2.0.0" - } - }, - "regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, - "resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "requires": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "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==" - }, - "rollup": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.6.1.tgz", - "integrity": "sha512-jZHaZotEHQaHLgKr8JnQiDT1rmatjgKlMekyksz+yk9jt/8z9quNjnKNRoaM0wd9DC2QKXjmWWuDYtM3jfF8pQ==", - "dev": true, - "requires": { - "@rollup/rollup-android-arm-eabi": "4.6.1", - "@rollup/rollup-android-arm64": "4.6.1", - "@rollup/rollup-darwin-arm64": "4.6.1", - "@rollup/rollup-darwin-x64": "4.6.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.6.1", - "@rollup/rollup-linux-arm64-gnu": "4.6.1", - "@rollup/rollup-linux-arm64-musl": "4.6.1", - "@rollup/rollup-linux-x64-gnu": "4.6.1", - "@rollup/rollup-linux-x64-musl": "4.6.1", - "@rollup/rollup-win32-arm64-msvc": "4.6.1", - "@rollup/rollup-win32-ia32-msvc": "4.6.1", - "@rollup/rollup-win32-x64-msvc": "4.6.1", - "fsevents": "~2.3.2" - } - }, - "scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "requires": { - "loose-envify": "^1.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true - }, - "stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" - }, - "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==" - }, - "tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" - }, - "tiny-warning": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" - }, - "toggle-selection": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" - }, - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, - "typescript": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", - "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", - "dev": true - }, - "uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", - "dev": true, - "optional": true - }, - "undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true - }, - "use-callback-ref": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.1.tgz", - "integrity": "sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==", - "requires": { - "tslib": "^2.0.0" - } - }, - "use-sidecar": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", - "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", - "requires": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - } - }, - "use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "requires": {} - }, - "vite": { - "version": "5.0.13", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.13.tgz", - "integrity": "sha512-/9ovhv2M2dGTuA+dY93B9trfyWMDRQw2jdVBhHNP6wr0oF34wG2i/N55801iZIpgUpnHDm4F/FabGQLyc+eOgg==", - "dev": true, - "requires": { - "esbuild": "^0.19.3", - "fsevents": "~2.3.3", - "postcss": "^8.4.32", - "rollup": "^4.2.0" - } - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true - }, - "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" - }, - "zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", - "dev": true - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 1a7a547f68..0000000000 --- a/frontend/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "frontend", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "lint": "biome check --apply-unsafe --no-errors-on-unmatched --files-ignore-unknown=true ./", - "preview": "vite preview", - "generate-client": "openapi-ts --input ./openapi.json --output ./src/client --client axios --exportSchemas true" - }, - "dependencies": { - "@chakra-ui/icons": "2.1.1", - "@chakra-ui/react": "2.8.2", - "@emotion/react": "11.11.3", - "@emotion/styled": "11.11.0", - "@tanstack/react-query": "^5.28.14", - "@tanstack/react-query-devtools": "^5.28.14", - "@tanstack/react-router": "1.19.1", - "axios": "1.7.4", - "form-data": "4.0.0", - "framer-motion": "10.16.16", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-error-boundary": "^4.0.13", - "react-hook-form": "7.49.3", - "react-icons": "5.0.1" - }, - "devDependencies": { - "@biomejs/biome": "1.6.1", - "@hey-api/openapi-ts": "^0.34.1", - "@playwright/test": "^1.45.2", - "@tanstack/router-devtools": "1.19.1", - "@tanstack/router-vite-plugin": "1.19.0", - "@types/node": "^20.10.5", - "@types/react": "^18.2.37", - "@types/react-dom": "^18.2.15", - "@vitejs/plugin-react-swc": "^3.5.0", - "dotenv": "^16.4.5", - "typescript": "^5.2.2", - "vite": "^5.0.13" - } -} diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts deleted file mode 100644 index dcdd6fec81..0000000000 --- a/frontend/playwright.config.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { defineConfig, devices } from '@playwright/test'; - - -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -// require('dotenv').config(); - -/** - * See https://playwright.dev/docs/test-configuration. - */ -export default defineConfig({ - testDir: './tests', - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://localhost:5173', - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - }, - - /* Configure projects for major browsers */ - projects: [ - { name: 'setup', testMatch: /.*\.setup\.ts/ }, - - { - name: 'chromium', - use: { - ...devices['Desktop Chrome'], - storageState: 'playwright/.auth/user.json', - }, - dependencies: ['setup'], - }, - - // { - // name: 'firefox', - // use: { - // ...devices['Desktop Firefox'], - // storageState: 'playwright/.auth/user.json', - // }, - // dependencies: ['setup'], - // }, - - // { - // name: 'webkit', - // use: { - // ...devices['Desktop Safari'], - // storageState: 'playwright/.auth/user.json', - // }, - // dependencies: ['setup'], - // }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, - ], - - /* Run your local dev server before starting the tests */ - webServer: { - command: 'npm run dev', - url: 'http://localhost:5173', - reuseExistingServer: !process.env.CI, - }, -}); diff --git a/frontend/public/assets/images/fastapi-logo.svg b/frontend/public/assets/images/fastapi-logo.svg deleted file mode 100644 index d3dad4bec8..0000000000 --- a/frontend/public/assets/images/fastapi-logo.svg +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - - - diff --git a/frontend/public/assets/images/favicon.png b/frontend/public/assets/images/favicon.png deleted file mode 100644 index e5b7c3ada7..0000000000 Binary files a/frontend/public/assets/images/favicon.png and /dev/null differ diff --git a/frontend/src/client/core/ApiError.ts b/frontend/src/client/core/ApiError.ts deleted file mode 100644 index 5499aa8f05..0000000000 --- a/frontend/src/client/core/ApiError.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { ApiRequestOptions } from "./ApiRequestOptions" -import type { ApiResult } from "./ApiResult" - -export class ApiError extends Error { - public readonly url: string - public readonly status: number - public readonly statusText: string - public readonly body: unknown - public readonly request: ApiRequestOptions - - constructor( - request: ApiRequestOptions, - response: ApiResult, - message: string, - ) { - super(message) - - this.name = "ApiError" - this.url = response.url - this.status = response.status - this.statusText = response.statusText - this.body = response.body - this.request = request - } -} diff --git a/frontend/src/client/core/ApiRequestOptions.ts b/frontend/src/client/core/ApiRequestOptions.ts deleted file mode 100644 index 4cc259284f..0000000000 --- a/frontend/src/client/core/ApiRequestOptions.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type ApiRequestOptions = { - readonly method: - | "GET" - | "PUT" - | "POST" - | "DELETE" - | "OPTIONS" - | "HEAD" - | "PATCH" - readonly url: string - readonly path?: Record - readonly cookies?: Record - readonly headers?: Record - readonly query?: Record - readonly formData?: Record - readonly body?: any - readonly mediaType?: string - readonly responseHeader?: string - readonly errors?: Record -} diff --git a/frontend/src/client/core/ApiResult.ts b/frontend/src/client/core/ApiResult.ts deleted file mode 100644 index f88b8c64f1..0000000000 --- a/frontend/src/client/core/ApiResult.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type ApiResult = { - readonly body: TData - readonly ok: boolean - readonly status: number - readonly statusText: string - readonly url: string -} diff --git a/frontend/src/client/core/CancelablePromise.ts b/frontend/src/client/core/CancelablePromise.ts deleted file mode 100644 index f47db79eae..0000000000 --- a/frontend/src/client/core/CancelablePromise.ts +++ /dev/null @@ -1,126 +0,0 @@ -export class CancelError extends Error { - constructor(message: string) { - super(message) - this.name = "CancelError" - } - - public get isCancelled(): boolean { - return true - } -} - -export interface OnCancel { - readonly isResolved: boolean - readonly isRejected: boolean - readonly isCancelled: boolean - - (cancelHandler: () => void): void -} - -export class CancelablePromise implements Promise { - private _isResolved: boolean - private _isRejected: boolean - private _isCancelled: boolean - readonly cancelHandlers: (() => void)[] - readonly promise: Promise - private _resolve?: (value: T | PromiseLike) => void - private _reject?: (reason?: unknown) => void - - constructor( - executor: ( - resolve: (value: T | PromiseLike) => void, - reject: (reason?: unknown) => void, - onCancel: OnCancel, - ) => void, - ) { - this._isResolved = false - this._isRejected = false - this._isCancelled = false - this.cancelHandlers = [] - this.promise = new Promise((resolve, reject) => { - this._resolve = resolve - this._reject = reject - - const onResolve = (value: T | PromiseLike): void => { - if (this._isResolved || this._isRejected || this._isCancelled) { - return - } - this._isResolved = true - if (this._resolve) this._resolve(value) - } - - const onReject = (reason?: unknown): void => { - if (this._isResolved || this._isRejected || this._isCancelled) { - return - } - this._isRejected = true - if (this._reject) this._reject(reason) - } - - const onCancel = (cancelHandler: () => void): void => { - if (this._isResolved || this._isRejected || this._isCancelled) { - return - } - this.cancelHandlers.push(cancelHandler) - } - - Object.defineProperty(onCancel, "isResolved", { - get: (): boolean => this._isResolved, - }) - - Object.defineProperty(onCancel, "isRejected", { - get: (): boolean => this._isRejected, - }) - - Object.defineProperty(onCancel, "isCancelled", { - get: (): boolean => this._isCancelled, - }) - - return executor(onResolve, onReject, onCancel as OnCancel) - }) - } - - get [Symbol.toStringTag]() { - return "Cancellable Promise" - } - - public then( - onFulfilled?: ((value: T) => TResult1 | PromiseLike) | null, - onRejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, - ): Promise { - return this.promise.then(onFulfilled, onRejected) - } - - public catch( - onRejected?: ((reason: unknown) => TResult | PromiseLike) | null, - ): Promise { - return this.promise.catch(onRejected) - } - - public finally(onFinally?: (() => void) | null): Promise { - return this.promise.finally(onFinally) - } - - public cancel(): void { - if (this._isResolved || this._isRejected || this._isCancelled) { - return - } - this._isCancelled = true - if (this.cancelHandlers.length) { - try { - for (const cancelHandler of this.cancelHandlers) { - cancelHandler() - } - } catch (error) { - console.warn("Cancellation threw an error", error) - return - } - } - this.cancelHandlers.length = 0 - if (this._reject) this._reject(new CancelError("Request aborted")) - } - - public get isCancelled(): boolean { - return this._isCancelled - } -} diff --git a/frontend/src/client/core/OpenAPI.ts b/frontend/src/client/core/OpenAPI.ts deleted file mode 100644 index 746df5e61d..0000000000 --- a/frontend/src/client/core/OpenAPI.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { AxiosRequestConfig, AxiosResponse } from "axios" -import type { ApiRequestOptions } from "./ApiRequestOptions" -import type { TResult } from "./types" - -type Headers = Record -type Middleware = (value: T) => T | Promise -type Resolver = (options: ApiRequestOptions) => Promise - -export class Interceptors { - _fns: Middleware[] - - constructor() { - this._fns = [] - } - - eject(fn: Middleware) { - const index = this._fns.indexOf(fn) - if (index !== -1) { - this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)] - } - } - - use(fn: Middleware) { - this._fns = [...this._fns, fn] - } -} - -export type OpenAPIConfig = { - BASE: string - CREDENTIALS: "include" | "omit" | "same-origin" - ENCODE_PATH?: ((path: string) => string) | undefined - HEADERS?: Headers | Resolver | undefined - PASSWORD?: string | Resolver | undefined - RESULT?: TResult - TOKEN?: string | Resolver | undefined - USERNAME?: string | Resolver | undefined - VERSION: string - WITH_CREDENTIALS: boolean - interceptors: { - request: Interceptors - response: Interceptors - } -} - -export const OpenAPI: OpenAPIConfig = { - BASE: "", - CREDENTIALS: "include", - ENCODE_PATH: undefined, - HEADERS: undefined, - PASSWORD: undefined, - RESULT: "body", - TOKEN: undefined, - USERNAME: undefined, - VERSION: "0.1.0", - WITH_CREDENTIALS: false, - interceptors: { request: new Interceptors(), response: new Interceptors() }, -} diff --git a/frontend/src/client/core/request.ts b/frontend/src/client/core/request.ts deleted file mode 100644 index 99d38b46f1..0000000000 --- a/frontend/src/client/core/request.ts +++ /dev/null @@ -1,376 +0,0 @@ -import axios from "axios" -import type { - AxiosError, - AxiosRequestConfig, - AxiosResponse, - AxiosInstance, -} from "axios" - -import { ApiError } from "./ApiError" -import type { ApiRequestOptions } from "./ApiRequestOptions" -import type { ApiResult } from "./ApiResult" -import { CancelablePromise } from "./CancelablePromise" -import type { OnCancel } from "./CancelablePromise" -import type { OpenAPIConfig } from "./OpenAPI" - -export const isString = (value: unknown): value is string => { - return typeof value === "string" -} - -export const isStringWithValue = (value: unknown): value is string => { - return isString(value) && value !== "" -} - -export const isBlob = (value: any): value is Blob => { - return value instanceof Blob -} - -export const isFormData = (value: unknown): value is FormData => { - return value instanceof FormData -} - -export const isSuccess = (status: number): boolean => { - return status >= 200 && status < 300 -} - -export const base64 = (str: string): string => { - try { - return btoa(str) - } catch (err) { - // @ts-ignore - return Buffer.from(str).toString("base64") - } -} - -export const getQueryString = (params: Record): string => { - const qs: string[] = [] - - const append = (key: string, value: unknown) => { - qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`) - } - - const encodePair = (key: string, value: unknown) => { - if (value === undefined || value === null) { - return - } - - if (Array.isArray(value)) { - value.forEach((v) => encodePair(key, v)) - } else if (typeof value === "object") { - Object.entries(value).forEach(([k, v]) => encodePair(`${key}[${k}]`, v)) - } else { - append(key, value) - } - } - - Object.entries(params).forEach(([key, value]) => encodePair(key, value)) - - return qs.length ? `?${qs.join("&")}` : "" -} - -const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => { - const encoder = config.ENCODE_PATH || encodeURI - - const path = options.url - .replace("{api-version}", config.VERSION) - .replace(/{(.*?)}/g, (substring: string, group: string) => { - if (options.path?.hasOwnProperty(group)) { - return encoder(String(options.path[group])) - } - return substring - }) - - const url = config.BASE + path - return options.query ? url + getQueryString(options.query) : url -} - -export const getFormData = ( - options: ApiRequestOptions, -): FormData | undefined => { - if (options.formData) { - const formData = new FormData() - - const process = (key: string, value: unknown) => { - if (isString(value) || isBlob(value)) { - formData.append(key, value) - } else { - formData.append(key, JSON.stringify(value)) - } - } - - Object.entries(options.formData) - .filter(([, value]) => value !== undefined && value !== null) - .forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach((v) => process(key, v)) - } else { - process(key, value) - } - }) - - return formData - } - return undefined -} - -type Resolver = (options: ApiRequestOptions) => Promise - -export const resolve = async ( - options: ApiRequestOptions, - resolver?: T | Resolver, -): Promise => { - if (typeof resolver === "function") { - return (resolver as Resolver)(options) - } - return resolver -} - -export const getHeaders = async ( - config: OpenAPIConfig, - options: ApiRequestOptions, -): Promise> => { - const [token, username, password, additionalHeaders] = await Promise.all([ - resolve(options, config.TOKEN), - resolve(options, config.USERNAME), - resolve(options, config.PASSWORD), - resolve(options, config.HEADERS), - ]) - - const headers = Object.entries({ - Accept: "application/json", - ...additionalHeaders, - ...options.headers, - }) - .filter(([, value]) => value !== undefined && value !== null) - .reduce( - (headers, [key, value]) => ({ - ...headers, - [key]: String(value), - }), - {} as Record, - ) - - if (isStringWithValue(token)) { - headers["Authorization"] = `Bearer ${token}` - } - - if (isStringWithValue(username) && isStringWithValue(password)) { - const credentials = base64(`${username}:${password}`) - headers["Authorization"] = `Basic ${credentials}` - } - - if (options.body !== undefined) { - if (options.mediaType) { - headers["Content-Type"] = options.mediaType - } else if (isBlob(options.body)) { - headers["Content-Type"] = options.body.type || "application/octet-stream" - } else if (isString(options.body)) { - headers["Content-Type"] = "text/plain" - } else if (!isFormData(options.body)) { - headers["Content-Type"] = "application/json" - } - } else if (options.formData !== undefined) { - if (options.mediaType) { - headers["Content-Type"] = options.mediaType - } - } - - return headers -} - -export const getRequestBody = (options: ApiRequestOptions): unknown => { - if (options.body) { - return options.body - } - return undefined -} - -export const sendRequest = async ( - config: OpenAPIConfig, - options: ApiRequestOptions, - url: string, - body: unknown, - formData: FormData | undefined, - headers: Record, - onCancel: OnCancel, - axiosClient: AxiosInstance, -): Promise> => { - const controller = new AbortController() - - let requestConfig: AxiosRequestConfig = { - data: body ?? formData, - headers, - method: options.method, - signal: controller.signal, - url, - withCredentials: config.WITH_CREDENTIALS, - } - - onCancel(() => controller.abort()) - - for (const fn of config.interceptors.request._fns) { - requestConfig = await fn(requestConfig) - } - - try { - return await axiosClient.request(requestConfig) - } catch (error) { - const axiosError = error as AxiosError - if (axiosError.response) { - return axiosError.response - } - throw error - } -} - -export const getResponseHeader = ( - response: AxiosResponse, - responseHeader?: string, -): string | undefined => { - if (responseHeader) { - const content = response.headers[responseHeader] - if (isString(content)) { - return content - } - } - return undefined -} - -export const getResponseBody = (response: AxiosResponse): unknown => { - if (response.status !== 204) { - return response.data - } - return undefined -} - -export const catchErrorCodes = ( - options: ApiRequestOptions, - result: ApiResult, -): void => { - const errors: Record = { - 400: "Bad Request", - 401: "Unauthorized", - 402: "Payment Required", - 403: "Forbidden", - 404: "Not Found", - 405: "Method Not Allowed", - 406: "Not Acceptable", - 407: "Proxy Authentication Required", - 408: "Request Timeout", - 409: "Conflict", - 410: "Gone", - 411: "Length Required", - 412: "Precondition Failed", - 413: "Payload Too Large", - 414: "URI Too Long", - 415: "Unsupported Media Type", - 416: "Range Not Satisfiable", - 417: "Expectation Failed", - 418: "Im a teapot", - 421: "Misdirected Request", - 422: "Unprocessable Content", - 423: "Locked", - 424: "Failed Dependency", - 425: "Too Early", - 426: "Upgrade Required", - 428: "Precondition Required", - 429: "Too Many Requests", - 431: "Request Header Fields Too Large", - 451: "Unavailable For Legal Reasons", - 500: "Internal Server Error", - 501: "Not Implemented", - 502: "Bad Gateway", - 503: "Service Unavailable", - 504: "Gateway Timeout", - 505: "HTTP Version Not Supported", - 506: "Variant Also Negotiates", - 507: "Insufficient Storage", - 508: "Loop Detected", - 510: "Not Extended", - 511: "Network Authentication Required", - ...options.errors, - } - - const error = errors[result.status] - if (error) { - throw new ApiError(options, result, error) - } - - if (!result.ok) { - const errorStatus = result.status ?? "unknown" - const errorStatusText = result.statusText ?? "unknown" - const errorBody = (() => { - try { - return JSON.stringify(result.body, null, 2) - } catch (e) { - return undefined - } - })() - - throw new ApiError( - options, - result, - `Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`, - ) - } -} - -/** - * Request method - * @param config The OpenAPI configuration object - * @param options The request options from the service - * @param axiosClient The axios client instance to use - * @returns CancelablePromise - * @throws ApiError - */ -export const request = ( - config: OpenAPIConfig, - options: ApiRequestOptions, - axiosClient: AxiosInstance = axios, -): CancelablePromise => { - return new CancelablePromise(async (resolve, reject, onCancel) => { - try { - const url = getUrl(config, options) - const formData = getFormData(options) - const body = getRequestBody(options) - const headers = await getHeaders(config, options) - - if (!onCancel.isCancelled) { - let response = await sendRequest( - config, - options, - url, - body, - formData, - headers, - onCancel, - axiosClient, - ) - - for (const fn of config.interceptors.response._fns) { - response = await fn(response) - } - - const responseBody = getResponseBody(response) - const responseHeader = getResponseHeader( - response, - options.responseHeader, - ) - - const result: ApiResult = { - url, - ok: isSuccess(response.status), - status: response.status, - statusText: response.statusText, - body: responseHeader ?? responseBody, - } - - catchErrorCodes(options, result) - - resolve(result.body) - } - } catch (error) { - reject(error) - } - }) -} diff --git a/frontend/src/client/core/types.ts b/frontend/src/client/core/types.ts deleted file mode 100644 index 199c08d3df..0000000000 --- a/frontend/src/client/core/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ApiResult } from "./ApiResult" - -export type TResult = "body" | "raw" - -export type TApiResponse = Exclude< - T, - "raw" -> extends never - ? ApiResult - : ApiResult["body"] - -export type TConfig = { - _result?: T -} diff --git a/frontend/src/client/index.ts b/frontend/src/client/index.ts deleted file mode 100644 index adf1d0cabf..0000000000 --- a/frontend/src/client/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { ApiError } from "./core/ApiError" -export { CancelablePromise, CancelError } from "./core/CancelablePromise" -export { OpenAPI } from "./core/OpenAPI" -export type { OpenAPIConfig } from "./core/OpenAPI" - -export * from "./models" -export * from "./schemas" -export * from "./services" diff --git a/frontend/src/client/models.ts b/frontend/src/client/models.ts deleted file mode 100644 index 2c8074ddd6..0000000000 --- a/frontend/src/client/models.ts +++ /dev/null @@ -1,99 +0,0 @@ -export type Body_login_login_access_token = { - grant_type?: string | null - username: string - password: string - scope?: string - client_id?: string | null - client_secret?: string | null -} - -export type HTTPValidationError = { - detail?: Array -} - -export type ItemCreate = { - title: string - description?: string | null -} - -export type ItemPublic = { - title: string - description?: string | null - id: string - owner_id: string -} - -export type ItemUpdate = { - title?: string | null - description?: string | null -} - -export type ItemsPublic = { - data: Array - count: number -} - -export type Message = { - message: string -} - -export type NewPassword = { - token: string - new_password: string -} - -export type Token = { - access_token: string - token_type?: string -} - -export type UpdatePassword = { - current_password: string - new_password: string -} - -export type UserCreate = { - email: string - is_active?: boolean - is_superuser?: boolean - full_name?: string | null - password: string -} - -export type UserPublic = { - email: string - is_active?: boolean - is_superuser?: boolean - full_name?: string | null - id: string -} - -export type UserRegister = { - email: string - password: string - full_name?: string | null -} - -export type UserUpdate = { - email?: string | null - is_active?: boolean - is_superuser?: boolean - full_name?: string | null - password?: string | null -} - -export type UserUpdateMe = { - full_name?: string | null - email?: string | null -} - -export type UsersPublic = { - data: Array - count: number -} - -export type ValidationError = { - loc: Array - msg: string - type: string -} diff --git a/frontend/src/client/schemas.ts b/frontend/src/client/schemas.ts deleted file mode 100644 index 9e92efd106..0000000000 --- a/frontend/src/client/schemas.ts +++ /dev/null @@ -1,444 +0,0 @@ -export const $Body_login_login_access_token = { - properties: { - grant_type: { - type: "any-of", - contains: [ - { - type: "string", - pattern: "password", - }, - { - type: "null", - }, - ], - }, - username: { - type: "string", - isRequired: true, - }, - password: { - type: "string", - isRequired: true, - }, - scope: { - type: "string", - default: "", - }, - client_id: { - type: "any-of", - contains: [ - { - type: "string", - }, - { - type: "null", - }, - ], - }, - client_secret: { - type: "any-of", - contains: [ - { - type: "string", - }, - { - type: "null", - }, - ], - }, - }, -} as const - -export const $HTTPValidationError = { - properties: { - detail: { - type: "array", - contains: { - type: "ValidationError", - }, - }, - }, -} as const - -export const $ItemCreate = { - properties: { - title: { - type: "string", - isRequired: true, - maxLength: 255, - minLength: 1, - }, - description: { - type: "any-of", - contains: [ - { - type: "string", - maxLength: 255, - }, - { - type: "null", - }, - ], - }, - }, -} as const - -export const $ItemPublic = { - properties: { - title: { - type: "string", - isRequired: true, - maxLength: 255, - minLength: 1, - }, - description: { - type: "any-of", - contains: [ - { - type: "string", - maxLength: 255, - }, - { - type: "null", - }, - ], - }, - id: { - type: "string", - isRequired: true, - format: "uuid", - }, - owner_id: { - type: "string", - isRequired: true, - format: "uuid", - }, - }, -} as const - -export const $ItemUpdate = { - properties: { - title: { - type: "any-of", - contains: [ - { - type: "string", - maxLength: 255, - minLength: 1, - }, - { - type: "null", - }, - ], - }, - description: { - type: "any-of", - contains: [ - { - type: "string", - maxLength: 255, - }, - { - type: "null", - }, - ], - }, - }, -} as const - -export const $ItemsPublic = { - properties: { - data: { - type: "array", - contains: { - type: "ItemPublic", - }, - isRequired: true, - }, - count: { - type: "number", - isRequired: true, - }, - }, -} as const - -export const $Message = { - properties: { - message: { - type: "string", - isRequired: true, - }, - }, -} as const - -export const $NewPassword = { - properties: { - token: { - type: "string", - isRequired: true, - }, - new_password: { - type: "string", - isRequired: true, - maxLength: 40, - minLength: 8, - }, - }, -} as const - -export const $Token = { - properties: { - access_token: { - type: "string", - isRequired: true, - }, - token_type: { - type: "string", - default: "bearer", - }, - }, -} as const - -export const $UpdatePassword = { - properties: { - current_password: { - type: "string", - isRequired: true, - maxLength: 40, - minLength: 8, - }, - new_password: { - type: "string", - isRequired: true, - maxLength: 40, - minLength: 8, - }, - }, -} as const - -export const $UserCreate = { - properties: { - email: { - type: "string", - isRequired: true, - format: "email", - maxLength: 255, - }, - is_active: { - type: "boolean", - default: true, - }, - is_superuser: { - type: "boolean", - default: false, - }, - full_name: { - type: "any-of", - contains: [ - { - type: "string", - maxLength: 255, - }, - { - type: "null", - }, - ], - }, - password: { - type: "string", - isRequired: true, - maxLength: 40, - minLength: 8, - }, - }, -} as const - -export const $UserPublic = { - properties: { - email: { - type: "string", - isRequired: true, - format: "email", - maxLength: 255, - }, - is_active: { - type: "boolean", - default: true, - }, - is_superuser: { - type: "boolean", - default: false, - }, - full_name: { - type: "any-of", - contains: [ - { - type: "string", - maxLength: 255, - }, - { - type: "null", - }, - ], - }, - id: { - type: "string", - isRequired: true, - format: "uuid", - }, - }, -} as const - -export const $UserRegister = { - properties: { - email: { - type: "string", - isRequired: true, - format: "email", - maxLength: 255, - }, - password: { - type: "string", - isRequired: true, - maxLength: 40, - minLength: 8, - }, - full_name: { - type: "any-of", - contains: [ - { - type: "string", - maxLength: 255, - }, - { - type: "null", - }, - ], - }, - }, -} as const - -export const $UserUpdate = { - properties: { - email: { - type: "any-of", - contains: [ - { - type: "string", - format: "email", - maxLength: 255, - }, - { - type: "null", - }, - ], - }, - is_active: { - type: "boolean", - default: true, - }, - is_superuser: { - type: "boolean", - default: false, - }, - full_name: { - type: "any-of", - contains: [ - { - type: "string", - maxLength: 255, - }, - { - type: "null", - }, - ], - }, - password: { - type: "any-of", - contains: [ - { - type: "string", - maxLength: 40, - minLength: 8, - }, - { - type: "null", - }, - ], - }, - }, -} as const - -export const $UserUpdateMe = { - properties: { - full_name: { - type: "any-of", - contains: [ - { - type: "string", - maxLength: 255, - }, - { - type: "null", - }, - ], - }, - email: { - type: "any-of", - contains: [ - { - type: "string", - format: "email", - maxLength: 255, - }, - { - type: "null", - }, - ], - }, - }, -} as const - -export const $UsersPublic = { - properties: { - data: { - type: "array", - contains: { - type: "UserPublic", - }, - isRequired: true, - }, - count: { - type: "number", - isRequired: true, - }, - }, -} as const - -export const $ValidationError = { - properties: { - loc: { - type: "array", - contains: { - type: "any-of", - contains: [ - { - type: "string", - }, - { - type: "number", - }, - ], - }, - isRequired: true, - }, - msg: { - type: "string", - isRequired: true, - }, - type: { - type: "string", - isRequired: true, - }, - }, -} as const diff --git a/frontend/src/client/services.ts b/frontend/src/client/services.ts deleted file mode 100644 index b99e4ac515..0000000000 --- a/frontend/src/client/services.ts +++ /dev/null @@ -1,517 +0,0 @@ -import type { CancelablePromise } from "./core/CancelablePromise" -import { OpenAPI } from "./core/OpenAPI" -import { request as __request } from "./core/request" - -import type { - Body_login_login_access_token, - Message, - NewPassword, - Token, - UserPublic, - UpdatePassword, - UserCreate, - UserRegister, - UsersPublic, - UserUpdate, - UserUpdateMe, - ItemCreate, - ItemPublic, - ItemsPublic, - ItemUpdate, -} from "./models" - -export type TDataLoginAccessToken = { - formData: Body_login_login_access_token -} -export type TDataRecoverPassword = { - email: string -} -export type TDataResetPassword = { - requestBody: NewPassword -} -export type TDataRecoverPasswordHtmlContent = { - email: string -} - -export class LoginService { - /** - * Login Access Token - * OAuth2 compatible token login, get an access token for future requests - * @returns Token Successful Response - * @throws ApiError - */ - public static loginAccessToken( - data: TDataLoginAccessToken, - ): CancelablePromise { - const { formData } = data - return __request(OpenAPI, { - method: "POST", - url: "/api/v1/login/access-token", - formData: formData, - mediaType: "application/x-www-form-urlencoded", - errors: { - 422: `Validation Error`, - }, - }) - } - - /** - * Test Token - * Test access token - * @returns UserPublic Successful Response - * @throws ApiError - */ - public static testToken(): CancelablePromise { - return __request(OpenAPI, { - method: "POST", - url: "/api/v1/login/test-token", - }) - } - - /** - * Recover Password - * Password Recovery - * @returns Message Successful Response - * @throws ApiError - */ - public static recoverPassword( - data: TDataRecoverPassword, - ): CancelablePromise { - const { email } = data - return __request(OpenAPI, { - method: "POST", - url: "/api/v1/password-recovery/{email}", - path: { - email, - }, - errors: { - 422: `Validation Error`, - }, - }) - } - - /** - * Reset Password - * Reset password - * @returns Message Successful Response - * @throws ApiError - */ - public static resetPassword( - data: TDataResetPassword, - ): CancelablePromise { - const { requestBody } = data - return __request(OpenAPI, { - method: "POST", - url: "/api/v1/reset-password/", - body: requestBody, - mediaType: "application/json", - errors: { - 422: `Validation Error`, - }, - }) - } - - /** - * Recover Password Html Content - * HTML Content for Password Recovery - * @returns string Successful Response - * @throws ApiError - */ - public static recoverPasswordHtmlContent( - data: TDataRecoverPasswordHtmlContent, - ): CancelablePromise { - const { email } = data - return __request(OpenAPI, { - method: "POST", - url: "/api/v1/password-recovery-html-content/{email}", - path: { - email, - }, - errors: { - 422: `Validation Error`, - }, - }) - } -} - -export type TDataReadUsers = { - limit?: number - skip?: number -} -export type TDataCreateUser = { - requestBody: UserCreate -} -export type TDataUpdateUserMe = { - requestBody: UserUpdateMe -} -export type TDataUpdatePasswordMe = { - requestBody: UpdatePassword -} -export type TDataRegisterUser = { - requestBody: UserRegister -} -export type TDataReadUserById = { - userId: string -} -export type TDataUpdateUser = { - requestBody: UserUpdate - userId: string -} -export type TDataDeleteUser = { - userId: string -} - -export class UsersService { - /** - * Read Users - * Retrieve users. - * @returns UsersPublic Successful Response - * @throws ApiError - */ - public static readUsers( - data: TDataReadUsers = {}, - ): CancelablePromise { - const { limit = 100, skip = 0 } = data - return __request(OpenAPI, { - method: "GET", - url: "/api/v1/users/", - query: { - skip, - limit, - }, - errors: { - 422: `Validation Error`, - }, - }) - } - - /** - * Create User - * Create new user. - * @returns UserPublic Successful Response - * @throws ApiError - */ - public static createUser( - data: TDataCreateUser, - ): CancelablePromise { - const { requestBody } = data - return __request(OpenAPI, { - method: "POST", - url: "/api/v1/users/", - body: requestBody, - mediaType: "application/json", - errors: { - 422: `Validation Error`, - }, - }) - } - - /** - * Read User Me - * Get current user. - * @returns UserPublic Successful Response - * @throws ApiError - */ - public static readUserMe(): CancelablePromise { - return __request(OpenAPI, { - method: "GET", - url: "/api/v1/users/me", - }) - } - - /** - * Delete User Me - * Delete own user. - * @returns Message Successful Response - * @throws ApiError - */ - public static deleteUserMe(): CancelablePromise { - return __request(OpenAPI, { - method: "DELETE", - url: "/api/v1/users/me", - }) - } - - /** - * Update User Me - * Update own user. - * @returns UserPublic Successful Response - * @throws ApiError - */ - public static updateUserMe( - data: TDataUpdateUserMe, - ): CancelablePromise { - const { requestBody } = data - return __request(OpenAPI, { - method: "PATCH", - url: "/api/v1/users/me", - body: requestBody, - mediaType: "application/json", - errors: { - 422: `Validation Error`, - }, - }) - } - - /** - * Update Password Me - * Update own password. - * @returns Message Successful Response - * @throws ApiError - */ - public static updatePasswordMe( - data: TDataUpdatePasswordMe, - ): CancelablePromise { - const { requestBody } = data - return __request(OpenAPI, { - method: "PATCH", - url: "/api/v1/users/me/password", - body: requestBody, - mediaType: "application/json", - errors: { - 422: `Validation Error`, - }, - }) - } - - /** - * Register User - * Create new user without the need to be logged in. - * @returns UserPublic Successful Response - * @throws ApiError - */ - public static registerUser( - data: TDataRegisterUser, - ): CancelablePromise { - const { requestBody } = data - return __request(OpenAPI, { - method: "POST", - url: "/api/v1/users/signup", - body: requestBody, - mediaType: "application/json", - errors: { - 422: `Validation Error`, - }, - }) - } - - /** - * Read User By Id - * Get a specific user by id. - * @returns UserPublic Successful Response - * @throws ApiError - */ - public static readUserById( - data: TDataReadUserById, - ): CancelablePromise { - const { userId } = data - return __request(OpenAPI, { - method: "GET", - url: "/api/v1/users/{user_id}", - path: { - user_id: userId, - }, - errors: { - 422: `Validation Error`, - }, - }) - } - - /** - * Update User - * Update a user. - * @returns UserPublic Successful Response - * @throws ApiError - */ - public static updateUser( - data: TDataUpdateUser, - ): CancelablePromise { - const { requestBody, userId } = data - return __request(OpenAPI, { - method: "PATCH", - url: "/api/v1/users/{user_id}", - path: { - user_id: userId, - }, - body: requestBody, - mediaType: "application/json", - errors: { - 422: `Validation Error`, - }, - }) - } - - /** - * Delete User - * Delete a user. - * @returns Message Successful Response - * @throws ApiError - */ - public static deleteUser(data: TDataDeleteUser): CancelablePromise { - const { userId } = data - return __request(OpenAPI, { - method: "DELETE", - url: "/api/v1/users/{user_id}", - path: { - user_id: userId, - }, - errors: { - 422: `Validation Error`, - }, - }) - } -} - -export type TDataTestEmail = { - emailTo: string -} - -export class UtilsService { - /** - * Test Email - * Test emails. - * @returns Message Successful Response - * @throws ApiError - */ - public static testEmail(data: TDataTestEmail): CancelablePromise { - const { emailTo } = data - return __request(OpenAPI, { - method: "POST", - url: "/api/v1/utils/test-email/", - query: { - email_to: emailTo, - }, - errors: { - 422: `Validation Error`, - }, - }) - } -} - -export type TDataReadItems = { - limit?: number - skip?: number -} -export type TDataCreateItem = { - requestBody: ItemCreate -} -export type TDataReadItem = { - id: string -} -export type TDataUpdateItem = { - id: string - requestBody: ItemUpdate -} -export type TDataDeleteItem = { - id: string -} - -export class ItemsService { - /** - * Read Items - * Retrieve items. - * @returns ItemsPublic Successful Response - * @throws ApiError - */ - public static readItems( - data: TDataReadItems = {}, - ): CancelablePromise { - const { limit = 100, skip = 0 } = data - return __request(OpenAPI, { - method: "GET", - url: "/api/v1/items/", - query: { - skip, - limit, - }, - errors: { - 422: `Validation Error`, - }, - }) - } - - /** - * Create Item - * Create new item. - * @returns ItemPublic Successful Response - * @throws ApiError - */ - public static createItem( - data: TDataCreateItem, - ): CancelablePromise { - const { requestBody } = data - return __request(OpenAPI, { - method: "POST", - url: "/api/v1/items/", - body: requestBody, - mediaType: "application/json", - errors: { - 422: `Validation Error`, - }, - }) - } - - /** - * Read Item - * Get item by ID. - * @returns ItemPublic Successful Response - * @throws ApiError - */ - public static readItem(data: TDataReadItem): CancelablePromise { - const { id } = data - return __request(OpenAPI, { - method: "GET", - url: "/api/v1/items/{id}", - path: { - id, - }, - errors: { - 422: `Validation Error`, - }, - }) - } - - /** - * Update Item - * Update an item. - * @returns ItemPublic Successful Response - * @throws ApiError - */ - public static updateItem( - data: TDataUpdateItem, - ): CancelablePromise { - const { id, requestBody } = data - return __request(OpenAPI, { - method: "PUT", - url: "/api/v1/items/{id}", - path: { - id, - }, - body: requestBody, - mediaType: "application/json", - errors: { - 422: `Validation Error`, - }, - }) - } - - /** - * Delete Item - * Delete an item. - * @returns Message Successful Response - * @throws ApiError - */ - public static deleteItem(data: TDataDeleteItem): CancelablePromise { - const { id } = data - return __request(OpenAPI, { - method: "DELETE", - url: "/api/v1/items/{id}", - path: { - id, - }, - errors: { - 422: `Validation Error`, - }, - }) - } -} diff --git a/frontend/src/components/Admin/AddUser.tsx b/frontend/src/components/Admin/AddUser.tsx deleted file mode 100644 index a24a18a78e..0000000000 --- a/frontend/src/components/Admin/AddUser.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { - Button, - Checkbox, - Flex, - FormControl, - FormErrorMessage, - FormLabel, - Input, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, -} from "@chakra-ui/react" -import { useMutation, useQueryClient } from "@tanstack/react-query" -import { type SubmitHandler, useForm } from "react-hook-form" - -import { type UserCreate, UsersService } from "../../client" -import type { ApiError } from "../../client/core/ApiError" -import useCustomToast from "../../hooks/useCustomToast" -import { emailPattern, handleError } from "../../utils" - -interface AddUserProps { - isOpen: boolean - onClose: () => void -} - -interface UserCreateForm extends UserCreate { - confirm_password: string -} - -const AddUser = ({ isOpen, onClose }: AddUserProps) => { - const queryClient = useQueryClient() - const showToast = useCustomToast() - const { - register, - handleSubmit, - reset, - getValues, - formState: { errors, isSubmitting }, - } = useForm({ - mode: "onBlur", - criteriaMode: "all", - defaultValues: { - email: "", - full_name: "", - password: "", - confirm_password: "", - is_superuser: false, - is_active: false, - }, - }) - - const mutation = useMutation({ - mutationFn: (data: UserCreate) => - UsersService.createUser({ requestBody: data }), - onSuccess: () => { - showToast("Success!", "User created successfully.", "success") - reset() - onClose() - }, - onError: (err: ApiError) => { - handleError(err, showToast) - }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ["users"] }) - }, - }) - - const onSubmit: SubmitHandler = (data) => { - mutation.mutate(data) - } - - return ( - <> - - - - Add User - - - - Email - - {errors.email && ( - {errors.email.message} - )} - - - Full name - - {errors.full_name && ( - {errors.full_name.message} - )} - - - Set Password - - {errors.password && ( - {errors.password.message} - )} - - - Confirm Password - - value === getValues().password || - "The passwords do not match", - })} - placeholder="Password" - type="password" - /> - {errors.confirm_password && ( - - {errors.confirm_password.message} - - )} - - - - - Is superuser? - - - - - Is active? - - - - - - - - - - - - ) -} - -export default AddUser diff --git a/frontend/src/components/Admin/EditUser.tsx b/frontend/src/components/Admin/EditUser.tsx deleted file mode 100644 index d7885ab174..0000000000 --- a/frontend/src/components/Admin/EditUser.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { - Button, - Checkbox, - Flex, - FormControl, - FormErrorMessage, - FormLabel, - Input, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, -} from "@chakra-ui/react" -import { useMutation, useQueryClient } from "@tanstack/react-query" -import { type SubmitHandler, useForm } from "react-hook-form" - -import { - type ApiError, - type UserPublic, - type UserUpdate, - UsersService, -} from "../../client" -import useCustomToast from "../../hooks/useCustomToast" -import { emailPattern, handleError } from "../../utils" - -interface EditUserProps { - user: UserPublic - isOpen: boolean - onClose: () => void -} - -interface UserUpdateForm extends UserUpdate { - confirm_password: string -} - -const EditUser = ({ user, isOpen, onClose }: EditUserProps) => { - const queryClient = useQueryClient() - const showToast = useCustomToast() - - const { - register, - handleSubmit, - reset, - getValues, - formState: { errors, isSubmitting, isDirty }, - } = useForm({ - mode: "onBlur", - criteriaMode: "all", - defaultValues: user, - }) - - const mutation = useMutation({ - mutationFn: (data: UserUpdateForm) => - UsersService.updateUser({ userId: user.id, requestBody: data }), - onSuccess: () => { - showToast("Success!", "User updated successfully.", "success") - onClose() - }, - onError: (err: ApiError) => { - handleError(err, showToast) - }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ["users"] }) - }, - }) - - const onSubmit: SubmitHandler = async (data) => { - if (data.password === "") { - data.password = undefined - } - mutation.mutate(data) - } - - const onCancel = () => { - reset() - onClose() - } - - return ( - <> - - - - Edit User - - - - Email - - {errors.email && ( - {errors.email.message} - )} - - - Full name - - - - Set Password - - {errors.password && ( - {errors.password.message} - )} - - - Confirm Password - - value === getValues().password || - "The passwords do not match", - })} - placeholder="Password" - type="password" - /> - {errors.confirm_password && ( - - {errors.confirm_password.message} - - )} - - - - - Is superuser? - - - - - Is active? - - - - - - - - - - - - - ) -} - -export default EditUser diff --git a/frontend/src/components/Common/ActionsMenu.tsx b/frontend/src/components/Common/ActionsMenu.tsx deleted file mode 100644 index 4ff94ee3ea..0000000000 --- a/frontend/src/components/Common/ActionsMenu.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { - Button, - Menu, - MenuButton, - MenuItem, - MenuList, - useDisclosure, -} from "@chakra-ui/react" -import { BsThreeDotsVertical } from "react-icons/bs" -import { FiEdit, FiTrash } from "react-icons/fi" - -import type { ItemPublic, UserPublic } from "../../client" -import EditUser from "../Admin/EditUser" -import EditItem from "../Items/EditItem" -import Delete from "./DeleteAlert" - -interface ActionsMenuProps { - type: string - value: ItemPublic | UserPublic - disabled?: boolean -} - -const ActionsMenu = ({ type, value, disabled }: ActionsMenuProps) => { - const editUserModal = useDisclosure() - const deleteModal = useDisclosure() - - return ( - <> - - } - variant="unstyled" - /> - - } - > - Edit {type} - - } - color="ui.danger" - > - Delete {type} - - - {type === "User" ? ( - - ) : ( - - )} - - - - ) -} - -export default ActionsMenu diff --git a/frontend/src/components/Common/DeleteAlert.tsx b/frontend/src/components/Common/DeleteAlert.tsx deleted file mode 100644 index 1528fc5fe1..0000000000 --- a/frontend/src/components/Common/DeleteAlert.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { - AlertDialog, - AlertDialogBody, - AlertDialogContent, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogOverlay, - Button, -} from "@chakra-ui/react" -import { useMutation, useQueryClient } from "@tanstack/react-query" -import React from "react" -import { useForm } from "react-hook-form" - -import { ItemsService, UsersService } from "../../client" -import useCustomToast from "../../hooks/useCustomToast" - -interface DeleteProps { - type: string - id: string - isOpen: boolean - onClose: () => void -} - -const Delete = ({ type, id, isOpen, onClose }: DeleteProps) => { - const queryClient = useQueryClient() - const showToast = useCustomToast() - const cancelRef = React.useRef(null) - const { - handleSubmit, - formState: { isSubmitting }, - } = useForm() - - const deleteEntity = async (id: string) => { - if (type === "Item") { - await ItemsService.deleteItem({ id: id }) - } else if (type === "User") { - await UsersService.deleteUser({ userId: id }) - } else { - throw new Error(`Unexpected type: ${type}`) - } - } - - const mutation = useMutation({ - mutationFn: deleteEntity, - onSuccess: () => { - showToast( - "Success", - `The ${type.toLowerCase()} was deleted successfully.`, - "success", - ) - onClose() - }, - onError: () => { - showToast( - "An error occurred.", - `An error occurred while deleting the ${type.toLowerCase()}.`, - "error", - ) - }, - onSettled: () => { - queryClient.invalidateQueries({ - queryKey: [type === "Item" ? "items" : "users"], - }) - }, - }) - - const onSubmit = async () => { - mutation.mutate(id) - } - - return ( - <> - - - - Delete {type} - - - {type === "User" && ( - - All items associated with this user will also be{" "} - permantly deleted. - - )} - Are you sure? You will not be able to undo this action. - - - - - - - - - - - ) -} - -export default Delete diff --git a/frontend/src/components/Common/Navbar.tsx b/frontend/src/components/Common/Navbar.tsx deleted file mode 100644 index 2aba31c362..0000000000 --- a/frontend/src/components/Common/Navbar.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import type { ComponentType, ElementType } from "react" - -import { Button, Flex, Icon, useDisclosure } from "@chakra-ui/react" -import { FaPlus } from "react-icons/fa" - -interface NavbarProps { - type: string - addModalAs: ComponentType | ElementType -} - -const Navbar = ({ type, addModalAs }: NavbarProps) => { - const addModal = useDisclosure() - - const AddModal = addModalAs - return ( - <> - - {/* TODO: Complete search functionality */} - {/* - - - - - */} - - - - - ) -} - -export default Navbar diff --git a/frontend/src/components/Common/NotFound.tsx b/frontend/src/components/Common/NotFound.tsx deleted file mode 100644 index 66ea559c01..0000000000 --- a/frontend/src/components/Common/NotFound.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Button, Container, Text } from "@chakra-ui/react" -import { Link } from "@tanstack/react-router" - -const NotFound = () => { - return ( - <> - - - 404 - - Oops! - Page not found. - - - - ) -} - -export default NotFound diff --git a/frontend/src/components/Common/Sidebar.tsx b/frontend/src/components/Common/Sidebar.tsx deleted file mode 100644 index 3cc522cc57..0000000000 --- a/frontend/src/components/Common/Sidebar.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { - Box, - Drawer, - DrawerBody, - DrawerCloseButton, - DrawerContent, - DrawerOverlay, - Flex, - IconButton, - Image, - Text, - useColorModeValue, - useDisclosure, -} from "@chakra-ui/react" -import { useQueryClient } from "@tanstack/react-query" -import { FiLogOut, FiMenu } from "react-icons/fi" - -import Logo from "/assets/images/fastapi-logo.svg" -import type { UserPublic } from "../../client" -import useAuth from "../../hooks/useAuth" -import SidebarItems from "./SidebarItems" - -const Sidebar = () => { - const queryClient = useQueryClient() - const bgColor = useColorModeValue("ui.light", "ui.dark") - const textColor = useColorModeValue("ui.dark", "ui.light") - const secBgColor = useColorModeValue("ui.secondary", "ui.darkSlate") - const currentUser = queryClient.getQueryData(["currentUser"]) - const { isOpen, onOpen, onClose } = useDisclosure() - const { logout } = useAuth() - - const handleLogout = async () => { - logout() - } - - return ( - <> - {/* Mobile */} - } - /> - - - - - - - - logo - - - - Log out - - - {currentUser?.email && ( - - Logged in as: {currentUser.email} - - )} - - - - - - {/* Desktop */} - - - - Logo - - - {currentUser?.email && ( - - Logged in as: {currentUser.email} - - )} - - - - ) -} - -export default Sidebar diff --git a/frontend/src/components/Common/SidebarItems.tsx b/frontend/src/components/Common/SidebarItems.tsx deleted file mode 100644 index 929e8f785e..0000000000 --- a/frontend/src/components/Common/SidebarItems.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Box, Flex, Icon, Text, useColorModeValue } from "@chakra-ui/react" -import { useQueryClient } from "@tanstack/react-query" -import { Link } from "@tanstack/react-router" -import { FiBriefcase, FiHome, FiSettings, FiUsers } from "react-icons/fi" - -import type { UserPublic } from "../../client" - -const items = [ - { icon: FiHome, title: "Dashboard", path: "/" }, - { icon: FiBriefcase, title: "Items", path: "/items" }, - { icon: FiSettings, title: "User Settings", path: "/settings" }, -] - -interface SidebarItemsProps { - onClose?: () => void -} - -const SidebarItems = ({ onClose }: SidebarItemsProps) => { - const queryClient = useQueryClient() - const textColor = useColorModeValue("ui.main", "ui.light") - const bgActive = useColorModeValue("#E2E8F0", "#4A5568") - const currentUser = queryClient.getQueryData(["currentUser"]) - - const finalItems = currentUser?.is_superuser - ? [...items, { icon: FiUsers, title: "Admin", path: "/admin" }] - : items - - const listItems = finalItems.map(({ icon, title, path }) => ( - - - {title} - - )) - - return ( - <> - {listItems} - - ) -} - -export default SidebarItems diff --git a/frontend/src/components/Common/UserMenu.tsx b/frontend/src/components/Common/UserMenu.tsx deleted file mode 100644 index e3d54ac26b..0000000000 --- a/frontend/src/components/Common/UserMenu.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { - Box, - IconButton, - Menu, - MenuButton, - MenuItem, - MenuList, -} from "@chakra-ui/react" -import { Link } from "@tanstack/react-router" -import { FaUserAstronaut } from "react-icons/fa" -import { FiLogOut, FiUser } from "react-icons/fi" - -import useAuth from "../../hooks/useAuth" - -const UserMenu = () => { - const { logout } = useAuth() - - const handleLogout = async () => { - logout() - } - - return ( - <> - {/* Desktop */} - - - } - bg="ui.main" - isRound - data-testid="user-menu" - /> - - } as={Link} to="settings"> - My profile - - } - onClick={handleLogout} - color="ui.danger" - fontWeight="bold" - > - Log out - - - - - - ) -} - -export default UserMenu diff --git a/frontend/src/components/Items/AddItem.tsx b/frontend/src/components/Items/AddItem.tsx deleted file mode 100644 index fa5682da3f..0000000000 --- a/frontend/src/components/Items/AddItem.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { - Button, - FormControl, - FormErrorMessage, - FormLabel, - Input, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, -} from "@chakra-ui/react" -import { useMutation, useQueryClient } from "@tanstack/react-query" -import { type SubmitHandler, useForm } from "react-hook-form" - -import { type ApiError, type ItemCreate, ItemsService } from "../../client" -import useCustomToast from "../../hooks/useCustomToast" -import { handleError } from "../../utils" - -interface AddItemProps { - isOpen: boolean - onClose: () => void -} - -const AddItem = ({ isOpen, onClose }: AddItemProps) => { - const queryClient = useQueryClient() - const showToast = useCustomToast() - const { - register, - handleSubmit, - reset, - formState: { errors, isSubmitting }, - } = useForm({ - mode: "onBlur", - criteriaMode: "all", - defaultValues: { - title: "", - description: "", - }, - }) - - const mutation = useMutation({ - mutationFn: (data: ItemCreate) => - ItemsService.createItem({ requestBody: data }), - onSuccess: () => { - showToast("Success!", "Item created successfully.", "success") - reset() - onClose() - }, - onError: (err: ApiError) => { - handleError(err, showToast) - }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ["items"] }) - }, - }) - - const onSubmit: SubmitHandler = (data) => { - mutation.mutate(data) - } - - return ( - <> - - - - Add Item - - - - Title - - {errors.title && ( - {errors.title.message} - )} - - - Description - - - - - - - - - - - - ) -} - -export default AddItem diff --git a/frontend/src/components/Items/EditItem.tsx b/frontend/src/components/Items/EditItem.tsx deleted file mode 100644 index 3d40cdc03a..0000000000 --- a/frontend/src/components/Items/EditItem.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { - Button, - FormControl, - FormErrorMessage, - FormLabel, - Input, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, -} from "@chakra-ui/react" -import { useMutation, useQueryClient } from "@tanstack/react-query" -import { type SubmitHandler, useForm } from "react-hook-form" - -import { - type ApiError, - type ItemPublic, - type ItemUpdate, - ItemsService, -} from "../../client" -import useCustomToast from "../../hooks/useCustomToast" -import { handleError } from "../../utils" - -interface EditItemProps { - item: ItemPublic - isOpen: boolean - onClose: () => void -} - -const EditItem = ({ item, isOpen, onClose }: EditItemProps) => { - const queryClient = useQueryClient() - const showToast = useCustomToast() - const { - register, - handleSubmit, - reset, - formState: { isSubmitting, errors, isDirty }, - } = useForm({ - mode: "onBlur", - criteriaMode: "all", - defaultValues: item, - }) - - const mutation = useMutation({ - mutationFn: (data: ItemUpdate) => - ItemsService.updateItem({ id: item.id, requestBody: data }), - onSuccess: () => { - showToast("Success!", "Item updated successfully.", "success") - onClose() - }, - onError: (err: ApiError) => { - handleError(err, showToast) - }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ["items"] }) - }, - }) - - const onSubmit: SubmitHandler = async (data) => { - mutation.mutate(data) - } - - const onCancel = () => { - reset() - onClose() - } - - return ( - <> - - - - Edit Item - - - - Title - - {errors.title && ( - {errors.title.message} - )} - - - Description - - - - - - - - - - - ) -} - -export default EditItem diff --git a/frontend/src/components/UserSettings/Appearance.tsx b/frontend/src/components/UserSettings/Appearance.tsx deleted file mode 100644 index a2ab4b0a60..0000000000 --- a/frontend/src/components/UserSettings/Appearance.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { - Badge, - Container, - Heading, - Radio, - RadioGroup, - Stack, - useColorMode, -} from "@chakra-ui/react" - -const Appearance = () => { - const { colorMode, toggleColorMode } = useColorMode() - - return ( - <> - - - Appearance - - - - {/* TODO: Add system default option */} - - Light Mode - - Default - - - - Dark Mode - - - - - - ) -} -export default Appearance diff --git a/frontend/src/components/UserSettings/ChangePassword.tsx b/frontend/src/components/UserSettings/ChangePassword.tsx deleted file mode 100644 index 73217939fc..0000000000 --- a/frontend/src/components/UserSettings/ChangePassword.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { - Box, - Button, - Container, - FormControl, - FormErrorMessage, - FormLabel, - Heading, - Input, - useColorModeValue, -} from "@chakra-ui/react" -import { useMutation } from "@tanstack/react-query" -import { type SubmitHandler, useForm } from "react-hook-form" - -import { type ApiError, type UpdatePassword, UsersService } from "../../client" -import useCustomToast from "../../hooks/useCustomToast" -import { confirmPasswordRules, handleError, passwordRules } from "../../utils" - -interface UpdatePasswordForm extends UpdatePassword { - confirm_password: string -} - -const ChangePassword = () => { - const color = useColorModeValue("inherit", "ui.light") - const showToast = useCustomToast() - const { - register, - handleSubmit, - reset, - getValues, - formState: { errors, isSubmitting }, - } = useForm({ - mode: "onBlur", - criteriaMode: "all", - }) - - const mutation = useMutation({ - mutationFn: (data: UpdatePassword) => - UsersService.updatePasswordMe({ requestBody: data }), - onSuccess: () => { - showToast("Success!", "Password updated successfully.", "success") - reset() - }, - onError: (err: ApiError) => { - handleError(err, showToast) - }, - }) - - const onSubmit: SubmitHandler = async (data) => { - mutation.mutate(data) - } - - return ( - <> - - - Change Password - - - - - Current Password - - - {errors.current_password && ( - - {errors.current_password.message} - - )} - - - Set Password - - {errors.new_password && ( - {errors.new_password.message} - )} - - - Confirm Password - - {errors.confirm_password && ( - - {errors.confirm_password.message} - - )} - - - - - - ) -} -export default ChangePassword diff --git a/frontend/src/components/UserSettings/DeleteAccount.tsx b/frontend/src/components/UserSettings/DeleteAccount.tsx deleted file mode 100644 index 7ca3b92c95..0000000000 --- a/frontend/src/components/UserSettings/DeleteAccount.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { - Button, - Container, - Heading, - Text, - useDisclosure, -} from "@chakra-ui/react" - -import DeleteConfirmation from "./DeleteConfirmation" - -const DeleteAccount = () => { - const confirmationModal = useDisclosure() - - return ( - <> - - - Delete Account - - - Permanently delete your data and everything associated with your - account. - - - - - - ) -} -export default DeleteAccount diff --git a/frontend/src/components/UserSettings/DeleteConfirmation.tsx b/frontend/src/components/UserSettings/DeleteConfirmation.tsx deleted file mode 100644 index 5bbdcdd6c7..0000000000 --- a/frontend/src/components/UserSettings/DeleteConfirmation.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { - AlertDialog, - AlertDialogBody, - AlertDialogContent, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogOverlay, - Button, -} from "@chakra-ui/react" -import { useMutation, useQueryClient } from "@tanstack/react-query" -import React from "react" -import { useForm } from "react-hook-form" - -import { type ApiError, UsersService } from "../../client" -import useAuth from "../../hooks/useAuth" -import useCustomToast from "../../hooks/useCustomToast" -import { handleError } from "../../utils" - -interface DeleteProps { - isOpen: boolean - onClose: () => void -} - -const DeleteConfirmation = ({ isOpen, onClose }: DeleteProps) => { - const queryClient = useQueryClient() - const showToast = useCustomToast() - const cancelRef = React.useRef(null) - const { - handleSubmit, - formState: { isSubmitting }, - } = useForm() - const { logout } = useAuth() - - const mutation = useMutation({ - mutationFn: () => UsersService.deleteUserMe(), - onSuccess: () => { - showToast( - "Success", - "Your account has been successfully deleted.", - "success", - ) - logout() - onClose() - }, - onError: (err: ApiError) => { - handleError(err, showToast) - }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ["currentUser"] }) - }, - }) - - const onSubmit = async () => { - mutation.mutate() - } - - return ( - <> - - - - Confirmation Required - - - All your account data will be{" "} - permanently deleted. If you are sure, please - click "Confirm" to proceed. This action cannot be - undone. - - - - - - - - - - - ) -} - -export default DeleteConfirmation diff --git a/frontend/src/components/UserSettings/UserInformation.tsx b/frontend/src/components/UserSettings/UserInformation.tsx deleted file mode 100644 index d066a846a6..0000000000 --- a/frontend/src/components/UserSettings/UserInformation.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { - Box, - Button, - Container, - Flex, - FormControl, - FormErrorMessage, - FormLabel, - Heading, - Input, - Text, - useColorModeValue, -} from "@chakra-ui/react" -import { useMutation, useQueryClient } from "@tanstack/react-query" -import { useState } from "react" -import { type SubmitHandler, useForm } from "react-hook-form" - -import { - type ApiError, - type UserPublic, - type UserUpdateMe, - UsersService, -} from "../../client" -import useAuth from "../../hooks/useAuth" -import useCustomToast from "../../hooks/useCustomToast" -import { emailPattern, handleError } from "../../utils" - -const UserInformation = () => { - const queryClient = useQueryClient() - const color = useColorModeValue("inherit", "ui.light") - const showToast = useCustomToast() - const [editMode, setEditMode] = useState(false) - const { user: currentUser } = useAuth() - const { - register, - handleSubmit, - reset, - getValues, - formState: { isSubmitting, errors, isDirty }, - } = useForm({ - mode: "onBlur", - criteriaMode: "all", - defaultValues: { - full_name: currentUser?.full_name, - email: currentUser?.email, - }, - }) - - const toggleEditMode = () => { - setEditMode(!editMode) - } - - const mutation = useMutation({ - mutationFn: (data: UserUpdateMe) => - UsersService.updateUserMe({ requestBody: data }), - onSuccess: () => { - showToast("Success!", "User updated successfully.", "success") - }, - onError: (err: ApiError) => { - handleError(err, showToast) - }, - onSettled: () => { - queryClient.invalidateQueries() - }, - }) - - const onSubmit: SubmitHandler = async (data) => { - mutation.mutate(data) - } - - const onCancel = () => { - reset() - toggleEditMode() - } - - return ( - <> - - - User Information - - - - - Full name - - {editMode ? ( - - ) : ( - - {currentUser?.full_name || "N/A"} - - )} - - - - Email - - {editMode ? ( - - ) : ( - - {currentUser?.email} - - )} - {errors.email && ( - {errors.email.message} - )} - - - - {editMode && ( - - )} - - - - - ) -} - -export default UserInformation diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts deleted file mode 100644 index 76b0abdfd3..0000000000 --- a/frontend/src/hooks/useAuth.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" -import { useNavigate } from "@tanstack/react-router" -import { useState } from "react" - -import { AxiosError } from "axios" -import { - type Body_login_login_access_token as AccessToken, - type ApiError, - LoginService, - type UserPublic, - type UserRegister, - UsersService, -} from "../client" -import useCustomToast from "./useCustomToast" - -const isLoggedIn = () => { - return localStorage.getItem("access_token") !== null -} - -const useAuth = () => { - const [error, setError] = useState(null) - const navigate = useNavigate() - const showToast = useCustomToast() - const queryClient = useQueryClient() - const { data: user, isLoading } = useQuery({ - queryKey: ["currentUser"], - queryFn: UsersService.readUserMe, - enabled: isLoggedIn(), - }) - - const signUpMutation = useMutation({ - mutationFn: (data: UserRegister) => - UsersService.registerUser({ requestBody: data }), - - onSuccess: () => { - navigate({ to: "/login" }) - showToast( - "Account created.", - "Your account has been created successfully.", - "success", - ) - }, - onError: (err: ApiError) => { - let errDetail = (err.body as any)?.detail - - if (err instanceof AxiosError) { - errDetail = err.message - } - - showToast("Something went wrong.", errDetail, "error") - }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ["users"] }) - }, - }) - - const login = async (data: AccessToken) => { - const response = await LoginService.loginAccessToken({ - formData: data, - }) - localStorage.setItem("access_token", response.access_token) - } - - const loginMutation = useMutation({ - mutationFn: login, - onSuccess: () => { - navigate({ to: "/" }) - }, - onError: (err: ApiError) => { - let errDetail = (err.body as any)?.detail - - if (err instanceof AxiosError) { - errDetail = err.message - } - - if (Array.isArray(errDetail)) { - errDetail = "Something went wrong" - } - - setError(errDetail) - }, - }) - - const logout = () => { - localStorage.removeItem("access_token") - navigate({ to: "/login" }) - } - - return { - signUpMutation, - loginMutation, - logout, - user, - isLoading, - error, - resetError: () => setError(null), - } -} - -export { isLoggedIn } -export default useAuth diff --git a/frontend/src/hooks/useCustomToast.ts b/frontend/src/hooks/useCustomToast.ts deleted file mode 100644 index 06bc8a6ab8..0000000000 --- a/frontend/src/hooks/useCustomToast.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useToast } from "@chakra-ui/react" -import { useCallback } from "react" - -const useCustomToast = () => { - const toast = useToast() - - const showToast = useCallback( - (title: string, description: string, status: "success" | "error") => { - toast({ - title, - description, - status, - isClosable: true, - position: "bottom-right", - }) - }, - [toast], - ) - - return showToast -} - -export default useCustomToast diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx deleted file mode 100644 index afc904538b..0000000000 --- a/frontend/src/main.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { ChakraProvider } from "@chakra-ui/react" -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" -import { RouterProvider, createRouter } from "@tanstack/react-router" -import ReactDOM from "react-dom/client" -import { routeTree } from "./routeTree.gen" - -import { StrictMode } from "react" -import { OpenAPI } from "./client" -import theme from "./theme" - -OpenAPI.BASE = import.meta.env.VITE_API_URL -OpenAPI.TOKEN = async () => { - return localStorage.getItem("access_token") || "" -} - -const queryClient = new QueryClient() - -const router = createRouter({ routeTree }) -declare module "@tanstack/react-router" { - interface Register { - router: typeof router - } -} - -ReactDOM.createRoot(document.getElementById("root")!).render( - - - - - - - , -) diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts deleted file mode 100644 index 0e78c9ba20..0000000000 --- a/frontend/src/routeTree.gen.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* prettier-ignore-start */ - -/* eslint-disable */ - -// @ts-nocheck - -// noinspection JSUnusedGlobalSymbols - -// This file is auto-generated by TanStack Router - -// Import Routes - -import { Route as rootRoute } from './routes/__root' -import { Route as SignupImport } from './routes/signup' -import { Route as ResetPasswordImport } from './routes/reset-password' -import { Route as RecoverPasswordImport } from './routes/recover-password' -import { Route as LoginImport } from './routes/login' -import { Route as LayoutImport } from './routes/_layout' -import { Route as LayoutIndexImport } from './routes/_layout/index' -import { Route as LayoutSettingsImport } from './routes/_layout/settings' -import { Route as LayoutItemsImport } from './routes/_layout/items' -import { Route as LayoutAdminImport } from './routes/_layout/admin' - -// Create/Update Routes - -const SignupRoute = SignupImport.update({ - path: '/signup', - getParentRoute: () => rootRoute, -} as any) - -const ResetPasswordRoute = ResetPasswordImport.update({ - path: '/reset-password', - getParentRoute: () => rootRoute, -} as any) - -const RecoverPasswordRoute = RecoverPasswordImport.update({ - path: '/recover-password', - getParentRoute: () => rootRoute, -} as any) - -const LoginRoute = LoginImport.update({ - path: '/login', - getParentRoute: () => rootRoute, -} as any) - -const LayoutRoute = LayoutImport.update({ - id: '/_layout', - getParentRoute: () => rootRoute, -} as any) - -const LayoutIndexRoute = LayoutIndexImport.update({ - path: '/', - getParentRoute: () => LayoutRoute, -} as any) - -const LayoutSettingsRoute = LayoutSettingsImport.update({ - path: '/settings', - getParentRoute: () => LayoutRoute, -} as any) - -const LayoutItemsRoute = LayoutItemsImport.update({ - path: '/items', - getParentRoute: () => LayoutRoute, -} as any) - -const LayoutAdminRoute = LayoutAdminImport.update({ - path: '/admin', - getParentRoute: () => LayoutRoute, -} as any) - -// Populate the FileRoutesByPath interface - -declare module '@tanstack/react-router' { - interface FileRoutesByPath { - '/_layout': { - preLoaderRoute: typeof LayoutImport - parentRoute: typeof rootRoute - } - '/login': { - preLoaderRoute: typeof LoginImport - parentRoute: typeof rootRoute - } - '/recover-password': { - preLoaderRoute: typeof RecoverPasswordImport - parentRoute: typeof rootRoute - } - '/reset-password': { - preLoaderRoute: typeof ResetPasswordImport - parentRoute: typeof rootRoute - } - '/signup': { - preLoaderRoute: typeof SignupImport - parentRoute: typeof rootRoute - } - '/_layout/admin': { - preLoaderRoute: typeof LayoutAdminImport - parentRoute: typeof LayoutImport - } - '/_layout/items': { - preLoaderRoute: typeof LayoutItemsImport - parentRoute: typeof LayoutImport - } - '/_layout/settings': { - preLoaderRoute: typeof LayoutSettingsImport - parentRoute: typeof LayoutImport - } - '/_layout/': { - preLoaderRoute: typeof LayoutIndexImport - parentRoute: typeof LayoutImport - } - } -} - -// Create and export the route tree - -export const routeTree = rootRoute.addChildren([ - LayoutRoute.addChildren([ - LayoutAdminRoute, - LayoutItemsRoute, - LayoutSettingsRoute, - LayoutIndexRoute, - ]), - LoginRoute, - RecoverPasswordRoute, - ResetPasswordRoute, - SignupRoute, -]) - -/* prettier-ignore-end */ diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx deleted file mode 100644 index 5da6383f2a..0000000000 --- a/frontend/src/routes/__root.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Outlet, createRootRoute } from "@tanstack/react-router" -import React, { Suspense } from "react" - -import NotFound from "../components/Common/NotFound" - -const loadDevtools = () => - Promise.all([ - import("@tanstack/router-devtools"), - import("@tanstack/react-query-devtools"), - ]).then(([routerDevtools, reactQueryDevtools]) => { - return { - default: () => ( - <> - - - - ), - } - }) - -const TanStackDevtools = - process.env.NODE_ENV === "production" ? () => null : React.lazy(loadDevtools) - -export const Route = createRootRoute({ - component: () => ( - <> - - - - - - ), - notFoundComponent: () => , -}) diff --git a/frontend/src/routes/_layout.tsx b/frontend/src/routes/_layout.tsx deleted file mode 100644 index 9a6cfa3b81..0000000000 --- a/frontend/src/routes/_layout.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Flex, Spinner } from "@chakra-ui/react" -import { Outlet, createFileRoute, redirect } from "@tanstack/react-router" - -import Sidebar from "../components/Common/Sidebar" -import UserMenu from "../components/Common/UserMenu" -import useAuth, { isLoggedIn } from "../hooks/useAuth" - -export const Route = createFileRoute("/_layout")({ - component: Layout, - beforeLoad: async () => { - if (!isLoggedIn()) { - throw redirect({ - to: "/login", - }) - } - }, -}) - -function Layout() { - const { isLoading } = useAuth() - - return ( - - - {isLoading ? ( - - - - ) : ( - - )} - - - ) -} diff --git a/frontend/src/routes/_layout/admin.tsx b/frontend/src/routes/_layout/admin.tsx deleted file mode 100644 index 644653ff79..0000000000 --- a/frontend/src/routes/_layout/admin.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { - Badge, - Box, - Button, - Container, - Flex, - Heading, - SkeletonText, - Table, - TableContainer, - Tbody, - Td, - Th, - Thead, - Tr, -} from "@chakra-ui/react" -import { useQuery, useQueryClient } from "@tanstack/react-query" -import { createFileRoute, useNavigate } from "@tanstack/react-router" -import { useEffect } from "react" -import { z } from "zod" - -import { type UserPublic, UsersService } from "../../client" -import AddUser from "../../components/Admin/AddUser" -import ActionsMenu from "../../components/Common/ActionsMenu" -import Navbar from "../../components/Common/Navbar" - -const usersSearchSchema = z.object({ - page: z.number().catch(1), -}) - -export const Route = createFileRoute("/_layout/admin")({ - component: Admin, - validateSearch: (search) => usersSearchSchema.parse(search), -}) - -const PER_PAGE = 5 - -function getUsersQueryOptions({ page }: { page: number }) { - return { - queryFn: () => - UsersService.readUsers({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }), - queryKey: ["users", { page }], - } -} - -function UsersTable() { - const queryClient = useQueryClient() - const currentUser = queryClient.getQueryData(["currentUser"]) - const { page } = Route.useSearch() - const navigate = useNavigate({ from: Route.fullPath }) - const setPage = (page: number) => - navigate({ search: (prev) => ({ ...prev, page }) }) - - const { - data: users, - isPending, - isPlaceholderData, - } = useQuery({ - ...getUsersQueryOptions({ page }), - placeholderData: (prevData) => prevData, - }) - - const hasNextPage = !isPlaceholderData && users?.data.length === PER_PAGE - const hasPreviousPage = page > 1 - - useEffect(() => { - if (hasNextPage) { - queryClient.prefetchQuery(getUsersQueryOptions({ page: page + 1 })) - } - }, [page, queryClient, hasNextPage]) - - return ( - <> - - - - - - - - - - - - {isPending ? ( - - - {new Array(4).fill(null).map((_, index) => ( - - ))} - - - ) : ( - - {users?.data.map((user) => ( - - - - - - - - ))} - - )} -
Full nameEmailRoleStatusActions
- -
- {user.full_name || "N/A"} - {currentUser?.id === user.id && ( - - You - - )} - - {user.email} - {user.is_superuser ? "Superuser" : "User"} - - - {user.is_active ? "Active" : "Inactive"} - - - -
-
- - - Page {page} - - - - ) -} - -function Admin() { - return ( - - - Users Management - - - - - - ) -} diff --git a/frontend/src/routes/_layout/index.tsx b/frontend/src/routes/_layout/index.tsx deleted file mode 100644 index 80cc934083..0000000000 --- a/frontend/src/routes/_layout/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Box, Container, Text } from "@chakra-ui/react" -import { createFileRoute } from "@tanstack/react-router" - -import useAuth from "../../hooks/useAuth" - -export const Route = createFileRoute("/_layout/")({ - component: Dashboard, -}) - -function Dashboard() { - const { user: currentUser } = useAuth() - - return ( - <> - - - - Hi, {currentUser?.full_name || currentUser?.email} ๐Ÿ‘‹๐Ÿผ - - Welcome back, nice to see you again! - - - - ) -} diff --git a/frontend/src/routes/_layout/items.tsx b/frontend/src/routes/_layout/items.tsx deleted file mode 100644 index 174fa83c9b..0000000000 --- a/frontend/src/routes/_layout/items.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { - Button, - Container, - Flex, - Heading, - SkeletonText, - Table, - TableContainer, - Tbody, - Td, - Th, - Thead, - Tr, -} from "@chakra-ui/react" -import { useQuery, useQueryClient } from "@tanstack/react-query" -import { createFileRoute, useNavigate } from "@tanstack/react-router" -import { useEffect } from "react" -import { z } from "zod" - -import { ItemsService } from "../../client" -import ActionsMenu from "../../components/Common/ActionsMenu" -import Navbar from "../../components/Common/Navbar" -import AddItem from "../../components/Items/AddItem" - -const itemsSearchSchema = z.object({ - page: z.number().catch(1), -}) - -export const Route = createFileRoute("/_layout/items")({ - component: Items, - validateSearch: (search) => itemsSearchSchema.parse(search), -}) - -const PER_PAGE = 5 - -function getItemsQueryOptions({ page }: { page: number }) { - return { - queryFn: () => - ItemsService.readItems({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }), - queryKey: ["items", { page }], - } -} - -function ItemsTable() { - const queryClient = useQueryClient() - const { page } = Route.useSearch() - const navigate = useNavigate({ from: Route.fullPath }) - const setPage = (page: number) => - navigate({ search: (prev) => ({ ...prev, page }) }) - - const { - data: items, - isPending, - isPlaceholderData, - } = useQuery({ - ...getItemsQueryOptions({ page }), - placeholderData: (prevData) => prevData, - }) - - const hasNextPage = !isPlaceholderData && items?.data.length === PER_PAGE - const hasPreviousPage = page > 1 - - useEffect(() => { - if (hasNextPage) { - queryClient.prefetchQuery(getItemsQueryOptions({ page: page + 1 })) - } - }, [page, queryClient, hasNextPage]) - - return ( - <> - - - - - - - - - - - {isPending ? ( - - - {new Array(4).fill(null).map((_, index) => ( - - ))} - - - ) : ( - - {items?.data.map((item) => ( - - - - - - - ))} - - )} -
IDTitleDescriptionActions
- -
{item.id} - {item.title} - - {item.description || "N/A"} - - -
-
- - - Page {page} - - - - ) -} - -function Items() { - return ( - - - Items Management - - - - - - ) -} diff --git a/frontend/src/routes/_layout/settings.tsx b/frontend/src/routes/_layout/settings.tsx deleted file mode 100644 index 68266c6b9a..0000000000 --- a/frontend/src/routes/_layout/settings.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { - Container, - Heading, - Tab, - TabList, - TabPanel, - TabPanels, - Tabs, -} from "@chakra-ui/react" -import { useQueryClient } from "@tanstack/react-query" -import { createFileRoute } from "@tanstack/react-router" - -import type { UserPublic } from "../../client" -import Appearance from "../../components/UserSettings/Appearance" -import ChangePassword from "../../components/UserSettings/ChangePassword" -import DeleteAccount from "../../components/UserSettings/DeleteAccount" -import UserInformation from "../../components/UserSettings/UserInformation" - -const tabsConfig = [ - { title: "My profile", component: UserInformation }, - { title: "Password", component: ChangePassword }, - { title: "Appearance", component: Appearance }, - { title: "Danger zone", component: DeleteAccount }, -] - -export const Route = createFileRoute("/_layout/settings")({ - component: UserSettings, -}) - -function UserSettings() { - const queryClient = useQueryClient() - const currentUser = queryClient.getQueryData(["currentUser"]) - const finalTabs = currentUser?.is_superuser - ? tabsConfig.slice(0, 3) - : tabsConfig - - return ( - - - User Settings - - - - {finalTabs.map((tab, index) => ( - {tab.title} - ))} - - - {finalTabs.map((tab, index) => ( - - - - ))} - - - - ) -} diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx deleted file mode 100644 index 20a9be6564..0000000000 --- a/frontend/src/routes/login.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons" -import { - Button, - Container, - FormControl, - FormErrorMessage, - Icon, - Image, - Input, - InputGroup, - InputRightElement, - Link, - Text, - useBoolean, -} from "@chakra-ui/react" -import { - Link as RouterLink, - createFileRoute, - redirect, -} from "@tanstack/react-router" -import { type SubmitHandler, useForm } from "react-hook-form" - -import Logo from "/assets/images/fastapi-logo.svg" -import type { Body_login_login_access_token as AccessToken } from "../client" -import useAuth, { isLoggedIn } from "../hooks/useAuth" -import { emailPattern } from "../utils" - -export const Route = createFileRoute("/login")({ - component: Login, - beforeLoad: async () => { - if (isLoggedIn()) { - throw redirect({ - to: "/", - }) - } - }, -}) - -function Login() { - const [show, setShow] = useBoolean() - const { loginMutation, error, resetError } = useAuth() - const { - register, - handleSubmit, - formState: { errors, isSubmitting }, - } = useForm({ - mode: "onBlur", - criteriaMode: "all", - defaultValues: { - username: "", - password: "", - }, - }) - - const onSubmit: SubmitHandler = async (data) => { - if (isSubmitting) return - - resetError() - - try { - await loginMutation.mutateAsync(data) - } catch { - // error is handled by useAuth hook - } - } - - return ( - <> - - FastAPI logo - - - {errors.username && ( - {errors.username.message} - )} - - - - - - - {show ? : } - - - - {error && {error}} - - - Forgot password? - - - - Don't have an account?{" "} - - Sign up - - - - - ) -} diff --git a/frontend/src/routes/recover-password.tsx b/frontend/src/routes/recover-password.tsx deleted file mode 100644 index 5716728bbb..0000000000 --- a/frontend/src/routes/recover-password.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { - Button, - Container, - FormControl, - FormErrorMessage, - Heading, - Input, - Text, -} from "@chakra-ui/react" -import { useMutation } from "@tanstack/react-query" -import { createFileRoute, redirect } from "@tanstack/react-router" -import { type SubmitHandler, useForm } from "react-hook-form" - -import { type ApiError, LoginService } from "../client" -import { isLoggedIn } from "../hooks/useAuth" -import useCustomToast from "../hooks/useCustomToast" -import { emailPattern, handleError } from "../utils" - -interface FormData { - email: string -} - -export const Route = createFileRoute("/recover-password")({ - component: RecoverPassword, - beforeLoad: async () => { - if (isLoggedIn()) { - throw redirect({ - to: "/", - }) - } - }, -}) - -function RecoverPassword() { - const { - register, - handleSubmit, - reset, - formState: { errors, isSubmitting }, - } = useForm() - const showToast = useCustomToast() - - const recoverPassword = async (data: FormData) => { - await LoginService.recoverPassword({ - email: data.email, - }) - } - - const mutation = useMutation({ - mutationFn: recoverPassword, - onSuccess: () => { - showToast( - "Email sent.", - "We sent an email with a link to get back into your account.", - "success", - ) - reset() - }, - onError: (err: ApiError) => { - handleError(err, showToast) - }, - }) - - const onSubmit: SubmitHandler = async (data) => { - mutation.mutate(data) - } - - return ( - - - Password Recovery - - - A password recovery email will be sent to the registered account. - - - - {errors.email && ( - {errors.email.message} - )} - - - - ) -} diff --git a/frontend/src/routes/reset-password.tsx b/frontend/src/routes/reset-password.tsx deleted file mode 100644 index f5ee763a3e..0000000000 --- a/frontend/src/routes/reset-password.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { - Button, - Container, - FormControl, - FormErrorMessage, - FormLabel, - Heading, - Input, - Text, -} from "@chakra-ui/react" -import { useMutation } from "@tanstack/react-query" -import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router" -import { type SubmitHandler, useForm } from "react-hook-form" - -import { type ApiError, LoginService, type NewPassword } from "../client" -import { isLoggedIn } from "../hooks/useAuth" -import useCustomToast from "../hooks/useCustomToast" -import { confirmPasswordRules, handleError, passwordRules } from "../utils" - -interface NewPasswordForm extends NewPassword { - confirm_password: string -} - -export const Route = createFileRoute("/reset-password")({ - component: ResetPassword, - beforeLoad: async () => { - if (isLoggedIn()) { - throw redirect({ - to: "/", - }) - } - }, -}) - -function ResetPassword() { - const { - register, - handleSubmit, - getValues, - reset, - formState: { errors }, - } = useForm({ - mode: "onBlur", - criteriaMode: "all", - defaultValues: { - new_password: "", - }, - }) - const showToast = useCustomToast() - const navigate = useNavigate() - - const resetPassword = async (data: NewPassword) => { - const token = new URLSearchParams(window.location.search).get("token") - if (!token) return - await LoginService.resetPassword({ - requestBody: { new_password: data.new_password, token: token }, - }) - } - - const mutation = useMutation({ - mutationFn: resetPassword, - onSuccess: () => { - showToast("Success!", "Password updated successfully.", "success") - reset() - navigate({ to: "/login" }) - }, - onError: (err: ApiError) => { - handleError(err, showToast) - }, - }) - - const onSubmit: SubmitHandler = async (data) => { - mutation.mutate(data) - } - - return ( - - - Reset Password - - - Please enter your new password and confirm it to reset your password. - - - Set Password - - {errors.new_password && ( - {errors.new_password.message} - )} - - - Confirm Password - - {errors.confirm_password && ( - {errors.confirm_password.message} - )} - - - - ) -} diff --git a/frontend/src/routes/signup.tsx b/frontend/src/routes/signup.tsx deleted file mode 100644 index b021e73698..0000000000 --- a/frontend/src/routes/signup.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { - Button, - Container, - Flex, - FormControl, - FormErrorMessage, - FormLabel, - Image, - Input, - Link, - Text, -} from "@chakra-ui/react" -import { - Link as RouterLink, - createFileRoute, - redirect, -} from "@tanstack/react-router" -import { type SubmitHandler, useForm } from "react-hook-form" - -import Logo from "/assets/images/fastapi-logo.svg" -import type { UserRegister } from "../client" -import useAuth, { isLoggedIn } from "../hooks/useAuth" -import { confirmPasswordRules, emailPattern, passwordRules } from "../utils" - -export const Route = createFileRoute("/signup")({ - component: SignUp, - beforeLoad: async () => { - if (isLoggedIn()) { - throw redirect({ - to: "/", - }) - } - }, -}) - -interface UserRegisterForm extends UserRegister { - confirm_password: string -} - -function SignUp() { - const { signUpMutation } = useAuth() - const { - register, - handleSubmit, - getValues, - formState: { errors, isSubmitting }, - } = useForm({ - mode: "onBlur", - criteriaMode: "all", - defaultValues: { - email: "", - full_name: "", - password: "", - confirm_password: "", - }, - }) - - const onSubmit: SubmitHandler = (data) => { - signUpMutation.mutate(data) - } - - return ( - <> - - - FastAPI logo - - - Full Name - - - {errors.full_name && ( - {errors.full_name.message} - )} - - - - Email - - - {errors.email && ( - {errors.email.message} - )} - - - - Password - - - {errors.password && ( - {errors.password.message} - )} - - - - Confirm Password - - - - {errors.confirm_password && ( - - {errors.confirm_password.message} - - )} - - - - Already have an account?{" "} - - Log In - - - - - - ) -} - -export default SignUp diff --git a/frontend/src/theme.tsx b/frontend/src/theme.tsx deleted file mode 100644 index 71675dddca..0000000000 --- a/frontend/src/theme.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { extendTheme } from "@chakra-ui/react" - -const disabledStyles = { - _disabled: { - backgroundColor: "ui.main", - }, -} - -const theme = extendTheme({ - colors: { - ui: { - main: "#009688", - secondary: "#EDF2F7", - success: "#48BB78", - danger: "#E53E3E", - light: "#FAFAFA", - dark: "#1A202C", - darkSlate: "#252D3D", - dim: "#A0AEC0", - }, - }, - components: { - Button: { - variants: { - primary: { - backgroundColor: "ui.main", - color: "ui.light", - _hover: { - backgroundColor: "#00766C", - }, - _disabled: { - ...disabledStyles, - _hover: { - ...disabledStyles, - }, - }, - }, - danger: { - backgroundColor: "ui.danger", - color: "ui.light", - _hover: { - backgroundColor: "#E32727", - }, - }, - }, - }, - Tabs: { - variants: { - enclosed: { - tab: { - _selected: { - color: "ui.main", - }, - }, - }, - }, - }, - }, -}) - -export default theme diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts deleted file mode 100644 index 99f906303c..0000000000 --- a/frontend/src/utils.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { ApiError } from "./client" - -export const emailPattern = { - value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, - message: "Invalid email address", -} - -export const namePattern = { - value: /^[A-Za-z\s\u00C0-\u017F]{1,30}$/, - message: "Invalid name", -} - -export const passwordRules = (isRequired = true) => { - const rules: any = { - minLength: { - value: 8, - message: "Password must be at least 8 characters", - }, - } - - if (isRequired) { - rules.required = "Password is required" - } - - return rules -} - -export const confirmPasswordRules = ( - getValues: () => any, - isRequired = true, -) => { - const rules: any = { - validate: (value: string) => { - const password = getValues().password || getValues().new_password - return value === password ? true : "The passwords do not match" - }, - } - - if (isRequired) { - rules.required = "Password confirmation is required" - } - - return rules -} - -export const handleError = (err: ApiError, showToast: any) => { - const errDetail = (err.body as any)?.detail - let errorMessage = errDetail || "Something went wrong." - if (Array.isArray(errDetail) && errDetail.length > 0) { - errorMessage = errDetail[0].msg - } - showToast("Error", errorMessage, "error") -} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts deleted file mode 100644 index 11f02fe2a0..0000000000 --- a/frontend/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/frontend/tests/auth.setup.ts b/frontend/tests/auth.setup.ts deleted file mode 100644 index 3882f4f810..0000000000 --- a/frontend/tests/auth.setup.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { test as setup } from "@playwright/test" -import { firstSuperuser, firstSuperuserPassword } from "./config.ts" - -const authFile = "playwright/.auth/user.json" - -setup("authenticate", async ({ page }) => { - await page.goto("/login") - await page.getByPlaceholder("Email").fill(firstSuperuser) - await page.getByPlaceholder("Password").fill(firstSuperuserPassword) - await page.getByRole("button", { name: "Log In" }).click() - await page.waitForURL("/") - await page.context().storageState({ path: authFile }) -}) diff --git a/frontend/tests/config.ts b/frontend/tests/config.ts deleted file mode 100644 index 188cb367e3..0000000000 --- a/frontend/tests/config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import path from "node:path" -import { fileURLToPath } from "node:url" -import dotenv from "dotenv" - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) - -dotenv.config({ path: path.join(__dirname, "../../.env") }) - -const { FIRST_SUPERUSER, FIRST_SUPERUSER_PASSWORD } = process.env - -if (typeof FIRST_SUPERUSER !== "string") { - throw new Error("Environment variable FIRST_SUPERUSER is undefined") -} - -if (typeof FIRST_SUPERUSER_PASSWORD !== "string") { - throw new Error("Environment variable FIRST_SUPERUSER_PASSWORD is undefined") -} - -export const firstSuperuser = FIRST_SUPERUSER as string -export const firstSuperuserPassword = FIRST_SUPERUSER_PASSWORD as string diff --git a/frontend/tests/login.spec.ts b/frontend/tests/login.spec.ts deleted file mode 100644 index 97c2284f40..0000000000 --- a/frontend/tests/login.spec.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { type Page, expect, test } from "@playwright/test" -import { firstSuperuser, firstSuperuserPassword } from "./config.ts" -import { randomPassword } from "./utils/random.ts" - -test.use({ storageState: { cookies: [], origins: [] } }) - -type OptionsType = { - exact?: boolean -} - -const fillForm = async (page: Page, email: string, password: string) => { - await page.getByPlaceholder("Email").fill(email) - await page.getByPlaceholder("Password", { exact: true }).fill(password) -} - -const verifyInput = async ( - page: Page, - placeholder: string, - options?: OptionsType, -) => { - const input = page.getByPlaceholder(placeholder, options) - await expect(input).toBeVisible() - await expect(input).toHaveText("") - await expect(input).toBeEditable() -} - -test("Inputs are visible, empty and editable", async ({ page }) => { - await page.goto("/login") - - await verifyInput(page, "Email") - await verifyInput(page, "Password", { exact: true }) -}) - -test("Log In button is visible", async ({ page }) => { - await page.goto("/login") - - await expect(page.getByRole("button", { name: "Log In" })).toBeVisible() -}) - -test("Forgot Password link is visible", async ({ page }) => { - await page.goto("/login") - - await expect( - page.getByRole("link", { name: "Forgot password?" }), - ).toBeVisible() -}) - -test("Log in with valid email and password ", async ({ page }) => { - await page.goto("/login") - - await fillForm(page, firstSuperuser, firstSuperuserPassword) - await page.getByRole("button", { name: "Log In" }).click() - - await page.waitForURL("/") - - await expect( - page.getByText("Welcome back, nice to see you again!"), - ).toBeVisible() -}) - -test("Log in with invalid email", async ({ page }) => { - await page.goto("/login") - - await fillForm(page, "invalidemail", firstSuperuserPassword) - await page.getByRole("button", { name: "Log In" }).click() - - await expect(page.getByText("Invalid email address")).toBeVisible() -}) - -test("Log in with invalid password", async ({ page }) => { - const password = randomPassword() - - await page.goto("/login") - await fillForm(page, firstSuperuser, password) - await page.getByRole("button", { name: "Log In" }).click() - - await expect(page.getByText("Incorrect email or password")).toBeVisible() -}) - -// Log out - -test("Successful log out", async ({ page }) => { - await page.goto("/login") - - await fillForm(page, firstSuperuser, firstSuperuserPassword) - await page.getByRole("button", { name: "Log In" }).click() - - await page.waitForURL("/") - - await expect( - page.getByText("Welcome back, nice to see you again!"), - ).toBeVisible() - - await page.getByTestId("user-menu").click() - await page.getByRole("menuitem", { name: "Log out" }).click() - await page.waitForURL("/login") -}) - -test("Logged-out user cannot access protected routes", async ({ page }) => { - await page.goto("/login") - - await fillForm(page, firstSuperuser, firstSuperuserPassword) - await page.getByRole("button", { name: "Log In" }).click() - - await page.waitForURL("/") - - await expect( - page.getByText("Welcome back, nice to see you again!"), - ).toBeVisible() - - await page.getByTestId("user-menu").click() - await page.getByRole("menuitem", { name: "Log out" }).click() - await page.waitForURL("/login") - - await page.goto("/settings") - await page.waitForURL("/login") -}) diff --git a/frontend/tests/reset-password.spec.ts b/frontend/tests/reset-password.spec.ts deleted file mode 100644 index 88ec798791..0000000000 --- a/frontend/tests/reset-password.spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { expect, test } from "@playwright/test" -import { findLastEmail } from "./utils/mailcatcher" -import { randomEmail, randomPassword } from "./utils/random" -import { logInUser, signUpNewUser } from "./utils/user" - -test.use({ storageState: { cookies: [], origins: [] } }) - -test("Password Recovery title is visible", async ({ page }) => { - await page.goto("/recover-password") - - await expect( - page.getByRole("heading", { name: "Password Recovery" }), - ).toBeVisible() -}) - -test("Input is visible, empty and editable", async ({ page }) => { - await page.goto("/recover-password") - - await expect(page.getByPlaceholder("Email")).toBeVisible() - await expect(page.getByPlaceholder("Email")).toHaveText("") - await expect(page.getByPlaceholder("Email")).toBeEditable() -}) - -test("Continue button is visible", async ({ page }) => { - await page.goto("/recover-password") - - await expect(page.getByRole("button", { name: "Continue" })).toBeVisible() -}) - -test("User can reset password successfully using the link", async ({ - page, - request, -}) => { - const fullName = "Test User" - const email = randomEmail() - const password = randomPassword() - const newPassword = randomPassword() - - // Sign up a new user - await signUpNewUser(page, fullName, email, password) - - await page.goto("/recover-password") - await page.getByPlaceholder("Email").fill(email) - - await page.getByRole("button", { name: "Continue" }).click() - - const emailData = await findLastEmail({ - request, - filter: (e) => e.recipients.includes(`<${email}>`), - timeout: 5000, - }) - - await page.goto(`http://localhost:1080/messages/${emailData.id}.html`) - - const selector = 'a[href*="/reset-password?token="]' - - let url = await page.getAttribute(selector, "href") - - // TODO: update var instead of doing a replace - url = url!.replace("http://localhost/", "http://localhost:5173/") - - // Set the new password and confirm it - await page.goto(url) - - await page.getByLabel("Set Password").fill(newPassword) - await page.getByLabel("Confirm Password").fill(newPassword) - await page.getByRole("button", { name: "Reset Password" }).click() - await expect(page.getByText("Password updated successfully")).toBeVisible() - - // Check if the user is able to login with the new password - await logInUser(page, email, newPassword) -}) - -test("Expired or invalid reset link", async ({ page }) => { - const password = randomPassword() - const invalidUrl = "/reset-password?token=invalidtoken" - - await page.goto(invalidUrl) - - await page.getByLabel("Set Password").fill(password) - await page.getByLabel("Confirm Password").fill(password) - await page.getByRole("button", { name: "Reset Password" }).click() - - await expect(page.getByText("Invalid token")).toBeVisible() -}) - -test("Weak new password validation", async ({ page, request }) => { - const fullName = "Test User" - const email = randomEmail() - const password = randomPassword() - const weakPassword = "123" - - // Sign up a new user - await signUpNewUser(page, fullName, email, password) - - await page.goto("/recover-password") - await page.getByPlaceholder("Email").fill(email) - await page.getByRole("button", { name: "Continue" }).click() - - const emailData = await findLastEmail({ - request, - filter: (e) => e.recipients.includes(`<${email}>`), - timeout: 5000, - }) - - await page.goto(`http://localhost:1080/messages/${emailData.id}.html`) - - const selector = 'a[href*="/reset-password?token="]' - let url = await page.getAttribute(selector, "href") - url = url!.replace("http://localhost/", "http://localhost:5173/") - - // Set a weak new password - await page.goto(url) - await page.getByLabel("Set Password").fill(weakPassword) - await page.getByLabel("Confirm Password").fill(weakPassword) - await page.getByRole("button", { name: "Reset Password" }).click() - - await expect( - page.getByText("Password must be at least 8 characters"), - ).toBeVisible() -}) diff --git a/frontend/tests/sign-up.spec.ts b/frontend/tests/sign-up.spec.ts deleted file mode 100644 index a666123280..0000000000 --- a/frontend/tests/sign-up.spec.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { type Page, expect, test } from "@playwright/test" - -import { randomEmail, randomPassword } from "./utils/random" - -test.use({ storageState: { cookies: [], origins: [] } }) - -type OptionsType = { - exact?: boolean -} - -const fillForm = async ( - page: Page, - full_name: string, - email: string, - password: string, - confirm_password: string, -) => { - await page.getByPlaceholder("Full Name").fill(full_name) - await page.getByPlaceholder("Email").fill(email) - await page.getByPlaceholder("Password", { exact: true }).fill(password) - await page.getByPlaceholder("Repeat Password").fill(confirm_password) -} - -const verifyInput = async ( - page: Page, - placeholder: string, - options?: OptionsType, -) => { - const input = page.getByPlaceholder(placeholder, options) - await expect(input).toBeVisible() - await expect(input).toHaveText("") - await expect(input).toBeEditable() -} - -test("Inputs are visible, empty and editable", async ({ page }) => { - await page.goto("/signup") - - await verifyInput(page, "Full Name") - await verifyInput(page, "Email") - await verifyInput(page, "Password", { exact: true }) - await verifyInput(page, "Repeat Password") -}) - -test("Sign Up button is visible", async ({ page }) => { - await page.goto("/signup") - - await expect(page.getByRole("button", { name: "Sign Up" })).toBeVisible() -}) - -test("Log In link is visible", async ({ page }) => { - await page.goto("/signup") - - await expect(page.getByRole("link", { name: "Log In" })).toBeVisible() -}) - -test("Sign up with valid name, email, and password", async ({ page }) => { - const full_name = "Test User" - const email = randomEmail() - const password = randomPassword() - - await page.goto("/signup") - await fillForm(page, full_name, email, password, password) - await page.getByRole("button", { name: "Sign Up" }).click() -}) - -test("Sign up with invalid email", async ({ page }) => { - await page.goto("/signup") - - await fillForm( - page, - "Playwright Test", - "invalid-email", - "changethis", - "changethis", - ) - await page.getByRole("button", { name: "Sign Up" }).click() - - await expect(page.getByText("Invalid email address")).toBeVisible() -}) - -test("Sign up with existing email", async ({ page }) => { - const fullName = "Test User" - const email = randomEmail() - const password = randomPassword() - - // Sign up with an email - await page.goto("/signup") - - await fillForm(page, fullName, email, password, password) - await page.getByRole("button", { name: "Sign Up" }).click() - - // Sign up again with the same email - await page.goto("/signup") - - await fillForm(page, fullName, email, password, password) - await page.getByRole("button", { name: "Sign Up" }).click() - - await page - .getByText("The user with this email already exists in the system") - .click() -}) - -test("Sign up with weak password", async ({ page }) => { - const fullName = "Test User" - const email = randomEmail() - const password = "weak" - - await page.goto("/signup") - - await fillForm(page, fullName, email, password, password) - await page.getByRole("button", { name: "Sign Up" }).click() - - await expect( - page.getByText("Password must be at least 8 characters"), - ).toBeVisible() -}) - -test("Sign up with mismatched passwords", async ({ page }) => { - const fullName = "Test User" - const email = randomEmail() - const password = randomPassword() - const password2 = randomPassword() - - await page.goto("/signup") - - await fillForm(page, fullName, email, password, password2) - await page.getByRole("button", { name: "Sign Up" }).click() - - await expect(page.getByText("Passwords do not match")).toBeVisible() -}) - -test("Sign up with missing full name", async ({ page }) => { - const fullName = "" - const email = randomEmail() - const password = randomPassword() - - await page.goto("/signup") - - await fillForm(page, fullName, email, password, password) - await page.getByRole("button", { name: "Sign Up" }).click() - - await expect(page.getByText("Full Name is required")).toBeVisible() -}) - -test("Sign up with missing email", async ({ page }) => { - const fullName = "Test User" - const email = "" - const password = randomPassword() - - await page.goto("/signup") - - await fillForm(page, fullName, email, password, password) - await page.getByRole("button", { name: "Sign Up" }).click() - - await expect(page.getByText("Email is required")).toBeVisible() -}) - -test("Sign up with missing password", async ({ page }) => { - const fullName = "" - const email = randomEmail() - const password = "" - - await page.goto("/signup") - - await fillForm(page, fullName, email, password, password) - await page.getByRole("button", { name: "Sign Up" }).click() - - await expect(page.getByText("Password is required")).toBeVisible() -}) diff --git a/frontend/tests/user-settings.spec.ts b/frontend/tests/user-settings.spec.ts deleted file mode 100644 index a3a8a27490..0000000000 --- a/frontend/tests/user-settings.spec.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { expect, test } from "@playwright/test" -import { firstSuperuser, firstSuperuserPassword } from "./config.ts" -import { randomEmail, randomPassword } from "./utils/random" -import { logInUser, logOutUser, signUpNewUser } from "./utils/user" - -const tabs = ["My profile", "Password", "Appearance"] - -// User Information - -test("My profile tab is active by default", async ({ page }) => { - await page.goto("/settings") - await expect(page.getByRole("tab", { name: "My profile" })).toHaveAttribute( - "aria-selected", - "true", - ) -}) - -test("All tabs are visible", async ({ page }) => { - await page.goto("/settings") - for (const tab of tabs) { - await expect(page.getByRole("tab", { name: tab })).toBeVisible() - } -}) - -test.describe("Edit user full name and email successfully", () => { - test.use({ storageState: { cookies: [], origins: [] } }) - - test("Edit user name with a valid name", async ({ page }) => { - const fullName = "Test User" - const email = randomEmail() - const updatedName = "Test User 2" - const password = randomPassword() - - // Sign up a new user - await signUpNewUser(page, fullName, email, password) - - // Log in the user - await logInUser(page, email, password) - - await page.goto("/settings") - await page.getByRole("tab", { name: "My profile" }).click() - await page.getByRole("button", { name: "Edit" }).click() - await page.getByLabel("Full name").fill(updatedName) - await page.getByRole("button", { name: "Save" }).click() - await expect(page.getByText("User updated successfully")).toBeVisible() - // Check if the new name is displayed on the page - await expect( - page.getByLabel("My profile").getByText(updatedName, { exact: true }), - ).toBeVisible() - }) - - test("Edit user email with a valid email", async ({ page }) => { - const fullName = "Test User" - const email = randomEmail() - const updatedEmail = randomEmail() - const password = randomPassword() - - // Sign up a new user - await signUpNewUser(page, fullName, email, password) - - // Log in the user - await logInUser(page, email, password) - - await page.goto("/settings") - await page.getByRole("tab", { name: "My profile" }).click() - await page.getByRole("button", { name: "Edit" }).click() - await page.getByLabel("Email").fill(updatedEmail) - await page.getByRole("button", { name: "Save" }).click() - await expect(page.getByText("User updated successfully")).toBeVisible() - await expect( - page.getByLabel("My profile").getByText(updatedEmail, { exact: true }), - ).toBeVisible() - }) -}) - -test.describe("Edit user with invalid data", () => { - test.use({ storageState: { cookies: [], origins: [] } }) - - test("Edit user email with an invalid email", async ({ page }) => { - const fullName = "Test User" - const email = randomEmail() - const password = randomPassword() - const invalidEmail = "" - - // Sign up a new user - await signUpNewUser(page, fullName, email, password) - - // Log in the user - await logInUser(page, email, password) - - await page.goto("/settings") - await page.getByRole("tab", { name: "My profile" }).click() - await page.getByRole("button", { name: "Edit" }).click() - await page.getByLabel("Email").fill(invalidEmail) - await page.locator("body").click() - await expect(page.getByText("Email is required")).toBeVisible() - }) - - test("Cancel edit action restores original name", async ({ page }) => { - const fullName = "Test User" - const email = randomEmail() - const password = randomPassword() - const updatedName = "Test User" - - // Sign up a new user - await signUpNewUser(page, fullName, email, password) - - // Log in the user - await logInUser(page, email, password) - - await page.goto("/settings") - await page.getByRole("tab", { name: "My profile" }).click() - await page.getByRole("button", { name: "Edit" }).click() - await page.getByLabel("Full name").fill(updatedName) - await page.getByRole("button", { name: "Cancel" }).first().click() - await expect( - page.getByLabel("My profile").getByText(fullName, { exact: true }), - ).toBeVisible() - }) - - test("Cancel edit action restores original email", async ({ page }) => { - const fullName = "Test User" - const email = randomEmail() - const password = randomPassword() - const updatedEmail = randomEmail() - - // Sign up a new user - await signUpNewUser(page, fullName, email, password) - - // Log in the user - await logInUser(page, email, password) - - await page.goto("/settings") - await page.getByRole("tab", { name: "My profile" }).click() - await page.getByRole("button", { name: "Edit" }).click() - await page.getByLabel("Email").fill(updatedEmail) - await page.getByRole("button", { name: "Cancel" }).first().click() - await expect( - page.getByLabel("My profile").getByText(email, { exact: true }), - ).toBeVisible() - }) -}) - -// Change Password - -test.describe("Change password successfully", () => { - test.use({ storageState: { cookies: [], origins: [] } }) - - test("Update password successfully", async ({ page }) => { - const fullName = "Test User" - const email = randomEmail() - const password = randomPassword() - const NewPassword = randomPassword() - - // Sign up a new user - await signUpNewUser(page, fullName, email, password) - - // Log in the user - await logInUser(page, email, password) - - await page.goto("/settings") - await page.getByRole("tab", { name: "Password" }).click() - await page.getByLabel("Current Password*").fill(password) - await page.getByLabel("Set Password*").fill(NewPassword) - await page.getByLabel("Confirm Password*").fill(NewPassword) - await page.getByRole("button", { name: "Save" }).click() - await expect(page.getByText("Password updated successfully.")).toBeVisible() - - await logOutUser(page) - - // Check if the user can log in with the new password - await logInUser(page, email, NewPassword) - }) -}) - -test.describe("Change password with invalid data", () => { - test.use({ storageState: { cookies: [], origins: [] } }) - - test("Update password with weak passwords", async ({ page }) => { - const fullName = "Test User" - const email = randomEmail() - const password = randomPassword() - const weakPassword = "weak" - - // Sign up a new user - await signUpNewUser(page, fullName, email, password) - - // Log in the user - await logInUser(page, email, password) - - await page.goto("/settings") - await page.getByRole("tab", { name: "Password" }).click() - await page.getByLabel("Current Password*").fill(password) - await page.getByLabel("Set Password*").fill(weakPassword) - await page.getByLabel("Confirm Password*").fill(weakPassword) - await expect( - page.getByText("Password must be at least 8 characters"), - ).toBeVisible() - }) - - test("New password and confirmation password do not match", async ({ - page, - }) => { - const fullName = "Test User" - const email = randomEmail() - const password = randomPassword() - const newPassword = randomPassword() - const confirmPassword = randomPassword() - - // Sign up a new user - await signUpNewUser(page, fullName, email, password) - - // Log in the user - await logInUser(page, email, password) - - await page.goto("/settings") - await page.getByRole("tab", { name: "Password" }).click() - await page.getByLabel("Current Password*").fill(password) - await page.getByLabel("Set Password*").fill(newPassword) - await page.getByLabel("Confirm Password*").fill(confirmPassword) - await page.getByRole("button", { name: "Save" }).click() - await expect(page.getByText("Passwords do not match")).toBeVisible() - }) - - test("Current password and new password are the same", async ({ page }) => { - const fullName = "Test User" - const email = randomEmail() - const password = randomPassword() - - // Sign up a new user - await signUpNewUser(page, fullName, email, password) - - // Log in the user - await logInUser(page, email, password) - - await page.goto("/settings") - await page.getByRole("tab", { name: "Password" }).click() - await page.getByLabel("Current Password*").fill(password) - await page.getByLabel("Set Password*").fill(password) - await page.getByLabel("Confirm Password*").fill(password) - await page.getByRole("button", { name: "Save" }).click() - await expect( - page.getByText("New password cannot be the same as the current one"), - ).toBeVisible() - }) -}) - -// Appearance - -test("Appearance tab is visible", async ({ page }) => { - await page.goto("/settings") - await page.getByRole("tab", { name: "Appearance" }).click() - await expect(page.getByLabel("Appearance")).toBeVisible() -}) - -test("User can switch from light mode to dark mode", async ({ page }) => { - await page.goto("/settings") - await page.getByRole("tab", { name: "Appearance" }).click() - await page.getByLabel("Appearance").locator("span").nth(3).click() - const isDarkMode = await page.evaluate(() => - document.body.classList.contains("chakra-ui-dark"), - ) - expect(isDarkMode).toBe(true) -}) - -test("User can switch from dark mode to light mode", async ({ page }) => { - await page.goto("/settings") - await page.getByRole("tab", { name: "Appearance" }).click() - await page.getByLabel("Appearance").locator("span").first().click() - const isLightMode = await page.evaluate(() => - document.body.classList.contains("chakra-ui-light"), - ) - expect(isLightMode).toBe(true) -}) - -test("Selected mode is preserved across sessions", async ({ page }) => { - await page.goto("/settings") - await page.getByRole("tab", { name: "Appearance" }).click() - await page.getByLabel("Appearance").locator("span").nth(3).click() - - await logOutUser(page) - - await logInUser(page, firstSuperuser, firstSuperuserPassword) - const isDarkMode = await page.evaluate(() => - document.body.classList.contains("chakra-ui-dark"), - ) - expect(isDarkMode).toBe(true) -}) diff --git a/frontend/tests/utils/mailcatcher.ts b/frontend/tests/utils/mailcatcher.ts deleted file mode 100644 index 601ce434fb..0000000000 --- a/frontend/tests/utils/mailcatcher.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { APIRequestContext } from "@playwright/test" - -type Email = { - id: number - recipients: string[] - subject: string -} - -async function findEmail({ - request, - filter, -}: { request: APIRequestContext; filter?: (email: Email) => boolean }) { - const response = await request.get("http://localhost:1080/messages") - - let emails = await response.json() - - if (filter) { - emails = emails.filter(filter) - } - - const email = emails[emails.length - 1] - - if (email) { - return email as Email - } - - return null -} - -export function findLastEmail({ - request, - filter, - timeout = 5000, -}: { - request: APIRequestContext - filter?: (email: Email) => boolean - timeout?: number -}) { - const timeoutPromise = new Promise((_, reject) => - setTimeout( - () => reject(new Error("Timeout while trying to get latest email")), - timeout, - ), - ) - - const checkEmails = async () => { - while (true) { - const emailData = await findEmail({ request, filter }) - - if (emailData) { - return emailData - } - // Wait for 100ms before checking again - await new Promise((resolve) => setTimeout(resolve, 100)) - } - } - - return Promise.race([timeoutPromise, checkEmails()]) -} diff --git a/frontend/tests/utils/random.ts b/frontend/tests/utils/random.ts deleted file mode 100644 index d96f0833ce..0000000000 --- a/frontend/tests/utils/random.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const randomEmail = () => - `test_${Math.random().toString(36).substring(7)}@example.com` - -export const randomTeamName = () => - `Team ${Math.random().toString(36).substring(7)}` - -export const randomPassword = () => `${Math.random().toString(36).substring(2)}` - -export const slugify = (text: string) => - text - .toLowerCase() - .replace(/\s+/g, "-") - .replace(/[^\w-]+/g, "") diff --git a/frontend/tests/utils/user.ts b/frontend/tests/utils/user.ts deleted file mode 100644 index 8fcfd26cb5..0000000000 --- a/frontend/tests/utils/user.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { type Page, expect } from "@playwright/test" - -export async function signUpNewUser( - page: Page, - name: string, - email: string, - password: string, -) { - await page.goto("/signup") - - await page.getByPlaceholder("Full Name").fill(name) - await page.getByPlaceholder("Email").fill(email) - await page.getByPlaceholder("Password", { exact: true }).fill(password) - await page.getByPlaceholder("Repeat Password").fill(password) - await page.getByRole("button", { name: "Sign Up" }).click() - await expect( - page.getByText("Your account has been created successfully"), - ).toBeVisible() - await page.goto("/login") -} - -export async function logInUser(page: Page, email: string, password: string) { - await page.goto("/login") - - await page.getByPlaceholder("Email").fill(email) - await page.getByPlaceholder("Password", { exact: true }).fill(password) - await page.getByRole("button", { name: "Log In" }).click() - await page.waitForURL("/") - await expect( - page.getByText("Welcome back, nice to see you again!"), - ).toBeVisible() -} - -export async function logOutUser(page: Page) { - await page.getByTestId("user-menu").click() - await page.getByRole("menuitem", { name: "Log out" }).click() - await page.goto("/login") -} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json deleted file mode 100644 index baadbb9fb1..0000000000 --- a/frontend/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src", "*.ts", "**/*.ts"], - "references": [{ "path": "./tsconfig.node.json" }] -} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json deleted file mode 100644 index 42872c59f5..0000000000 --- a/frontend/tsconfig.node.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts deleted file mode 100644 index 572745b8cf..0000000000 --- a/frontend/vite.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { TanStackRouterVite } from "@tanstack/router-vite-plugin" -import react from "@vitejs/plugin-react-swc" -import { defineConfig } from "vite" - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [react(), TanStackRouterVite()], -}) diff --git a/img/dashboard-create.png b/img/dashboard-create.png deleted file mode 100644 index a394141f7b..0000000000 Binary files a/img/dashboard-create.png and /dev/null differ diff --git a/img/dashboard-dark.png b/img/dashboard-dark.png deleted file mode 100644 index 51040a157b..0000000000 Binary files a/img/dashboard-dark.png and /dev/null differ diff --git a/img/dashboard-items.png b/img/dashboard-items.png deleted file mode 100644 index f50e2e834e..0000000000 Binary files a/img/dashboard-items.png and /dev/null differ diff --git a/img/dashboard-user-settings.png b/img/dashboard-user-settings.png deleted file mode 100644 index 8da2e21df7..0000000000 Binary files a/img/dashboard-user-settings.png and /dev/null differ diff --git a/img/dashboard.png b/img/dashboard.png deleted file mode 100644 index 0f034d691b..0000000000 Binary files a/img/dashboard.png and /dev/null differ diff --git a/img/docs.png b/img/docs.png deleted file mode 100644 index d61c2071c7..0000000000 Binary files a/img/docs.png and /dev/null differ diff --git a/img/github-social-preview.png b/img/github-social-preview.png deleted file mode 100644 index f1dc5959fb..0000000000 Binary files a/img/github-social-preview.png and /dev/null differ diff --git a/img/github-social-preview.svg b/img/github-social-preview.svg deleted file mode 100644 index 4b7a75760e..0000000000 --- a/img/github-social-preview.svg +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - image/svg+xml - - - - - - - - - - FastAPI - - Full Stack - Template - diff --git a/img/login.png b/img/login.png deleted file mode 100644 index 66e3a7202f..0000000000 Binary files a/img/login.png and /dev/null differ diff --git a/backend/app/tests/api/routes/__init__.py b/letsencrypt/acme.json similarity index 100% rename from backend/app/tests/api/routes/__init__.py rename to letsencrypt/acme.json diff --git a/scripts/build-push.sh b/scripts/build-push.sh deleted file mode 100644 index 3fa3aa7e6b..0000000000 --- a/scripts/build-push.sh +++ /dev/null @@ -1,10 +0,0 @@ -#! /usr/bin/env sh - -# Exit in case of error -set -e - -TAG=${TAG?Variable not set} \ -FRONTEND_ENV=${FRONTEND_ENV-production} \ -sh ./scripts/build.sh - -docker-compose -f docker-compose.yml push diff --git a/scripts/build.sh b/scripts/build.sh deleted file mode 100644 index 21528c538e..0000000000 --- a/scripts/build.sh +++ /dev/null @@ -1,10 +0,0 @@ -#! /usr/bin/env sh - -# Exit in case of error -set -e - -TAG=${TAG?Variable not set} \ -FRONTEND_ENV=${FRONTEND_ENV-production} \ -docker-compose \ --f docker-compose.yml \ -build diff --git a/scripts/generate-client.sh b/scripts/generate-client.sh deleted file mode 100644 index 1327ee6fd1..0000000000 --- a/scripts/generate-client.sh +++ /dev/null @@ -1,8 +0,0 @@ -#! /usr/bin/env bash - -PYTHONPATH=backend python -c "import app.main; import json; print(json.dumps(app.main.app.openapi()))" > openapi.json -node frontend/modify-openapi-operationids.js -mv openapi.json frontend/ -cd frontend -npm run generate-client -npx biome format --write ./src/client diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000000..250321aa52 --- /dev/null +++ b/types.d.ts @@ -0,0 +1,67 @@ +interface RestaurantMenuResponse { + status: 'success' | 'error'; + restaurant_info: { + cuisine_type: string; + venue: { + name: string; + address: string; + locality: string; + city: string; + latitude: string; + longitude: string; + zipcode: string; + rating: string; + timing: string; + avg_cost_for_two: number; + }; + }; + menu: Array<{ + name: string; + description: string; + subcategories: Array<{ + name: string; + description: string; + items: Array<{ + name: string; + description: string; + is_veg: boolean; + image_url: string; + variants: Array<{ + name: string; + price: number; + is_default: boolean; + }>; + }>; + }>; + }>; + } + + + + + interface MenuItem { + id: string; + name: string; + description: string; + price: number; + is_veg: boolean; + spice_level: 'None' | 'Mild' | 'Medium' | 'Spicy' | 'Hot'; + image_url: any; // Using 'any' for image imports, could be refined based on your setup + } + + interface MenuSubcategory { + subcategory: string; + items: MenuItem[]; + } + + interface MenuCategory { + category: string; + subcategories: MenuSubcategory[]; + } + + interface MenuData { + menu: MenuCategory[]; + } + + // Assuming isWeb is defined elsewhere in your codebase + declare const isWeb: boolean; \ No newline at end of file