diff --git a/.copier/.copier-answers.yml.jinja b/.copier/.copier-answers.yml.jinja new file mode 100644 index 0000000000..0028a2398a --- /dev/null +++ b/.copier/.copier-answers.yml.jinja @@ -0,0 +1 @@ +{{ _copier_answers|to_json -}} diff --git a/.copier/update_dotenv.py b/.copier/update_dotenv.py new file mode 100644 index 0000000000..6576885626 --- /dev/null +++ b/.copier/update_dotenv.py @@ -0,0 +1,26 @@ +from pathlib import Path +import json + +# Update the .env file with the answers from the .copier-answers.yml file +# without using Jinja2 templates in the .env file, this way the code works as is +# without needing Copier, but if Copier is used, the .env file will be updated +root_path = Path(__file__).parent.parent +answers_path = Path(__file__).parent / ".copier-answers.yml" +answers = json.loads(answers_path.read_text()) +env_path = root_path / ".env" +env_content = env_path.read_text() +lines = [] +for line in env_content.splitlines(): + for key, value in answers.items(): + upper_key = key.upper() + if line.startswith(f"{upper_key}="): + if " " in value: + content = f"{upper_key}={value!r}" + else: + content = f"{upper_key}={value}" + new_line = line.replace(line, content) + lines.append(new_line) + break + else: + lines.append(line) +env_path.write_text("\n".join(lines)) diff --git a/.env b/.env new file mode 100644 index 0000000000..1d44286e25 --- /dev/null +++ b/.env @@ -0,0 +1,45 @@ +# Domain +# This would be set to the production domain with an env var on deployment +# used by Traefik to transmit traffic and aqcuire TLS certificates +DOMAIN=localhost +# To test the local Traefik config +# DOMAIN=localhost.tiangolo.com + +# Used by the backend to generate links in emails to the frontend +FRONTEND_HOST=http://localhost:5173 +# In staging and production, set this env var to the frontend host, e.g. +# FRONTEND_HOST=https://dashboard.example.com + +# Environment: local, staging, production +ENVIRONMENT=local + +PROJECT_NAME="Full Stack FastAPI Project" +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 + +# Emails +SMTP_HOST= +SMTP_USER= +SMTP_PASSWORD= +EMAILS_FROM_EMAIL=info@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 + +SENTRY_DSN= + +# Configure these with your own Docker registry images +DOCKER_IMAGE_BACKEND=backend +DOCKER_IMAGE_FRONTEND=frontend diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..efdba87644 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto +*.sh text eol=lf diff --git a/.github/DISCUSSION_TEMPLATE/questions.yml b/.github/DISCUSSION_TEMPLATE/questions.yml new file mode 100644 index 0000000000..af6abf4661 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/questions.yml @@ -0,0 +1,118 @@ +labels: [question] +body: + - type: markdown + attributes: + value: | + Thanks for your interest in this project! πŸš€ + + Please follow these instructions, fill every question, and do every step. πŸ™ + + I'm asking this because answering questions and solving problems in GitHub is what consumes most of the time. + + I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling questions. + + All that, on top of all the incredible help provided by a bunch of community members, that give a lot of their time to come here and help others. + + That's a lot of work, but if more users came to help others like them just a little bit more, it would be much less effort for them (and you and me πŸ˜…). + + By asking questions in a structured way (following this) it will be much easier to help you. + + And there's a high chance that you will find the solution along the way and you won't even have to submit it and wait for an answer. 😎 + + As there are too many questions, I'll have to discard and close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. πŸ€“ + - type: checkboxes + id: checks + attributes: + label: First Check + description: Please confirm and check all the following options. + options: + - label: I added a very descriptive title here. + required: true + - label: I used the GitHub search to find a similar question and didn't find it. + required: true + - label: I searched in the documentation/README. + required: true + - label: I already searched in Google "How to do X" and didn't find any information. + required: true + - label: I already read and followed all the tutorial in the docs/README and didn't find an answer. + required: true + - type: checkboxes + id: help + attributes: + label: Commit to Help + description: | + After submitting this, I commit to one of: + + * Read open questions until I find 2 where I can help someone and add a comment to help there. + * I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future. + + options: + - label: I commit to help with one of those options πŸ‘† + required: true + - type: textarea + id: example + attributes: + label: Example Code + description: | + Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case. + + If I (or someone) can copy it, run it, and see it right away, there's a much higher chance I (or someone) will be able to help you. + + placeholder: | + Write your example code here. + render: Text + validations: + required: true + - type: textarea + id: description + attributes: + label: Description + description: | + What is the problem, question, or error? + + Write a short description telling me what you are doing, what you expect to happen, and what is currently happening. + placeholder: | + * Open the browser and call the endpoint `/`. + * It returns a JSON with `{"message": "Hello World"}`. + * But I expected it to return `{"message": "Hello Morty"}`. + validations: + required: true + - type: dropdown + id: os + attributes: + label: Operating System + description: What operating system are you on? + multiple: true + options: + - Linux + - Windows + - macOS + - Other + validations: + required: true + - type: textarea + id: os-details + attributes: + label: Operating System Details + description: You can add more details about your operating system here, in particular if you chose "Other". + validations: + required: true + - type: input + id: python-version + attributes: + label: Python Version + description: | + What Python version are you using? + + You can find the Python version with: + + ```bash + python --version + ``` + validations: + required: true + - type: textarea + id: context + attributes: + label: Additional Context + description: Add any additional context information or screenshots you think are useful. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..50bde36072 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,10 @@ +blank_issues_enabled: false +contact_links: + - name: Security Contact + about: Please report security vulnerabilities to security@tiangolo.com + - name: Question or Problem + about: Ask a question or ask about a problem in GitHub Discussions. + url: https://github.com/fastapi/full-stack-fastapi-template/discussions/categories/questions + - name: Feature Request + about: To suggest an idea or ask about a feature, please start with a question saying what you would like to achieve. There might be a way to do it already. + url: https://github.com/fastapi/full-stack-fastapi-template/discussions/categories/questions diff --git a/.github/ISSUE_TEMPLATE/privileged.yml b/.github/ISSUE_TEMPLATE/privileged.yml new file mode 100644 index 0000000000..6438848c83 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/privileged.yml @@ -0,0 +1,22 @@ +name: Privileged +description: You are @tiangolo or he asked you directly to create an issue here. If not, check the other options. πŸ‘‡ +body: + - type: markdown + attributes: + value: | + Thanks for your interest in this project! πŸš€ + + If you are not @tiangolo or he didn't ask you directly to create an issue here, please start the conversation in a [Question in GitHub Discussions](https://github.com/tiangolo/full-stack-fastapi-template/discussions/categories/questions) instead. + - type: checkboxes + id: privileged + attributes: + label: Privileged issue + description: Confirm that you are allowed to create an issue here. + options: + - label: I'm @tiangolo or he asked me directly to create an issue here. + required: true + - type: textarea + id: content + attributes: + label: Issue Content + description: Add the content of the issue here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..d0c15ccc7c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,41 @@ +version: 2 +updates: + # GitHub Actions + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + commit-message: + prefix: ⬆ + # Python uv + - package-ecosystem: uv + directory: /backend + schedule: + interval: weekly + commit-message: + prefix: ⬆ + # npm + - package-ecosystem: npm + directory: /frontend + schedule: + interval: weekly + commit-message: + prefix: ⬆ + ignore: + - dependency-name: "@hey-api/openapi-ts" + # Docker + - package-ecosystem: docker + directories: + - /backend + - /frontend + schedule: + interval: weekly + commit-message: + prefix: ⬆ + # Docker Compose + - package-ecosystem: docker-compose + directory: / + schedule: + interval: weekly + commit-message: + prefix: ⬆ diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000000..ed657c23d7 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,25 @@ +docs: + - all: + - changed-files: + - any-glob-to-any-file: + - '**/*.md' + - all-globs-to-all-files: + - '!frontend/**' + - '!backend/**' + - '!.github/**' + - '!scripts/**' + - '!.gitignore' + - '!.pre-commit-config.yaml' + +internal: + - all: + - changed-files: + - any-glob-to-any-file: + - .github/** + - scripts/** + - .gitignore + - .pre-commit-config.yaml + - all-globs-to-all-files: + - '!./**/*.md' + - '!frontend/**' + - '!backend/**' diff --git a/.github/workflows/add-to-project.yml b/.github/workflows/add-to-project.yml new file mode 100644 index 0000000000..dccea83f35 --- /dev/null +++ b/.github/workflows/add-to-project.yml @@ -0,0 +1,18 @@ +name: Add to Project + +on: + pull_request_target: + issues: + types: + - opened + - reopened + +jobs: + add-to-project: + name: Add to project + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v1.0.2 + with: + project-url: https://github.com/orgs/fastapi/projects/2 + github-token: ${{ secrets.PROJECTS_TOKEN }} diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml new file mode 100644 index 0000000000..9bcecc90b8 --- /dev/null +++ b/.github/workflows/deploy-production.yml @@ -0,0 +1,32 @@ +name: Deploy to Production + +on: + release: + types: + - published + +jobs: + deploy: + # Do not deploy in the main repository, only in user projects + if: github.repository_owner != 'fastapi' + runs-on: + - self-hosted + - production + env: + ENVIRONMENT: production + DOMAIN: ${{ secrets.DOMAIN_PRODUCTION }} + STACK_NAME: ${{ secrets.STACK_NAME_PRODUCTION }} + SECRET_KEY: ${{ secrets.SECRET_KEY }} + FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }} + FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }} + SMTP_HOST: ${{ secrets.SMTP_HOST }} + SMTP_USER: ${{ secrets.SMTP_USER }} + SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }} + EMAILS_FROM_EMAIL: ${{ secrets.EMAILS_FROM_EMAIL }} + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + steps: + - name: Checkout + uses: actions/checkout@v5 + - run: docker compose -f docker-compose.yml --project-name ${{ secrets.STACK_NAME_PRODUCTION }} build + - run: docker compose -f docker-compose.yml --project-name ${{ secrets.STACK_NAME_PRODUCTION }} up -d diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml new file mode 100644 index 0000000000..31770d8ee0 --- /dev/null +++ b/.github/workflows/deploy-staging.yml @@ -0,0 +1,32 @@ +name: Deploy to Staging + +on: + push: + branches: + - master + +jobs: + deploy: + # Do not deploy in the main repository, only in user projects + if: github.repository_owner != 'fastapi' + runs-on: + - self-hosted + - staging + env: + ENVIRONMENT: staging + DOMAIN: ${{ secrets.DOMAIN_STAGING }} + STACK_NAME: ${{ secrets.STACK_NAME_STAGING }} + SECRET_KEY: ${{ secrets.SECRET_KEY }} + FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }} + FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }} + SMTP_HOST: ${{ secrets.SMTP_HOST }} + SMTP_USER: ${{ secrets.SMTP_USER }} + SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }} + EMAILS_FROM_EMAIL: ${{ secrets.EMAILS_FROM_EMAIL }} + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + steps: + - name: Checkout + uses: actions/checkout@v5 + - run: docker compose -f docker-compose.yml --project-name ${{ secrets.STACK_NAME_STAGING }} build + - run: docker compose -f docker-compose.yml --project-name ${{ secrets.STACK_NAME_STAGING }} up -d diff --git a/.github/workflows/detect-conflicts.yml b/.github/workflows/detect-conflicts.yml new file mode 100644 index 0000000000..aba329db85 --- /dev/null +++ b/.github/workflows/detect-conflicts.yml @@ -0,0 +1,19 @@ +name: "Conflict detector" +on: + push: + pull_request_target: + types: [synchronize] + +jobs: + main: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Check if PRs have merge conflicts + uses: eps1lon/actions-label-merge-conflict@v3 + with: + dirtyLabel: "conflicts" + repoToken: "${{ secrets.GITHUB_TOKEN }}" + commentOnDirty: "This pull request has a merge conflict that needs to be resolved." diff --git a/.github/workflows/generate-client.yml b/.github/workflows/generate-client.yml new file mode 100644 index 0000000000..123fef2839 --- /dev/null +++ b/.github/workflows/generate-client.yml @@ -0,0 +1,60 @@ +name: Generate Client + +on: + pull_request: + types: + - opened + - synchronize + +jobs: + generate-client: + permissions: + contents: write + runs-on: ubuntu-latest + steps: + # For PRs from forks + - uses: actions/checkout@v5 + # For PRs from the same repo + - uses: actions/checkout@v5 + if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.FULL_STACK_FASTAPI_TEMPLATE_REPO_TOKEN }} + - uses: actions/setup-node@v6 + with: + node-version: lts/* + - uses: actions/setup-python@v6 + with: + python-version: "3.10" + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + version: "0.4.15" + enable-cache: true + - name: Install dependencies + run: npm ci + working-directory: frontend + - run: uv sync + working-directory: backend + - run: uv run bash scripts/generate-client.sh + env: + VIRTUAL_ENV: backend/.venv + SECRET_KEY: just-for-generating-client + POSTGRES_PASSWORD: just-for-generating-client + FIRST_SUPERUSER_PASSWORD: just-for-generating-client + - name: Add changes to git + run: | + git config --local user.email "github-actions@github.com" + git config --local user.name "github-actions" + git add frontend/src/client + # Same repo PRs + - name: Push changes + if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) + run: | + git diff --staged --quiet || git commit -m "✨ Autogenerate frontend client" + git push + # Fork PRs + - name: Check changes + if: ( github.event_name == 'pull_request' && github.secret_source != 'Actions' ) + run: | + git diff --staged --quiet || (echo "Changes detected in generated client, run scripts/generate-client.sh and commit the changes" && exit 1) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index 42748fbb2b..1d1073fe19 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -2,27 +2,49 @@ name: Issue Manager on: schedule: - - cron: "0 0 * * *" + - cron: "21 17 * * *" issue_comment: types: - created - - edited issues: types: - labeled + pull_request_target: + types: + - labeled + workflow_dispatch: + +permissions: + issues: write + pull-requests: write jobs: issue-manager: + if: github.repository_owner == 'fastapi' runs-on: ubuntu-latest steps: - - uses: tiangolo/issue-manager@0.2.0 + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: tiangolo/issue-manager@0.6.0 with: token: ${{ secrets.GITHUB_TOKEN }} config: > { "answered": { - "users": ["tiangolo"], "delay": 864000, - "message": "Assuming the original issue was solved, it will be automatically closed now. But feel free to add more comments or create new issues." + "message": "Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs." + }, + "waiting": { + "delay": 2628000, + "message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR.", + "reminder": { + "before": "P3D", + "message": "Heads-up: this will be closed in 3 days unless there’s new activity." + }, + "invalid": { + "delay": 0, + "message": "This was marked as invalid and will be closed now. If this is an error, please provide additional details." } } diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000000..7aeb448e6f --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,33 @@ +name: Labels +on: + pull_request_target: + types: + - opened + - synchronize + - reopened + # For label-checker + - labeled + - unlabeled + +jobs: + labeler: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v6 + if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }} + - run: echo "Done adding labels" + # Run this after labeler applied labels + check-labels: + needs: + - labeler + permissions: + pull-requests: read + runs-on: ubuntu-latest + steps: + - uses: docker://agilepathway/pull-request-label-checker:latest + with: + one_of: breaking,security,feature,bug,refactor,upgrade,docs,lang-all,internal + repo_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml new file mode 100644 index 0000000000..d1ea9def1d --- /dev/null +++ b/.github/workflows/latest-changes.yml @@ -0,0 +1,40 @@ +name: Latest Changes + +on: + pull_request_target: + branches: + - master + types: + - closed + workflow_dispatch: + inputs: + number: + description: PR number + required: true + debug_enabled: + description: "Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)" + required: false + default: "false" + +jobs: + latest-changes: + runs-on: ubuntu-latest + permissions: + pull-requests: read + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v5 + with: + # To allow latest-changes to commit to the main branch + token: ${{ secrets.LATEST_CHANGES }} + - uses: tiangolo/latest-changes@0.4.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + latest_changes_file: ./release-notes.md + latest_changes_header: "## Latest Changes" + end_regex: "^## " + debug_logs: true + label_header_prefix: "### " diff --git a/.github/workflows/lint-backend.yml b/.github/workflows/lint-backend.yml new file mode 100644 index 0000000000..4f486bdbdf --- /dev/null +++ b/.github/workflows/lint-backend.yml @@ -0,0 +1,28 @@ +name: Lint Backend + +on: + push: + branches: + - master + pull_request: + types: + - opened + - synchronize + +jobs: + lint-backend: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.10" + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + version: "0.4.15" + enable-cache: true + - run: uv run bash scripts/lint.sh + working-directory: backend diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000000..13569c2adc --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,131 @@ +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: + changes: + runs-on: ubuntu-latest + # Set job outputs to values from filter step + outputs: + changed: ${{ steps.filter.outputs.changed }} + steps: + - uses: actions/checkout@v5 + # For pull requests it's not necessary to checkout the code but for the main branch it is + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + changed: + - backend/** + - frontend/** + - .env + - docker-compose*.yml + - .github/workflows/playwright.yml + + test-playwright: + needs: + - changes + if: ${{ needs.changes.outputs.changed == 'true' }} + timeout-minutes: 60 + runs-on: ubuntu-latest + strategy: + matrix: + shardIndex: [1, 2, 3, 4] + shardTotal: [4] + fail-fast: false + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v6 + with: + node-version: lts/* + - uses: actions/setup-python@v6 + 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 uv + uses: astral-sh/setup-uv@v7 + with: + version: "0.4.15" + enable-cache: true + - run: uv sync + working-directory: backend + - run: npm ci + working-directory: frontend + - run: uv run bash scripts/generate-client.sh + env: + VIRTUAL_ENV: backend/.venv + - run: docker compose build + - run: docker compose down -v --remove-orphans + - name: Run Playwright tests + run: docker compose run --rm playwright npx playwright test --fail-on-flaky-tests --trace=retain-on-failure --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + - run: docker compose down -v --remove-orphans + - name: Upload blob report to GitHub Actions Artifacts + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: blob-report-${{ matrix.shardIndex }} + path: frontend/blob-report + include-hidden-files: true + retention-days: 1 + + merge-playwright-reports: + needs: + - test-playwright + - changes + # Merge reports after playwright-tests, even if some shards have failed + if: ${{ !cancelled() && needs.changes.outputs.changed == 'true' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v6 + with: + node-version: 20 + - name: Install dependencies + run: npm ci + working-directory: frontend + - name: Download blob reports from GitHub Actions Artifacts + uses: actions/download-artifact@v5 + with: + path: frontend/all-blob-reports + pattern: blob-report-* + merge-multiple: true + - name: Merge into HTML Report + run: npx playwright merge-reports --reporter html ./all-blob-reports + working-directory: frontend + - name: Upload HTML report + uses: actions/upload-artifact@v4 + with: + name: html-report--attempt-${{ github.run_attempt }} + path: frontend/playwright-report + retention-days: 30 + include-hidden-files: true + + # https://github.com/marketplace/actions/alls-green#why + alls-green-playwright: # This job does nothing and is only used for the branch protection + if: always() + needs: + - test-playwright + 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) }} + allowed-skips: test-playwright diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml new file mode 100644 index 0000000000..d9b3ac542a --- /dev/null +++ b/.github/workflows/smokeshow.yml @@ -0,0 +1,35 @@ +name: Smokeshow + +on: + workflow_run: + workflows: [Test Backend] + types: [completed] + +jobs: + smokeshow: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + permissions: + actions: read + statuses: write + + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: "3.10" + - run: pip install smokeshow + - uses: actions/download-artifact@v5 + with: + name: coverage-html + path: backend/htmlcov + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + - run: smokeshow upload backend/htmlcov + env: + SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage} + SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 90 + SMOKESHOW_GITHUB_CONTEXT: coverage + SMOKESHOW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SMOKESHOW_GITHUB_PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + SMOKESHOW_AUTH_KEY: ${{ secrets.SMOKESHOW_AUTH_KEY }} diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml new file mode 100644 index 0000000000..e2976e64d5 --- /dev/null +++ b/.github/workflows/test-backend.yml @@ -0,0 +1,41 @@ +name: Test Backend + +on: + push: + branches: + - master + pull_request: + types: + - opened + - synchronize + +jobs: + test-backend: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.10" + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + version: "0.4.15" + enable-cache: true + - run: docker compose down -v --remove-orphans + - run: docker compose up -d db mailcatcher + - name: Migrate DB + run: uv run bash scripts/prestart.sh + working-directory: backend + - name: Run tests + run: uv run bash scripts/tests-start.sh "Coverage for ${{ github.sha }}" + working-directory: backend + - run: docker compose down -v --remove-orphans + - name: Store coverage files + uses: actions/upload-artifact@v4 + with: + name: coverage-html + path: backend/htmlcov + include-hidden-files: true diff --git a/.github/workflows/test-docker-compose.yml b/.github/workflows/test-docker-compose.yml new file mode 100644 index 0000000000..c14d9dd630 --- /dev/null +++ b/.github/workflows/test-docker-compose.yml @@ -0,0 +1,26 @@ +name: Test Docker Compose + +on: + push: + branches: + - master + pull_request: + types: + - opened + - synchronize + +jobs: + + test-docker-compose: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + - run: docker compose build + - run: docker compose down -v --remove-orphans + - run: docker compose up -d --wait backend frontend adminer + - name: Test backend is up + run: curl http://localhost:8000/api/v1/utils/health-check + - name: Test frontend is up + run: curl http://localhost:5173 + - run: docker compose down -v --remove-orphans diff --git a/.gitignore b/.gitignore index 5d778b370c..a6dd346572 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .vscode -testing-project -.mypy_cache -poetry.lock -dev-link/ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..37534e2ab0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + - id: check-toml + - id: check-yaml + args: + - --unsafe + - id: end-of-file-fixer + exclude: | + (?x)^( + frontend/src/client/.*| + backend/app/email-templates/build/.* + )$ + - id: trailing-whitespace + exclude: ^frontend/src/client/.* + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.2.2 + hooks: + - id: ruff + args: + - --fix + - id: ruff-format + - repo: local + hooks: + - id: local-biome-check + name: biome check + entry: bash -c 'cd frontend && npm run lint' + language: system + types: [text] + files: ^frontend/ + +ci: + autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks + autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ad7e0349a3..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -sudo: required - -language: python - -install: - - pip install cookiecutter - -services: - - docker - -script: -- bash ./scripts/test.sh diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..24eae850d0 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Debug FastAPI Project backend: Python Debugger", + "type": "debugpy", + "request": "launch", + "module": "uvicorn", + "args": [ + "app.main:app", + "--reload" + ], + "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/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index d95d76171c..0000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,83 +0,0 @@ -# Contributing - -Here are some short guidelines to guide you if you want to contribute to the development of the Full Stack FastAPI PostgreSQL project generator itself. - -After you clone the project, there are several scripts that can help during development. - -* `./scripts/dev-fsfp.sh`: - -Generate a new default project `dev-fsfp`. - -Call it from one level above the project directory. So, if the project is at `~/code/full-stack-fastapi-postgresql/`, call it from `~/code/`, like: - -```console -$ cd ~/code/ - -$ bash ./full-stack-fastapi-postgresql/scripts/dev-fsfp.sh -``` - -It will generate a new project with all the defaults at `~/code/dev-fsfp/`. - -You can go to that directory with a full new project, edit files and test things, for example: - -```console -$ cd ./dev-fsfp/ - -$ docker-compose up -d -``` - -It is outside of the project generator directory to let you add Git to it and compare versions and changes. - -* `./scripts/dev-fsfp-back.sh`: - -Move the changes from a project `dev-fsfp` back to the project generator. - -You would call it after calling `./scripts/dev-fsfp.sh` and adding some modifications to `dev-fsfp`. - -Call it from one level above the project directory. So, if the project is at `~/code/full-stack-fastapi-postgresql/`, call it from `~/code/`, like: - -```console -$ cd ~/code/ - -$ bash ./full-stack-fastapi-postgresql/scripts/dev-fsfp-back.sh -``` - -That will also contain all the generated files with the generated variables, but it will let you compare the changes in `dev-fsfp` and the source in the project generator with git, and see what to commit. - -* `./scripts/discard-dev-files.sh`: - -After using `./scripts/dev-fsfp-back.sh`, there will be a bunch of generated files with the variables for the generated project that you don't want to commit, like `README.md` and `.gitlab-ci.yml`. - -To discard all those changes at once, run `discard-dev-files.sh` from the root of the project, e.g.: - -```console -$ cd ~/code/full-stack-fastapi-postgresql/ - -$ bash ./scripts/dev-fsfp-back.sh -``` - -* `./scripts/test.sh`: - -Run the tests. It creates a project `testing-project` *inside* of the project generator and runs its tests. - -Call it from the root of the project, e.g.: - -```console -$ cd ~/code/full-stack-fastapi-postgresql/ - -$ bash ./scripts/test.sh -``` - -* `./scripts/dev-link.sh`: - -Set up a local directory with links to the files for live development with the source files. - -This script generates a project `dev-link` *inside* the project generator, just to generate the `.env` and `./frontend/.env` files. - -Then it removes everything except those 2 files. - -Then it creates links for each of the source files, and adds those 2 files back. - -The end result is that you can go into the `dev-link` directory and develop locally with it as if it was a generated project, with all the variables set. But all the changes are actually done directly in the source files. - -This is probably a lot faster to iterate than using `./scripts/dev-fsfp.sh`. But it's tested only in Linux, it might not work in other systems. diff --git a/README.md b/README.md index d85e943834..afe124f3fb 100644 --- a/README.md +++ b/README.md @@ -1,270 +1,239 @@ -# Full Stack FastAPI and PostgreSQL - Base Project Generator +# 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. -[![Build Status](https://travis-ci.com/tiangolo/full-stack-fastapi-postgresql.svg?branch=master)](https://travis-ci.com/tiangolo/full-stack-fastapi-postgresql) +### Dashboard Login -Generate a backend and frontend stack using Python, including interactive API documentation. +[![API docs](img/login.png)](https://github.com/fastapi/full-stack-fastapi-template) -### Interactive API documentation +### Dashboard - Admin -[![API docs](img/docs.png)](https://github.com/tiangolo/full-stack-fastapi-postgresql) +[![API docs](img/dashboard.png)](https://github.com/fastapi/full-stack-fastapi-template) -### Alternative API documentation +### Dashboard - Create User -[![API docs](img/redoc.png)](https://github.com/tiangolo/full-stack-fastapi-postgresql) +[![API docs](img/dashboard-create.png)](https://github.com/fastapi/full-stack-fastapi-template) -### Dashboard Login +### Dashboard - Items -[![API docs](img/login.png)](https://github.com/tiangolo/full-stack-fastapi-postgresql) +[![API docs](img/dashboard-items.png)](https://github.com/fastapi/full-stack-fastapi-template) -### Dashboard - Create User +### 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 -[![API docs](img/dashboard.png)](https://github.com/tiangolo/full-stack-fastapi-postgresql) - -## Features - -* Full **Docker** integration (Docker based). -* Docker Swarm Mode deployment. -* **Docker Compose** integration and optimization for local development. -* **Production ready** Python web server using Uvicorn and Gunicorn. -* Python **FastAPI** backend: - * **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). - * **Intuitive**: Great editor support. Completion everywhere. Less time debugging. - * **Easy**: Designed to be easy to use and learn. Less time reading docs. - * **Short**: Minimize code duplication. Multiple features from each parameter declaration. - * **Robust**: Get production-ready code. With automatic interactive documentation. - * **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI and JSON Schema. - * **Many other features** including automatic validation, serialization, interactive documentation, authentication with OAuth2 JWT tokens, etc. -* **Secure password** hashing by default. -* **JWT token** authentication. -* **SQLAlchemy** models (independent of Flask extensions, so they can be used with Celery workers directly). -* Basic starting models for users (modify and remove as you need). -* **Alembic** migrations. -* **CORS** (Cross Origin Resource Sharing). -* **Celery** worker that can import and use models and code from the rest of the backend selectively. -* REST backend tests based on **Pytest**, integrated with Docker, so you can test the full API interaction, independent on the database. As it runs in Docker, it can build a new data store from scratch each time (so you can use ElasticSearch, MongoDB, CouchDB, or whatever you want, and just test that the API works). -* Easy Python integration with **Jupyter Kernels** for remote or in-Docker development with extensions like Atom Hydrogen or Visual Studio Code Jupyter. -* **Vue** frontend: - * Generated with Vue CLI. - * **JWT Authentication** handling. - * Login view. - * After login, main dashboard view. - * Main dashboard with user creation and edition. - * Self user edition. - * **Vuex**. - * **Vue-router**. - * **Vuetify** for beautiful material design components. - * **TypeScript**. - * Docker server based on **Nginx** (configured to play nicely with Vue-router). - * Docker multi-stage building, so you don't need to save or commit compiled code. - * Frontend tests ran at build time (can be disabled too). - * Made as modular as possible, so it works out of the box, but you can re-generate with Vue CLI or create it as you need, and re-use what you want. - * It's also easy to remove it if you have an API-only app, check the instructions in the generated `README.md`. -* **PGAdmin** for PostgreSQL database, you can modify it to use PHPMyAdmin and MySQL easily. -* **Flower** for Celery jobs monitoring. -* Load balancing between frontend and backend with **Traefik**, so you can have both under the same domain, separated by path, but served by different containers. -* Traefik integration, including Let's Encrypt **HTTPS** certificates automatic generation. -* GitLab **CI** (continuous integration), including frontend and backend testing. - -## How to use it - -Go to the directory where you want to create your project and run: +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 -pip install cookiecutter -cookiecutter https://github.com/tiangolo/full-stack-fastapi-postgresql +git clone git@github.com:fastapi/full-stack-fastapi-template.git my-full-stack ``` -### Generate passwords +- Enter into the new directory: + +```bash +cd my-full-stack +``` -You will be asked to provide passwords and secret keys for several components. Open another terminal and run: +- Set the new origin to your new repository, copy it from the GitHub interface, for example: ```bash -openssl rand -hex 32 -# Outputs something like: 99d3b1f01aa639e4a76f4fc281fc834747a543720ba4c8a8648ba755aef9be7f +git remote set-url origin git@github.com:octocat/my-full-stack.git ``` -Copy the contents and use that as password / secret key. And run that again to generate another secure key. +- 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 +``` -### Input variables +- Push the code to your new repository: -The generator (cookiecutter) will ask you for some data, you might want to have at hand before generating the project. +```bash +git push -u origin master +``` -The input variables, with their default values (some auto generated) are: +### Update From the Original Template -* `project_name`: The name of the project -* `project_slug`: The development friendly name of the project. By default, based on the project name -* `domain_main`: The domain in where to deploy the project for production (from the branch `production`), used by the load balancer, backend, etc. By default, based on the project slug. -* `domain_staging`: The domain in where to deploy while staging (before production) (from the branch `master`). By default, based on the main domain. +After cloning the repository, and after doing changes, you might want to get the latest changes from this original template. -* `docker_swarm_stack_name_main`: The name of the stack while deploying to Docker in Swarm mode for production. By default, based on the domain. -* `docker_swarm_stack_name_staging`: The name of the stack while deploying to Docker in Swarm mode for staging. By default, based on the domain. +- Make sure you added the original repository as a remote, you can check it with: -* `secret_key`: Backend server secret key. Use the method above to generate it. -* `first_superuser`: The first superuser generated, with it you will be able to create more users, etc. By default, based on the domain. -* `first_superuser_password`: First superuser password. Use the method above to generate it. -* `backend_cors_origins`: Origins (domains, more or less) that are enabled for CORS (Cross Origin Resource Sharing). This allows a frontend in one domain (e.g. `https://dashboard.example.com`) to communicate with this backend, that could be living in another domain (e.g. `https://api.example.com`). It can also be used to allow your local frontend (with a custom `hosts` domain mapping, as described in the project's `README.md`) that could be living in `http://dev.example.com:8080` to communicate with the backend at `https://stag.example.com`. Notice the `http` vs `https` and the `dev.` prefix for local development vs the "staging" `stag.` prefix. By default, it includes origins for production, staging and development, with ports commonly used during local development by several popular frontend frameworks (Vue with `:8080`, React, Angular). -* `smtp_port`: Port to use to send emails via SMTP. By default `587`. -* `smtp_host`: Host to use to send emails, it would be given by your email provider, like Mailgun, Sparkpost, etc. -* `smtp_user`: The user to use in the SMTP connection. The value will be given by your email provider. -* `smtp_password`: The password to be used in the SMTP connection. The value will be given by the email provider. -* `smtp_emails_from_email`: The email account to use as the sender in the notification emails, it would be something like `info@your-custom-domain.com`. - -* `postgres_password`: Postgres database password. Use the method above to generate it. (You could easily modify it to use MySQL, MariaDB, etc). -* `pgadmin_default_user`: PGAdmin default user, to log-in to the PGAdmin interface. -* `pgadmin_default_user_password`: PGAdmin default user password. Generate it with the method above. - -* `traefik_constraint_tag`: The tag to be used by the internal Traefik load balancer (for example, to divide requests between backend and frontend) for production. Used to separate this stack from any other stack you might have. This should identify each stack in each environment (production, staging, etc). -* `traefik_constraint_tag_staging`: The Traefik tag to be used while on staging. -* `traefik_public_constraint_tag`: The tag that should be used by stack services that should communicate with the public. +```bash +git remote -v -* `flower_auth`: Basic HTTP authentication for flower, in the form`user:password`. By default: "`admin:changethis`". +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) +``` -* `sentry_dsn`: Key URL (DSN) of Sentry, for live error reporting. You can use the open source version or a free account. E.g.: `https://1234abcd:5678ef@sentry.example.com/30`. +- Pull the latest changes without merging: -* `docker_image_prefix`: Prefix to use for Docker image names. If you are using GitLab Docker registry it would be based on your code repository. E.g.: `git.example.com/development-team/my-awesome-project/`. -* `docker_image_backend`: Docker image name for the backend. By default, it will be based on your Docker image prefix, e.g.: `git.example.com/development-team/my-awesome-project/backend`. And depending on your environment, a different tag will be appended ( `prod`, `stag`, `branch` ). So, the final image names used will be like: `git.example.com/development-team/my-awesome-project/backend:prod`. -* `docker_image_celeryworker`: Docker image for the celery worker. By default, based on your Docker image prefix. -* `docker_image_frontend`: Docker image for the frontend. By default, based on your Docker image prefix. +```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. -## How to deploy +- If there are conflicts, solve them in your editor. -This stack can be adjusted and used with several deployment options that are compatible with Docker Compose, but it is designed to be used in a cluster controlled with pure Docker in Swarm Mode with a Traefik main load balancer proxy handling automatic HTTPS certificates, using the ideas from DockerSwarm.rocks. +- Once you are done, commit the changes: -Please refer to DockerSwarm.rocks to see how to deploy such a cluster in 20 minutes. +```bash +git merge --continue +``` -## More details +### Configure -After using this generator, your new project (the directory created) will contain an extensive `README.md` with instructions for development, deployment, etc. You can pre-read [the project `README.md` template here too](./{{cookiecutter.project_slug}}/README.md). +You can then update configs in the `.env` files to customize your configurations. -## Sibling project generators +Before deploying it, make sure you change at least the values for: -* Full Stack FastAPI Couchbase: [https://github.com/tiangolo/full-stack-fastapi-couchbase](https://github.com/tiangolo/full-stack-fastapi-couchbase). +- `SECRET_KEY` +- `FIRST_SUPERUSER_PASSWORD` +- `POSTGRES_PASSWORD` -## Release Notes +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. -### Latest Changes +## How To Use It - Alternative With Copier -* Update issue-manager. PR [#211](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/211). -* Add [GitHub Sponsors](https://github.com/sponsors/tiangolo) button. PR [#201](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/201). -* Add consistent errors for env vars not set. PR [#200](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/200). -* Upgrade Traefik to version 2, keeping in sync with DockerSwarm.rocks. PR [#199](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/199). -* Add docs about reporting test coverage in HTML. PR [#161](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/161). -* Run tests with `TestClient`. PR [#160](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/160). -* Refactor backend: - * Simplify configs for tools and format to better support editor integration. - * Add mypy configurations and plugins. - * Add types to all the codebase. - * Update types for SQLAlchemy models with plugin. - * Update and refactor CRUD utils. - * Refactor DB sessions to use dependencies with `yield`. - * Refactor dependencies, security, CRUD, models, schemas, etc. To simplify code and improve autocompletion. - * Change from PyJWT to Python-JOSE as it supports additional use cases. - * Fix JWT tokens using user email/ID as the subject in `sub`. - * PR [#158](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/158). -* Add docs about removing the frontend, for an API-only app. PR [#156](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/156). -* Simplify scripts and development, update docs and configs. PR [#155](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/155). -* Simplify `docker-compose.*.yml` files, refactor deployment to reduce config files. PR [#153](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/153). -* Simplify env var files, merge to a single `.env` file. PR [#151](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/151). +This repository also supports generating a new project using [Copier](https://copier.readthedocs.io). -### 0.5.0 +It will copy all the files, ask you configuration questions, and update the `.env` files with your answers. -* Make the Traefik public network a fixed default of `traefik-public` as done in DockerSwarm.rocks, to simplify development and iteration of the project generator. PR [#150](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/150). -* Update to PostgreSQL 12. PR [#148](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/148). by [@RCheese](https://github.com/RCheese). -* Use Poetry for package management. Initial PR [#144](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/144) by [@RCheese](https://github.com/RCheese). -* Fix Windows line endings for shell scripts after project generation with Cookiecutter hooks. PR [#149](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/149). -* Upgrade Vue CLI to version 4. PR [#120](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/120) by [@br3ndonland](https://github.com/br3ndonland). -* Remove duplicate `login` tag. PR [#135](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/135) by [@Nonameentered](https://github.com/Nonameentered). -* Fix showing email in dashboard when there's no user's full name. PR [#129](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/129) by [@rlonka](https://github.com/rlonka). -* Format code with Black and Flake8. PR [#121](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/121) by [@br3ndonland](https://github.com/br3ndonland). -* Simplify SQLAlchemy Base class. PR [#117](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/117) by [@airibarne](https://github.com/airibarne). -* Update CRUD utils for users, handling password hashing. PR [#106](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/106) by [@mocsar](https://github.com/mocsar). -* Use `.` instead of `source` for interoperability. PR [#98](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/98) by [@gucharbon](https://github.com/gucharbon). -* Use Pydantic's `BaseSettings` for settings/configs and env vars. PR [#87](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/87) by [@StephenBrown2](https://github.com/StephenBrown2). -* Remove `package-lock.json` to let everyone lock their own versions (depending on OS, etc). -* Simplify Traefik service labels PR [#139](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/139). -* Add email validation. PR [#40](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/40) by [@kedod](https://github.com/kedod). -* Fix typo in README. PR [#83](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/83) by [@ashears](https://github.com/ashears). -* Fix typo in README. PR [#80](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/80) by [@abjoker](https://github.com/abjoker). -* Fix function name `read_item` and response code. PR [#74](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/74) by [@jcaguirre89](https://github.com/jcaguirre89). -* Fix typo in comment. PR [#70](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/70) by [@daniel-butler](https://github.com/daniel-butler). -* Fix Flower Docker configuration. PR [#37](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/37) by [@dmontagu](https://github.com/dmontagu). -* Add new CRUD utils based on DB and Pydantic models. Initial PR [#23](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/23) by [@ebreton](https://github.com/ebreton). -* Add normal user testing Pytest fixture. PR [#20](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/20) by [@ebreton](https://github.com/ebreton). +### Install Copier -### 0.4.0 +You can install Copier with: -* Fix security on resetting a password. Receive token as body, not query. PR [#34](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/34). +```bash +pip install copier +``` -* Fix security on resetting a password. Receive it as body, not query. PR [#33](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/33) by [@dmontagu](https://github.com/dmontagu). +Or better, if you have [`pipx`](https://pipx.pypa.io/), you can run it with: -* Fix SQLAlchemy class lookup on initialization. PR [#29](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/29) by [@ebreton](https://github.com/ebreton). +```bash +pipx install copier +``` -* Fix SQLAlchemy operation errors on database restart. PR [#32](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/32) by [@ebreton](https://github.com/ebreton). +**Note**: If you have `pipx`, installing copier is optional, you could run it directly. -* Fix locations of scripts in generated README. PR [#19](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/19) by [@ebreton](https://github.com/ebreton). +### Generate a Project With Copier -* Forward arguments from script to `pytest` inside container. PR [#17](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/17) by [@ebreton](https://github.com/ebreton). +Decide a name for your new project's directory, you will use it below. For example, `my-awesome-project`. -* Update development scripts. +Go to the directory that will be the parent of your project, and run the command with your project's name: -* Read Alembic configs from env vars. PR #9 by @ebreton. +```bash +copier copy https://github.com/fastapi/full-stack-fastapi-template my-awesome-project --trust +``` -* Create DB Item objects from all Pydantic model's fields. +If you have `pipx` and you didn't install `copier`, you can run it directly: -* Update Jupyter Lab installation and util script/environment variable for local development. +```bash +pipx run copier copy https://github.com/fastapi/full-stack-fastapi-template my-awesome-project --trust +``` -### 0.3.0 +**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. -* PR #14: - * Update CRUD utils to use types better. - * Simplify Pydantic model names, from `UserInCreate` to `UserCreate`, etc. - * Upgrade packages. - * Add new generic "Items" models, crud utils, endpoints, and tests. To facilitate re-using them to create new functionality. As they are simple and generic (not like Users), it's easier to copy-paste and adapt them to each use case. - * Update endpoints/*path operations* to simplify code and use new utilities, prefix and tags in `include_router`. - * Update testing utils. - * Update linting rules, relax vulture to reduce false positives. - * Update migrations to include new Items. - * Update project README.md with tips about how to start with backend. +### Input Variables -* Upgrade Python to 3.7 as Celery is now compatible too. PR #10 by @ebreton. +Copier will ask you for some data, you might want to have at hand before generating the project. -### 0.2.2 +But don't worry, you can just update any of that in the `.env` files afterwards. -* Fix frontend hijacking /docs in development. Using latest https://github.com/tiangolo/node-frontend with custom Nginx configs in frontend. PR #6. +The input variables, with their default values (some auto generated) are: -### 0.2.1 +- `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. -* Fix documentation for *path operation* to get user by ID. PR #4 by @mpclarkson in FastAPI. +## Backend Development -* Set `/start-reload.sh` as a command override for development by default. +Backend docs: [backend/README.md](./backend/README.md). -* Update generated README. +## Frontend Development -### 0.2.0 +Frontend docs: [frontend/README.md](./frontend/README.md). -**PR #2**: +## Deployment -* Simplify and update backend `Dockerfile`s. -* Refactor and simplify backend code, improve naming, imports, modules and "namespaces". -* Improve and simplify Vuex integration with TypeScript accessors. -* Standardize frontend components layout, buttons order, etc. -* Add local development scripts (to develop this project generator itself). -* Add logs to startup modules to detect errors early. -* Improve FastAPI dependency utilities, to simplify and reduce code (to require a superuser). +Deployment docs: [deployment.md](./deployment.md). -### 0.1.2 +## Development -* Fix path operation to update self-user, set parameters as body payload. +General development docs: [development.md](./development.md). -### 0.1.1 +This includes using Docker Compose, custom local domains, `.env` configurations, etc. -Several bug fixes since initial publication, including: +## Release Notes -* Order of path operations for users. -* Frontend sending login data in the correct format. -* Add https://localhost variants to CORS. +Check the file [release-notes.md](./release-notes.md). ## License -This project is licensed under the terms of the MIT license. +The Full Stack FastAPI Template is licensed under the terms of the MIT license. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..0045fb8182 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,29 @@ +# Security Policy + +Security is very important for this project and its community. πŸ”’ + +Learn more about it below. πŸ‘‡ + +## Versions + +The latest version or release is supported. + +You are encouraged to write tests for your application and update your versions frequently after ensuring that your tests are passing. This way you will benefit from the latest features, bug fixes, and **security fixes**. + +## Reporting a Vulnerability + +If you think you found a vulnerability, and even if you are not sure about it, please report it right away by sending an email to: security@tiangolo.com. Please try to be as explicit as possible, describing all the steps and example code to reproduce the security issue. + +I (the author, [@tiangolo](https://twitter.com/tiangolo)) will review it thoroughly and get back to you. + +## Public Discussions + +Please restrain from publicly discussing a potential security vulnerability. πŸ™Š + +It's better to discuss privately and try to find a solution first, to limit the potential impact as much as possible. + +--- + +Thanks for your help! + +The community and I thank you for that. πŸ™‡ diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000000..c0de4abf73 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,8 @@ +# Python +__pycache__ +app.egg-info +*.pyc +.mypy_cache +.coverage +htmlcov +.venv diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000000..63f67bcd21 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,8 @@ +__pycache__ +app.egg-info +*.pyc +.mypy_cache +.coverage +htmlcov +.cache +.venv diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000000..fc672e525f --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,44 @@ +FROM python:3.10 + +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app/ + +# Install uv +# Ref: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv +COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /uvx /bin/ + +# Place executables in the environment at the front of the path +# Ref: https://docs.astral.sh/uv/guides/integration/docker/#using-the-environment +ENV PATH="/app/.venv/bin:$PATH" + +# Compile bytecode +# Ref: https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode +ENV UV_COMPILE_BYTECODE=1 + +# uv Cache +# Ref: https://docs.astral.sh/uv/guides/integration/docker/#caching +ENV UV_LINK_MODE=copy + +# Install dependencies +# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --frozen --no-install-project + +ENV PYTHONPATH=/app + +COPY ./scripts /app/scripts + +COPY ./pyproject.toml ./uv.lock ./alembic.ini /app/ + +COPY ./app /app/app +COPY ./tests /app/tests + +# Sync the project +# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync + +CMD ["fastapi", "run", "--workers", "4", "app/main.py"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000000..c217000fc2 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,172 @@ +# FastAPI Project - Backend + +## Requirements + +* [Docker](https://www.docker.com/). +* [uv](https://docs.astral.sh/uv/) for Python package and environment management. + +## Docker Compose + +Start the local development environment with Docker Compose following the guide in [../development.md](../development.md). + +## General Workflow + +By default, the dependencies are managed with [uv](https://docs.astral.sh/uv/), go there and install it. + +From `./backend/` you can install all the dependencies with: + +```console +$ uv sync +``` + +Then you can activate the virtual environment with: + +```console +$ source .venv/bin/activate +``` + +Make sure your editor is using the correct Python virtual environment, with the interpreter at `backend/.venv/bin/python`. + +Modify or add SQLModel models for data and SQL tables in `./backend/app/models.py`, API endpoints in `./backend/app/api/`, CRUD (Create, Read, Update, Delete) utils in `./backend/app/crud.py`. + +## VS Code + +There are already configurations in place to run the backend through the VS Code debugger, so that you can use breakpoints, pause and explore variables, etc. + +The setup is also already configured so you can run the tests through the VS Code Python tests tab. + +## Docker Compose Override + +During development, you can change Docker Compose settings that will only affect the local development environment in the file `docker-compose.override.yml`. + +The changes to that file only affect the local development environment, not the production environment. So, you can add "temporary" changes that help the development workflow. + +For example, the directory with the backend code is synchronized in the Docker container, copying the code you change live to the directory inside the container. That allows you to test your changes right away, without having to build the Docker image again. It should only be done during development, for production, you should build the Docker image with a recent version of the backend code. But during development, it allows you to iterate very fast. + +There is also a command override that runs `fastapi run --reload` instead of the default `fastapi run`. It starts a single server process (instead of multiple, as would be for production) and reloads the process whenever the code changes. Have in mind that if you have a syntax error and save the Python file, it will break and exit, and the container will stop. After that, you can restart the container by fixing the error and running again: + +```console +$ docker compose watch +``` + +There is also a commented out `command` override, you can uncomment it and comment the default one. It makes the backend container run a process that does "nothing", but keeps the container alive. That allows you to get inside your running container and execute commands inside, for example a Python interpreter to test installed dependencies, or start the development server that reloads when it detects changes. + +To get inside the container with a `bash` session you can start the stack with: + +```console +$ docker compose watch +``` + +and then in another terminal, `exec` inside the running container: + +```console +$ 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 `fastapi run --reload` command to run the debug live reloading server. + +```console +$ fastapi run --reload app/main.py +``` + +...it will look like: + +```console +root@7f2607af31c3:/app# fastapi run --reload app/main.py +``` + +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/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 scripts/tests-start.sh +``` + +That `/app/scripts/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 scripts/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`. + +* After changing a model (for example, adding a column), inside the container, create a revision, e.g.: + +```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 +``` + +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: + +```python +SQLModel.metadata.create_all(engine) +``` + +and comment the line in the file `scripts/prestart.sh` that contains: + +```console +$ 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. + +## 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. + +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. diff --git a/{{cookiecutter.project_slug}}/backend/app/alembic.ini b/backend/alembic.ini similarity index 98% rename from {{cookiecutter.project_slug}}/backend/app/alembic.ini rename to backend/alembic.ini index 921aaf17b8..24841c2bfb 100755 --- a/{{cookiecutter.project_slug}}/backend/app/alembic.ini +++ b/backend/alembic.ini @@ -2,7 +2,7 @@ [alembic] # path to migration scripts -script_location = alembic +script_location = app/alembic # template used to generate migration files # file_template = %%(rev)s_%%(slug)s diff --git a/{{cookiecutter.project_slug}}/backend/app/app/__init__.py b/backend/app/__init__.py similarity index 100% rename from {{cookiecutter.project_slug}}/backend/app/app/__init__.py rename to backend/app/__init__.py diff --git a/backend/app/alembic/README b/backend/app/alembic/README new file mode 100755 index 0000000000..2500aa1bcf --- /dev/null +++ b/backend/app/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. diff --git a/{{cookiecutter.project_slug}}/backend/app/alembic/env.py b/backend/app/alembic/env.py similarity index 82% rename from {{cookiecutter.project_slug}}/backend/app/alembic/env.py rename to backend/app/alembic/env.py index 3ba3420643..7f29c04680 100755 --- a/{{cookiecutter.project_slug}}/backend/app/alembic/env.py +++ b/backend/app/alembic/env.py @@ -1,10 +1,8 @@ -from __future__ import with_statement - import os +from logging.config import fileConfig from alembic import context from sqlalchemy import engine_from_config, pool -from logging.config import fileConfig # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -20,9 +18,10 @@ # target_metadata = mymodel.Base.metadata # target_metadata = None -from app.db.base import Base # noqa +from app.models import SQLModel # noqa +from app.core.config import settings # noqa -target_metadata = Base.metadata +target_metadata = SQLModel.metadata # other values from the config, defined by the needs of env.py, # can be acquired: @@ -31,11 +30,7 @@ def get_url(): - user = os.getenv("POSTGRES_USER", "postgres") - password = os.getenv("POSTGRES_PASSWORD", "") - server = os.getenv("POSTGRES_SERVER", "db") - db = os.getenv("POSTGRES_DB", "app") - return f"postgresql://{user}:{password}@{server}/{db}" + return str(settings.SQLALCHEMY_DATABASE_URI) def run_migrations_offline(): @@ -69,7 +64,9 @@ def run_migrations_online(): configuration = config.get_section(config.config_ini_section) configuration["sqlalchemy.url"] = get_url() connectable = engine_from_config( - configuration, prefix="sqlalchemy.", poolclass=pool.NullPool, + configuration, + prefix="sqlalchemy.", + poolclass=pool.NullPool, ) with connectable.connect() as connection: diff --git a/{{cookiecutter.project_slug}}/backend/app/alembic/script.py.mako b/backend/app/alembic/script.py.mako similarity index 94% rename from {{cookiecutter.project_slug}}/backend/app/alembic/script.py.mako rename to backend/app/alembic/script.py.mako index 2c0156303a..217a9a8b7b 100755 --- a/{{cookiecutter.project_slug}}/backend/app/alembic/script.py.mako +++ b/backend/app/alembic/script.py.mako @@ -7,6 +7,7 @@ Create Date: ${create_date} """ from alembic import op import sqlalchemy as sa +import sqlmodel.sql.sqltypes ${imports if imports else ""} # revision identifiers, used by Alembic. diff --git a/{{cookiecutter.project_slug}}/backend/app/alembic/versions/.keep b/backend/app/alembic/versions/.keep similarity index 100% rename from {{cookiecutter.project_slug}}/backend/app/alembic/versions/.keep rename to backend/app/alembic/versions/.keep diff --git a/backend/app/alembic/versions/1a31ce608336_add_cascade_delete_relationships.py b/backend/app/alembic/versions/1a31ce608336_add_cascade_delete_relationships.py new file mode 100644 index 0000000000..10e47a1456 --- /dev/null +++ b/backend/app/alembic/versions/1a31ce608336_add_cascade_delete_relationships.py @@ -0,0 +1,37 @@ +"""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/9c0a54914c78_add_max_length_for_string_varchar_.py b/backend/app/alembic/versions/9c0a54914c78_add_max_length_for_string_varchar_.py new file mode 100755 index 0000000000..78a41773b9 --- /dev/null +++ b/backend/app/alembic/versions/9c0a54914c78_add_max_length_for_string_varchar_.py @@ -0,0 +1,69 @@ +"""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/d98dd8ec85a3_edit_replace_id_integers_in_all_models_.py b/backend/app/alembic/versions/d98dd8ec85a3_edit_replace_id_integers_in_all_models_.py new file mode 100755 index 0000000000..37af1fa215 --- /dev/null +++ b/backend/app/alembic/versions/d98dd8ec85a3_edit_replace_id_integers_in_all_models_.py @@ -0,0 +1,90 @@ +"""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 new file mode 100644 index 0000000000..7529ea91fa --- /dev/null +++ b/backend/app/alembic/versions/e2412789c190_initialize_models.py @@ -0,0 +1,54 @@ +"""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/{{cookiecutter.project_slug}}/backend/app/app/api/__init__.py b/backend/app/api/__init__.py similarity index 100% rename from {{cookiecutter.project_slug}}/backend/app/app/api/__init__.py rename to backend/app/api/__init__.py diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py new file mode 100644 index 0000000000..c2b83c841d --- /dev/null +++ b/backend/app/api/deps.py @@ -0,0 +1,57 @@ +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 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" +) + + +def get_db() -> Generator[Session, None, None]: + with Session(engine) as session: + yield session + + +SessionDep = Annotated[Session, Depends(get_db)] +TokenDep = Annotated[str, Depends(reusable_oauth2)] + + +def get_current_user(session: SessionDep, token: TokenDep) -> User: + 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", + ) + 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 + + +CurrentUser = Annotated[User, Depends(get_current_user)] + + +def get_current_active_superuser(current_user: CurrentUser) -> User: + if not current_user.is_superuser: + raise HTTPException( + status_code=403, detail="The user doesn't have enough privileges" + ) + return current_user diff --git a/backend/app/api/main.py b/backend/app/api/main.py new file mode 100644 index 0000000000..eac18c8e8f --- /dev/null +++ b/backend/app/api/main.py @@ -0,0 +1,14 @@ +from fastapi import APIRouter + +from app.api.routes import items, login, private, users, utils +from app.core.config import settings + +api_router = APIRouter() +api_router.include_router(login.router) +api_router.include_router(users.router) +api_router.include_router(utils.router) +api_router.include_router(items.router) + + +if settings.ENVIRONMENT == "local": + api_router.include_router(private.router) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/__init__.py b/backend/app/api/routes/__init__.py similarity index 100% rename from {{cookiecutter.project_slug}}/backend/app/app/api/api_v1/__init__.py rename to backend/app/api/routes/__init__.py diff --git a/backend/app/api/routes/items.py b/backend/app/api/routes/items.py new file mode 100644 index 0000000000..177dc1e476 --- /dev/null +++ b/backend/app/api/routes/items.py @@ -0,0 +1,109 @@ +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(prefix="/items", tags=["items"]) + + +@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 new file mode 100644 index 0000000000..980c66f86f --- /dev/null +++ b/backend/app/api/routes/login.py @@ -0,0 +1,124 @@ +from datetime import timedelta +from typing import Annotated, Any + +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, +) + +router = APIRouter(tags=["login"]) + + +@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 + ) + ) + + +@router.post("/login/test-token", response_model=UserPublic) +def test_token(current_user: CurrentUser) -> Any: + """ + Test access token + """ + return current_user + + +@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) + + 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.", + ) + 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.", + ) + 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} + ) diff --git a/backend/app/api/routes/private.py b/backend/app/api/routes/private.py new file mode 100644 index 0000000000..9f33ef1900 --- /dev/null +++ b/backend/app/api/routes/private.py @@ -0,0 +1,38 @@ +from typing import Any + +from fastapi import APIRouter +from pydantic import BaseModel + +from app.api.deps import SessionDep +from app.core.security import get_password_hash +from app.models import ( + User, + UserPublic, +) + +router = APIRouter(tags=["private"], prefix="/private") + + +class PrivateUserCreate(BaseModel): + email: str + password: str + full_name: str + is_verified: bool = False + + +@router.post("/users/", response_model=UserPublic) +def create_user(user_in: PrivateUserCreate, session: SessionDep) -> Any: + """ + Create a new user. + """ + + user = User( + email=user_in.email, + full_name=user_in.full_name, + hashed_password=get_password_hash(user_in.password), + ) + + session.add(user) + session.commit() + + return user diff --git a/backend/app/api/routes/users.py b/backend/app/api/routes/users.py new file mode 100644 index 0000000000..6429818458 --- /dev/null +++ b/backend/app/api/routes/users.py @@ -0,0 +1,226 @@ +import uuid +from typing import Any + +from fastapi import APIRouter, Depends, HTTPException +from sqlmodel import col, delete, func, select + +from app import crud +from app.api.deps import ( + CurrentUser, + SessionDep, + get_current_active_superuser, +) +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.utils import generate_new_account_email, send_email + +router = APIRouter(prefix="/users", tags=["users"]) + + +@router.get( + "/", + dependencies=[Depends(get_current_active_superuser)], + response_model=UsersPublic, +) +def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any: + """ + Retrieve users. + """ + + 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() + + return UsersPublic(data=users, count=count) + + +@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.", + ) + + 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" + ) + 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.patch( + "/{user_id}", + dependencies=[Depends(get_current_active_superuser)], + response_model=UserPublic, +) +def update_user( + *, + 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") diff --git a/backend/app/api/routes/utils.py b/backend/app/api/routes/utils.py new file mode 100644 index 0000000000..fc093419b3 --- /dev/null +++ b/backend/app/api/routes/utils.py @@ -0,0 +1,31 @@ +from fastapi import APIRouter, Depends +from pydantic.networks import EmailStr + +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(prefix="/utils", tags=["utils"]) + + +@router.post( + "/test-email/", + dependencies=[Depends(get_current_active_superuser)], + status_code=201, +) +def test_email(email_to: EmailStr) -> Message: + """ + Test emails. + """ + 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") + + +@router.get("/health-check/") +async def health_check() -> bool: + return True diff --git a/{{cookiecutter.project_slug}}/backend/app/app/backend_pre_start.py b/backend/app/backend_pre_start.py similarity index 68% rename from {{cookiecutter.project_slug}}/backend/app/app/backend_pre_start.py rename to backend/app/backend_pre_start.py index 3363a41542..c2f8e29ae1 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/backend_pre_start.py +++ b/backend/app/backend_pre_start.py @@ -1,8 +1,10 @@ import logging +from sqlalchemy import Engine +from sqlmodel import Session, select from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed -from app.db.session import SessionLocal +from app.core.db import engine logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -17,11 +19,11 @@ before=before_log(logger, logging.INFO), after=after_log(logger, logging.WARN), ) -def init() -> None: +def init(db_engine: Engine) -> None: try: - db = SessionLocal() - # Try to create session to check if DB is awake - db.execute("SELECT 1") + with Session(db_engine) as session: + # Try to create session to check if DB is awake + session.exec(select(1)) except Exception as e: logger.error(e) raise e @@ -29,7 +31,7 @@ def init() -> None: def main() -> None: logger.info("Initializing service") - init() + init(engine) logger.info("Service finished initializing") diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/__init__.py b/backend/app/core/__init__.py similarity index 100% rename from {{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/__init__.py rename to backend/app/core/__init__.py diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000000..6a8ca50bb1 --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,119 @@ +import secrets +import warnings +from typing import Annotated, Any, Literal + +from pydantic import ( + AnyUrl, + BeforeValidator, + EmailStr, + HttpUrl, + PostgresDsn, + computed_field, + model_validator, +) +from pydantic_settings import BaseSettings, SettingsConfigDict +from typing_extensions import Self + + +def parse_cors(v: Any) -> list[str] | str: + if isinstance(v, str) and not v.startswith("["): + return [i.strip() for i in v.split(",") if i.strip()] + elif isinstance(v, list | str): + return v + raise ValueError(v) + + +class Settings(BaseSettings): + model_config = SettingsConfigDict( + # Use top level .env file (one level above ./backend/) + env_file="../.env", + env_ignore_empty=True, + extra="ignore", + ) + 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 + FRONTEND_HOST: str = "http://localhost:5173" + ENVIRONMENT: Literal["local", "staging", "production"] = "local" + + BACKEND_CORS_ORIGINS: Annotated[ + list[AnyUrl] | str, BeforeValidator(parse_cors) + ] = [] + + @computed_field # type: ignore[prop-decorator] + @property + def all_cors_origins(self) -> list[str]: + return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [ + self.FRONTEND_HOST + ] + + PROJECT_NAME: str + SENTRY_DSN: HttpUrl | None = None + POSTGRES_SERVER: str + POSTGRES_PORT: int = 5432 + POSTGRES_USER: str + POSTGRES_PASSWORD: str = "" + POSTGRES_DB: str = "" + + @computed_field # type: ignore[prop-decorator] + @property + def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: + return PostgresDsn.build( + scheme="postgresql+psycopg", + username=self.POSTGRES_USER, + password=self.POSTGRES_PASSWORD, + host=self.POSTGRES_SERVER, + port=self.POSTGRES_PORT, + path=self.POSTGRES_DB, + ) + + SMTP_TLS: bool = True + SMTP_SSL: bool = False + SMTP_PORT: int = 587 + SMTP_HOST: str | None = None + SMTP_USER: str | None = None + SMTP_PASSWORD: str | None = None + EMAILS_FROM_EMAIL: EmailStr | None = None + EMAILS_FROM_NAME: EmailStr | None = None + + @model_validator(mode="after") + def _set_default_emails_from(self) -> Self: + if not self.EMAILS_FROM_NAME: + self.EMAILS_FROM_NAME = self.PROJECT_NAME + return self + + EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48 + + @computed_field # type: ignore[prop-decorator] + @property + def emails_enabled(self) -> bool: + return bool(self.SMTP_HOST and self.EMAILS_FROM_EMAIL) + + EMAIL_TEST_USER: EmailStr = "test@example.com" + FIRST_SUPERUSER: EmailStr + FIRST_SUPERUSER_PASSWORD: str + + def _check_default_secret(self, var_name: str, value: str | None) -> None: + if value == "changethis": + message = ( + f'The value of {var_name} is "changethis", ' + "for security, please change it, at least for deployments." + ) + if self.ENVIRONMENT == "local": + warnings.warn(message, stacklevel=1) + else: + raise ValueError(message) + + @model_validator(mode="after") + def _enforce_non_default_secrets(self) -> Self: + self._check_default_secret("SECRET_KEY", self.SECRET_KEY) + self._check_default_secret("POSTGRES_PASSWORD", self.POSTGRES_PASSWORD) + self._check_default_secret( + "FIRST_SUPERUSER_PASSWORD", self.FIRST_SUPERUSER_PASSWORD + ) + + return self + + +settings = Settings() # type: ignore diff --git a/backend/app/core/db.py b/backend/app/core/db.py new file mode 100644 index 0000000000..ba991fb36d --- /dev/null +++ b/backend/app/core/db.py @@ -0,0 +1,33 @@ +from sqlmodel import Session, create_engine, select + +from app import crud +from app.core.config import settings +from app.models import User, UserCreate + +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 + + # 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) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/core/security.py b/backend/app/core/security.py similarity index 58% rename from {{cookiecutter.project_slug}}/backend/app/app/core/security.py rename to backend/app/core/security.py index 6c6ee8bc30..7aff7cfb32 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/core/security.py +++ b/backend/app/core/security.py @@ -1,7 +1,7 @@ -from datetime import datetime, timedelta -from typing import Any, Union +from datetime import datetime, timedelta, timezone +from typing import Any -from jose import jwt +import jwt from passlib.context import CryptContext from app.core.config import settings @@ -12,15 +12,8 @@ ALGORITHM = "HS256" -def create_access_token( - subject: Union[str, Any], expires_delta: timedelta = None -) -> str: - if expires_delta: - expire = datetime.utcnow() + expires_delta - else: - expire = datetime.utcnow() + 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)} encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/backend/app/crud.py b/backend/app/crud.py new file mode 100644 index 0000000000..905bf48724 --- /dev/null +++ b/backend/app/crud.py @@ -0,0 +1,54 @@ +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 new file mode 100644 index 0000000000..344505033b --- /dev/null +++ b/backend/app/email-templates/build/new_account.html @@ -0,0 +1,25 @@ +
{{ 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 new file mode 100644 index 0000000000..4148a5b773 --- /dev/null +++ b/backend/app/email-templates/build/reset_password.html @@ -0,0 +1,25 @@ +
{{ 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 new file mode 100644 index 0000000000..04d0d85092 --- /dev/null +++ b/backend/app/email-templates/build/test_email.html @@ -0,0 +1,25 @@ +
{{ 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 new file mode 100644 index 0000000000..f41a3e3cf1 --- /dev/null +++ b/backend/app/email-templates/src/new_account.mjml @@ -0,0 +1,15 @@ + + + + + {{ 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 new file mode 100644 index 0000000000..743f5d77f4 --- /dev/null +++ b/backend/app/email-templates/src/reset_password.mjml @@ -0,0 +1,17 @@ + + + + + {{ 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 new file mode 100644 index 0000000000..45d58d6bac --- /dev/null +++ b/backend/app/email-templates/src/test_email.mjml @@ -0,0 +1,11 @@ + + + + + {{ project_name }} + Test email for: {{ email }} + + + + + diff --git a/{{cookiecutter.project_slug}}/backend/app/app/initial_data.py b/backend/app/initial_data.py similarity index 67% rename from {{cookiecutter.project_slug}}/backend/app/app/initial_data.py rename to backend/app/initial_data.py index c50646d2df..d806c3d381 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/initial_data.py +++ b/backend/app/initial_data.py @@ -1,15 +1,16 @@ import logging -from app.db.init_db import init_db -from app.db.session import SessionLocal +from sqlmodel import Session + +from app.core.db import engine, init_db logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def init() -> None: - db = SessionLocal() - init_db(db) + with Session(engine) as session: + init_db(session) def main() -> None: diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000000..9a95801e74 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,33 @@ +import sentry_sdk +from fastapi import FastAPI +from fastapi.routing import APIRoute +from starlette.middleware.cors import CORSMiddleware + +from app.api.main import api_router +from app.core.config import settings + + +def custom_generate_unique_id(route: APIRoute) -> str: + return f"{route.tags[0]}-{route.name}" + + +if settings.SENTRY_DSN and settings.ENVIRONMENT != "local": + sentry_sdk.init(dsn=str(settings.SENTRY_DSN), enable_tracing=True) + +app = FastAPI( + title=settings.PROJECT_NAME, + openapi_url=f"{settings.API_V1_STR}/openapi.json", + generate_unique_id_function=custom_generate_unique_id, +) + +# Set all CORS enabled origins +if settings.all_cors_origins: + app.add_middleware( + CORSMiddleware, + allow_origins=settings.all_cors_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + +app.include_router(api_router, prefix=settings.API_V1_STR) diff --git a/backend/app/models.py b/backend/app/models.py new file mode 100644 index 0000000000..2d060ba0b4 --- /dev/null +++ b/backend/app/models.py @@ -0,0 +1,113 @@ +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=128) + + +class UserRegister(SQLModel): + email: EmailStr = Field(max_length=255) + password: str = Field(min_length=8, max_length=128) + 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=128) + + +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=128) + new_password: str = Field(min_length=8, max_length=128) + + +# 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) + 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=128) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests_pre_start.py b/backend/app/tests_pre_start.py similarity index 74% rename from {{cookiecutter.project_slug}}/backend/app/app/tests_pre_start.py rename to backend/app/tests_pre_start.py index 81de37134f..0ce6045635 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests_pre_start.py +++ b/backend/app/tests_pre_start.py @@ -1,8 +1,10 @@ import logging +from sqlalchemy import Engine +from sqlmodel import Session, select from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed -from app.db.session import SessionLocal +from app.core.db import engine logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -17,11 +19,11 @@ before=before_log(logger, logging.INFO), after=after_log(logger, logging.WARN), ) -def init() -> None: +def init(db_engine: Engine) -> None: try: # Try to create session to check if DB is awake - db = SessionLocal() - db.execute("SELECT 1") + with Session(db_engine) as session: + session.exec(select(1)) except Exception as e: logger.error(e) raise e @@ -29,7 +31,7 @@ def init() -> None: def main() -> None: logger.info("Initializing service") - init() + init(engine) logger.info("Service finished initializing") diff --git a/backend/app/utils.py b/backend/app/utils.py new file mode 100644 index 0000000000..ac029f6342 --- /dev/null +++ b/backend/app/utils.py @@ -0,0 +1,123 @@ +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 import security +from app.core.config import settings + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +@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) + logger.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.FRONTEND_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.FRONTEND_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=security.ALGORITHM, + ) + return encoded_jwt + + +def verify_password_reset_token(token: str) -> str | None: + try: + decoded_token = jwt.decode( + token, settings.SECRET_KEY, algorithms=[security.ALGORITHM] + ) + return str(decoded_token["sub"]) + except InvalidTokenError: + return None diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000000..d72454c28a --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,80 @@ +[project] +name = "app" +version = "0.1.0" +description = "" +requires-python = ">=3.10,<4.0" +dependencies = [ + "fastapi[standard]<1.0.0,>=0.114.2", + "python-multipart<1.0.0,>=0.0.7", + "email-validator<3.0.0.0,>=2.1.0.post1", + "passlib[bcrypt]<2.0.0,>=1.7.4", + "tenacity<9.0.0,>=8.2.3", + "pydantic>2.0", + "emails<1.0,>=0.6", + "jinja2<4.0.0,>=3.1.4", + "alembic<2.0.0,>=1.12.1", + "httpx<1.0.0,>=0.25.1", + "psycopg[binary]<4.0.0,>=3.1.13", + "sqlmodel<1.0.0,>=0.0.21", + # Pin bcrypt until passlib supports the latest + "bcrypt==4.3.0", + "pydantic-settings<3.0.0,>=2.2.1", + "sentry-sdk[fastapi]<2.0.0,>=1.40.6", + "pyjwt<3.0.0,>=2.8.0", +] + +[tool.uv] +dev-dependencies = [ + "pytest<8.0.0,>=7.4.3", + "mypy<2.0.0,>=1.8.0", + "ruff<1.0.0,>=0.2.2", + "pre-commit<4.0.0,>=3.6.2", + "types-passlib<2.0.0.0,>=1.7.7.20240106", + "coverage<8.0.0,>=7.4.3", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.mypy] +strict = true +exclude = ["venv", ".venv", "alembic"] + +[tool.ruff] +target-version = "py310" +exclude = ["alembic"] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "ARG001", # unused arguments in functions + "T201", # print statements are not allowed +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "W191", # indentation contains tabs + "B904", # Allow raising exceptions without from e, for HTTPException +] + +[tool.ruff.lint.pyupgrade] +# Preserve types, even if a file imports `from __future__ import annotations`. +keep-runtime-typing = true + +[tool.coverage.run] +source = ["app"] +dynamic_context = "test_function" + +[tool.coverage.report] +show_missing = true +sort = "-Cover" + +[tool.coverage.html] +show_contexts = true diff --git a/backend/scripts/format.sh b/backend/scripts/format.sh new file mode 100755 index 0000000000..7be2f81205 --- /dev/null +++ b/backend/scripts/format.sh @@ -0,0 +1,5 @@ +#!/bin/sh -e +set -x + +ruff check app scripts --fix +ruff format app scripts diff --git a/backend/scripts/lint.sh b/backend/scripts/lint.sh new file mode 100644 index 0000000000..b3b2b4ecc7 --- /dev/null +++ b/backend/scripts/lint.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e +set -x + +mypy app +ruff check app +ruff format app --check diff --git a/{{cookiecutter.project_slug}}/backend/app/prestart.sh b/backend/scripts/prestart.sh similarity index 59% rename from {{cookiecutter.project_slug}}/backend/app/prestart.sh rename to backend/scripts/prestart.sh index fc1e5f1890..1b395d513f 100644 --- a/{{cookiecutter.project_slug}}/backend/app/prestart.sh +++ b/backend/scripts/prestart.sh @@ -1,10 +1,13 @@ #! /usr/bin/env bash +set -e +set -x + # Let the DB start -python /app/app/backend_pre_start.py +python app/backend_pre_start.py # Run migrations alembic upgrade head # Create initial data in DB -python /app/app/initial_data.py +python app/initial_data.py diff --git a/backend/scripts/test.sh b/backend/scripts/test.sh new file mode 100755 index 0000000000..38c3e8909a --- /dev/null +++ b/backend/scripts/test.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e +set -x + +coverage run -m pytest tests/ +coverage report +coverage html --title "${@-coverage}" diff --git a/backend/scripts/tests-start.sh b/backend/scripts/tests-start.sh new file mode 100644 index 0000000000..89dcb0da23 --- /dev/null +++ b/backend/scripts/tests-start.sh @@ -0,0 +1,7 @@ +#! /usr/bin/env bash +set -e +set -x + +python app/tests_pre_start.py + +bash scripts/test.sh "$@" diff --git a/{{cookiecutter.project_slug}}/backend/app/app/core/__init__.py b/backend/tests/__init__.py similarity index 100% rename from {{cookiecutter.project_slug}}/backend/app/app/core/__init__.py rename to backend/tests/__init__.py diff --git a/{{cookiecutter.project_slug}}/backend/app/app/db/__init__.py b/backend/tests/api/__init__.py similarity index 100% rename from {{cookiecutter.project_slug}}/backend/app/app/db/__init__.py rename to backend/tests/api/__init__.py diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/__init__.py b/backend/tests/api/routes/__init__.py similarity index 100% rename from {{cookiecutter.project_slug}}/backend/app/app/tests/__init__.py rename to backend/tests/api/routes/__init__.py diff --git a/backend/tests/api/routes/test_items.py b/backend/tests/api/routes/test_items.py new file mode 100644 index 0000000000..db950b4535 --- /dev/null +++ b/backend/tests/api/routes/test_items.py @@ -0,0 +1,164 @@ +import uuid + +from fastapi.testclient import TestClient +from sqlmodel import Session + +from app.core.config import settings +from 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/tests/api/routes/test_login.py b/backend/tests/api/routes/test_login.py new file mode 100644 index 0000000000..ee166913bd --- /dev/null +++ b/backend/tests/api/routes/test_login.py @@ -0,0 +1,118 @@ +from unittest.mock import patch + +from fastapi.testclient import TestClient +from sqlmodel import Session + +from app.core.config import settings +from app.core.security import verify_password +from app.crud import create_user +from app.models import UserCreate +from app.utils import generate_password_reset_token +from tests.utils.user import user_authentication_headers +from tests.utils.utils import random_email, random_lower_string + + +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, db: Session) -> None: + email = random_email() + password = random_lower_string() + new_password = random_lower_string() + + user_create = UserCreate( + email=email, + full_name="Test User", + password=password, + is_active=True, + is_superuser=False, + ) + user = create_user(session=db, user_create=user_create) + token = generate_password_reset_token(email=email) + headers = user_authentication_headers(client=client, email=email, password=password) + data = {"new_password": new_password, "token": token} + + r = client.post( + f"{settings.API_V1_STR}/reset-password/", + headers=headers, + json=data, + ) + + assert r.status_code == 200 + assert r.json() == {"message": "Password updated successfully"} + + db.refresh(user) + assert verify_password(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/tests/api/routes/test_private.py b/backend/tests/api/routes/test_private.py new file mode 100644 index 0000000000..1e1f985021 --- /dev/null +++ b/backend/tests/api/routes/test_private.py @@ -0,0 +1,26 @@ +from fastapi.testclient import TestClient +from sqlmodel import Session, select + +from app.core.config import settings +from app.models import User + + +def test_create_user(client: TestClient, db: Session) -> None: + r = client.post( + f"{settings.API_V1_STR}/private/users/", + json={ + "email": "pollo@listo.com", + "password": "password123", + "full_name": "Pollo Listo", + }, + ) + + assert r.status_code == 200 + + data = r.json() + + user = db.exec(select(User).where(User.id == data["id"])).first() + + assert user + assert user.email == "pollo@listo.com" + assert user.full_name == "Pollo Listo" diff --git a/backend/tests/api/routes/test_users.py b/backend/tests/api/routes/test_users.py new file mode 100644 index 0000000000..39e053e554 --- /dev/null +++ b/backend/tests/api/routes/test_users.py @@ -0,0 +1,486 @@ +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 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/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 0000000000..8ddab7b321 --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,42 @@ +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 tests.utils.user import authentication_token_from_email +from 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/{{cookiecutter.project_slug}}/backend/app/app/tests/api/__init__.py b/backend/tests/crud/__init__.py similarity index 100% rename from {{cookiecutter.project_slug}}/backend/app/app/tests/api/__init__.py rename to backend/tests/crud/__init__.py diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_user.py b/backend/tests/crud/test_user.py similarity index 66% rename from {{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_user.py rename to backend/tests/crud/test_user.py index 2caee5b870..10bda25e25 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_user.py +++ b/backend/tests/crud/test_user.py @@ -1,17 +1,17 @@ from fastapi.encoders import jsonable_encoder -from sqlalchemy.orm import Session +from sqlmodel import Session from app import crud from app.core.security import verify_password -from app.schemas.user import UserCreate, UserUpdate -from app.tests.utils.utils import random_email, random_lower_string +from app.models import User, UserCreate, UserUpdate +from 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.user.create(db, obj_in=user_in) + user = crud.create_user(session=db, user_create=user_in) assert user.email == email assert hasattr(user, "hashed_password") @@ -20,8 +20,8 @@ def test_authenticate_user(db: Session) -> None: email = random_email() password = random_lower_string() user_in = UserCreate(email=email, password=password) - user = crud.user.create(db, obj_in=user_in) - authenticated_user = crud.user.authenticate(db, 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 @@ -29,7 +29,7 @@ def test_authenticate_user(db: Session) -> None: def test_not_authenticate_user(db: Session) -> None: email = random_email() password = random_lower_string() - user = crud.user.authenticate(db, email=email, password=password) + user = crud.authenticate(session=db, email=email, password=password) assert user is None @@ -37,44 +37,40 @@ 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.user.create(db, obj_in=user_in) - is_active = crud.user.is_active(user) - assert is_active is True + 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.user.create(db, obj_in=user_in) - is_active = crud.user.is_active(user) - assert is_active + 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.user.create(db, obj_in=user_in) - is_superuser = crud.user.is_superuser(user) - assert is_superuser is 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.user.create(db, obj_in=user_in) - is_superuser = crud.user.is_superuser(user) - assert is_superuser is False + 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.user.create(db, obj_in=user_in) - user_2 = crud.user.get(db, id=user.id) + 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) @@ -84,11 +80,12 @@ 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.user.create(db, obj_in=user_in) + 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) - crud.user.update(db, db_obj=user, obj_in=user_in_update) - user_2 = crud.user.get(db, id=user.id) + 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/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/__init__.py b/backend/tests/scripts/__init__.py similarity index 100% rename from {{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/__init__.py rename to backend/tests/scripts/__init__.py diff --git a/backend/tests/scripts/test_backend_pre_start.py b/backend/tests/scripts/test_backend_pre_start.py new file mode 100644 index 0000000000..631690fcf6 --- /dev/null +++ b/backend/tests/scripts/test_backend_pre_start.py @@ -0,0 +1,33 @@ +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/tests/scripts/test_test_pre_start.py b/backend/tests/scripts/test_test_pre_start.py new file mode 100644 index 0000000000..a176f380de --- /dev/null +++ b/backend/tests/scripts/test_test_pre_start.py @@ -0,0 +1,33 @@ +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/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/__init__.py b/backend/tests/utils/__init__.py similarity index 100% rename from {{cookiecutter.project_slug}}/backend/app/app/tests/crud/__init__.py rename to backend/tests/utils/__init__.py diff --git a/backend/tests/utils/item.py b/backend/tests/utils/item.py new file mode 100644 index 0000000000..ee51b351a6 --- /dev/null +++ b/backend/tests/utils/item.py @@ -0,0 +1,16 @@ +from sqlmodel import Session + +from app import crud +from app.models import Item, ItemCreate +from tests.utils.user import create_random_user +from 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/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/user.py b/backend/tests/utils/user.py similarity index 60% rename from {{cookiecutter.project_slug}}/backend/app/app/tests/utils/user.py rename to backend/tests/utils/user.py index 097056c197..5867431ed8 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/user.py +++ b/backend/tests/utils/user.py @@ -1,18 +1,15 @@ -from typing import Dict - from fastapi.testclient import TestClient -from sqlalchemy.orm import Session +from sqlmodel import Session from app import crud from app.core.config import settings -from app.models.user import User -from app.schemas.user import UserCreate, UserUpdate -from app.tests.utils.utils import random_email, random_lower_string +from app.models import User, UserCreate, UserUpdate +from tests.utils.utils import random_email, random_lower_string def user_authentication_headers( *, client: TestClient, email: str, password: str -) -> Dict[str, str]: +) -> dict[str, str]: data = {"username": email, "password": password} r = client.post(f"{settings.API_V1_STR}/login/access-token", data=data) @@ -25,26 +22,28 @@ def user_authentication_headers( def create_random_user(db: Session) -> User: email = random_email() password = random_lower_string() - user_in = UserCreate(username=email, email=email, password=password) - user = crud.user.create(db=db, obj_in=user_in) + 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]: +) -> 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.user.get_by_email(db, email=email) + user = crud.get_user_by_email(session=db, email=email) if not user: - user_in_create = UserCreate(username=email, email=email, password=password) - user = crud.user.create(db, obj_in=user_in_create) + 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) - user = crud.user.update(db, db_obj=user, obj_in=user_in_update) + 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/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/utils.py b/backend/tests/utils/utils.py similarity index 88% rename from {{cookiecutter.project_slug}}/backend/app/app/tests/utils/utils.py rename to backend/tests/utils/utils.py index 021fc22017..184bac44d9 100644 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/utils.py +++ b/backend/tests/utils/utils.py @@ -1,6 +1,5 @@ import random import string -from typing import Dict from fastapi.testclient import TestClient @@ -15,7 +14,7 @@ def random_email() -> str: return f"{random_lower_string()}@{random_lower_string()}.com" -def get_superuser_token_headers(client: TestClient) -> Dict[str, str]: +def get_superuser_token_headers(client: TestClient) -> dict[str, str]: login_data = { "username": settings.FIRST_SUPERUSER, "password": settings.FIRST_SUPERUSER_PASSWORD, diff --git a/backend/uv.lock b/backend/uv.lock new file mode 100644 index 0000000000..e746adbb88 --- /dev/null +++ b/backend/uv.lock @@ -0,0 +1,1655 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <4.0" +resolution-markers = [ + "python_full_version < '3.13'", + "python_full_version >= '3.13'", +] + +[[package]] +name = "alembic" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/45/6f4555f2039f364c3ce31399529dcf48dd60726ff3715ad67f547d87dfd2/alembic-1.17.0.tar.gz", hash = "sha256:4652a0b3e19616b57d652b82bfa5e38bf5dbea0813eed971612671cb9e90c0fe", size = 1975526, upload-time = "2025-10-11T18:40:13.585Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/1f/38e29b06bfed7818ebba1f84904afdc8153ef7b6c7e0d8f3bc6643f5989c/alembic-1.17.0-py3-none-any.whl", hash = "sha256:80523bc437d41b35c5db7e525ad9d908f79de65c27d6a5a5eab6df348a352d99", size = 247449, upload-time = "2025-10-11T18:40:16.288Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/49/f3f17ec11c4a91fe79275c426658e509b07547f874b14c1a526d86a83fc8/anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb", size = 170983, upload-time = "2024-09-21T10:33:28.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/ef/7a4f225581a0d7886ea28359179cb861d7fbcdefad29663fc1167b86f69f/anyio-4.6.0-py3-none-any.whl", hash = "sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a", size = 89631, upload-time = "2024-09-21T10:33:27.05Z" }, +] + +[[package]] +name = "app" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "alembic" }, + { name = "bcrypt" }, + { name = "email-validator" }, + { name = "emails" }, + { name = "fastapi", extra = ["standard"] }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "passlib", extra = ["bcrypt"] }, + { name = "psycopg", extra = ["binary"] }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt" }, + { name = "python-multipart" }, + { name = "sentry-sdk", extra = ["fastapi"] }, + { name = "sqlmodel" }, + { name = "tenacity" }, +] + +[package.dev-dependencies] +dev = [ + { name = "coverage" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "ruff" }, + { name = "types-passlib" }, +] + +[package.metadata] +requires-dist = [ + { name = "alembic", specifier = ">=1.12.1,<2.0.0" }, + { name = "bcrypt", specifier = "==4.3.0" }, + { name = "email-validator", specifier = ">=2.1.0.post1,<3.0.0.0" }, + { name = "emails", specifier = ">=0.6,<1.0" }, + { name = "fastapi", extras = ["standard"], specifier = ">=0.114.2,<1.0.0" }, + { name = "httpx", specifier = ">=0.25.1,<1.0.0" }, + { name = "jinja2", specifier = ">=3.1.4,<4.0.0" }, + { name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4,<2.0.0" }, + { name = "psycopg", extras = ["binary"], specifier = ">=3.1.13,<4.0.0" }, + { name = "pydantic", specifier = ">2.0" }, + { name = "pydantic-settings", specifier = ">=2.2.1,<3.0.0" }, + { name = "pyjwt", specifier = ">=2.8.0,<3.0.0" }, + { name = "python-multipart", specifier = ">=0.0.7,<1.0.0" }, + { name = "sentry-sdk", extras = ["fastapi"], specifier = ">=1.40.6,<2.0.0" }, + { name = "sqlmodel", specifier = ">=0.0.21,<1.0.0" }, + { name = "tenacity", specifier = ">=8.2.3,<9.0.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "coverage", specifier = ">=7.4.3,<8.0.0" }, + { name = "mypy", specifier = ">=1.8.0,<2.0.0" }, + { name = "pre-commit", specifier = ">=3.6.2,<4.0.0" }, + { name = "pytest", specifier = ">=7.4.3,<8.0.0" }, + { name = "ruff", specifier = ">=0.2.2,<1.0.0" }, + { name = "types-passlib", specifier = ">=1.7.7.20240106,<2.0.0.0" }, +] + +[[package]] +name = "bcrypt" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697, upload-time = "2025-02-28T01:24:09.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719, upload-time = "2025-02-28T01:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001, upload-time = "2025-02-28T01:22:38.078Z" }, + { url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451, upload-time = "2025-02-28T01:22:40.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792, upload-time = "2025-02-28T01:22:43.144Z" }, + { url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752, upload-time = "2025-02-28T01:22:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762, upload-time = "2025-02-28T01:22:47.023Z" }, + { url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384, upload-time = "2025-02-28T01:22:49.221Z" }, + { url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329, upload-time = "2025-02-28T01:22:51.603Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241, upload-time = "2025-02-28T01:22:53.283Z" }, + { url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617, upload-time = "2025-02-28T01:22:55.461Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751, upload-time = "2025-02-28T01:22:57.81Z" }, + { url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965, upload-time = "2025-02-28T01:22:59.181Z" }, + { url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316, upload-time = "2025-02-28T01:23:00.763Z" }, + { url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752, upload-time = "2025-02-28T01:23:02.908Z" }, + { url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019, upload-time = "2025-02-28T01:23:05.838Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174, upload-time = "2025-02-28T01:23:07.274Z" }, + { url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870, upload-time = "2025-02-28T01:23:09.151Z" }, + { url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601, upload-time = "2025-02-28T01:23:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660, upload-time = "2025-02-28T01:23:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083, upload-time = "2025-02-28T01:23:14.5Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237, upload-time = "2025-02-28T01:23:16.686Z" }, + { url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737, upload-time = "2025-02-28T01:23:18.897Z" }, + { url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741, upload-time = "2025-02-28T01:23:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472, upload-time = "2025-02-28T01:23:23.183Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606, upload-time = "2025-02-28T01:23:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867, upload-time = "2025-02-28T01:23:26.875Z" }, + { url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589, upload-time = "2025-02-28T01:23:28.381Z" }, + { url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794, upload-time = "2025-02-28T01:23:30.187Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969, upload-time = "2025-02-28T01:23:31.945Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158, upload-time = "2025-02-28T01:23:34.161Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285, upload-time = "2025-02-28T01:23:35.765Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583, upload-time = "2025-02-28T01:23:38.021Z" }, + { url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896, upload-time = "2025-02-28T01:23:39.575Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492, upload-time = "2025-02-28T01:23:40.901Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213, upload-time = "2025-02-28T01:23:42.653Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162, upload-time = "2025-02-28T01:23:43.964Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856, upload-time = "2025-02-28T01:23:46.011Z" }, + { url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726, upload-time = "2025-02-28T01:23:47.575Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664, upload-time = "2025-02-28T01:23:49.059Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128, upload-time = "2025-02-28T01:23:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598, upload-time = "2025-02-28T01:23:51.775Z" }, + { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" }, + { url = "https://files.pythonhosted.org/packages/55/2d/0c7e5ab0524bf1a443e34cdd3926ec6f5879889b2f3c32b2f5074e99ed53/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1", size = 275367, upload-time = "2025-02-28T01:23:54.578Z" }, + { url = "https://files.pythonhosted.org/packages/10/4f/f77509f08bdff8806ecc4dc472b6e187c946c730565a7470db772d25df70/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d", size = 280644, upload-time = "2025-02-28T01:23:56.547Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/7d9dc16a3a4d530d0a9b845160e9e5d8eb4f00483e05d44bb4116a1861da/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492", size = 274881, upload-time = "2025-02-28T01:23:57.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/c4/ae6921088adf1e37f2a3a6a688e72e7d9e45fdd3ae5e0bc931870c1ebbda/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90", size = 280203, upload-time = "2025-02-28T01:23:59.331Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b1/1289e21d710496b88340369137cc4c5f6ee036401190ea116a7b4ae6d32a/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a", size = 275103, upload-time = "2025-02-28T01:24:00.764Z" }, + { url = "https://files.pythonhosted.org/packages/94/41/19be9fe17e4ffc5d10b7b67f10e459fc4eee6ffe9056a88de511920cfd8d/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce", size = 280513, upload-time = "2025-02-28T01:24:02.243Z" }, + { url = "https://files.pythonhosted.org/packages/aa/73/05687a9ef89edebdd8ad7474c16d8af685eb4591c3c38300bb6aad4f0076/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8", size = 274685, upload-time = "2025-02-28T01:24:04.512Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/47bba97924ebe86a62ef83dc75b7c8a881d53c535f83e2c54c4bd701e05c/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938", size = 280110, upload-time = "2025-02-28T01:24:05.896Z" }, +] + +[[package]] +name = "cachetools" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/38/a0f315319737ecf45b4319a8cd1f3a908e29d9277b46942263292115eee7/cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a", size = 27661, upload-time = "2024-08-18T20:28:44.639Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/07/14f8ad37f2d12a5ce41206c21820d8cb6561b728e51fad4530dff0552a67/cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", size = 9524, upload-time = "2024-08-18T20:28:43.404Z" }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507, upload-time = "2024-08-30T01:55:04.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321, upload-time = "2024-08-30T01:55:02.591Z" }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809, upload-time = "2023-11-01T04:04:59.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/61/095a0aa1a84d1481998b534177c8566fdc50bb1233ea9a0478cd3cc075bd/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", size = 194219, upload-time = "2023-11-01T04:02:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/f7cf5e5134175de79ad2059edf2adce18e0685ebdb9227ff0139975d0e93/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", size = 122521, upload-time = "2023-11-01T04:02:32.452Z" }, + { url = "https://files.pythonhosted.org/packages/46/6a/d5c26c41c49b546860cc1acabdddf48b0b3fb2685f4f5617ac59261b44ae/charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", size = 120383, upload-time = "2023-11-01T04:02:34.11Z" }, + { url = "https://files.pythonhosted.org/packages/b8/60/e2f67915a51be59d4539ed189eb0a2b0d292bf79270410746becb32bc2c3/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", size = 138223, upload-time = "2023-11-01T04:02:36.213Z" }, + { url = "https://files.pythonhosted.org/packages/05/8c/eb854996d5fef5e4f33ad56927ad053d04dc820e4a3d39023f35cad72617/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", size = 148101, upload-time = "2023-11-01T04:02:38.067Z" }, + { url = "https://files.pythonhosted.org/packages/f6/93/bb6cbeec3bf9da9b2eba458c15966658d1daa8b982c642f81c93ad9b40e1/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", size = 140699, upload-time = "2023-11-01T04:02:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/3702ba2a7470666a62fd81c58a4c40be00670e5006a67f4d626e57f013ae/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", size = 142065, upload-time = "2023-11-01T04:02:41.357Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ba/3f5e7be00b215fa10e13d64b1f6237eb6ebea66676a41b2bcdd09fe74323/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", size = 144505, upload-time = "2023-11-01T04:02:43.108Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/3b96a435c5109dd5b6adc8a59ba1d678b302a97938f032e3770cc84cd354/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", size = 139425, upload-time = "2023-11-01T04:02:45.427Z" }, + { url = "https://files.pythonhosted.org/packages/43/05/3bf613e719efe68fb3a77f9c536a389f35b95d75424b96b426a47a45ef1d/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", size = 145287, upload-time = "2023-11-01T04:02:46.705Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/a0bc646900994df12e07b4ae5c713f2b3e5998f58b9d3720cce2aa45652f/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", size = 149929, upload-time = "2023-11-01T04:02:48.098Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5c/97d97248af4920bc68687d9c3b3c0f47c910e21a8ff80af4565a576bd2f0/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", size = 141605, upload-time = "2023-11-01T04:02:49.605Z" }, + { url = "https://files.pythonhosted.org/packages/a8/31/47d018ef89f95b8aded95c589a77c072c55e94b50a41aa99c0a2008a45a4/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", size = 142646, upload-time = "2023-11-01T04:02:51.35Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/4fecf1d58bedb1340a50f165ba1c7ddc0400252d6832ff619c4568b36cc0/charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", size = 92846, upload-time = "2023-11-01T04:02:52.679Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a0/4af29e22cb5942488cf45630cbdd7cefd908768e69bdd90280842e4e8529/charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", size = 100343, upload-time = "2023-11-01T04:02:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647, upload-time = "2023-11-01T04:02:55.329Z" }, + { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434, upload-time = "2023-11-01T04:02:57.173Z" }, + { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979, upload-time = "2023-11-01T04:02:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582, upload-time = "2023-11-01T04:02:59.776Z" }, + { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645, upload-time = "2023-11-01T04:03:02.186Z" }, + { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398, upload-time = "2023-11-01T04:03:04.255Z" }, + { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273, upload-time = "2023-11-01T04:03:05.983Z" }, + { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577, upload-time = "2023-11-01T04:03:07.567Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747, upload-time = "2023-11-01T04:03:08.886Z" }, + { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375, upload-time = "2023-11-01T04:03:10.613Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474, upload-time = "2023-11-01T04:03:11.973Z" }, + { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232, upload-time = "2023-11-01T04:03:13.505Z" }, + { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859, upload-time = "2023-11-01T04:03:17.362Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509, upload-time = "2023-11-01T04:03:21.453Z" }, + { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870, upload-time = "2023-11-01T04:03:22.723Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892, upload-time = "2023-11-01T04:03:24.135Z" }, + { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213, upload-time = "2023-11-01T04:03:25.66Z" }, + { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404, upload-time = "2023-11-01T04:03:27.04Z" }, + { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275, upload-time = "2023-11-01T04:03:28.466Z" }, + { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518, upload-time = "2023-11-01T04:03:29.82Z" }, + { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182, upload-time = "2023-11-01T04:03:31.511Z" }, + { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869, upload-time = "2023-11-01T04:03:32.887Z" }, + { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042, upload-time = "2023-11-01T04:03:34.412Z" }, + { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275, upload-time = "2023-11-01T04:03:35.759Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819, upload-time = "2023-11-01T04:03:37.216Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415, upload-time = "2023-11-01T04:03:38.694Z" }, + { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212, upload-time = "2023-11-01T04:03:40.07Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167, upload-time = "2023-11-01T04:03:41.491Z" }, + { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041, upload-time = "2023-11-01T04:03:42.836Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397, upload-time = "2023-11-01T04:03:44.467Z" }, + { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543, upload-time = "2023-11-01T04:04:58.622Z" }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121, upload-time = "2023-08-17T17:29:11.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941, upload-time = "2023-08-17T17:29:10.08Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791, upload-time = "2024-08-04T19:45:30.9Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690, upload-time = "2024-08-04T19:43:07.695Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127, upload-time = "2024-08-04T19:43:10.15Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654, upload-time = "2024-08-04T19:43:12.405Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598, upload-time = "2024-08-04T19:43:14.078Z" }, + { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732, upload-time = "2024-08-04T19:43:16.632Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816, upload-time = "2024-08-04T19:43:19.049Z" }, + { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325, upload-time = "2024-08-04T19:43:21.246Z" }, + { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418, upload-time = "2024-08-04T19:43:22.945Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343, upload-time = "2024-08-04T19:43:25.121Z" }, + { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136, upload-time = "2024-08-04T19:43:26.851Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796, upload-time = "2024-08-04T19:43:29.115Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244, upload-time = "2024-08-04T19:43:31.285Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279, upload-time = "2024-08-04T19:43:33.581Z" }, + { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859, upload-time = "2024-08-04T19:43:35.301Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549, upload-time = "2024-08-04T19:43:37.578Z" }, + { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477, upload-time = "2024-08-04T19:43:39.92Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134, upload-time = "2024-08-04T19:43:41.453Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910, upload-time = "2024-08-04T19:43:43.037Z" }, + { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348, upload-time = "2024-08-04T19:43:44.787Z" }, + { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230, upload-time = "2024-08-04T19:43:46.707Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983, upload-time = "2024-08-04T19:43:49.082Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221, upload-time = "2024-08-04T19:43:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342, upload-time = "2024-08-04T19:43:53.746Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371, upload-time = "2024-08-04T19:43:55.993Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455, upload-time = "2024-08-04T19:43:57.618Z" }, + { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924, upload-time = "2024-08-04T19:44:00.012Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252, upload-time = "2024-08-04T19:44:01.713Z" }, + { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897, upload-time = "2024-08-04T19:44:03.898Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606, upload-time = "2024-08-04T19:44:05.532Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373, upload-time = "2024-08-04T19:44:07.079Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007, upload-time = "2024-08-04T19:44:09.453Z" }, + { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269, upload-time = "2024-08-04T19:44:11.045Z" }, + { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886, upload-time = "2024-08-04T19:44:12.83Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037, upload-time = "2024-08-04T19:44:15.393Z" }, + { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038, upload-time = "2024-08-04T19:44:17.466Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690, upload-time = "2024-08-04T19:44:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765, upload-time = "2024-08-04T19:44:20.994Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611, upload-time = "2024-08-04T19:44:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671, upload-time = "2024-08-04T19:44:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368, upload-time = "2024-08-04T19:44:26.276Z" }, + { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758, upload-time = "2024-08-04T19:44:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035, upload-time = "2024-08-04T19:44:30.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839, upload-time = "2024-08-04T19:44:32.412Z" }, + { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569, upload-time = "2024-08-04T19:44:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927, upload-time = "2024-08-04T19:44:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401, upload-time = "2024-08-04T19:44:38.155Z" }, + { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301, upload-time = "2024-08-04T19:44:39.883Z" }, + { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598, upload-time = "2024-08-04T19:44:41.59Z" }, + { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307, upload-time = "2024-08-04T19:44:43.301Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453, upload-time = "2024-08-04T19:44:45.677Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926, upload-time = "2024-08-04T19:45:28.875Z" }, +] + +[[package]] +name = "cssselect" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/91/d51202cc41fbfca7fa332f43a5adac4b253962588c7cc5a54824b019081c/cssselect-1.2.0.tar.gz", hash = "sha256:666b19839cfaddb9ce9d36bfe4c969132c647b92fc9088c4e23f786b30f1b3dc", size = 41423, upload-time = "2022-10-27T13:25:41.71Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/a9/2da08717a6862c48f1d61ef957a7bba171e7eefa6c0aa0ceb96a140c2a6b/cssselect-1.2.0-py2.py3-none-any.whl", hash = "sha256:da1885f0c10b60c03ed5eccbb6b68d6eff248d91976fcde348f395d54c9fd35e", size = 18687, upload-time = "2022-10-27T13:25:40.153Z" }, +] + +[[package]] +name = "cssutils" +version = "2.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/9f/329d26121fe165be44b1dfff21aa0dc348f04633931f1d20ed6cf448a236/cssutils-2.11.1.tar.gz", hash = "sha256:0563a76513b6af6eebbe788c3bf3d01c920e46b3f90c8416738c5cfc773ff8e2", size = 711657, upload-time = "2024-06-04T15:51:39.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/ec/bb273b7208c606890dc36540fe667d06ce840a6f62f9fae7e658fcdc90fb/cssutils-2.11.1-py3-none-any.whl", hash = "sha256:a67bfdfdff4f3867fab43698ec4897c1a828eca5973f4073321b3bccaf1199b1", size = 385747, upload-time = "2024-06-04T15:51:37.499Z" }, +] + +[[package]] +name = "distlib" +version = "0.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/91/e2df406fb4efacdf46871c25cde65d3c6ee5e173b7e5a4547a47bae91920/distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64", size = 609931, upload-time = "2023-12-12T07:14:03.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/41/9307e4f5f9976bc8b7fea0b66367734e8faf3ec84bc0d412d8cfabbb66cd/distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", size = 468850, upload-time = "2023-12-12T07:13:59.966Z" }, +] + +[[package]] +name = "dnspython" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/7d/c871f55054e403fdfd6b8f65fd6d1c4e147ed100d3e9f9ba1fe695403939/dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc", size = 332727, upload-time = "2024-02-18T18:48:48.952Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/a1/8c5287991ddb8d3e4662f71356d9656d91ab3a36618c3dd11b280df0d255/dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50", size = 307696, upload-time = "2024-02-18T18:48:46.786Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "emails" +version = "0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chardet" }, + { name = "cssutils" }, + { name = "lxml" }, + { name = "premailer" }, + { name = "python-dateutil" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/f9/c1e315aa82ed9f037186c30109200fb4b4c51b5483b8065daa0ca836a336/emails-0.6.tar.gz", hash = "sha256:a4c2d67ea8b8831967a750d8edc6e77040d7693143fe280e6d2a367d9c36ff88", size = 44066, upload-time = "2020-06-19T11:20:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/7e/b648d640d88d31de49e566832aca9cce025c52d6349b0a0fc65e9df1f4c5/emails-0.6-py2.py3-none-any.whl", hash = "sha256:72c1e3198075709cc35f67e1b49e2da1a2bc087e9b444073db61a379adfb7f3c", size = 56250, upload-time = "2020-06-19T11:20:40.466Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, +] + +[[package]] +name = "fastapi" +version = "0.115.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/5e/bf0471f14bf6ebfbee8208148a3396d1a23298531a6cc10776c59f4c0f87/fastapi-0.115.0.tar.gz", hash = "sha256:f93b4ca3529a8ebc6fc3fcf710e5efa8de3df9b41570958abf1d97d843138004", size = 302295, upload-time = "2024-09-17T19:18:12.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/ab/a1f7eed031aeb1c406a6e9d45ca04bff401c8a25a30dd0e4fd2caae767c3/fastapi-0.115.0-py3-none-any.whl", hash = "sha256:17ea427674467486e997206a5ab25760f6b09e069f099b96f5b55a32fb6f1631", size = 94625, upload-time = "2024-09-17T19:18:10.962Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/f8/1ad5ce32d029aeb9117e9a5a9b3e314a8477525d60c12a9b7730a3c186ec/fastapi_cli-0.0.5.tar.gz", hash = "sha256:d30e1239c6f46fcb95e606f02cdda59a1e2fa778a54b64686b3ff27f6211ff9f", size = 15571, upload-time = "2024-08-02T05:48:13.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/ea/4b5011012ac925fe2f83b19d0e09cee9d324141ec7bf5e78bb2817f96513/fastapi_cli-0.0.5-py3-none-any.whl", hash = "sha256:e94d847524648c748a5350673546bbf9bcaeb086b33c24f2e82e021436866a46", size = 9489, upload-time = "2024-08-02T05:48:11.609Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037, upload-time = "2024-09-17T19:02:01.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163, upload-time = "2024-09-17T19:02:00.268Z" }, +] + +[[package]] +name = "greenlet" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022, upload-time = "2024-09-20T18:21:04.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235, upload-time = "2024-09-20T17:07:18.761Z" }, + { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168, upload-time = "2024-09-20T17:36:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826, upload-time = "2024-09-20T17:39:16.921Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443, upload-time = "2024-09-20T17:44:21.896Z" }, + { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295, upload-time = "2024-09-20T17:08:37.951Z" }, + { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544, upload-time = "2024-09-20T17:08:27.894Z" }, + { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456, upload-time = "2024-09-20T17:44:11.755Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111, upload-time = "2024-09-20T17:09:22.104Z" }, + { url = "https://files.pythonhosted.org/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392, upload-time = "2024-09-20T17:28:51.988Z" }, + { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479, upload-time = "2024-09-20T17:07:22.332Z" }, + { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404, upload-time = "2024-09-20T17:36:45.588Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813, upload-time = "2024-09-20T17:39:19.052Z" }, + { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517, upload-time = "2024-09-20T17:44:24.101Z" }, + { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831, upload-time = "2024-09-20T17:08:40.577Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413, upload-time = "2024-09-20T17:08:31.728Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619, upload-time = "2024-09-20T17:44:14.222Z" }, + { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198, upload-time = "2024-09-20T17:09:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930, upload-time = "2024-09-20T17:25:18.656Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260, upload-time = "2024-09-20T17:08:07.301Z" }, + { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064, upload-time = "2024-09-20T17:36:47.628Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420, upload-time = "2024-09-20T17:39:21.258Z" }, + { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035, upload-time = "2024-09-20T17:44:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105, upload-time = "2024-09-20T17:08:42.048Z" }, + { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077, upload-time = "2024-09-20T17:08:33.707Z" }, + { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975, upload-time = "2024-09-20T17:44:15.989Z" }, + { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955, upload-time = "2024-09-20T17:09:25.539Z" }, + { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655, upload-time = "2024-09-20T17:21:22.427Z" }, + { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990, upload-time = "2024-09-20T17:08:26.312Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175, upload-time = "2024-09-20T17:36:48.983Z" }, + { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425, upload-time = "2024-09-20T17:39:22.705Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736, upload-time = "2024-09-20T17:44:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347, upload-time = "2024-09-20T17:08:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583, upload-time = "2024-09-20T17:08:36.85Z" }, + { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039, upload-time = "2024-09-20T17:44:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716, upload-time = "2024-09-20T17:09:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490, upload-time = "2024-09-20T17:17:09.501Z" }, + { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731, upload-time = "2024-09-20T17:36:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304, upload-time = "2024-09-20T17:39:24.55Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537, upload-time = "2024-09-20T17:44:31.102Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506, upload-time = "2024-09-20T17:08:47.852Z" }, + { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753, upload-time = "2024-09-20T17:08:38.079Z" }, + { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731, upload-time = "2024-09-20T17:44:20.556Z" }, + { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112, upload-time = "2024-09-20T17:09:28.753Z" }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418, upload-time = "2022-09-25T15:40:01.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259, upload-time = "2022-09-25T15:39:59.68Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/b0/5e8b8674f8d203335a62fdfcfa0d11ebe09e23613c3391033cbba35f7926/httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61", size = 83234, upload-time = "2024-03-27T18:29:07.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/d4/e5d7e4f2174f8a4d63c8897d79eb8fe2503f7ecc03282fee1fa2719c2704/httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5", size = 77926, upload-time = "2024-03-27T18:29:04.098Z" }, +] + +[[package]] +name = "httptools" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/1d/d77686502fced061b3ead1c35a2d70f6b281b5f723c4eff7a2277c04e4a2/httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a", size = 191228, upload-time = "2023-10-16T17:42:36.003Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/6a/80bce0216b63babf51cdc34814c3f0f10489e13ab89fb6bc91202736a8a2/httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f", size = 149778, upload-time = "2023-10-16T17:41:35.97Z" }, + { url = "https://files.pythonhosted.org/packages/bd/7d/4cd75356dfe0ed0b40ca6873646bf9ff7b5138236c72338dc569dc57d509/httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563", size = 77604, upload-time = "2023-10-16T17:41:38.361Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/6348ce41fb5c1484f35184c172efb8854a288e6090bb54e2210598268369/httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58", size = 346717, upload-time = "2023-10-16T17:41:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/65/e7/dd5ba95c84047118a363f0755ad78e639e0529be92424bb020496578aa3b/httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185", size = 341442, upload-time = "2023-10-16T17:41:42.492Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/b37d596bc32be291477a8912bf9d1508d7e8553aa11a30cd871fd89cbae4/httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142", size = 354531, upload-time = "2023-10-16T17:41:44.488Z" }, + { url = "https://files.pythonhosted.org/packages/99/c9/53ed7176583ec4b4364d941a08624288f2ae55b4ff58b392cdb68db1e1ed/httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658", size = 347754, upload-time = "2023-10-16T17:41:46.567Z" }, + { url = "https://files.pythonhosted.org/packages/1e/fc/8a26c2adcd3f141e4729897633f03832b71ebea6f4c31cce67a92ded1961/httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b", size = 58165, upload-time = "2023-10-16T17:41:48.859Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d1/53283b96ed823d5e4d89ee9aa0f29df5a1bdf67f148e061549a595d534e4/httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1", size = 145855, upload-time = "2023-10-16T17:41:50.407Z" }, + { url = "https://files.pythonhosted.org/packages/80/dd/cebc9d4b1d4b70e9f3d40d1db0829a28d57ca139d0b04197713816a11996/httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0", size = 75604, upload-time = "2023-10-16T17:41:52.204Z" }, + { url = "https://files.pythonhosted.org/packages/76/7a/45c5a9a2e9d21f7381866eb7b6ead5a84d8fe7e54e35208eeb18320a29b4/httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc", size = 324784, upload-time = "2023-10-16T17:41:53.617Z" }, + { url = "https://files.pythonhosted.org/packages/59/23/047a89e66045232fb82c50ae57699e40f70e073ae5ccd53f54e532fbd2a2/httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2", size = 318547, upload-time = "2023-10-16T17:41:55.847Z" }, + { url = "https://files.pythonhosted.org/packages/82/f5/50708abc7965d7d93c0ee14a148ccc6d078a508f47fe9357c79d5360f252/httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837", size = 330211, upload-time = "2023-10-16T17:41:57.576Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1e/9823ca7aab323c0e0e9dd82ce835a6e93b69f69aedffbc94d31e327f4283/httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d", size = 322174, upload-time = "2023-10-16T17:41:59.369Z" }, + { url = "https://files.pythonhosted.org/packages/14/e4/20d28dfe7f5b5603b6b04c33bb88662ad749de51f0c539a561f235f42666/httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3", size = 55434, upload-time = "2023-10-16T17:42:01.414Z" }, + { url = "https://files.pythonhosted.org/packages/60/13/b62e086b650752adf9094b7e62dab97f4cb7701005664544494b7956a51e/httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0", size = 146354, upload-time = "2023-10-16T17:42:03.324Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5d/9ad32b79b6c24524087e78aa3f0a2dfcf58c11c90e090e4593b35def8a86/httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2", size = 75785, upload-time = "2023-10-16T17:42:04.731Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a4/b503851c40f20bcbd453db24ed35d961f62abdae0dccc8f672cd5d350d87/httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90", size = 345396, upload-time = "2023-10-16T17:42:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9a/aa406864f3108e06f7320425a528ff8267124dead1fd72a3e9da2067f893/httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503", size = 344741, upload-time = "2023-10-16T17:42:08.543Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3a/3fd8dfb987c4247651baf2ac6f28e8e9f889d484ca1a41a9ad0f04dfe300/httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84", size = 345096, upload-time = "2023-10-16T17:42:10.081Z" }, + { url = "https://files.pythonhosted.org/packages/80/01/379f6466d8e2edb861c1f44ccac255ed1f8a0d4c5c666a1ceb34caad7555/httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb", size = 343535, upload-time = "2023-10-16T17:42:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/60860e9ee87a7d4712b98f7e1411730520053b9d69e9e42b0b9751809c17/httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949", size = 55660, upload-time = "2023-10-16T17:42:13.711Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "identify" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bb/25024dbcc93516c492b75919e76f389bac754a3e4248682fba32b250c880/identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98", size = 99097, upload-time = "2024-09-14T23:50:32.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/0c/4ef72754c050979fdcc06c744715ae70ea37e734816bb6514f79df77a42f/identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", size = 98972, upload-time = "2024-09-14T23:50:30.747Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "lxml" +version = "5.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/6b/20c3a4b24751377aaa6307eb230b66701024012c29dd374999cc92983269/lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f", size = 3679318, upload-time = "2024-08-10T18:17:29.668Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ce/2789e39eddf2b13fac29878bfa465f0910eb6b0096e29090e5176bc8cf43/lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656", size = 8124570, upload-time = "2024-08-10T18:09:04.096Z" }, + { url = "https://files.pythonhosted.org/packages/24/a8/f4010166a25d41715527129af2675981a50d3bbf7df09c5d9ab8ca24fbf9/lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d", size = 4413042, upload-time = "2024-08-10T18:09:08.841Z" }, + { url = "https://files.pythonhosted.org/packages/41/a4/7e45756cecdd7577ddf67a68b69c1db0f5ddbf0c9f65021ee769165ffc5a/lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a", size = 5139213, upload-time = "2024-08-10T18:09:12.622Z" }, + { url = "https://files.pythonhosted.org/packages/02/e2/ecf845b12323c92748077e1818b64e8b4dba509a4cb12920b3762ebe7552/lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8", size = 4838814, upload-time = "2024-08-10T18:09:16.222Z" }, + { url = "https://files.pythonhosted.org/packages/12/91/619f9fb72cf75e9ceb8700706f7276f23995f6ad757e6d400fbe35ca4990/lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330", size = 5425084, upload-time = "2024-08-10T18:09:19.795Z" }, + { url = "https://files.pythonhosted.org/packages/25/3b/162a85a8f0fd2a3032ec3f936636911c6e9523a8e263fffcfd581ce98b54/lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965", size = 4875993, upload-time = "2024-08-10T18:09:23.776Z" }, + { url = "https://files.pythonhosted.org/packages/43/af/dd3f58cc7d946da6ae42909629a2b1d5dd2d1b583334d4af9396697d6863/lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22", size = 5012462, upload-time = "2024-08-10T18:09:27.642Z" }, + { url = "https://files.pythonhosted.org/packages/69/c1/5ea46b2d4c98f5bf5c83fffab8a0ad293c9bc74df9ecfbafef10f77f7201/lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b", size = 4815288, upload-time = "2024-08-10T18:09:31.633Z" }, + { url = "https://files.pythonhosted.org/packages/1d/51/a0acca077ad35da458f4d3f729ef98effd2b90f003440d35fc36323f8ae6/lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7", size = 5472435, upload-time = "2024-08-10T18:09:35.758Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6b/0989c9368986961a6b0f55b46c80404c4b758417acdb6d87bfc3bd5f4967/lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8", size = 4976354, upload-time = "2024-08-10T18:09:39.51Z" }, + { url = "https://files.pythonhosted.org/packages/05/9e/87492d03ff604fbf656ed2bf3e2e8d28f5d58ea1f00ff27ac27b06509079/lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32", size = 5029973, upload-time = "2024-08-10T18:09:42.978Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cc/9ae1baf5472af88e19e2c454b3710c1be9ecafb20eb474eeabcd88a055d2/lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86", size = 4888837, upload-time = "2024-08-10T18:09:46.185Z" }, + { url = "https://files.pythonhosted.org/packages/d2/10/5594ffaec8c120d75b17e3ad23439b740a51549a9b5fd7484b2179adfe8f/lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5", size = 5530555, upload-time = "2024-08-10T18:09:50.366Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9b/de17f05377c8833343b629905571fb06cff2028f15a6f58ae2267662e341/lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03", size = 5405314, upload-time = "2024-08-10T18:09:54.58Z" }, + { url = "https://files.pythonhosted.org/packages/8a/b4/227be0f1f3cca8255925985164c3838b8b36e441ff0cc10c1d3c6bdba031/lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7", size = 5079303, upload-time = "2024-08-10T18:09:58.032Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ee/19abcebb7fc40319bb71cd6adefa1ad94d09b5660228715854d6cc420713/lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80", size = 3475126, upload-time = "2024-08-10T18:10:01.43Z" }, + { url = "https://files.pythonhosted.org/packages/a1/35/183d32551447e280032b2331738cd850da435a42f850b71ebeaab42c1313/lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3", size = 3805065, upload-time = "2024-08-10T18:10:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a8/449faa2a3cbe6a99f8d38dcd51a3ee8844c17862841a6f769ea7c2a9cd0f/lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b", size = 8141056, upload-time = "2024-08-10T18:10:09.455Z" }, + { url = "https://files.pythonhosted.org/packages/ac/8a/ae6325e994e2052de92f894363b038351c50ee38749d30cc6b6d96aaf90f/lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18", size = 4425238, upload-time = "2024-08-10T18:10:13.348Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fb/128dddb7f9086236bce0eeae2bfb316d138b49b159f50bc681d56c1bdd19/lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442", size = 5095197, upload-time = "2024-08-10T18:10:16.825Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f9/a181a8ef106e41e3086629c8bdb2d21a942f14c84a0e77452c22d6b22091/lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4", size = 4809809, upload-time = "2024-08-10T18:10:20.046Z" }, + { url = "https://files.pythonhosted.org/packages/25/2f/b20565e808f7f6868aacea48ddcdd7e9e9fb4c799287f21f1a6c7c2e8b71/lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f", size = 5407593, upload-time = "2024-08-10T18:10:23.641Z" }, + { url = "https://files.pythonhosted.org/packages/23/0e/caac672ec246d3189a16c4d364ed4f7d6bf856c080215382c06764058c08/lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e", size = 4866657, upload-time = "2024-08-10T18:10:26.528Z" }, + { url = "https://files.pythonhosted.org/packages/67/a4/1f5fbd3f58d4069000522196b0b776a014f3feec1796da03e495cf23532d/lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c", size = 4967017, upload-time = "2024-08-10T18:10:29.639Z" }, + { url = "https://files.pythonhosted.org/packages/ee/73/623ecea6ca3c530dd0a4ed0d00d9702e0e85cd5624e2d5b93b005fe00abd/lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16", size = 4810730, upload-time = "2024-08-10T18:10:33.387Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/fb84fb8e3c298f3a245ae3ea6221c2426f1bbaa82d10a88787412a498145/lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79", size = 5455154, upload-time = "2024-08-10T18:10:36.897Z" }, + { url = "https://files.pythonhosted.org/packages/b1/72/4d1ad363748a72c7c0411c28be2b0dc7150d91e823eadad3b91a4514cbea/lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080", size = 4969416, upload-time = "2024-08-10T18:10:40.331Z" }, + { url = "https://files.pythonhosted.org/packages/42/07/b29571a58a3a80681722ea8ed0ba569211d9bb8531ad49b5cacf6d409185/lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654", size = 5013672, upload-time = "2024-08-10T18:10:43.768Z" }, + { url = "https://files.pythonhosted.org/packages/b9/93/bde740d5a58cf04cbd38e3dd93ad1e36c2f95553bbf7d57807bc6815d926/lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d", size = 4878644, upload-time = "2024-08-10T18:10:47.901Z" }, + { url = "https://files.pythonhosted.org/packages/56/b5/645c8c02721d49927c93181de4017164ec0e141413577687c3df8ff0800f/lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763", size = 5511531, upload-time = "2024-08-10T18:10:51.581Z" }, + { url = "https://files.pythonhosted.org/packages/85/3f/6a99a12d9438316f4fc86ef88c5d4c8fb674247b17f3173ecadd8346b671/lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec", size = 5402065, upload-time = "2024-08-10T18:10:54.841Z" }, + { url = "https://files.pythonhosted.org/packages/80/8a/df47bff6ad5ac57335bf552babfb2408f9eb680c074ec1ba412a1a6af2c5/lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be", size = 5069775, upload-time = "2024-08-10T18:10:57.808Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/e7ad0f0fbe4b6368c5ee1e3ef0c3365098d806d42379c46c1ba2802a52f7/lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9", size = 3474226, upload-time = "2024-08-10T18:11:00.73Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b5/91c2249bfac02ee514ab135e9304b89d55967be7e53e94a879b74eec7a5c/lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1", size = 3814971, upload-time = "2024-08-10T18:11:03.743Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/d1f1c5e40c64bf62afd7a3f9b34ce18a586a1cccbf71e783cd0a6d8e8971/lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859", size = 8171753, upload-time = "2024-08-10T18:11:07.859Z" }, + { url = "https://files.pythonhosted.org/packages/bd/83/26b1864921869784355459f374896dcf8b44d4af3b15d7697e9156cb2de9/lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e", size = 4441955, upload-time = "2024-08-10T18:11:12.251Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/e9bff9fb359226c25cda3538f664f54f2804f4b37b0d7c944639e1a51f69/lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f", size = 5050778, upload-time = "2024-08-10T18:11:16.233Z" }, + { url = "https://files.pythonhosted.org/packages/88/69/6972bfafa8cd3ddc8562b126dd607011e218e17be313a8b1b9cc5a0ee876/lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e", size = 4748628, upload-time = "2024-08-10T18:11:19.507Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ea/a6523c7c7f6dc755a6eed3d2f6d6646617cad4d3d6d8ce4ed71bfd2362c8/lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179", size = 5322215, upload-time = "2024-08-10T18:11:23.708Z" }, + { url = "https://files.pythonhosted.org/packages/99/37/396fbd24a70f62b31d988e4500f2068c7f3fd399d2fd45257d13eab51a6f/lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a", size = 4813963, upload-time = "2024-08-10T18:11:26.997Z" }, + { url = "https://files.pythonhosted.org/packages/09/91/e6136f17459a11ce1757df864b213efbeab7adcb2efa63efb1b846ab6723/lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3", size = 4923353, upload-time = "2024-08-10T18:11:30.478Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7c/2eeecf87c9a1fca4f84f991067c693e67340f2b7127fc3eca8fa29d75ee3/lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1", size = 4740541, upload-time = "2024-08-10T18:11:34.344Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ed/4c38ba58defca84f5f0d0ac2480fdcd99fc7ae4b28fc417c93640a6949ae/lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d", size = 5346504, upload-time = "2024-08-10T18:11:37.595Z" }, + { url = "https://files.pythonhosted.org/packages/a5/22/bbd3995437e5745cb4c2b5d89088d70ab19d4feabf8a27a24cecb9745464/lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c", size = 4898077, upload-time = "2024-08-10T18:11:40.867Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/94537acfb5b8f18235d13186d247bca478fea5e87d224644e0fe907df976/lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99", size = 4946543, upload-time = "2024-08-10T18:11:44.954Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e8/4b15df533fe8e8d53363b23a41df9be907330e1fa28c7ca36893fad338ee/lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff", size = 4816841, upload-time = "2024-08-10T18:11:49.046Z" }, + { url = "https://files.pythonhosted.org/packages/1a/e7/03f390ea37d1acda50bc538feb5b2bda6745b25731e4e76ab48fae7106bf/lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a", size = 5417341, upload-time = "2024-08-10T18:11:52.295Z" }, + { url = "https://files.pythonhosted.org/packages/ea/99/d1133ab4c250da85a883c3b60249d3d3e7c64f24faff494cf0fd23f91e80/lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8", size = 5327539, upload-time = "2024-08-10T18:11:55.98Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ed/e6276c8d9668028213df01f598f385b05b55a4e1b4662ee12ef05dab35aa/lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d", size = 5012542, upload-time = "2024-08-10T18:11:59.351Z" }, + { url = "https://files.pythonhosted.org/packages/36/88/684d4e800f5aa28df2a991a6a622783fb73cf0e46235cfa690f9776f032e/lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30", size = 3486454, upload-time = "2024-08-10T18:12:02.696Z" }, + { url = "https://files.pythonhosted.org/packages/fc/82/ace5a5676051e60355bd8fb945df7b1ba4f4fb8447f2010fb816bfd57724/lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f", size = 3816857, upload-time = "2024-08-10T18:12:06.456Z" }, + { url = "https://files.pythonhosted.org/packages/94/6a/42141e4d373903bfea6f8e94b2f554d05506dfda522ada5343c651410dc8/lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a", size = 8156284, upload-time = "2024-08-10T18:12:10.439Z" }, + { url = "https://files.pythonhosted.org/packages/91/5e/fa097f0f7d8b3d113fb7312c6308af702f2667f22644441715be961f2c7e/lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd", size = 4432407, upload-time = "2024-08-10T18:12:13.917Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a1/b901988aa6d4ff937f2e5cfc114e4ec561901ff00660c3e56713642728da/lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51", size = 5048331, upload-time = "2024-08-10T18:12:17.204Z" }, + { url = "https://files.pythonhosted.org/packages/30/0f/b2a54f48e52de578b71bbe2a2f8160672a8a5e103df3a78da53907e8c7ed/lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b", size = 4744835, upload-time = "2024-08-10T18:12:21.172Z" }, + { url = "https://files.pythonhosted.org/packages/82/9d/b000c15538b60934589e83826ecbc437a1586488d7c13f8ee5ff1f79a9b8/lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002", size = 5316649, upload-time = "2024-08-10T18:12:24.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ee/ffbb9eaff5e541922611d2c56b175c45893d1c0b8b11e5a497708a6a3b3b/lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4", size = 4812046, upload-time = "2024-08-10T18:12:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/15/ff/7ff89d567485c7b943cdac316087f16b2399a8b997007ed352a1248397e5/lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492", size = 4918597, upload-time = "2024-08-10T18:12:32.278Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/535b6ed8c048412ff51268bdf4bf1cf052a37aa7e31d2e6518038a883b29/lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3", size = 4738071, upload-time = "2024-08-10T18:12:35.407Z" }, + { url = "https://files.pythonhosted.org/packages/7a/8f/cbbfa59cb4d4fd677fe183725a76d8c956495d7a3c7f111ab8f5e13d2e83/lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4", size = 5342213, upload-time = "2024-08-10T18:12:38.73Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fb/db4c10dd9958d4b52e34d1d1f7c1f434422aeaf6ae2bbaaff2264351d944/lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367", size = 4893749, upload-time = "2024-08-10T18:12:42.606Z" }, + { url = "https://files.pythonhosted.org/packages/f2/38/bb4581c143957c47740de18a3281a0cab7722390a77cc6e610e8ebf2d736/lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832", size = 4945901, upload-time = "2024-08-10T18:12:45.944Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d5/18b7de4960c731e98037bd48fa9f8e6e8f2558e6fbca4303d9b14d21ef3b/lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff", size = 4815447, upload-time = "2024-08-10T18:12:49.051Z" }, + { url = "https://files.pythonhosted.org/packages/97/a8/cd51ceaad6eb849246559a8ef60ae55065a3df550fc5fcd27014361c1bab/lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd", size = 5411186, upload-time = "2024-08-10T18:12:52.388Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/1e3dabab519481ed7b1fdcba21dcfb8832f57000733ef0e71cf6d09a5e03/lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb", size = 5324481, upload-time = "2024-08-10T18:12:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/b6/17/71e9984cf0570cd202ac0a1c9ed5c1b8889b0fc8dc736f5ef0ffb181c284/lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b", size = 5011053, upload-time = "2024-08-10T18:12:59.714Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/9f7e6d3312a91e30829368c2b3217e750adef12a6f8eb10498249f4e8d72/lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957", size = 3485634, upload-time = "2024-08-10T18:13:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/7d/db/214290d58ad68c587bd5d6af3d34e56830438733d0d0856c0275fde43652/lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d", size = 3814417, upload-time = "2024-08-10T18:13:05.791Z" }, + { url = "https://files.pythonhosted.org/packages/99/f7/b73a431c8500565aa500e99e60b448d305eaf7c0b4c893c7c5a8a69cc595/lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c", size = 3925431, upload-time = "2024-08-10T18:15:59.002Z" }, + { url = "https://files.pythonhosted.org/packages/db/48/4a206623c0d093d0e3b15f415ffb4345b0bdf661a3d0b15a112948c033c7/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a", size = 4216683, upload-time = "2024-08-10T18:16:03.004Z" }, + { url = "https://files.pythonhosted.org/packages/54/47/577820c45dd954523ae8453b632d91e76da94ca6d9ee40d8c98dd86f916b/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005", size = 4326732, upload-time = "2024-08-10T18:16:06.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/de/96cb6d3269bc994b4f5ede8ca7bf0840f5de0a278bc6e50cb317ff71cafa/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce", size = 4218377, upload-time = "2024-08-10T18:16:10.836Z" }, + { url = "https://files.pythonhosted.org/packages/a5/43/19b1ef6cbffa4244a217f95cc5f41a6cb4720fed33510a49670b03c5f1a0/lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83", size = 4351237, upload-time = "2024-08-10T18:16:14.652Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b2/6a22fb5c0885da3b00e116aee81f0b829ec9ac8f736cd414b4a09413fc7d/lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba", size = 3487557, upload-time = "2024-08-10T18:16:18.255Z" }, +] + +[[package]] +name = "mako" +version = "1.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/03/fb5ba97ff65ce64f6d35b582aacffc26b693a98053fa831ab43a437cbddb/Mako-1.3.5.tar.gz", hash = "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc", size = 392738, upload-time = "2024-05-14T12:22:05.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/62/70f5a0c2dd208f9f3f2f9afd103aec42ee4d9ad2401d78342f75e9b8da36/Mako-1.3.5-py3-none-any.whl", hash = "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a", size = 78565, upload-time = "2024-05-14T12:22:08.522Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206, upload-time = "2024-02-02T16:30:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079, upload-time = "2024-02-02T16:30:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620, upload-time = "2024-02-02T16:30:08.31Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818, upload-time = "2024-02-02T16:30:09.577Z" }, + { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493, upload-time = "2024-02-02T16:30:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630, upload-time = "2024-02-02T16:30:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745, upload-time = "2024-02-02T16:30:14.222Z" }, + { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021, upload-time = "2024-02-02T16:30:16.032Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659, upload-time = "2024-02-02T16:30:17.079Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213, upload-time = "2024-02-02T16:30:18.251Z" }, + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219, upload-time = "2024-02-02T16:30:19.988Z" }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098, upload-time = "2024-02-02T16:30:21.063Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014, upload-time = "2024-02-02T16:30:22.926Z" }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220, upload-time = "2024-02-02T16:30:24.76Z" }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756, upload-time = "2024-02-02T16:30:25.877Z" }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988, upload-time = "2024-02-02T16:30:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718, upload-time = "2024-02-02T16:30:28.111Z" }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317, upload-time = "2024-02-02T16:30:29.214Z" }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670, upload-time = "2024-02-02T16:30:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224, upload-time = "2024-02-02T16:30:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215, upload-time = "2024-02-02T16:30:33.081Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069, upload-time = "2024-02-02T16:30:34.148Z" }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452, upload-time = "2024-02-02T16:30:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462, upload-time = "2024-02-02T16:30:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869, upload-time = "2024-02-02T16:30:37.834Z" }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906, upload-time = "2024-02-02T16:30:39.366Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296, upload-time = "2024-02-02T16:30:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038, upload-time = "2024-02-02T16:30:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572, upload-time = "2024-02-02T16:30:43.326Z" }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127, upload-time = "2024-02-02T16:30:44.418Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", size = 121020, upload-time = "2024-09-05T15:28:22.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", size = 60952, upload-time = "2024-09-05T15:28:20.141Z" }, +] + +[[package]] +name = "mypy" +version = "1.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/86/5d7cbc4974fd564550b80fbb8103c05501ea11aa7835edf3351d90095896/mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", size = 3078806, upload-time = "2024-08-24T22:50:11.357Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/cd/815368cd83c3a31873e5e55b317551500b12f2d1d7549720632f32630333/mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a", size = 10939401, upload-time = "2024-08-24T22:49:18.929Z" }, + { url = "https://files.pythonhosted.org/packages/f1/27/e18c93a195d2fad75eb96e1f1cbc431842c332e8eba2e2b77eaf7313c6b7/mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef", size = 10111697, upload-time = "2024-08-24T22:49:32.504Z" }, + { url = "https://files.pythonhosted.org/packages/dc/08/cdc1fc6d0d5a67d354741344cc4aa7d53f7128902ebcbe699ddd4f15a61c/mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383", size = 12500508, upload-time = "2024-08-24T22:49:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/64/12/aad3af008c92c2d5d0720ea3b6674ba94a98cdb86888d389acdb5f218c30/mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8", size = 13020712, upload-time = "2024-08-24T22:49:49.399Z" }, + { url = "https://files.pythonhosted.org/packages/03/e6/a7d97cc124a565be5e9b7d5c2a6ebf082379ffba99646e4863ed5bbcb3c3/mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7", size = 9567319, upload-time = "2024-08-24T22:49:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/e2/aa/cc56fb53ebe14c64f1fe91d32d838d6f4db948b9494e200d2f61b820b85d/mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385", size = 10859630, upload-time = "2024-08-24T22:49:51.895Z" }, + { url = "https://files.pythonhosted.org/packages/04/c8/b19a760fab491c22c51975cf74e3d253b8c8ce2be7afaa2490fbf95a8c59/mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca", size = 10037973, upload-time = "2024-08-24T22:49:21.428Z" }, + { url = "https://files.pythonhosted.org/packages/88/57/7e7e39f2619c8f74a22efb9a4c4eff32b09d3798335625a124436d121d89/mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104", size = 12416659, upload-time = "2024-08-24T22:49:35.02Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a6/37f7544666b63a27e46c48f49caeee388bf3ce95f9c570eb5cfba5234405/mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4", size = 12897010, upload-time = "2024-08-24T22:49:29.725Z" }, + { url = "https://files.pythonhosted.org/packages/84/8b/459a513badc4d34acb31c736a0101c22d2bd0697b969796ad93294165cfb/mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6", size = 9562873, upload-time = "2024-08-24T22:49:40.448Z" }, + { url = "https://files.pythonhosted.org/packages/35/3a/ed7b12ecc3f6db2f664ccf85cb2e004d3e90bec928e9d7be6aa2f16b7cdf/mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318", size = 10990335, upload-time = "2024-08-24T22:49:54.245Z" }, + { url = "https://files.pythonhosted.org/packages/04/e4/1a9051e2ef10296d206519f1df13d2cc896aea39e8683302f89bf5792a59/mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36", size = 10007119, upload-time = "2024-08-24T22:49:03.451Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3c/350a9da895f8a7e87ade0028b962be0252d152e0c2fbaafa6f0658b4d0d4/mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987", size = 12506856, upload-time = "2024-08-24T22:50:08.804Z" }, + { url = "https://files.pythonhosted.org/packages/b6/49/ee5adf6a49ff13f4202d949544d3d08abb0ea1f3e7f2a6d5b4c10ba0360a/mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca", size = 12952066, upload-time = "2024-08-24T22:50:03.89Z" }, + { url = "https://files.pythonhosted.org/packages/27/c0/b19d709a42b24004d720db37446a42abadf844d5c46a2c442e2a074d70d9/mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70", size = 9664000, upload-time = "2024-08-24T22:49:59.703Z" }, + { url = "https://files.pythonhosted.org/packages/42/3a/bdf730640ac523229dd6578e8a581795720a9321399de494374afc437ec5/mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12", size = 2619625, upload-time = "2024-08-24T22:50:01.842Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433, upload-time = "2023-02-04T12:11:27.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload-time = "2023-02-04T12:11:25.002Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "packaging" +version = "24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788, upload-time = "2024-06-09T23:19:24.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985, upload-time = "2024-06-09T23:19:21.909Z" }, +] + +[[package]] +name = "passlib" +version = "1.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" }, +] + +[package.optional-dependencies] +bcrypt = [ + { name = "bcrypt" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439, upload-time = "2024-09-17T19:06:49.212Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "pre-commit" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/10/97ee2fa54dff1e9da9badbc5e35d0bbaef0776271ea5907eccf64140f72f/pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af", size = 177815, upload-time = "2024-07-28T19:59:01.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/92/caae8c86e94681b42c246f0bca35c059a2f0529e5b92619f6aba4cf7e7b6/pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f", size = 204643, upload-time = "2024-07-28T19:58:59.335Z" }, +] + +[[package]] +name = "premailer" +version = "3.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "cssselect" }, + { name = "cssutils" }, + { name = "lxml" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/e49bd31941eff2987076383fa6d811eb785a28f498f5bb131e981bd71e13/premailer-3.10.0.tar.gz", hash = "sha256:d1875a8411f5dc92b53ef9f193db6c0f879dc378d618e0ad292723e388bfe4c2", size = 24342, upload-time = "2021-08-02T20:32:54.328Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/07/4e8d94f94c7d41ca5ddf8a9695ad87b888104e2fd41a35546c1dc9ca74ac/premailer-3.10.0-py2.py3-none-any.whl", hash = "sha256:021b8196364d7df96d04f9ade51b794d0b77bcc19e998321c515633a2273be1a", size = 19544, upload-time = "2021-08-02T20:32:52.771Z" }, +] + +[[package]] +name = "psycopg" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/70/d1e4c251be6e0752cbc7408f0556f8f922690837309442b9019122295712/psycopg-3.2.2.tar.gz", hash = "sha256:8bad2e497ce22d556dac1464738cb948f8d6bab450d965cf1d8a8effd52412e0", size = 155483, upload-time = "2024-09-15T21:11:36.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/89/e63ec25b80290c4a923cdb5ecd5dbc85e310f93fb84b7f294006c9269d95/psycopg-3.2.2-py3-none-any.whl", hash = "sha256:babf565d459d8f72fb65da5e211dd0b58a52c51e4e1fa9cadecff42d6b7619b2", size = 197852, upload-time = "2024-09-15T20:40:20.156Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/42/f5a181d07c0ae5c8091449fda45d562d3b0861c127b94d7009eaea45c61f/psycopg_binary-3.2.2-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:8eacbf58d4f8d7bc82e0a60476afa2622b5a58f639a3cc2710e3e37b72aff3cb", size = 3381668, upload-time = "2024-09-15T20:40:33.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fb/66d2e3e5d550ba3b9d33e30bf6d5beb871a85eb95553c851fce7f09f8a1e/psycopg_binary-3.2.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:d07e62476ee8c54853b2b8cfdf3858a574218103b4cd213211f64326c7812437", size = 3502272, upload-time = "2024-09-15T20:40:44.934Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8d/758da39eca57f046ee712ad4c310840bcc08d889042d1b297cd28c78e909/psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c22e615ee0ecfc6687bb8a39a4ed9d6bac030b5e72ac15e7324fd6e48979af71", size = 4467251, upload-time = "2024-09-15T20:41:00.229Z" }, + { url = "https://files.pythonhosted.org/packages/91/bb/1abb1ccc318eb878acf9637479334de7406529516126e4af48b16dd85426/psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec29c7ec136263628e3f09a53e51d0a4b1ad765a6e45135707bfa848b39113f9", size = 4268614, upload-time = "2024-09-15T20:41:16.305Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1a/14b4ae68f1c7cfba543883987d2f134eca31b0983bb684a52e0f51f3ac21/psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:035753f80cbbf6aceca6386f53e139df70c7aca057b0592711047b5a8cfef8bb", size = 4512352, upload-time = "2024-09-15T20:42:04.018Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/53df01c7c7cffb351cafa88c58692fab0ab962edd89f22974cbfc38b6677/psycopg_binary-3.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9ee99336151ff7c30682f2ef9cb1174d235bc1471322faabba97f9db1398167", size = 4212477, upload-time = "2024-09-15T20:51:45.68Z" }, + { url = "https://files.pythonhosted.org/packages/b7/31/c918927692fc5a9c4db0a7c454e1595e9d40378d5c526d26505f310e4068/psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a60674dff4a4194e88312b463fb84ac80924c2b9e25d0e0460f3176bf1af4a6b", size = 3137907, upload-time = "2024-09-15T20:51:58.211Z" }, + { url = "https://files.pythonhosted.org/packages/cb/65/538aa057b3e8245a31ea8baac93df9947ee1b2ebf4c02014a556cddd875e/psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3c701507a49340de422d77a6ce95918a0019990bbf27daec35aa40050c6eadb6", size = 3113363, upload-time = "2024-09-15T20:52:35.883Z" }, + { url = "https://files.pythonhosted.org/packages/dc/81/eaee4f05bcba19984615e90319c429d125d07e5f0fe8c8ec3025901de4df/psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1b3c5a04eaf8866e399315cff2e810260cce10b797437a9f49fd71b5f4b94d0a", size = 3220512, upload-time = "2024-09-15T20:52:48.037Z" }, + { url = "https://files.pythonhosted.org/packages/48/cc/1d0f82a47216f925e36be6f6d7be61984a5168ff8c0496c57f468cc0e219/psycopg_binary-3.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0ad9c09de4c262f516ae6891d042a4325649b18efa39dd82bbe0f7bc95c37bfb", size = 3255023, upload-time = "2024-09-15T20:53:00.3Z" }, + { url = "https://files.pythonhosted.org/packages/d0/29/c45760ba6218eae37474aa5f46c1f55b290a6d4b86c0c59e60fa5613257a/psycopg_binary-3.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:bf1d3582185cb43ecc27403bee2f5405b7a45ccaab46c8508d9a9327341574fc", size = 2921688, upload-time = "2024-09-15T20:53:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/1f/1a/76299ad86a01f57a67961c4a45ce06c6eb8e76b8bc7bfb92548c62a6fa72/psycopg_binary-3.2.2-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:554d208757129d34fa47b7c890f9ef922f754e99c6b089cb3a209aa0fe282682", size = 3390336, upload-time = "2024-09-15T20:53:41.564Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1d/04fbcadd568eb0ee04b0d99286fe4ffd6c76c9cdd130e58d477617b77941/psycopg_binary-3.2.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:71dc3cc10d1fd7d26a3079d0a5b4a8e8ad0d7b89a702ceb7605a52e4395be122", size = 3507406, upload-time = "2024-09-15T20:53:51.312Z" }, + { url = "https://files.pythonhosted.org/packages/60/00/094a437f68d83fef4dd139630dfb0e060fcf2a7ac68fffdb63b2f3eaa43a/psycopg_binary-3.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a86f578d63f2e1fdf87c9adaed4ff23d7919bda8791cf1380fa4cf3a857ccb8b", size = 4463745, upload-time = "2024-09-15T20:54:22.632Z" }, + { url = "https://files.pythonhosted.org/packages/ea/de/0303e807a33251dec41aec709c3041b9ffd86b67d997088c504a24e90ba3/psycopg_binary-3.2.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a4eb737682c02a602a12aa85a492608066f77793dab681b1c4e885fedc160b1", size = 4263212, upload-time = "2024-09-15T20:54:41Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0d/8fa059bd936bb8e95164cc549d2eaaeaeb7df3a069bbb0ea01b48fab10a4/psycopg_binary-3.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e120a576e74e4e612c48f4b021e322e320ca102534d78a0ca4db2ffd058ae8d", size = 4513242, upload-time = "2024-09-15T20:55:14.6Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a5/9904c4ae040eef6cdb81c04e43b834302cfd3e47ee7cab8878d114abb168/psycopg_binary-3.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:849d518e7d4c6186e1e48ea2ac2671912edf7e732fffe6f01dfed61cf0245de4", size = 4207852, upload-time = "2024-09-15T20:55:58.721Z" }, + { url = "https://files.pythonhosted.org/packages/07/b7/24438b2ecb3ae8ceea44cf6e2bb92baac6be9b3d92c2940c89b3aa8e520e/psycopg_binary-3.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ee2b19152bcec8f356f989c31768702be5f139b4d51094273c4a9ddc8c55380", size = 3134053, upload-time = "2024-09-15T20:56:18.302Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/d0157858ad814cdc6cf9f9b7543c736f6b56ab9d8dc1b4ca56908ec03586/psycopg_binary-3.2.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:00273dd011892e8216fcef76b42f775ddaa6348664a7fffae2a27c9557f45bfa", size = 3110817, upload-time = "2024-09-15T20:57:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/9f/fc/8554c822a80a08cd17b9e2a4e8fc098c940e972e01bc9e3f3774b9e02d54/psycopg_binary-3.2.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4bcb489615d7e56d1de42937e6a0fc13f766505729afdb54c2947a52db295220", size = 3214760, upload-time = "2024-09-15T20:57:42.418Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4d/a12d8a301fbd4416ebdb3f019c777a17edea0452278f630f83237cbcc3d4/psycopg_binary-3.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:06963f88916a177df95aaed27101af0989ba206654743b1a0e050b9d8e734686", size = 3253951, upload-time = "2024-09-15T20:58:21.52Z" }, + { url = "https://files.pythonhosted.org/packages/09/0f/120b190ddaf6afed1eaa2fbc89e29ec810d8af44ff3599521f69f89b64b3/psycopg_binary-3.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed1ad836a0c21890c7f84e73c7ef1ed0950e0e4b0d8e49b609b6fd9c13f2ca21", size = 2924949, upload-time = "2024-09-15T20:58:50.955Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9a/68b76a795fe620c8848c758d12860b8b94998f374882dbf8ea4bc343b9e1/psycopg_binary-3.2.2-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:0dd314229885a81f9497875295d8788e651b78945627540f1e78ed71595e614a", size = 3361334, upload-time = "2024-09-15T20:59:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/0d/0c/f91242672c58bce7c290e11128569fe66ed27552388499cd80d75a5d4d0d/psycopg_binary-3.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:989acbe2f552769cdb780346cea32d86e7c117044238d5172ac10b025fe47194", size = 3504380, upload-time = "2024-09-15T20:59:52.35Z" }, + { url = "https://files.pythonhosted.org/packages/e4/45/5fa47240357dea3646f3492d20141a5869cfaedcd5c64499622db7b17a8f/psycopg_binary-3.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:566b1c530898590f0ac9d949cf94351c08d73c89f8800c74c0a63ffd89a383c8", size = 4443783, upload-time = "2024-09-15T21:00:26.976Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e5/9da098d1f7c1b064b39a2499cb4dfebe8fa5a48a132c3f544dab994199c4/psycopg_binary-3.2.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68d03efab7e2830a0df3aa4c29a708930e3f6b9fd98774ff9c4fd1f33deafecc", size = 4247070, upload-time = "2024-09-15T21:00:53.399Z" }, + { url = "https://files.pythonhosted.org/packages/ba/44/c905a0ce2c66c0250a4ddce8eef41edc728bd2055ecaf8bd23468110c3f4/psycopg_binary-3.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e1f013bfb744023df23750fde51edcb606def8328473361db3c192c392c6060", size = 4483735, upload-time = "2024-09-15T21:01:14.105Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/9f6bfcff78b643d220e088d91103fde70d193b9745d8999c7654ad45cd65/psycopg_binary-3.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a06136aab55a2de7dd4e2555badae276846827cfb023e6ba1b22f7a7b88e3f1b", size = 4186284, upload-time = "2024-09-15T21:01:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/44/48/79e7886a28818fdb4d5d39a86b5769bb33681ac23efe23accdaab42514c6/psycopg_binary-3.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:020c5154be144a1440cf87eae012b9004fb414ae4b9e7b1b9fb808fe39e96e83", size = 3110593, upload-time = "2024-09-15T21:02:11.07Z" }, + { url = "https://files.pythonhosted.org/packages/5c/93/83d5610d259feb1d4d2d37cc0e1781f0d1632c885f5e2f85808b5b196552/psycopg_binary-3.2.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef341c556aeaa43a2729b07b04e20bfffdcf3d96c4a96e728ca94fe4ce632d8c", size = 3095074, upload-time = "2024-09-15T21:02:48.695Z" }, + { url = "https://files.pythonhosted.org/packages/b6/94/3126db7a06fa9fe2ab3b1d6dd7a4add6bc1596b6864e01a77239702827b4/psycopg_binary-3.2.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66de2dd7d37bf66eb234ca9d907f5cd8caca43ff8d8a50dd5c15844d1cf0390c", size = 3184181, upload-time = "2024-09-15T21:03:02.707Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0e/6cce5ffaa25a25ede5ff08e757232bb425cacafe622627f29d286774073b/psycopg_binary-3.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2eb6f8f410dbbb71b8c633f283b8588b63bee0a7321f00ab76e9c800c593f732", size = 3229942, upload-time = "2024-09-15T21:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/10/31/951247b07205711115307f36ec3dbf6726101e086562febf6f989cbd6b95/psycopg_binary-3.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b45553c6b614d02e1486585980afdfd18f0000aac668e2e87c6e32da1adb051a", size = 2912528, upload-time = "2024-09-15T21:03:36.449Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/245f749abdfc33b42ec2bc4d89fe2cdb29cd40dca7156d0e09308c33f933/psycopg_binary-3.2.2-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:1ee891287c2da57e7fee31fbe2fbcdf57125768133d811b02e9523d5a052eb28", size = 3358682, upload-time = "2024-09-15T21:03:44.113Z" }, + { url = "https://files.pythonhosted.org/packages/93/dc/047a90e2bfd80a8414f5a203c7ff1747e3b3f43231c3c8059e8be91849cc/psycopg_binary-3.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5e95e4a8076ac7611e571623e1113fa84fd48c0459601969ffbf534d7aa236e7", size = 3500354, upload-time = "2024-09-15T21:03:54.719Z" }, + { url = "https://files.pythonhosted.org/packages/df/72/b905dec41c30a8aad21f7767b21d3e5d3b9a7e92c1844678e4083d79257b/psycopg_binary-3.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6269d79a3d7d76b6fcf0fafae8444da00e83777a6c68c43851351a571ad37155", size = 4445322, upload-time = "2024-09-15T21:04:19.048Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/aef11d4cda1af4a8181fbd578af39d6920232624fc6222f6b2f9758cc0e0/psycopg_binary-3.2.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6dd5d21a298c3c53af20ced8da4ae4cd038c6fe88c80842a8888fa3660b2094", size = 4248626, upload-time = "2024-09-15T21:04:34.589Z" }, + { url = "https://files.pythonhosted.org/packages/6c/75/39ed8598f44188e4985f31f2639aa9894851fdfbf061bf926744b08b5790/psycopg_binary-3.2.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cf64e41e238620f05aad862f06bc8424f8f320d8075f1499bd85a225d18bd57", size = 4485767, upload-time = "2024-09-15T21:04:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/ecdc4cf957d0658f77cc6fa61f6ee2e5118c914e5f93497375023389a1e5/psycopg_binary-3.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c482c3236ded54add31136a91d5223b233ec301f297fa2db79747404222dca6", size = 4188840, upload-time = "2024-09-15T21:05:11.089Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/af4c47a665d13d2477085f77fb64195da5d6463dd54fc3a8bdfd5c082d24/psycopg_binary-3.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0718be095cefdad712542169d16fa58b3bd9200a3de1b0217ae761cdec1cf569", size = 3114998, upload-time = "2024-09-15T21:05:20.018Z" }, + { url = "https://files.pythonhosted.org/packages/38/8f/6d56168d2ce7e7d802e09a4288faceb52f28bd4023cde72ede9e848c9f9b/psycopg_binary-3.2.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fb303b03c243a9041e1873b596e246f7caaf01710b312fafa65b1db5cd77dd6f", size = 3095882, upload-time = "2024-09-15T21:05:34.196Z" }, + { url = "https://files.pythonhosted.org/packages/8b/76/c77643d97292673d8a5e3eea643812d585993155658f840c86bfa855e077/psycopg_binary-3.2.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:705da5bc4364bd7529473225fca02b795653bc5bd824dbe43e1df0b1a40fe691", size = 3189435, upload-time = "2024-09-15T21:05:49.625Z" }, + { url = "https://files.pythonhosted.org/packages/30/31/b4ea793bdf44acca51e3fa6f68cc80d03725e8ef87fc2ee2b332c49fa521/psycopg_binary-3.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:05406b96139912574571b1c56bb023839a9146cf4b57c4548f36251dd5909fa1", size = 3233951, upload-time = "2024-09-15T21:06:06.577Z" }, + { url = "https://files.pythonhosted.org/packages/49/e3/633d6d05e40651acb30458e296c90e878fa4caf3b3c21bb9e6adc912b811/psycopg_binary-3.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:7c357cf87e8d7612cfe781225be7669f35038a765d1b53ec9605f6c5aef9ee85", size = 2913412, upload-time = "2024-09-15T21:06:21.959Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/3d/9b8ca77b0f76fcdbf8bc6b72474e264283f461284ca84ac3fde570c6c49a/pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e", size = 2111197, upload-time = "2025-10-14T10:19:43.303Z" }, + { url = "https://files.pythonhosted.org/packages/59/92/b7b0fe6ed4781642232755cb7e56a86e2041e1292f16d9ae410a0ccee5ac/pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b", size = 1917909, upload-time = "2025-10-14T10:19:45.194Z" }, + { url = "https://files.pythonhosted.org/packages/52/8c/3eb872009274ffa4fb6a9585114e161aa1a0915af2896e2d441642929fe4/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd", size = 1969905, upload-time = "2025-10-14T10:19:46.567Z" }, + { url = "https://files.pythonhosted.org/packages/f4/21/35adf4a753bcfaea22d925214a0c5b880792e3244731b3f3e6fec0d124f7/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945", size = 2051938, upload-time = "2025-10-14T10:19:48.237Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d0/cdf7d126825e36d6e3f1eccf257da8954452934ede275a8f390eac775e89/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706", size = 2250710, upload-time = "2025-10-14T10:19:49.619Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1c/af1e6fd5ea596327308f9c8d1654e1285cc3d8de0d584a3c9d7705bf8a7c/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba", size = 2367445, upload-time = "2025-10-14T10:19:51.269Z" }, + { url = "https://files.pythonhosted.org/packages/d3/81/8cece29a6ef1b3a92f956ea6da6250d5b2d2e7e4d513dd3b4f0c7a83dfea/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b", size = 2072875, upload-time = "2025-10-14T10:19:52.671Z" }, + { url = "https://files.pythonhosted.org/packages/e3/37/a6a579f5fc2cd4d5521284a0ab6a426cc6463a7b3897aeb95b12f1ba607b/pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d", size = 2191329, upload-time = "2025-10-14T10:19:54.214Z" }, + { url = "https://files.pythonhosted.org/packages/ae/03/505020dc5c54ec75ecba9f41119fd1e48f9e41e4629942494c4a8734ded1/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700", size = 2151658, upload-time = "2025-10-14T10:19:55.843Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5d/2c0d09fb53aa03bbd2a214d89ebfa6304be7df9ed86ee3dc7770257f41ee/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6", size = 2316777, upload-time = "2025-10-14T10:19:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/ea/4b/c2c9c8f5e1f9c864b57d08539d9d3db160e00491c9f5ee90e1bfd905e644/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9", size = 2320705, upload-time = "2025-10-14T10:19:59.016Z" }, + { url = "https://files.pythonhosted.org/packages/28/c3/a74c1c37f49c0a02c89c7340fafc0ba816b29bd495d1a31ce1bdeacc6085/pydantic_core-2.41.4-cp310-cp310-win32.whl", hash = "sha256:0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57", size = 1975464, upload-time = "2025-10-14T10:20:00.581Z" }, + { url = "https://files.pythonhosted.org/packages/d6/23/5dd5c1324ba80303368f7569e2e2e1a721c7d9eb16acb7eb7b7f85cb1be2/pydantic_core-2.41.4-cp310-cp310-win_amd64.whl", hash = "sha256:a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc", size = 2024497, upload-time = "2025-10-14T10:20:03.018Z" }, + { url = "https://files.pythonhosted.org/packages/62/4c/f6cbfa1e8efacd00b846764e8484fe173d25b8dab881e277a619177f3384/pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80", size = 2109062, upload-time = "2025-10-14T10:20:04.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/40b72d3868896bfcd410e1bd7e516e762d326201c48e5b4a06446f6cf9e8/pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae", size = 1916301, upload-time = "2025-10-14T10:20:06.857Z" }, + { url = "https://files.pythonhosted.org/packages/94/4d/d203dce8bee7faeca791671c88519969d98d3b4e8f225da5b96dad226fc8/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827", size = 1968728, upload-time = "2025-10-14T10:20:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/65/f5/6a66187775df87c24d526985b3a5d78d861580ca466fbd9d4d0e792fcf6c/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f", size = 2050238, upload-time = "2025-10-14T10:20:09.766Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b9/78336345de97298cf53236b2f271912ce11f32c1e59de25a374ce12f9cce/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def", size = 2249424, upload-time = "2025-10-14T10:20:11.732Z" }, + { url = "https://files.pythonhosted.org/packages/99/bb/a4584888b70ee594c3d374a71af5075a68654d6c780369df269118af7402/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2", size = 2366047, upload-time = "2025-10-14T10:20:13.647Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8d/17fc5de9d6418e4d2ae8c675f905cdafdc59d3bf3bf9c946b7ab796a992a/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8", size = 2071163, upload-time = "2025-10-14T10:20:15.307Z" }, + { url = "https://files.pythonhosted.org/packages/54/e7/03d2c5c0b8ed37a4617430db68ec5e7dbba66358b629cd69e11b4d564367/pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265", size = 2190585, upload-time = "2025-10-14T10:20:17.3Z" }, + { url = "https://files.pythonhosted.org/packages/be/fc/15d1c9fe5ad9266a5897d9b932b7f53d7e5cfc800573917a2c5d6eea56ec/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c", size = 2150109, upload-time = "2025-10-14T10:20:19.143Z" }, + { url = "https://files.pythonhosted.org/packages/26/ef/e735dd008808226c83ba56972566138665b71477ad580fa5a21f0851df48/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a", size = 2315078, upload-time = "2025-10-14T10:20:20.742Z" }, + { url = "https://files.pythonhosted.org/packages/90/00/806efdcf35ff2ac0f938362350cd9827b8afb116cc814b6b75cf23738c7c/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e", size = 2318737, upload-time = "2025-10-14T10:20:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/41/7e/6ac90673fe6cb36621a2283552897838c020db343fa86e513d3f563b196f/pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03", size = 1974160, upload-time = "2025-10-14T10:20:23.817Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9d/7c5e24ee585c1f8b6356e1d11d40ab807ffde44d2db3b7dfd6d20b09720e/pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e", size = 2021883, upload-time = "2025-10-14T10:20:25.48Z" }, + { url = "https://files.pythonhosted.org/packages/33/90/5c172357460fc28b2871eb4a0fb3843b136b429c6fa827e4b588877bf115/pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db", size = 1968026, upload-time = "2025-10-14T10:20:27.039Z" }, + { url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043, upload-time = "2025-10-14T10:20:28.561Z" }, + { url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699, upload-time = "2025-10-14T10:20:30.217Z" }, + { url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121, upload-time = "2025-10-14T10:20:32.246Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590, upload-time = "2025-10-14T10:20:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869, upload-time = "2025-10-14T10:20:35.965Z" }, + { url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169, upload-time = "2025-10-14T10:20:37.627Z" }, + { url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165, upload-time = "2025-10-14T10:20:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067, upload-time = "2025-10-14T10:20:41.015Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997, upload-time = "2025-10-14T10:20:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187, upload-time = "2025-10-14T10:20:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204, upload-time = "2025-10-14T10:20:46.781Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536, upload-time = "2025-10-14T10:20:48.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132, upload-time = "2025-10-14T10:20:50.421Z" }, + { url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483, upload-time = "2025-10-14T10:20:52.35Z" }, + { url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" }, + { url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" }, + { url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" }, + { url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" }, + { url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" }, + { url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" }, + { url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" }, + { url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" }, + { url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" }, + { url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" }, + { url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" }, + { url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" }, + { url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" }, + { url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" }, + { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" }, + { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" }, + { url = "https://files.pythonhosted.org/packages/b0/12/5ba58daa7f453454464f92b3ca7b9d7c657d8641c48e370c3ebc9a82dd78/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a1b2cfec3879afb742a7b0bcfa53e4f22ba96571c9e54d6a3afe1052d17d843b", size = 2122139, upload-time = "2025-10-14T10:22:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/21/fb/6860126a77725c3108baecd10fd3d75fec25191d6381b6eb2ac660228eac/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:d175600d975b7c244af6eb9c9041f10059f20b8bbffec9e33fdd5ee3f67cdc42", size = 1936674, upload-time = "2025-10-14T10:22:49.555Z" }, + { url = "https://files.pythonhosted.org/packages/de/be/57dcaa3ed595d81f8757e2b44a38240ac5d37628bce25fb20d02c7018776/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f184d657fa4947ae5ec9c47bd7e917730fa1cbb78195037e32dcbab50aca5ee", size = 1956398, upload-time = "2025-10-14T10:22:52.19Z" }, + { url = "https://files.pythonhosted.org/packages/2f/1d/679a344fadb9695f1a6a294d739fbd21d71fa023286daeea8c0ed49e7c2b/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed810568aeffed3edc78910af32af911c835cc39ebbfacd1f0ab5dd53028e5c", size = 2138674, upload-time = "2025-10-14T10:22:54.499Z" }, + { url = "https://files.pythonhosted.org/packages/c4/48/ae937e5a831b7c0dc646b2ef788c27cd003894882415300ed21927c21efa/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537", size = 2112087, upload-time = "2025-10-14T10:22:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/5e/db/6db8073e3d32dae017da7e0d16a9ecb897d0a4d92e00634916e486097961/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94", size = 1920387, upload-time = "2025-10-14T10:22:59.342Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c1/dd3542d072fcc336030d66834872f0328727e3b8de289c662faa04aa270e/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c", size = 1951495, upload-time = "2025-10-14T10:23:02.089Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c6/db8d13a1f8ab3f1eb08c88bd00fd62d44311e3456d1e85c0e59e0a0376e7/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335", size = 2139008, upload-time = "2025-10-14T10:23:04.539Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d4/912e976a2dd0b49f31c98a060ca90b353f3b73ee3ea2fd0030412f6ac5ec/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00", size = 2106739, upload-time = "2025-10-14T10:23:06.934Z" }, + { url = "https://files.pythonhosted.org/packages/71/f0/66ec5a626c81eba326072d6ee2b127f8c139543f1bf609b4842978d37833/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9", size = 1932549, upload-time = "2025-10-14T10:23:09.24Z" }, + { url = "https://files.pythonhosted.org/packages/c4/af/625626278ca801ea0a658c2dcf290dc9f21bb383098e99e7c6a029fccfc0/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2", size = 2135093, upload-time = "2025-10-14T10:23:11.626Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/2fba049f54e0f4975fef66be654c597a1d005320fa141863699180c7697d/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258", size = 2187971, upload-time = "2025-10-14T10:23:14.437Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/65ab839a2dfcd3b949202f9d920c34f9de5a537c3646662bdf2f7d999680/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347", size = 2147939, upload-time = "2025-10-14T10:23:16.831Z" }, + { url = "https://files.pythonhosted.org/packages/44/58/627565d3d182ce6dfda18b8e1c841eede3629d59c9d7cbc1e12a03aeb328/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa", size = 2311400, upload-time = "2025-10-14T10:23:19.234Z" }, + { url = "https://files.pythonhosted.org/packages/24/06/8a84711162ad5a5f19a88cead37cca81b4b1f294f46260ef7334ae4f24d3/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a", size = 2316840, upload-time = "2025-10-14T10:23:21.738Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8b/b7bb512a4682a2f7fbfae152a755d37351743900226d29bd953aaf870eaa/pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d", size = 2149135, upload-time = "2025-10-14T10:23:24.379Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7d/138e902ed6399b866f7cfe4435d22445e16fff888a1c00560d9dc79a780f/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5", size = 2104721, upload-time = "2025-10-14T10:23:26.906Z" }, + { url = "https://files.pythonhosted.org/packages/47/13/0525623cf94627f7b53b4c2034c81edc8491cbfc7c28d5447fa318791479/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2", size = 1931608, upload-time = "2025-10-14T10:23:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f9/744bc98137d6ef0a233f808bfc9b18cf94624bf30836a18d3b05d08bf418/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd", size = 2132986, upload-time = "2025-10-14T10:23:32.057Z" }, + { url = "https://files.pythonhosted.org/packages/17/c8/629e88920171173f6049386cc71f893dff03209a9ef32b4d2f7e7c264bcf/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c", size = 2187516, upload-time = "2025-10-14T10:23:34.871Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/4f2734688d98488782218ca61bcc118329bf5de05bb7fe3adc7dd79b0b86/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405", size = 2146146, upload-time = "2025-10-14T10:23:37.342Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f2/ab385dbd94a052c62224b99cf99002eee99dbec40e10006c78575aead256/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8", size = 2311296, upload-time = "2025-10-14T10:23:40.145Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8e/e4f12afe1beeb9823bba5375f8f258df0cc61b056b0195fb1cf9f62a1a58/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308", size = 2315386, upload-time = "2025-10-14T10:23:42.624Z" }, + { url = "https://files.pythonhosted.org/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f", size = 2147775, upload-time = "2025-10-14T10:23:45.406Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/c5/dbbc27b814c71676593d1c3f718e6cd7d4f00652cefa24b75f7aa3efb25e/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180", size = 188394, upload-time = "2025-09-24T14:19:11.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905, upload-time = "2024-05-04T13:42:02.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513, upload-time = "2024-05-04T13:41:57.345Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[[package]] +name = "pytest" +version = "7.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287, upload-time = "2023-12-31T12:00:13.963Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115, upload-time = "2024-01-23T06:33:00.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863, upload-time = "2024-01-23T06:32:58.246Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, +] + +[[package]] +name = "rich" +version = "13.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/76/40f084cb7db51c9d1fa29a7120717892aeda9a7711f6225692c957a93535/rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a", size = 222080, upload-time = "2024-09-10T12:52:44.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/11/dadb85e2bd6b1f1ae56669c3e1f0410797f9605d752d68fb47b77f525b31/rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06", size = 241608, upload-time = "2024-09-10T12:52:42.714Z" }, +] + +[[package]] +name = "ruff" +version = "0.6.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/7c/3045a526c57cef4b5ec4d5d154692e31429749a49810a53e785de334c4f6/ruff-0.6.7.tar.gz", hash = "sha256:44e52129d82266fa59b587e2cd74def5637b730a69c4542525dfdecfaae38bd5", size = 3073785, upload-time = "2024-09-21T17:35:55.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/c4/1c5c636f83f905c537785016e9cdd7a36df53c025a2d07940580ecb37bcf/ruff-0.6.7-py3-none-linux_armv6l.whl", hash = "sha256:08277b217534bfdcc2e1377f7f933e1c7957453e8a79764d004e44c40db923f2", size = 10336748, upload-time = "2024-09-21T17:35:12.756Z" }, + { url = "https://files.pythonhosted.org/packages/84/d9/aa15a56be7ad796f4d7625362aff588f9fc013bbb7323a63571628a2cf2d/ruff-0.6.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c6707a32e03b791f4448dc0dce24b636cbcdee4dd5607adc24e5ee73fd86c00a", size = 9958833, upload-time = "2024-09-21T17:35:15.709Z" }, + { url = "https://files.pythonhosted.org/packages/27/25/5dd1c32bfc3ad3136c8ebe84312d1bdd2e6c908ac7f60692ec009b7050a8/ruff-0.6.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:533d66b7774ef224e7cf91506a7dafcc9e8ec7c059263ec46629e54e7b1f90ab", size = 9633369, upload-time = "2024-09-21T17:35:18.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/3e/01b25484f3cb08fe6fddedf1f55f3f3c0af861a5b5f5082fbe60ab4b2596/ruff-0.6.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17a86aac6f915932d259f7bec79173e356165518859f94649d8c50b81ff087e9", size = 10637415, upload-time = "2024-09-21T17:35:21.178Z" }, + { url = "https://files.pythonhosted.org/packages/8a/c9/5bb9b849e4777e0f961de43edf95d2af0ab34999a5feee957be096887876/ruff-0.6.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3f8822defd260ae2460ea3832b24d37d203c3577f48b055590a426a722d50ef", size = 10097389, upload-time = "2024-09-21T17:35:23.232Z" }, + { url = "https://files.pythonhosted.org/packages/52/cf/e08f1c290c7d848ddfb2ae811f24f445c18e1d3e50e01c38ffa7f5a50494/ruff-0.6.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba4efe5c6dbbb58be58dd83feedb83b5e95c00091bf09987b4baf510fee5c99", size = 10951440, upload-time = "2024-09-21T17:35:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2d/ca8aa0da5841913c302d8034c6de0ce56c401c685184d8dd23cfdd0003f9/ruff-0.6.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:525201b77f94d2b54868f0cbe5edc018e64c22563da6c5c2e5c107a4e85c1c0d", size = 11708900, upload-time = "2024-09-21T17:35:27.943Z" }, + { url = "https://files.pythonhosted.org/packages/89/fc/9a83c57baee977c82392e19a328b52cebdaf61601af3d99498e278ef5104/ruff-0.6.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8854450839f339e1049fdbe15d875384242b8e85d5c6947bb2faad33c651020b", size = 11258892, upload-time = "2024-09-21T17:35:31.014Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a3/254cc7afef702c68ae9079290c2a1477ae0e81478589baf745026d8a4eb5/ruff-0.6.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f0b62056246234d59cbf2ea66e84812dc9ec4540518e37553513392c171cb18", size = 12367932, upload-time = "2024-09-21T17:35:34.456Z" }, + { url = "https://files.pythonhosted.org/packages/9f/55/53f10c1bd8c3b2ae79aed18e62b22c6346f9296aa0ec80489b8442bd06a9/ruff-0.6.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b1462fa56c832dc0cea5b4041cfc9c97813505d11cce74ebc6d1aae068de36b", size = 10838629, upload-time = "2024-09-21T17:35:37.212Z" }, + { url = "https://files.pythonhosted.org/packages/84/72/fb335c2b25432c63d15383ecbd7bfc1915e68cdf8d086a08042052144255/ruff-0.6.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:02b083770e4cdb1495ed313f5694c62808e71764ec6ee5db84eedd82fd32d8f5", size = 10648824, upload-time = "2024-09-21T17:35:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/92/a8/d57e135a8ad99b6a0c6e2a5c590bcacdd57f44340174f4409c3893368610/ruff-0.6.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c05fd37013de36dfa883a3854fae57b3113aaa8abf5dea79202675991d48624", size = 10174368, upload-time = "2024-09-21T17:35:41.21Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6f/1a30a6e81dcf2fa9ff3f7011eb87fe76c12a3c6bba74db6a1977d763de1f/ruff-0.6.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f49c9caa28d9bbfac4a637ae10327b3db00f47d038f3fbb2195c4d682e925b14", size = 10514383, upload-time = "2024-09-21T17:35:43.244Z" }, + { url = "https://files.pythonhosted.org/packages/0b/25/df6f2575bc9fe43a6dedfd8dee12896f09a94303e2c828d5f85856bb69a0/ruff-0.6.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a0e1655868164e114ba43a908fd2d64a271a23660195017c17691fb6355d59bb", size = 10902340, upload-time = "2024-09-21T17:35:45.839Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/f2c1031e2fb7b94f9bf0603744e73db4ef90081b0eb1b9639a6feefd52ea/ruff-0.6.7-py3-none-win32.whl", hash = "sha256:a939ca435b49f6966a7dd64b765c9df16f1faed0ca3b6f16acdf7731969deb35", size = 8448033, upload-time = "2024-09-21T17:35:48.558Z" }, + { url = "https://files.pythonhosted.org/packages/97/80/193d1604a3f7d75eb1b2a7ce6bf0fdbdbc136889a65caacea6ffb29501b1/ruff-0.6.7-py3-none-win_amd64.whl", hash = "sha256:590445eec5653f36248584579c06252ad2e110a5d1f32db5420de35fb0e1c977", size = 9273543, upload-time = "2024-09-21T17:35:50.551Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a8/4abb5a9f58f51e4b1ea386be5ab2e547035bc1ee57200d1eca2f8909a33e/ruff-0.6.7-py3-none-win_arm64.whl", hash = "sha256:b28f0d5e2f771c1fe3c7a45d3f53916fc74a480698c4b5731f0bea61e52137c8", size = 8618044, upload-time = "2024-09-21T17:35:53.123Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "1.45.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/28/02c0cd9184f9108e3c52519f9628b215077a3854240e0b17ae845e664855/sentry_sdk-1.45.1.tar.gz", hash = "sha256:a16c997c0f4e3df63c0fc5e4207ccb1ab37900433e0f72fef88315d317829a26", size = 244774, upload-time = "2024-07-26T13:48:32.375Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/9f/105366a122efa93f0cb1914f841747d160788e4d022d0488d2d44c2ba26c/sentry_sdk-1.45.1-py2.py3-none-any.whl", hash = "sha256:608887855ccfe39032bfd03936e3a1c4f4fc99b3a4ac49ced54a4220de61c9c1", size = 267163, upload-time = "2024-07-26T13:48:29.38Z" }, +] + +[package.optional-dependencies] +fastapi = [ + { name = "fastapi" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041, upload-time = "2021-05-05T14:18:18.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053, upload-time = "2021-05-05T14:18:17.237Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.35" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'WIN32') or (python_full_version < '3.13' and platform_machine == 'aarch64') or (python_full_version < '3.13' and platform_machine == 'amd64') or (python_full_version < '3.13' and platform_machine == 'ppc64le') or (python_full_version < '3.13' and platform_machine == 'win32') or (python_full_version < '3.13' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/48/4f190a83525f5cefefa44f6adc9e6386c4de5218d686c27eda92eb1f5424/sqlalchemy-2.0.35.tar.gz", hash = "sha256:e11d7ea4d24f0a262bccf9a7cd6284c976c5369dac21db237cff59586045ab9f", size = 9562798, upload-time = "2024-09-16T20:30:05.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/61/19395d0ae78c94f6f80c8adf39a142f3fe56cfb2235d8f2317d6dae1bf0e/SQLAlchemy-2.0.35-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67219632be22f14750f0d1c70e62f204ba69d28f62fd6432ba05ab295853de9b", size = 2090086, upload-time = "2024-09-16T21:29:05.376Z" }, + { url = "https://files.pythonhosted.org/packages/e6/82/06b5fcbe5d49043e40cf4e01e3b33c471c8d9292d478420b08538cae8928/SQLAlchemy-2.0.35-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4668bd8faf7e5b71c0319407b608f278f279668f358857dbfd10ef1954ac9f90", size = 2081278, upload-time = "2024-09-16T21:29:07.224Z" }, + { url = "https://files.pythonhosted.org/packages/68/d1/7fb7ee46949a5fb34005795b1fc06a8fef67587a66da731c14e545f7eb5b/SQLAlchemy-2.0.35-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb8bea573863762bbf45d1e13f87c2d2fd32cee2dbd50d050f83f87429c9e1ea", size = 3063763, upload-time = "2024-09-17T01:18:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ff/a1eacd78b31e52a5073e9924fb4722ecc2a72f093ca8181ed81fc61aed2e/SQLAlchemy-2.0.35-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f552023710d4b93d8fb29a91fadf97de89c5926c6bd758897875435f2a939f33", size = 3072032, upload-time = "2024-09-16T21:23:30.311Z" }, + { url = "https://files.pythonhosted.org/packages/21/ae/ddfecf149a6d16af87408bca7bd108eef7ef23d376cc8464317efb3cea3f/SQLAlchemy-2.0.35-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:016b2e665f778f13d3c438651dd4de244214b527a275e0acf1d44c05bc6026a9", size = 3028092, upload-time = "2024-09-17T01:18:16.133Z" }, + { url = "https://files.pythonhosted.org/packages/cc/51/3e84d42121662a160bacd311cfacb29c1e6a229d59dd8edb09caa8ab283b/SQLAlchemy-2.0.35-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7befc148de64b6060937231cbff8d01ccf0bfd75aa26383ffdf8d82b12ec04ff", size = 3053543, upload-time = "2024-09-16T21:23:32.274Z" }, + { url = "https://files.pythonhosted.org/packages/3e/7a/039c78105958da3fc361887f0a82c974cb6fa5bba965c1689ec778be1c01/SQLAlchemy-2.0.35-cp310-cp310-win32.whl", hash = "sha256:22b83aed390e3099584b839b93f80a0f4a95ee7f48270c97c90acd40ee646f0b", size = 2062372, upload-time = "2024-09-16T21:03:04.722Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/f31e927d32f9729f69d150ffe47e7cf51e3e0bb2148fc400b3e93a92ca4c/SQLAlchemy-2.0.35-cp310-cp310-win_amd64.whl", hash = "sha256:a29762cd3d116585278ffb2e5b8cc311fb095ea278b96feef28d0b423154858e", size = 2086485, upload-time = "2024-09-16T21:03:06.66Z" }, + { url = "https://files.pythonhosted.org/packages/c3/46/9215a35bf98c3a2528e987791e6180eb51624d2c7d5cb8e2d96a6450b657/SQLAlchemy-2.0.35-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e21f66748ab725ade40fa7af8ec8b5019c68ab00b929f6643e1b1af461eddb60", size = 2091274, upload-time = "2024-09-16T21:07:13.344Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/919673c5101a0c633658d58b11b454b251ca82300941fba801201434755d/SQLAlchemy-2.0.35-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8a6219108a15fc6d24de499d0d515c7235c617b2540d97116b663dade1a54d62", size = 2081672, upload-time = "2024-09-16T21:07:14.807Z" }, + { url = "https://files.pythonhosted.org/packages/67/ea/a6b0597cbda12796be2302153369dbbe90573fdab3bc4885f8efac499247/SQLAlchemy-2.0.35-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042622a5306c23b972192283f4e22372da3b8ddf5f7aac1cc5d9c9b222ab3ff6", size = 3200083, upload-time = "2024-09-16T22:45:15.766Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d6/97bdc8d714fb21762f2092511f380f18cdb2d985d516071fa925bb433a90/SQLAlchemy-2.0.35-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:627dee0c280eea91aed87b20a1f849e9ae2fe719d52cbf847c0e0ea34464b3f7", size = 3200080, upload-time = "2024-09-16T21:18:19.033Z" }, + { url = "https://files.pythonhosted.org/packages/87/d2/8c2adaf2ade4f6f1b725acd0b0be9210bb6a2df41024729a8eec6a86fe5a/SQLAlchemy-2.0.35-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4fdcd72a789c1c31ed242fd8c1bcd9ea186a98ee8e5408a50e610edfef980d71", size = 3137108, upload-time = "2024-09-16T22:45:19.167Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/ea05d0bfa8f2b25ae34591895147152854fc950f491c4ce362ae06035db8/SQLAlchemy-2.0.35-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89b64cd8898a3a6f642db4eb7b26d1b28a497d4022eccd7717ca066823e9fb01", size = 3157437, upload-time = "2024-09-16T21:18:21.988Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5d/8ad6df01398388a766163d27960b3365f1bbd8bb7b05b5cad321a8b69b25/SQLAlchemy-2.0.35-cp311-cp311-win32.whl", hash = "sha256:6a93c5a0dfe8d34951e8a6f499a9479ffb9258123551fa007fc708ae2ac2bc5e", size = 2061935, upload-time = "2024-09-16T20:54:10.564Z" }, + { url = "https://files.pythonhosted.org/packages/ff/68/8557efc0c32c8e2c147cb6512237448b8ed594a57cd015fda67f8e56bb3f/SQLAlchemy-2.0.35-cp311-cp311-win_amd64.whl", hash = "sha256:c68fe3fcde03920c46697585620135b4ecfdfc1ed23e75cc2c2ae9f8502c10b8", size = 2087281, upload-time = "2024-09-16T20:54:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2b/fff87e6db0da31212c98bbc445f83fb608ea92b96bda3f3f10e373bac76c/SQLAlchemy-2.0.35-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:eb60b026d8ad0c97917cb81d3662d0b39b8ff1335e3fabb24984c6acd0c900a2", size = 2089790, upload-time = "2024-09-16T21:07:16.161Z" }, + { url = "https://files.pythonhosted.org/packages/68/92/4bb761bd82764d5827bf6b6095168c40fb5dbbd23670203aef2f96ba6bc6/SQLAlchemy-2.0.35-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6921ee01caf375363be5e9ae70d08ce7ca9d7e0e8983183080211a062d299468", size = 2080266, upload-time = "2024-09-16T21:07:18.277Z" }, + { url = "https://files.pythonhosted.org/packages/22/46/068a65db6dc253c6f25a7598d99e0a1d60b14f661f9d09ef6c73c718fa4e/SQLAlchemy-2.0.35-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cdf1a0dbe5ced887a9b127da4ffd7354e9c1a3b9bb330dce84df6b70ccb3a8d", size = 3229760, upload-time = "2024-09-16T22:45:20.863Z" }, + { url = "https://files.pythonhosted.org/packages/6e/36/59830dafe40dda592304debd4cd86e583f63472f3a62c9e2695a5795e786/SQLAlchemy-2.0.35-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93a71c8601e823236ac0e5d087e4f397874a421017b3318fd92c0b14acf2b6db", size = 3240649, upload-time = "2024-09-16T21:18:23.996Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/844c50c6996f9c7f000c959dd1a7436a6c94e449ee113046a1d19e470089/SQLAlchemy-2.0.35-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e04b622bb8a88f10e439084486f2f6349bf4d50605ac3e445869c7ea5cf0fa8c", size = 3176138, upload-time = "2024-09-16T22:45:22.518Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/336b18cac68eecb67de474fc15c85f13be4e615c6f5bae87ea38c6734ce0/SQLAlchemy-2.0.35-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1b56961e2d31389aaadf4906d453859f35302b4eb818d34a26fab72596076bb8", size = 3202753, upload-time = "2024-09-16T21:18:25.966Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f3/ee1e62fabdc10910b5ef720ae08e59bc785f26652876af3a50b89b97b412/SQLAlchemy-2.0.35-cp312-cp312-win32.whl", hash = "sha256:0f9f3f9a3763b9c4deb8c5d09c4cc52ffe49f9876af41cc1b2ad0138878453cf", size = 2060113, upload-time = "2024-09-16T20:54:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/60/63/a3cef44a52979169d884f3583d0640e64b3c28122c096474a1d7cfcaf1f3/SQLAlchemy-2.0.35-cp312-cp312-win_amd64.whl", hash = "sha256:25b0f63e7fcc2a6290cb5f7f5b4fc4047843504983a28856ce9b35d8f7de03cc", size = 2085839, upload-time = "2024-09-16T20:54:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c6/33c706449cdd92b1b6d756b247761e27d32230fd6b2de5f44c4c3e5632b2/SQLAlchemy-2.0.35-py3-none-any.whl", hash = "sha256:2ab3f0336c0387662ce6221ad30ab3a5e6499aab01b9790879b6578fd9b8faa1", size = 1881276, upload-time = "2024-09-16T23:14:28.324Z" }, +] + +[[package]] +name = "sqlmodel" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/4b/c2ad0496f5bdc6073d9b4cef52be9c04f2b37a5773441cc6600b1857648b/sqlmodel-0.0.24.tar.gz", hash = "sha256:cc5c7613c1a5533c9c7867e1aab2fd489a76c9e8a061984da11b4e613c182423", size = 116780, upload-time = "2025-03-07T05:43:32.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/91/484cd2d05569892b7fef7f5ceab3bc89fb0f8a8c0cde1030d383dbc5449c/sqlmodel-0.0.24-py3-none-any.whl", hash = "sha256:6778852f09370908985b667d6a3ab92910d0d5ec88adcaf23dbc242715ff7193", size = 28622, upload-time = "2025-03-07T05:43:30.37Z" }, +] + +[[package]] +name = "starlette" +version = "0.38.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/b4/e25c3b688ef703d85e55017c6edd0cbf38e5770ab748234363d54ff0251a/starlette-0.38.6.tar.gz", hash = "sha256:863a1588f5574e70a821dadefb41e4881ea451a47a3cd1b4df359d4ffefe5ead", size = 2569491, upload-time = "2024-09-22T17:01:45.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/9c/93f7bc03ff03199074e81974cc148908ead60dcf189f68ba1761a0ee35cf/starlette-0.38.6-py3-none-any.whl", hash = "sha256:4517a1409e2e73ee4951214ba012052b9e16f60e90d73cfb06192c19203bbb05", size = 71451, upload-time = "2024-09-22T17:01:43.076Z" }, +] + +[[package]] +name = "tenacity" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/4d/6a19536c50b849338fcbe9290d562b52cbdcf30d8963d3588a68a4107df1/tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78", size = 47309, upload-time = "2024-07-05T07:25:31.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687", size = 28165, upload-time = "2024-07-05T07:25:29.591Z" }, +] + +[[package]] +name = "tomli" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164, upload-time = "2022-02-08T10:54:04.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757, upload-time = "2022-02-08T10:54:02.017Z" }, +] + +[[package]] +name = "typer" +version = "0.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/58/a79003b91ac2c6890fc5d90145c662fd5771c6f11447f116b63300436bc9/typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722", size = 98953, upload-time = "2024-08-24T21:17:57.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/2b/886d13e742e514f704c33c4caa7df0f3b89e5a25ef8db02aa9ca3d9535d5/typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b", size = 47288, upload-time = "2024-08-24T21:17:55.451Z" }, +] + +[[package]] +name = "types-passlib" +version = "1.7.7.20240819" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/19/5041c4bce2909c67fc3f9471ad67972d94c31cb591a970a8faf1220a3717/types-passlib-1.7.7.20240819.tar.gz", hash = "sha256:8fc8df71623845032293d5cf7f8091f0adfeba02d387a2888684b8413f14b3d0", size = 18386, upload-time = "2024-08-19T02:32:53.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/4b/606ac25e89908e4577cd1aa19ffbebe55a6720cff69303db68701f3cc388/types_passlib-1.7.7.20240819-py3-none-any.whl", hash = "sha256:c4d299083497b66e12258c7b77c08952574213fdf7009da3135d8181a6a25f23", size = 33240, upload-time = "2024-08-19T02:32:51.874Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2024.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/5b/e025d02cb3b66b7b76093404392d4b44343c69101cc85f4d180dd5784717/tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", size = 190559, upload-time = "2024-02-11T23:22:40.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/58/f9c9e6be752e9fcb8b6a0ee9fb87e6e7a1f6bcab2cdc73f02bb7ba91ada0/tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252", size = 345370, upload-time = "2024-02-11T23:22:38.223Z" }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677, upload-time = "2024-09-12T10:52:18.401Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338, upload-time = "2024-09-12T10:52:16.589Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.30.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/01/5e637e7aa9dd031be5376b9fb749ec20b86f5a5b6a49b87fabd374d5fa9f/uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788", size = 42825, upload-time = "2024-08-13T09:27:35.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/8e/cdc7d6263db313030e4c257dd5ba3909ebc4e4fb53ad62d5f09b1a2f5458/uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5", size = 62835, upload-time = "2024-08-13T09:27:33.536Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/f1/dc9577455e011ad43d9379e836ee73f40b4f99c02946849a44f7ae64835e/uvloop-0.20.0.tar.gz", hash = "sha256:4603ca714a754fc8d9b197e325db25b2ea045385e8a3ad05d3463de725fdf469", size = 2329938, upload-time = "2024-08-15T19:36:29.28Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/69/cc1ad125ea8ce4a4d3ba7d9836062c3fc9063cf163ddf0f168e73f3268e3/uvloop-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9ebafa0b96c62881d5cafa02d9da2e44c23f9f0cd829f3a32a6aff771449c996", size = 1363922, upload-time = "2024-08-15T19:35:38.135Z" }, + { url = "https://files.pythonhosted.org/packages/f7/45/5a3f7a32372e4a90dfd83f30507183ec38990b8c5930ed7e36c6a15af47b/uvloop-0.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:35968fc697b0527a06e134999eef859b4034b37aebca537daeb598b9d45a137b", size = 760386, upload-time = "2024-08-15T19:35:39.68Z" }, + { url = "https://files.pythonhosted.org/packages/9e/a5/9e973b25ade12c938940751bce71d0cb36efee3489014471f7d9c0a3c379/uvloop-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b16696f10e59d7580979b420eedf6650010a4a9c3bd8113f24a103dfdb770b10", size = 3432586, upload-time = "2024-08-15T19:35:41.513Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e0/0bec8a25b2e9cf14fdfcf0229637b437c923b4e5ca22f8e988363c49bb51/uvloop-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b04d96188d365151d1af41fa2d23257b674e7ead68cfd61c725a422764062ae", size = 3431802, upload-time = "2024-08-15T19:35:43.263Z" }, + { url = "https://files.pythonhosted.org/packages/95/3b/14cef46dcec6237d858666a4a1fdb171361528c70fcd930bfc312920e7a9/uvloop-0.20.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94707205efbe809dfa3a0d09c08bef1352f5d3d6612a506f10a319933757c006", size = 4144444, upload-time = "2024-08-15T19:35:45.083Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5a/0ac516562ff783f760cab3b061f10fdeb4a9f985ad4b44e7e4564ff11691/uvloop-0.20.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89e8d33bb88d7263f74dc57d69f0063e06b5a5ce50bb9a6b32f5fcbe655f9e73", size = 4147039, upload-time = "2024-08-15T19:35:46.821Z" }, + { url = "https://files.pythonhosted.org/packages/64/bf/45828beccf685b7ed9638d9b77ef382b470c6ca3b5bff78067e02ffd5663/uvloop-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e50289c101495e0d1bb0bfcb4a60adde56e32f4449a67216a1ab2750aa84f037", size = 1320593, upload-time = "2024-08-15T19:35:48.431Z" }, + { url = "https://files.pythonhosted.org/packages/27/c0/3c24e50bee7802a2add96ca9f0d5eb0ebab07e0a5615539d38aeb89499b9/uvloop-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e237f9c1e8a00e7d9ddaa288e535dc337a39bcbf679f290aee9d26df9e72bce9", size = 736676, upload-time = "2024-08-15T19:35:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/ffa3c72954eae36825acfafd2b6a9221d79abd2670c0d25e04d6ef4a2007/uvloop-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:746242cd703dc2b37f9d8b9f173749c15e9a918ddb021575a0205ec29a38d31e", size = 3494573, upload-time = "2024-08-15T19:35:52.011Z" }, + { url = "https://files.pythonhosted.org/packages/46/6d/4caab3a36199ba52b98d519feccfcf48921d7a6649daf14a93c7e77497e9/uvloop-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82edbfd3df39fb3d108fc079ebc461330f7c2e33dbd002d146bf7c445ba6e756", size = 3489932, upload-time = "2024-08-15T19:35:53.599Z" }, + { url = "https://files.pythonhosted.org/packages/e4/4f/49c51595bd794945c88613df88922c38076eae2d7653f4624aa6f4980b07/uvloop-0.20.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:80dc1b139516be2077b3e57ce1cb65bfed09149e1d175e0478e7a987863b68f0", size = 4185596, upload-time = "2024-08-15T19:35:55.416Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/7e256731260d313f5049717d1c4582d52a3b132424c95e16954a50ab95d3/uvloop-0.20.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f44af67bf39af25db4c1ac27e82e9665717f9c26af2369c404be865c8818dcf", size = 4185746, upload-time = "2024-08-15T19:35:56.96Z" }, + { url = "https://files.pythonhosted.org/packages/2d/64/31cbd379d6e260ac8de3f672f904e924f09715c3f192b09f26cc8e9f574c/uvloop-0.20.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4b75f2950ddb6feed85336412b9a0c310a2edbcf4cf931aa5cfe29034829676d", size = 1324302, upload-time = "2024-08-15T19:35:58.384Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6b/9207e7177ff30f78299401f2e1163ea41130d4fd29bcdc6d12572c06b728/uvloop-0.20.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:77fbc69c287596880ecec2d4c7a62346bef08b6209749bf6ce8c22bbaca0239e", size = 738105, upload-time = "2024-08-15T19:36:00.106Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/b64b10f577519d875992dc07e2365899a1a4c0d28327059ce1e1bdfb6854/uvloop-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6462c95f48e2d8d4c993a2950cd3d31ab061864d1c226bbf0ee2f1a8f36674b9", size = 4090658, upload-time = "2024-08-15T19:36:01.423Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f8/5ceea6876154d926604f10c1dd896adf9bce6d55a55911364337b8a5ed8d/uvloop-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:649c33034979273fa71aa25d0fe120ad1777c551d8c4cd2c0c9851d88fcb13ab", size = 4173357, upload-time = "2024-08-15T19:36:03.367Z" }, + { url = "https://files.pythonhosted.org/packages/18/b2/117ab6bfb18274753fbc319607bf06e216bd7eea8be81d5bac22c912d6a7/uvloop-0.20.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a609780e942d43a275a617c0839d85f95c334bad29c4c0918252085113285b5", size = 4029868, upload-time = "2024-08-15T19:36:05.035Z" }, + { url = "https://files.pythonhosted.org/packages/6f/52/deb4be09060637ef4752adaa0b75bf770c20c823e8108705792f99cd4a6f/uvloop-0.20.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aea15c78e0d9ad6555ed201344ae36db5c63d428818b4b2a42842b3870127c00", size = 4115980, upload-time = "2024-08-15T19:36:07.376Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.26.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/4c/66ce54c8736ff164e85117ca36b02a1e14c042a6963f85eeda82664fda4e/virtualenv-20.26.5.tar.gz", hash = "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4", size = 9371932, upload-time = "2024-09-17T21:48:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/1d/e1a44fdd6d30829ba21fc58b5d98a67e7aae8f4165f11d091e53aec12560/virtualenv-20.26.5-py3-none-any.whl", hash = "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6", size = 5999288, upload-time = "2024-09-17T21:48:51.283Z" }, +] + +[[package]] +name = "watchfiles" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/27/2ba23c8cc85796e2d41976439b08d52f691655fdb9401362099502d1f0cf/watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1", size = 37870, upload-time = "2024-08-28T16:21:37.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/a1/631c12626378b9f1538664aa221feb5c60dfafbd7f60b451f8d0bdbcdedd/watchfiles-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0", size = 375096, upload-time = "2024-08-28T16:19:47.704Z" }, + { url = "https://files.pythonhosted.org/packages/f7/5c/f27c979c8a10aaa2822286c1bffdce3db731cd1aa4224b9f86623e94bbfe/watchfiles-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c", size = 367425, upload-time = "2024-08-28T16:19:49.66Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/1889e5649885484d29f6c792ef274454d0a26b20d6ed5fdba5409335ccb6/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361", size = 437705, upload-time = "2024-08-28T16:19:51.068Z" }, + { url = "https://files.pythonhosted.org/packages/85/8a/01d9a22e839f0d1d547af11b1fcac6ba6f889513f1b2e6f221d9d60d9585/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3", size = 433636, upload-time = "2024-08-28T16:19:52.799Z" }, + { url = "https://files.pythonhosted.org/packages/62/32/a93db78d340c7ef86cde469deb20e36c6b2a873edee81f610e94bbba4e06/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571", size = 451069, upload-time = "2024-08-28T16:19:54.111Z" }, + { url = "https://files.pythonhosted.org/packages/99/c2/e9e2754fae3c2721c9a7736f92dab73723f1968ed72535fff29e70776008/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd", size = 469306, upload-time = "2024-08-28T16:19:55.616Z" }, + { url = "https://files.pythonhosted.org/packages/4c/45/f317d9e3affb06c3c27c478de99f7110143e87f0f001f0f72e18d0e1ddce/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a", size = 476187, upload-time = "2024-08-28T16:19:56.915Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d3/f1f37248abe0114916921e638f71c7d21fe77e3f2f61750e8057d0b68ef2/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e", size = 425743, upload-time = "2024-08-28T16:19:57.957Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e8/c7037ea38d838fd81a59cd25761f106ee3ef2cfd3261787bee0c68908171/watchfiles-0.24.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c", size = 612327, upload-time = "2024-08-28T16:19:59.4Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/0e6e228aafe01a7995fbfd2a4edb221bb11a2744803b65a5663fb85e5063/watchfiles-0.24.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188", size = 595096, upload-time = "2024-08-28T16:20:01.003Z" }, + { url = "https://files.pythonhosted.org/packages/63/d5/4780e8bf3de3b4b46e7428a29654f7dc041cad6b19fd86d083e4b6f64bbe/watchfiles-0.24.0-cp310-none-win32.whl", hash = "sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735", size = 264149, upload-time = "2024-08-28T16:20:02.833Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/5148898ba55fc9c111a2a4a5fb67ad3fa7eb2b3d7f0618241ed88749313d/watchfiles-0.24.0-cp310-none-win_amd64.whl", hash = "sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04", size = 277542, upload-time = "2024-08-28T16:20:03.876Z" }, + { url = "https://files.pythonhosted.org/packages/85/02/366ae902cd81ca5befcd1854b5c7477b378f68861597cef854bd6dc69fbe/watchfiles-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428", size = 375579, upload-time = "2024-08-28T16:20:04.865Z" }, + { url = "https://files.pythonhosted.org/packages/bc/67/d8c9d256791fe312fea118a8a051411337c948101a24586e2df237507976/watchfiles-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c", size = 367726, upload-time = "2024-08-28T16:20:06.111Z" }, + { url = "https://files.pythonhosted.org/packages/b1/dc/a8427b21ef46386adf824a9fec4be9d16a475b850616cfd98cf09a97a2ef/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43", size = 437735, upload-time = "2024-08-28T16:20:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/3a/21/0b20bef581a9fbfef290a822c8be645432ceb05fb0741bf3c032e0d90d9a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327", size = 433644, upload-time = "2024-08-28T16:20:09.15Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e8/d5e5f71cc443c85a72e70b24269a30e529227986096abe091040d6358ea9/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5", size = 450928, upload-time = "2024-08-28T16:20:11.152Z" }, + { url = "https://files.pythonhosted.org/packages/61/ee/bf17f5a370c2fcff49e1fec987a6a43fd798d8427ea754ce45b38f9e117a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61", size = 469072, upload-time = "2024-08-28T16:20:12.345Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/03b66d425986de3fc6077e74a74c78da298f8cb598887f664a4485e55543/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15", size = 475517, upload-time = "2024-08-28T16:20:13.555Z" }, + { url = "https://files.pythonhosted.org/packages/70/eb/82f089c4f44b3171ad87a1b433abb4696f18eb67292909630d886e073abe/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823", size = 425480, upload-time = "2024-08-28T16:20:15.037Z" }, + { url = "https://files.pythonhosted.org/packages/53/20/20509c8f5291e14e8a13104b1808cd7cf5c44acd5feaecb427a49d387774/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab", size = 612322, upload-time = "2024-08-28T16:20:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/df/2b/5f65014a8cecc0a120f5587722068a975a692cadbe9fe4ea56b3d8e43f14/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec", size = 595094, upload-time = "2024-08-28T16:20:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/98/006d8043a82c0a09d282d669c88e587b3a05cabdd7f4900e402250a249ac/watchfiles-0.24.0-cp311-none-win32.whl", hash = "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d", size = 264191, upload-time = "2024-08-28T16:20:18.472Z" }, + { url = "https://files.pythonhosted.org/packages/8a/8b/badd9247d6ec25f5f634a9b3d0d92e39c045824ec7e8afcedca8ee52c1e2/watchfiles-0.24.0-cp311-none-win_amd64.whl", hash = "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c", size = 277527, upload-time = "2024-08-28T16:20:20.096Z" }, + { url = "https://files.pythonhosted.org/packages/af/19/35c957c84ee69d904299a38bae3614f7cede45f07f174f6d5a2f4dbd6033/watchfiles-0.24.0-cp311-none-win_arm64.whl", hash = "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633", size = 266253, upload-time = "2024-08-28T16:20:21.381Z" }, + { url = "https://files.pythonhosted.org/packages/35/82/92a7bb6dc82d183e304a5f84ae5437b59ee72d48cee805a9adda2488b237/watchfiles-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a", size = 374137, upload-time = "2024-08-28T16:20:23.055Z" }, + { url = "https://files.pythonhosted.org/packages/87/91/49e9a497ddaf4da5e3802d51ed67ff33024597c28f652b8ab1e7c0f5718b/watchfiles-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370", size = 367733, upload-time = "2024-08-28T16:20:24.543Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d8/90eb950ab4998effea2df4cf3a705dc594f6bc501c5a353073aa990be965/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6", size = 437322, upload-time = "2024-08-28T16:20:25.572Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a2/300b22e7bc2a222dd91fce121cefa7b49aa0d26a627b2777e7bdfcf1110b/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b", size = 433409, upload-time = "2024-08-28T16:20:26.628Z" }, + { url = "https://files.pythonhosted.org/packages/99/44/27d7708a43538ed6c26708bcccdde757da8b7efb93f4871d4cc39cffa1cc/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e", size = 452142, upload-time = "2024-08-28T16:20:28.003Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ec/c4e04f755be003129a2c5f3520d2c47026f00da5ecb9ef1e4f9449637571/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea", size = 469414, upload-time = "2024-08-28T16:20:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4e/cdd7de3e7ac6432b0abf282ec4c1a1a2ec62dfe423cf269b86861667752d/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f", size = 472962, upload-time = "2024-08-28T16:20:31.314Z" }, + { url = "https://files.pythonhosted.org/packages/27/69/e1da9d34da7fc59db358424f5d89a56aaafe09f6961b64e36457a80a7194/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234", size = 425705, upload-time = "2024-08-28T16:20:32.427Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c1/24d0f7357be89be4a43e0a656259676ea3d7a074901f47022f32e2957798/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef", size = 612851, upload-time = "2024-08-28T16:20:33.527Z" }, + { url = "https://files.pythonhosted.org/packages/c7/af/175ba9b268dec56f821639c9893b506c69fd999fe6a2e2c51de420eb2f01/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968", size = 594868, upload-time = "2024-08-28T16:20:34.639Z" }, + { url = "https://files.pythonhosted.org/packages/44/81/1f701323a9f70805bc81c74c990137123344a80ea23ab9504a99492907f8/watchfiles-0.24.0-cp312-none-win32.whl", hash = "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444", size = 264109, upload-time = "2024-08-28T16:20:35.692Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0b/32cde5bc2ebd9f351be326837c61bdeb05ad652b793f25c91cac0b48a60b/watchfiles-0.24.0-cp312-none-win_amd64.whl", hash = "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896", size = 277055, upload-time = "2024-08-28T16:20:36.849Z" }, + { url = "https://files.pythonhosted.org/packages/4b/81/daade76ce33d21dbec7a15afd7479de8db786e5f7b7d249263b4ea174e08/watchfiles-0.24.0-cp312-none-win_arm64.whl", hash = "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418", size = 266169, upload-time = "2024-08-28T16:20:38.149Z" }, + { url = "https://files.pythonhosted.org/packages/30/dc/6e9f5447ae14f645532468a84323a942996d74d5e817837a5c8ce9d16c69/watchfiles-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48", size = 373764, upload-time = "2024-08-28T16:20:39.263Z" }, + { url = "https://files.pythonhosted.org/packages/79/c0/c3a9929c372816c7fc87d8149bd722608ea58dc0986d3ef7564c79ad7112/watchfiles-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90", size = 367873, upload-time = "2024-08-28T16:20:40.399Z" }, + { url = "https://files.pythonhosted.org/packages/2e/11/ff9a4445a7cfc1c98caf99042df38964af12eed47d496dd5d0d90417349f/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94", size = 438381, upload-time = "2024-08-28T16:20:41.371Z" }, + { url = "https://files.pythonhosted.org/packages/48/a3/763ba18c98211d7bb6c0f417b2d7946d346cdc359d585cc28a17b48e964b/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e", size = 432809, upload-time = "2024-08-28T16:20:42.504Z" }, + { url = "https://files.pythonhosted.org/packages/30/4c/616c111b9d40eea2547489abaf4ffc84511e86888a166d3a4522c2ba44b5/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827", size = 451801, upload-time = "2024-08-28T16:20:43.696Z" }, + { url = "https://files.pythonhosted.org/packages/b6/be/d7da83307863a422abbfeb12903a76e43200c90ebe5d6afd6a59d158edea/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df", size = 468886, upload-time = "2024-08-28T16:20:44.847Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/3dfe131ee59d5e90b932cf56aba5c996309d94dafe3d02d204364c23461c/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab", size = 472973, upload-time = "2024-08-28T16:20:45.991Z" }, + { url = "https://files.pythonhosted.org/packages/42/6c/279288cc5653a289290d183b60a6d80e05f439d5bfdfaf2d113738d0f932/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f", size = 425282, upload-time = "2024-08-28T16:20:47.579Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d7/58afe5e85217e845edf26d8780c2d2d2ae77675eeb8d1b8b8121d799ce52/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b", size = 612540, upload-time = "2024-08-28T16:20:48.915Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/b96eeb9fe3fda137200dd2f31553670cbc731b1e13164fd69b49870b76ec/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18", size = 593625, upload-time = "2024-08-28T16:20:50.543Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e5/c326fe52ee0054107267608d8cea275e80be4455b6079491dfd9da29f46f/watchfiles-0.24.0-cp313-none-win32.whl", hash = "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07", size = 263899, upload-time = "2024-08-28T16:20:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8b/8a7755c5e7221bb35fe4af2dc44db9174f90ebf0344fd5e9b1e8b42d381e/watchfiles-0.24.0-cp313-none-win_amd64.whl", hash = "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366", size = 276622, upload-time = "2024-08-28T16:20:52.82Z" }, + { url = "https://files.pythonhosted.org/packages/df/94/1ad200e937ec91b2a9d6b39ae1cf9c2b1a9cc88d5ceb43aa5c6962eb3c11/watchfiles-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f", size = 376986, upload-time = "2024-08-28T16:21:26.895Z" }, + { url = "https://files.pythonhosted.org/packages/ee/fd/d9e020d687ccf90fe95efc513fbb39a8049cf5a3ff51f53c59fcf4c47a5d/watchfiles-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b", size = 369445, upload-time = "2024-08-28T16:21:28.157Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/c0279b35053555d10ef03559c5aebfcb0c703d9c70a7b4e532df74b9b0e8/watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4", size = 439383, upload-time = "2024-08-28T16:21:29.515Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c4/08b3c2cda45db5169148a981c2100c744a4a222fa7ae7644937c0c002069/watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a", size = 426804, upload-time = "2024-08-28T16:21:30.687Z" }, +] + +[[package]] +name = "websockets" +version = "13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/73/9223dbc7be3dcaf2a7bbf756c351ec8da04b1fa573edaf545b95f6b0c7fd/websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878", size = 158549, upload-time = "2024-09-21T17:34:21.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/94/d15dbfc6a5eb636dbc754303fba18208f2e88cf97e733e1d64fb9cb5c89e/websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee", size = 157815, upload-time = "2024-09-21T17:32:27.107Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/c04af33f4663945a26f5e8cf561eb140c35452b50af47a83c3fbcfe62ae1/websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7", size = 155466, upload-time = "2024-09-21T17:32:28.428Z" }, + { url = "https://files.pythonhosted.org/packages/35/e8/719f08d12303ea643655e52d9e9851b2dadbb1991d4926d9ce8862efa2f5/websockets-13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6", size = 155716, upload-time = "2024-09-21T17:32:29.905Z" }, + { url = "https://files.pythonhosted.org/packages/91/e1/14963ae0252a8925f7434065d25dcd4701d5e281a0b4b460a3b5963d2594/websockets-13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b", size = 164806, upload-time = "2024-09-21T17:32:31.384Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fa/ab28441bae5e682a0f7ddf3d03440c0c352f930da419301f4a717f675ef3/websockets-13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa", size = 163810, upload-time = "2024-09-21T17:32:32.384Z" }, + { url = "https://files.pythonhosted.org/packages/44/77/dea187bd9d16d4b91566a2832be31f99a40d0f5bfa55eeb638eb2c3bc33d/websockets-13.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700", size = 164125, upload-time = "2024-09-21T17:32:33.398Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d9/3af14544e83f1437eb684b399e6ba0fa769438e869bf5d83d74bc197fae8/websockets-13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c", size = 164532, upload-time = "2024-09-21T17:32:35.109Z" }, + { url = "https://files.pythonhosted.org/packages/1c/8a/6d332eabe7d59dfefe4b8ba6f46c8c5fabb15b71c8a8bc3d2b65de19a7b6/websockets-13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0", size = 163948, upload-time = "2024-09-21T17:32:36.214Z" }, + { url = "https://files.pythonhosted.org/packages/1a/91/a0aeadbaf3017467a1ee03f8fb67accdae233fe2d5ad4b038c0a84e357b0/websockets-13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f", size = 163898, upload-time = "2024-09-21T17:32:37.277Z" }, + { url = "https://files.pythonhosted.org/packages/71/31/a90fb47c63e0ae605be914b0b969d7c6e6ffe2038cd744798e4b3fbce53b/websockets-13.1-cp310-cp310-win32.whl", hash = "sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe", size = 158706, upload-time = "2024-09-21T17:32:38.755Z" }, + { url = "https://files.pythonhosted.org/packages/93/ca/9540a9ba80da04dc7f36d790c30cae4252589dbd52ccdc92e75b0be22437/websockets-13.1-cp310-cp310-win_amd64.whl", hash = "sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a", size = 159141, upload-time = "2024-09-21T17:32:40.495Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f0/cf0b8a30d86b49e267ac84addbebbc7a48a6e7bb7c19db80f62411452311/websockets-13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19", size = 157813, upload-time = "2024-09-21T17:32:42.188Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e7/22285852502e33071a8cf0ac814f8988480ec6db4754e067b8b9d0e92498/websockets-13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5", size = 155469, upload-time = "2024-09-21T17:32:43.858Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/c8c7c1e5b40ee03c5cc235955b0fb1ec90e7e37685a5f69229ad4708dcde/websockets-13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd", size = 155717, upload-time = "2024-09-21T17:32:44.914Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/c50999b9b848b1332b07c7fd8886179ac395cb766fda62725d1539e7bc6c/websockets-13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02", size = 165379, upload-time = "2024-09-21T17:32:45.933Z" }, + { url = "https://files.pythonhosted.org/packages/bc/49/4a4ad8c072f18fd79ab127650e47b160571aacfc30b110ee305ba25fffc9/websockets-13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7", size = 164376, upload-time = "2024-09-21T17:32:46.987Z" }, + { url = "https://files.pythonhosted.org/packages/af/9b/8c06d425a1d5a74fd764dd793edd02be18cf6fc3b1ccd1f29244ba132dc0/websockets-13.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096", size = 164753, upload-time = "2024-09-21T17:32:48.046Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5b/0acb5815095ff800b579ffc38b13ab1b915b317915023748812d24e0c1ac/websockets-13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084", size = 165051, upload-time = "2024-09-21T17:32:49.271Z" }, + { url = "https://files.pythonhosted.org/packages/30/93/c3891c20114eacb1af09dedfcc620c65c397f4fd80a7009cd12d9457f7f5/websockets-13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3", size = 164489, upload-time = "2024-09-21T17:32:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/28/09/af9e19885539759efa2e2cd29b8b3f9eecef7ecefea40d46612f12138b36/websockets-13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9", size = 164438, upload-time = "2024-09-21T17:32:52.223Z" }, + { url = "https://files.pythonhosted.org/packages/b6/08/6f38b8e625b3d93de731f1d248cc1493327f16cb45b9645b3e791782cff0/websockets-13.1-cp311-cp311-win32.whl", hash = "sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f", size = 158710, upload-time = "2024-09-21T17:32:53.244Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/ec8832ecb9bb04a8d318149005ed8cee0ba4e0205835da99e0aa497a091f/websockets-13.1-cp311-cp311-win_amd64.whl", hash = "sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557", size = 159137, upload-time = "2024-09-21T17:32:54.721Z" }, + { url = "https://files.pythonhosted.org/packages/df/46/c426282f543b3c0296cf964aa5a7bb17e984f58dde23460c3d39b3148fcf/websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc", size = 157821, upload-time = "2024-09-21T17:32:56.442Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/22529867010baac258da7c45848f9415e6cf37fef00a43856627806ffd04/websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49", size = 155480, upload-time = "2024-09-21T17:32:57.698Z" }, + { url = "https://files.pythonhosted.org/packages/29/2c/bdb339bfbde0119a6e84af43ebf6275278698a2241c2719afc0d8b0bdbf2/websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd", size = 155715, upload-time = "2024-09-21T17:32:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/8612029ea04c5c22bf7af2fd3d63876c4eaeef9b97e86c11972a43aa0e6c/websockets-13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0", size = 165647, upload-time = "2024-09-21T17:33:00.495Z" }, + { url = "https://files.pythonhosted.org/packages/56/04/1681ed516fa19ca9083f26d3f3a302257e0911ba75009533ed60fbb7b8d1/websockets-13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6", size = 164592, upload-time = "2024-09-21T17:33:02.223Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/a96417a49c0ed132bb6087e8e39a37db851c70974f5c724a4b2a70066996/websockets-13.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9", size = 165012, upload-time = "2024-09-21T17:33:03.288Z" }, + { url = "https://files.pythonhosted.org/packages/40/8b/fccf294919a1b37d190e86042e1a907b8f66cff2b61e9befdbce03783e25/websockets-13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68", size = 165311, upload-time = "2024-09-21T17:33:04.728Z" }, + { url = "https://files.pythonhosted.org/packages/c1/61/f8615cf7ce5fe538476ab6b4defff52beb7262ff8a73d5ef386322d9761d/websockets-13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14", size = 164692, upload-time = "2024-09-21T17:33:05.829Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f1/a29dd6046d3a722d26f182b783a7997d25298873a14028c4760347974ea3/websockets-13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf", size = 164686, upload-time = "2024-09-21T17:33:06.823Z" }, + { url = "https://files.pythonhosted.org/packages/0f/99/ab1cdb282f7e595391226f03f9b498f52109d25a2ba03832e21614967dfa/websockets-13.1-cp312-cp312-win32.whl", hash = "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c", size = 158712, upload-time = "2024-09-21T17:33:07.877Z" }, + { url = "https://files.pythonhosted.org/packages/46/93/e19160db48b5581feac8468330aa11b7292880a94a37d7030478596cc14e/websockets-13.1-cp312-cp312-win_amd64.whl", hash = "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3", size = 159145, upload-time = "2024-09-21T17:33:09.202Z" }, + { url = "https://files.pythonhosted.org/packages/51/20/2b99ca918e1cbd33c53db2cace5f0c0cd8296fc77558e1908799c712e1cd/websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6", size = 157828, upload-time = "2024-09-21T17:33:10.987Z" }, + { url = "https://files.pythonhosted.org/packages/b8/47/0932a71d3d9c0e9483174f60713c84cee58d62839a143f21a2bcdbd2d205/websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708", size = 155487, upload-time = "2024-09-21T17:33:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/a9/60/f1711eb59ac7a6c5e98e5637fef5302f45b6f76a2c9d64fd83bbb341377a/websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418", size = 155721, upload-time = "2024-09-21T17:33:13.909Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e6/ba9a8db7f9d9b0e5f829cf626ff32677f39824968317223605a6b419d445/websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a", size = 165609, upload-time = "2024-09-21T17:33:14.967Z" }, + { url = "https://files.pythonhosted.org/packages/c1/22/4ec80f1b9c27a0aebd84ccd857252eda8418ab9681eb571b37ca4c5e1305/websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f", size = 164556, upload-time = "2024-09-21T17:33:17.113Z" }, + { url = "https://files.pythonhosted.org/packages/27/ac/35f423cb6bb15600438db80755609d27eda36d4c0b3c9d745ea12766c45e/websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5", size = 164993, upload-time = "2024-09-21T17:33:18.168Z" }, + { url = "https://files.pythonhosted.org/packages/31/4e/98db4fd267f8be9e52e86b6ee4e9aa7c42b83452ea0ea0672f176224b977/websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135", size = 165360, upload-time = "2024-09-21T17:33:19.233Z" }, + { url = "https://files.pythonhosted.org/packages/3f/15/3f0de7cda70ffc94b7e7024544072bc5b26e2c1eb36545291abb755d8cdb/websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2", size = 164745, upload-time = "2024-09-21T17:33:20.361Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/66b6b756aebbd680b934c8bdbb6dcb9ce45aad72cde5f8a7208dbb00dd36/websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6", size = 164732, upload-time = "2024-09-21T17:33:23.103Z" }, + { url = "https://files.pythonhosted.org/packages/35/c6/12e3aab52c11aeb289e3dbbc05929e7a9d90d7a9173958477d3ef4f8ce2d/websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d", size = 158709, upload-time = "2024-09-21T17:33:24.196Z" }, + { url = "https://files.pythonhosted.org/packages/41/d8/63d6194aae711d7263df4498200c690a9c39fb437ede10f3e157a6343e0d/websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2", size = 159144, upload-time = "2024-09-21T17:33:25.96Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/6da22cb3ad5b8c606963f9a5f9f88656256fecc29d420b4b2bf9e0c7d56f/websockets-13.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238", size = 155499, upload-time = "2024-09-21T17:33:54.917Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ba/22833d58629088fcb2ccccedfae725ac0bbcd713319629e97125b52ac681/websockets-13.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5", size = 155737, upload-time = "2024-09-21T17:33:56.052Z" }, + { url = "https://files.pythonhosted.org/packages/95/54/61684fe22bdb831e9e1843d972adadf359cf04ab8613285282baea6a24bb/websockets-13.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9", size = 157095, upload-time = "2024-09-21T17:33:57.21Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/6652fb82440813822022a9301a30afde85e5ff3fb2aebb77f34aabe2b4e8/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6", size = 156701, upload-time = "2024-09-21T17:33:59.061Z" }, + { url = "https://files.pythonhosted.org/packages/67/33/ae82a7b860fa8a08aba68818bdf7ff61f04598aa5ab96df4cd5a3e418ca4/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a", size = 156654, upload-time = "2024-09-21T17:34:00.944Z" }, + { url = "https://files.pythonhosted.org/packages/63/0b/a1b528d36934f833e20f6da1032b995bf093d55cb416b9f2266f229fb237/websockets-13.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23", size = 159192, upload-time = "2024-09-21T17:34:02.656Z" }, + { url = "https://files.pythonhosted.org/packages/56/27/96a5cd2626d11c8280656c6c71d8ab50fe006490ef9971ccd154e0c42cd2/websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f", size = 152134, upload-time = "2024-09-21T17:34:19.904Z" }, +] diff --git a/cookiecutter.json b/cookiecutter.json deleted file mode 100644 index fc0e6fab00..0000000000 --- a/cookiecutter.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "project_name": "Base Project", - "project_slug": "{{ cookiecutter.project_name|lower|replace(' ', '-') }}", - "domain_main": "{{cookiecutter.project_slug}}.com", - "domain_staging": "stag.{{cookiecutter.domain_main}}", - - "docker_swarm_stack_name_main": "{{cookiecutter.domain_main|replace('.', '-')}}", - "docker_swarm_stack_name_staging": "{{cookiecutter.domain_staging|replace('.', '-')}}", - - "secret_key": "changethis", - "first_superuser": "admin@{{cookiecutter.domain_main}}", - "first_superuser_password": "changethis", - "backend_cors_origins": "[\"http://localhost\", \"http://localhost:4200\", \"http://localhost:3000\", \"http://localhost:8080\", \"https://localhost\", \"https://localhost:4200\", \"https://localhost:3000\", \"https://localhost:8080\", \"http://dev.{{cookiecutter.domain_main}}\", \"https://{{cookiecutter.domain_staging}}\", \"https://{{cookiecutter.domain_main}}\", \"http://local.dockertoolbox.tiangolo.com\", \"http://localhost.tiangolo.com\"]", - "smtp_port": "587", - "smtp_host": "", - "smtp_user": "", - "smtp_password": "", - "smtp_emails_from_email": "info@{{cookiecutter.domain_main}}", - - "postgres_password": "changethis", - "pgadmin_default_user": "{{cookiecutter.first_superuser}}", - "pgadmin_default_user_password": "{{cookiecutter.first_superuser_password}}", - - "traefik_constraint_tag": "{{cookiecutter.domain_main}}", - "traefik_constraint_tag_staging": "{{cookiecutter.domain_staging}}", - "traefik_public_constraint_tag": "traefik-public", - - "flower_auth": "admin:{{cookiecutter.first_superuser_password}}", - - "sentry_dsn": "", - - "docker_image_prefix": "", - - "docker_image_backend": "{{cookiecutter.docker_image_prefix}}backend", - "docker_image_celeryworker": "{{cookiecutter.docker_image_prefix}}celeryworker", - "docker_image_frontend": "{{cookiecutter.docker_image_prefix}}frontend", - - "_copy_without_render": [ - "frontend/src/**/*.html", - "frontend/src/**/*.vue", - "frontend/node_modules/*", - "backend/app/app/email-templates/**" - ] -} diff --git a/copier.yml b/copier.yml new file mode 100644 index 0000000000..f98e3fc861 --- /dev/null +++ b/copier.yml @@ -0,0 +1,100 @@ +project_name: + type: str + help: The name of the project, shown to API users (in .env) + default: FastAPI Project + +stack_name: + type: str + help: The name of the stack used for Docker Compose labels (no spaces) (in .env) + default: fastapi-project + +secret_key: + type: str + help: | + 'The secret key for the project, used for security, + stored in .env, you can generate one with: + python -c "import secrets; print(secrets.token_urlsafe(32))"' + default: changethis + +first_superuser: + type: str + help: The email of the first superuser (in .env) + default: admin@example.com + +first_superuser_password: + type: str + help: The password of the first superuser (in .env) + default: changethis + +smtp_host: + type: str + help: The SMTP server host to send emails, you can set it later in .env + default: "" + +smtp_user: + type: str + help: The SMTP server user to send emails, you can set it later in .env + default: "" + +smtp_password: + type: str + help: The SMTP server password to send emails, you can set it later in .env + default: "" + +emails_from_email: + type: str + help: The email account to send emails from, you can set it later in .env + default: info@example.com + +postgres_password: + type: str + help: | + 'The password for the PostgreSQL database, stored in .env, + you can generate one with: + python -c "import secrets; print(secrets.token_urlsafe(32))"' + default: changethis + +sentry_dsn: + type: str + help: The DSN for Sentry, if you are using it, you can set it later in .env + default: "" + +_exclude: + # Global + - .vscode + - .mypy_cache + # Python + - __pycache__ + - app.egg-info + - "*.pyc" + - .mypy_cache + - .coverage + - htmlcov + - .cache + - .venv + # Frontend + # Logs + - logs + - "*.log" + - npm-debug.log* + - yarn-debug.log* + - yarn-error.log* + - pnpm-debug.log* + - lerna-debug.log* + - node_modules + - dist + - dist-ssr + - "*.local" + # Editor directories and files + - .idea + - .DS_Store + - "*.suo" + - "*.ntvs*" + - "*.njsproj" + - "*.sln" + - "*.sw?" + +_answers_file: .copier/.copier-answers.yml + +_tasks: + - ["{{ _copier_python }}", .copier/update_dotenv.py] diff --git a/deployment.md b/deployment.md new file mode 100644 index 0000000000..970032b387 --- /dev/null +++ b/deployment.md @@ -0,0 +1,309 @@ +# FastAPI Project - Deployment + +You can deploy the project using Docker Compose to a remote server. + +This project expects you to have a Traefik proxy handling communication to the outside world and HTTPS certificates. + +You can use CI/CD (continuous integration and continuous deployment) systems to deploy automatically, there are already configurations to do it with GitHub Actions. + +But you have to configure a couple things first. πŸ€“ + +## Preparation + +* Have a remote server ready and available. +* Configure the DNS records of your domain to point to the IP of the server you just created. +* Configure a wildcard subdomain for your domain, so that you can have multiple subdomains for different services, e.g. `*.fastapi-project.example.com`. This will be useful for accessing different components, like `dashboard.fastapi-project.example.com`, `api.fastapi-project.example.com`, `traefik.fastapi-project.example.com`, `adminer.fastapi-project.example.com`, etc. And also for `staging`, like `dashboard.staging.fastapi-project.example.com`, `adminer.staging.fastapi-project.example.com`, etc. +* Install and configure [Docker](https://docs.docker.com/engine/install/) on the remote server (Docker Engine, not Docker Desktop). + +## Public Traefik + +We need a Traefik proxy to handle incoming connections and HTTPS certificates. + +You need to do these next steps only once. + +### Traefik Docker Compose + +* Create a remote directory to store your Traefik Docker Compose file: + +```bash +mkdir -p /root/code/traefik-public/ +``` + +Copy the Traefik Docker Compose file to your server. You could do it by running the command `rsync` in your local terminal: + +```bash +rsync -a docker-compose.traefik.yml root@your-server.example.com:/root/code/traefik-public/ +``` + +### Traefik Public Network + +This Traefik will expect a Docker "public network" named `traefik-public` to communicate with your stack(s). + +This way, there will be a single public Traefik proxy that handles the communication (HTTP and HTTPS) with the outside world, and then behind that, you could have one or more stacks with different domains, even if they are on the same single server. + +To create a Docker "public network" named `traefik-public` run the following command in your remote server: + +```bash +docker network create traefik-public +``` + +### Traefik Environment Variables + +The Traefik Docker Compose file expects some environment variables to be set in your terminal before starting it. You can do it by running the following commands in your remote server. + +* Create the username for HTTP Basic Auth, e.g.: + +```bash +export USERNAME=admin +``` + +* Create an environment variable with the password for HTTP Basic Auth, e.g.: + +```bash +export PASSWORD=changethis +``` + +* Use openssl to generate the "hashed" version of the password for HTTP Basic Auth and store it in an environment variable: + +```bash +export HASHED_PASSWORD=$(openssl passwd -apr1 $PASSWORD) +``` + +To verify that the hashed password is correct, you can print it: + +```bash +echo $HASHED_PASSWORD +``` + +* Create an environment variable with the domain name for your server, e.g.: + +```bash +export DOMAIN=fastapi-project.example.com +``` + +* Create an environment variable with the email for Let's Encrypt, e.g.: + +```bash +export EMAIL=admin@example.com +``` + +**Note**: you need to set a different email, an email `@example.com` won't work. + +### Start the Traefik Docker Compose + +Go to the directory where you copied the Traefik Docker Compose file in your remote server: + +```bash +cd /root/code/traefik-public/ +``` + +Now with the environment variables set and the `docker-compose.traefik.yml` in place, you can start the Traefik Docker Compose running the following command: + +```bash +docker compose -f docker-compose.traefik.yml up -d +``` + +## Deploy the FastAPI Project + +Now that you have Traefik in place you can deploy your FastAPI project with Docker Compose. + +**Note**: You might want to jump ahead to the section about Continuous Deployment with GitHub Actions. + +## Environment Variables + +You need to set some environment variables first. + +Set the `ENVIRONMENT`, by default `local` (for development), but when deploying to a server you would put something like `staging` or `production`: + +```bash +export ENVIRONMENT=production +``` + +Set the `DOMAIN`, by default `localhost` (for development), but when deploying you would use your own domain, for example: + +```bash +export DOMAIN=fastapi-project.example.com +``` + +You can set several variables, like: + +* `PROJECT_NAME`: The name of the project, used in the API for the docs and emails. +* `STACK_NAME`: The name of the stack used for Docker Compose labels and project name, this should be different for `staging`, `production`, etc. You could use the same domain replacing dots with dashes, e.g. `fastapi-project-example-com` and `staging-fastapi-project-example-com`. +* `BACKEND_CORS_ORIGINS`: A list of allowed CORS origins separated by commas. +* `SECRET_KEY`: The secret key for the FastAPI project, used to sign tokens. +* `FIRST_SUPERUSER`: The email of the first superuser, this superuser will be the one that can create new users. +* `FIRST_SUPERUSER_PASSWORD`: The password of the first superuser. +* `SMTP_HOST`: The SMTP server host to send emails, this would come from your email provider (E.g. Mailgun, Sparkpost, Sendgrid, etc). +* `SMTP_USER`: The SMTP server user to send emails. +* `SMTP_PASSWORD`: The SMTP server password to send emails. +* `EMAILS_FROM_EMAIL`: The email account to send emails from. +* `POSTGRES_SERVER`: The hostname of the PostgreSQL server. You can leave the default of `db`, provided by the same Docker Compose. You normally wouldn't need to change this unless you are using a third-party provider. +* `POSTGRES_PORT`: The port of the PostgreSQL server. You can leave the default. You normally wouldn't need to change this unless you are using a third-party provider. +* `POSTGRES_PASSWORD`: The Postgres password. +* `POSTGRES_USER`: The Postgres user, you can leave the default. +* `POSTGRES_DB`: The database name to use for this application. You can leave the default of `app`. +* `SENTRY_DSN`: The DSN for Sentry, if you are using it. + +## GitHub Actions Environment Variables + +There are some environment variables only used by GitHub Actions that you can configure: + +* `LATEST_CHANGES`: Used by the GitHub Action [latest-changes](https://github.com/tiangolo/latest-changes) to automatically add release notes based on the PRs merged. It's a personal access token, read the docs for details. +* `SMOKESHOW_AUTH_KEY`: Used to handle and publish the code coverage using [Smokeshow](https://github.com/samuelcolvin/smokeshow), follow their instructions to create a (free) Smokeshow key. + +### 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. + +### Deploy with Docker Compose + +With the environment variables in place, you can deploy with Docker Compose: + +```bash +docker compose -f docker-compose.yml up -d +``` + +For production you wouldn't want to have the overrides in `docker-compose.override.yml`, that's why we explicitly specify `docker-compose.yml` as the file to use. + +## Continuous Deployment (CD) + +You can use GitHub Actions to deploy your project automatically. 😎 + +You can have multiple environment deployments. + +There are already two environments configured, `staging` and `production`. πŸš€ + +### Install GitHub Actions Runner + +* On your remote server, create a user for your GitHub Actions: + +```bash +sudo adduser github +``` + +* Add Docker permissions to the `github` user: + +```bash +sudo usermod -aG docker github +``` + +* Temporarily switch to the `github` user: + +```bash +sudo su - github +``` + +* Go to the `github` user's home directory: + +```bash +cd +``` + +* [Install a GitHub Action self-hosted runner following the official guide](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/adding-self-hosted-runners#adding-a-self-hosted-runner-to-a-repository). + +* When asked about labels, add a label for the environment, e.g. `production`. You can also add labels later. + +After installing, the guide would tell you to run a command to start the runner. Nevertheless, it would stop once you terminate that process or if your local connection to your server is lost. + +To make sure it runs on startup and continues running, you can install it as a service. To do that, exit the `github` user and go back to the `root` user: + +```bash +exit +``` + +After you do it, you will be on the previous user again. And you will be on the previous directory, belonging to that user. + +Before being able to go the `github` user directory, you need to become the `root` user (you might already be): + +```bash +sudo su +``` + +* As the `root` user, go to the `actions-runner` directory inside of the `github` user's home directory: + +```bash +cd /home/github/actions-runner +``` + +* Install the self-hosted runner as a service with the user `github`: + +```bash +./svc.sh install github +``` + +* Start the service: + +```bash +./svc.sh start +``` + +* Check the status of the service: + +```bash +./svc.sh status +``` + +You can read more about it in the official guide: [Configuring the self-hosted runner application as a service](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/configuring-the-self-hosted-runner-application-as-a-service). + +### Set Secrets + +On your repository, configure secrets for the environment variables you need, the same ones described above, including `SECRET_KEY`, etc. Follow the [official GitHub guide for setting repository secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository). + +The current Github Actions workflows expect these secrets: + +* `DOMAIN_PRODUCTION` +* `DOMAIN_STAGING` +* `STACK_NAME_PRODUCTION` +* `STACK_NAME_STAGING` +* `EMAILS_FROM_EMAIL` +* `FIRST_SUPERUSER` +* `FIRST_SUPERUSER_PASSWORD` +* `POSTGRES_PASSWORD` +* `SECRET_KEY` +* `LATEST_CHANGES` +* `SMOKESHOW_AUTH_KEY` + +## GitHub Action Deployment Workflows + +There are GitHub Action workflows in the `.github/workflows` directory already configured for deploying to the environments (GitHub Actions runners with the labels): + +* `staging`: after pushing (or merging) to the branch `master`. +* `production`: after publishing a release. + +If you need to add extra environments you could use those as a starting point. + +## URLs + +Replace `fastapi-project.example.com` with your domain. + +### Main Traefik Dashboard + +Traefik UI: `https://traefik.fastapi-project.example.com` + +### Production + +Frontend: `https://dashboard.fastapi-project.example.com` + +Backend API docs: `https://api.fastapi-project.example.com/docs` + +Backend API base URL: `https://api.fastapi-project.example.com` + +Adminer: `https://adminer.fastapi-project.example.com` + +### Staging + +Frontend: `https://dashboard.staging.fastapi-project.example.com` + +Backend API docs: `https://api.staging.fastapi-project.example.com/docs` + +Backend API base URL: `https://api.staging.fastapi-project.example.com` + +Adminer: `https://adminer.staging.fastapi-project.example.com` diff --git a/development.md b/development.md new file mode 100644 index 0000000000..d7d41d73f1 --- /dev/null +++ b/development.md @@ -0,0 +1,207 @@ +# FastAPI Project - Development + +## Docker Compose + +* Start the local stack with Docker Compose: + +```bash +docker compose watch +``` + +* Now you can open your browser and interact with these URLs: + +Frontend, built with Docker, with routes handled based on the path: http://localhost:5173 + +Backend, JSON based web API based on OpenAPI: http://localhost:8000 + +Automatic interactive documentation with Swagger UI (from the OpenAPI backend): http://localhost:8000/docs + +Adminer, database web administration: http://localhost:8080 + +Traefik UI, to see how the routes are being handled by the proxy: http://localhost:8090 + +**Note**: The first time you start your stack, it might take a minute for it to be ready. While the backend waits for the database to be ready and configures everything. You can check the logs to monitor it. + +To check the logs, run (in another terminal): + +```bash +docker compose logs +``` + +To check the logs of a specific service, add the name of the service, e.g.: + +```bash +docker compose logs backend +``` + +## Local Development + +The Docker Compose files are configured so that each of the services is available in a different port in `localhost`. + +For the backend and frontend, they use the same port that would be used by their local development server, so, the backend is at `http://localhost:8000` and the frontend at `http://localhost:5173`. + +This way, you could turn off a Docker Compose service and start its local development service, and everything would keep working, because it all uses the same ports. + +For example, you can stop that `frontend` service in the Docker Compose, in another terminal, run: + +```bash +docker compose stop frontend +``` + +And then start the local frontend development server: + +```bash +cd frontend +npm run dev +``` + +Or you could stop the `backend` Docker Compose service: + +```bash +docker compose stop backend +``` + +And then you can run the local development server for the backend: + +```bash +cd backend +fastapi dev app/main.py +``` + +## Docker Compose in `localhost.tiangolo.com` + +When you start the Docker Compose stack, it uses `localhost` by default, with different ports for each service (backend, frontend, adminer, etc). + +When you deploy it to production (or staging), it will deploy each service in a different subdomain, like `api.example.com` for the backend and `dashboard.example.com` for the frontend. + +In the guide about [deployment](deployment.md) you can read about Traefik, the configured proxy. That's the component in charge of transmitting traffic to each service based on the subdomain. + +If you want to test that it's all working locally, you can edit the local `.env` file, and change: + +```dotenv +DOMAIN=localhost.tiangolo.com +``` + +That will be used by the Docker Compose files to configure the base domain for the services. + +Traefik will use this to transmit traffic at `api.localhost.tiangolo.com` to the backend, and traffic at `dashboard.localhost.tiangolo.com` to the frontend. + +The domain `localhost.tiangolo.com` is a special domain that is configured (with all its subdomains) to point to `127.0.0.1`. This way you can use that for your local development. + +After you update it, run again: + +```bash +docker compose watch +``` + +When deploying, for example in production, the main Traefik is configured outside of the Docker Compose files. For local development, there's an included Traefik in `docker-compose.override.yml`, just to let you test that the domains work as expected, for example with `api.localhost.tiangolo.com` and `dashboard.localhost.tiangolo.com`. + +## Docker Compose files and env vars + +There is a main `docker-compose.yml` file with all the configurations that apply to the whole stack, it is used automatically by `docker compose`. + +And there's also a `docker-compose.override.yml` with overrides for development, for example to mount the source code as a volume. It is used automatically by `docker compose` to apply overrides on top of `docker-compose.yml`. + +These Docker Compose files use the `.env` file containing configurations to be injected as environment variables in the containers. + +They also use some additional configurations taken from environment variables set in the scripts before calling the `docker compose` command. + +After changing variables, make sure you restart the stack: + +```bash +docker compose watch +``` + +## The .env file + +The `.env` file is the one that contains all your configurations, generated keys and passwords, etc. + +Depending on your workflow, you could want to exclude it from Git, for example if your project is public. In that case, you would have to make sure to set up a way for your CI tools to obtain it while building or deploying your project. + +One way to do it could be to add each environment variable to your CI/CD system, and updating the `docker-compose.yml` file to read that specific env var instead of reading the `.env` file. + +## Pre-commits and code linting + +we are using a tool called [pre-commit](https://pre-commit.com/) for code linting and formatting. + +When you install it, it runs right before making a commit in git. This way it ensures that the code is consistent and formatted even before it is committed. + +You can find a file `.pre-commit-config.yaml` with configurations at the root of the project. + +#### Install pre-commit to run automatically + +`pre-commit` is already part of the dependencies of the project, but you could also install it globally if you prefer to, following [the official pre-commit docs](https://pre-commit.com/). + +After having the `pre-commit` tool installed and available, you need to "install" it in the local repository, so that it runs automatically before each commit. + +Using `uv`, you could do it with: + +```bash +❯ uv run pre-commit install +pre-commit installed at .git/hooks/pre-commit +``` + +Now whenever you try to commit, e.g. with: + +```bash +git commit +``` + +...pre-commit will run and check and format the code you are about to commit, and will ask you to add that code (stage it) with git again before committing. + +Then you can `git add` the modified/fixed files again and now you can commit. + +#### Running pre-commit hooks manually + +you can also run `pre-commit` manually on all the files, you can do it using `uv` with: + +```bash +❯ uv run pre-commit run --all-files +check for added large files..............................................Passed +check toml...............................................................Passed +check yaml...............................................................Passed +ruff.....................................................................Passed +ruff-format..............................................................Passed +eslint...................................................................Passed +prettier.................................................................Passed +``` + +## URLs + +The production or staging URLs would use these same paths, but with your own domain. + +### Development URLs + +Development URLs, for local development. + +Frontend: http://localhost:5173 + +Backend: http://localhost:8000 + +Automatic Interactive Docs (Swagger UI): http://localhost:8000/docs + +Automatic Alternative Docs (ReDoc): http://localhost:8000/redoc + +Adminer: http://localhost:8080 + +Traefik UI: http://localhost:8090 + +MailCatcher: http://localhost:1080 + +### Development URLs with `localhost.tiangolo.com` Configured + +Development URLs, for local development. + +Frontend: http://dashboard.localhost.tiangolo.com + +Backend: http://api.localhost.tiangolo.com + +Automatic Interactive Docs (Swagger UI): http://api.localhost.tiangolo.com/docs + +Automatic Alternative Docs (ReDoc): http://api.localhost.tiangolo.com/redoc + +Adminer: http://localhost.tiangolo.com:8080 + +Traefik UI: http://localhost.tiangolo.com:8090 + +MailCatcher: http://localhost.tiangolo.com:1080 \ No newline at end of file diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000000..0751abe901 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,133 @@ +services: + + # Local services are available on their ports, but also available on: + # http://api.localhost.tiangolo.com: backend + # http://dashboard.localhost.tiangolo.com: frontend + # etc. To enable it, update .env, set: + # DOMAIN=localhost.tiangolo.com + proxy: + image: traefik:3.0 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + ports: + - "80:80" + - "8090:8080" + # Duplicate the command from docker-compose.yml to add --api.insecure=true + command: + # Enable Docker in Traefik, so that it reads labels from Docker services + - --providers.docker + # Add a constraint to only use services with the label for this stack + - --providers.docker.constraints=Label(`traefik.constraint-label`, `traefik-public`) + # Do not expose all Docker services, only the ones explicitly exposed + - --providers.docker.exposedbydefault=false + # Create an entrypoint "http" listening on port 80 + - --entrypoints.http.address=:80 + # Create an entrypoint "https" listening on port 443 + - --entrypoints.https.address=:443 + # Enable the access log, with HTTP requests + - --accesslog + # Enable the Traefik log, for configurations and errors + - --log + # Enable debug logging for local development + - --log.level=DEBUG + # Enable the Dashboard and API + - --api + # Enable the Dashboard and API in insecure mode for local development + - --api.insecure=true + labels: + # Enable Traefik for this service, to make it available in the public network + - traefik.enable=true + - traefik.constraint-label=traefik-public + # Dummy https-redirect middleware that doesn't really redirect, only to + # allow running it locally + - traefik.http.middlewares.https-redirect.contenttype.autodetect=false + networks: + - traefik-public + - default + + db: + restart: "no" + ports: + - "5432:5432" + + adminer: + restart: "no" + ports: + - "8080:8080" + + backend: + restart: "no" + ports: + - "8000:8000" + build: + context: ./backend + # command: sleep infinity # Infinite loop to keep container alive doing nothing + command: + - fastapi + - run + - --reload + - "app/main.py" + develop: + watch: + - path: ./backend + action: sync + target: /app + ignore: + - ./backend/.venv + - .venv + - path: ./backend/pyproject.toml + action: rebuild + # TODO: remove once coverage is done locally + volumes: + - ./backend/htmlcov:/app/htmlcov + environment: + SMTP_HOST: "mailcatcher" + SMTP_PORT: "1025" + SMTP_TLS: "false" + EMAILS_FROM_EMAIL: "noreply@example.com" + + mailcatcher: + image: schickling/mailcatcher + ports: + - "1080:1080" + - "1025:1025" + + frontend: + restart: "no" + ports: + - "5173:80" + build: + context: ./frontend + args: + - VITE_API_URL=http://localhost:8000 + - NODE_ENV=development + + playwright: + build: + context: ./frontend + dockerfile: Dockerfile.playwright + args: + - VITE_API_URL=http://backend:8000 + - NODE_ENV=production + ipc: host + depends_on: + - backend + - mailcatcher + env_file: + - .env + environment: + - VITE_API_URL=http://backend:8000 + - MAILCATCHER_HOST=http://mailcatcher:1080 + # For the reports when run locally + - PLAYWRIGHT_HTML_HOST=0.0.0.0 + - CI=${CI} + volumes: + - ./frontend/blob-report:/app/blob-report + - ./frontend/test-results:/app/test-results + ports: + - 9323:9323 + +networks: + traefik-public: + # For local dev, don't expect an external Traefik network + external: false diff --git a/docker-compose.traefik.yml b/docker-compose.traefik.yml new file mode 100644 index 0000000000..886d6dcc2f --- /dev/null +++ b/docker-compose.traefik.yml @@ -0,0 +1,77 @@ +services: + traefik: + image: traefik:3.0 + ports: + # Listen on port 80, default for HTTP, necessary to redirect to HTTPS + - 80:80 + # Listen on port 443, default for HTTPS + - 443:443 + restart: always + labels: + # Enable Traefik for this service, to make it available in the public network + - traefik.enable=true + # Use the traefik-public network (declared below) + - traefik.docker.network=traefik-public + # Define the port inside of the Docker service to use + - traefik.http.services.traefik-dashboard.loadbalancer.server.port=8080 + # Make Traefik use this domain (from an environment variable) in HTTP + - traefik.http.routers.traefik-dashboard-http.entrypoints=http + - traefik.http.routers.traefik-dashboard-http.rule=Host(`traefik.${DOMAIN?Variable not set}`) + # traefik-https the actual router using HTTPS + - traefik.http.routers.traefik-dashboard-https.entrypoints=https + - traefik.http.routers.traefik-dashboard-https.rule=Host(`traefik.${DOMAIN?Variable not set}`) + - traefik.http.routers.traefik-dashboard-https.tls=true + # Use the "le" (Let's Encrypt) resolver created below + - traefik.http.routers.traefik-dashboard-https.tls.certresolver=le + # Use the special Traefik service api@internal with the web UI/Dashboard + - traefik.http.routers.traefik-dashboard-https.service=api@internal + # https-redirect middleware to redirect HTTP to HTTPS + - traefik.http.middlewares.https-redirect.redirectscheme.scheme=https + - traefik.http.middlewares.https-redirect.redirectscheme.permanent=true + # traefik-http set up only to use the middleware to redirect to https + - traefik.http.routers.traefik-dashboard-http.middlewares=https-redirect + # admin-auth middleware with HTTP Basic auth + # Using the environment variables USERNAME and HASHED_PASSWORD + - traefik.http.middlewares.admin-auth.basicauth.users=${USERNAME?Variable not set}:${HASHED_PASSWORD?Variable not set} + # Enable HTTP Basic auth, using the middleware created above + - traefik.http.routers.traefik-dashboard-https.middlewares=admin-auth + volumes: + # Add Docker as a mounted volume, so that Traefik can read the labels of other services + - /var/run/docker.sock:/var/run/docker.sock:ro + # Mount the volume to store the certificates + - traefik-public-certificates:/certificates + command: + # Enable Docker in Traefik, so that it reads labels from Docker services + - --providers.docker + # Do not expose all Docker services, only the ones explicitly exposed + - --providers.docker.exposedbydefault=false + # Create an entrypoint "http" listening on port 80 + - --entrypoints.http.address=:80 + # Create an entrypoint "https" listening on port 443 + - --entrypoints.https.address=:443 + # Create the certificate resolver "le" for Let's Encrypt, uses the environment variable EMAIL + - --certificatesresolvers.le.acme.email=${EMAIL?Variable not set} + # Store the Let's Encrypt certificates in the mounted volume + - --certificatesresolvers.le.acme.storage=/certificates/acme.json + # Use the TLS Challenge for Let's Encrypt + - --certificatesresolvers.le.acme.tlschallenge=true + # Enable the access log, with HTTP requests + - --accesslog + # Enable the Traefik log, for configurations and errors + - --log + # Enable the Dashboard and API + - --api + networks: + # Use the public network created to be shared between Traefik and + # any other service that needs to be publicly available with HTTPS + - traefik-public + +volumes: + # Create a volume to store the certificates, even if the container is recreated + traefik-public-certificates: + +networks: + # Use the previously created public network "traefik-public", shared with other + # services that need to be publicly available via this Traefik + traefik-public: + external: true diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..b1aa17ed43 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,171 @@ +services: + + db: + image: postgres:17 + restart: always + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 10s + retries: 5 + start_period: 30s + timeout: 10s + volumes: + - app-db-data:/var/lib/postgresql/data/pgdata + env_file: + - .env + environment: + - PGDATA=/var/lib/postgresql/data/pgdata + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set} + - POSTGRES_USER=${POSTGRES_USER?Variable not set} + - POSTGRES_DB=${POSTGRES_DB?Variable not set} + + adminer: + image: adminer + restart: always + networks: + - traefik-public + - default + depends_on: + - db + environment: + - ADMINER_DESIGN=pepa-linha-dark + labels: + - traefik.enable=true + - traefik.docker.network=traefik-public + - traefik.constraint-label=traefik-public + - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-http.rule=Host(`adminer.${DOMAIN?Variable not set}`) + - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-http.entrypoints=http + - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-http.middlewares=https-redirect + - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.rule=Host(`adminer.${DOMAIN?Variable not set}`) + - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.entrypoints=https + - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.tls=true + - traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.tls.certresolver=le + - traefik.http.services.${STACK_NAME?Variable not set}-adminer.loadbalancer.server.port=8080 + + prestart: + image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}' + build: + context: ./backend + networks: + - traefik-public + - default + depends_on: + db: + condition: service_healthy + restart: true + command: bash scripts/prestart.sh + env_file: + - .env + environment: + - DOMAIN=${DOMAIN} + - FRONTEND_HOST=${FRONTEND_HOST?Variable not set} + - ENVIRONMENT=${ENVIRONMENT} + - BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS} + - SECRET_KEY=${SECRET_KEY?Variable not set} + - FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set} + - FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD?Variable not set} + - SMTP_HOST=${SMTP_HOST} + - SMTP_USER=${SMTP_USER} + - SMTP_PASSWORD=${SMTP_PASSWORD} + - EMAILS_FROM_EMAIL=${EMAILS_FROM_EMAIL} + - POSTGRES_SERVER=db + - POSTGRES_PORT=${POSTGRES_PORT} + - POSTGRES_DB=${POSTGRES_DB} + - POSTGRES_USER=${POSTGRES_USER?Variable not set} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set} + - SENTRY_DSN=${SENTRY_DSN} + + backend: + image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}' + restart: always + networks: + - traefik-public + - default + depends_on: + db: + condition: service_healthy + restart: true + prestart: + condition: service_completed_successfully + env_file: + - .env + environment: + - DOMAIN=${DOMAIN} + - FRONTEND_HOST=${FRONTEND_HOST?Variable not set} + - ENVIRONMENT=${ENVIRONMENT} + - BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS} + - SECRET_KEY=${SECRET_KEY?Variable not set} + - FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set} + - FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD?Variable not set} + - SMTP_HOST=${SMTP_HOST} + - SMTP_USER=${SMTP_USER} + - SMTP_PASSWORD=${SMTP_PASSWORD} + - EMAILS_FROM_EMAIL=${EMAILS_FROM_EMAIL} + - POSTGRES_SERVER=db + - POSTGRES_PORT=${POSTGRES_PORT} + - POSTGRES_DB=${POSTGRES_DB} + - POSTGRES_USER=${POSTGRES_USER?Variable not set} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set} + - SENTRY_DSN=${SENTRY_DSN} + + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/utils/health-check/"] + interval: 10s + timeout: 5s + retries: 5 + + build: + context: ./backend + labels: + - 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=8000 + + - traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.rule=Host(`api.${DOMAIN?Variable not set}`) + - traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.entrypoints=http + + - traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.rule=Host(`api.${DOMAIN?Variable not set}`) + - 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 + + # Enable redirection for HTTP and HTTPS + - traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.middlewares=https-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://api.${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(`dashboard.${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(`dashboard.${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 redirection for HTTP and HTTPS + - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.middlewares=https-redirect +volumes: + app-db-data: + +networks: + traefik-public: + # Allow setting it to false for testing + external: true diff --git a/{{cookiecutter.project_slug}}/frontend/.dockerignore b/frontend/.dockerignore old mode 100755 new mode 100644 similarity index 72% rename from {{cookiecutter.project_slug}}/frontend/.dockerignore rename to frontend/.dockerignore index 3c3629e647..f06235c460 --- a/{{cookiecutter.project_slug}}/frontend/.dockerignore +++ b/frontend/.dockerignore @@ -1 +1,2 @@ node_modules +dist diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000000..27fcbfe8c8 --- /dev/null +++ b/frontend/.env @@ -0,0 +1,2 @@ +VITE_API_URL=http://localhost:8000 +MAILCATCHER_HOST=http://localhost:1080 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000000..75e25e0ef4 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,30 @@ +# 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/ +/playwright/.auth/ \ No newline at end of file diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100644 index 0000000000..a45fd52cc5 --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/{{cookiecutter.project_slug}}/frontend/Dockerfile b/frontend/Dockerfile similarity index 61% rename from {{cookiecutter.project_slug}}/frontend/Dockerfile rename to frontend/Dockerfile index 75157e0968..ee30d000f3 100644 --- a/{{cookiecutter.project_slug}}/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,5 +1,5 @@ # Stage 0, "build-stage", based on Node.js, to build and compile the frontend -FROM tiangolo/node-frontend:10 as build-stage +FROM node:24 AS build-stage WORKDIR /app @@ -9,20 +9,15 @@ RUN npm install COPY ./ /app/ -ARG FRONTEND_ENV=production - -ENV VUE_APP_ENV=${FRONTEND_ENV} - -# Comment out the next line to disable tests -RUN npm run test:unit +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.15 +FROM nginx:1 COPY --from=build-stage /app/dist/ /usr/share/nginx/html -COPY --from=build-stage /nginx.conf /etc/nginx/conf.d/default.conf +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/Dockerfile.playwright b/frontend/Dockerfile.playwright new file mode 100644 index 0000000000..cc7ed6034a --- /dev/null +++ b/frontend/Dockerfile.playwright @@ -0,0 +1,11 @@ +FROM mcr.microsoft.com/playwright:v1.56.1-noble + +WORKDIR /app + +COPY package*.json /app/ + +RUN npm install + +COPY ./ /app/ + +ARG VITE_API_URL=${VITE_API_URL} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000000..bbb73cb447 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,154 @@ +# 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` and `playwright`. + +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-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 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://api.my-domain.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 --wait backend +``` + +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). diff --git a/frontend/biome.json b/frontend/biome.json new file mode 100644 index 0000000000..f90bd7e90f --- /dev/null +++ b/frontend/biome.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.3/schema.json", + "assist": { "actions": { "source": { "organizeImports": "on" } } }, + "files": { + "includes": [ + "**", + "!**/dist/**/*", + "!**/node_modules/**/*", + "!**/src/routeTree.gen.ts", + "!**/src/client/**/*", + "!**/src/components/ui/**/*", + "!**/playwright-report", + "!**/playwright.config.ts" + ] + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "off", + "noArrayIndexKey": "off" + }, + "style": { + "noNonNullAssertion": "off", + "noParameterAssign": "error", + "useSelfClosingElements": "error", + "noUselessElse": "error" + } + } + }, + "formatter": { + "indentStyle": "space" + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "semicolons": "asNeeded" + } + } +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000000..57621a268b --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + Full Stack FastAPI Project + + + +
+ + + diff --git a/{{cookiecutter.project_slug}}/frontend/nginx-backend-not-found.conf b/frontend/nginx-backend-not-found.conf similarity index 100% rename from {{cookiecutter.project_slug}}/frontend/nginx-backend-not-found.conf rename to frontend/nginx-backend-not-found.conf diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000000..ba4d9aad6c --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,11 @@ +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/openapi-ts.config.ts b/frontend/openapi-ts.config.ts new file mode 100644 index 0000000000..b5a69e20eb --- /dev/null +++ b/frontend/openapi-ts.config.ts @@ -0,0 +1,33 @@ +import { defineConfig } from "@hey-api/openapi-ts" + +export default defineConfig({ + input: "./openapi.json", + output: "./src/client", + + plugins: [ + "legacy/axios", + { + name: "@hey-api/sdk", + // NOTE: this doesn't allow tree-shaking + asClass: true, + operationId: true, + classNameBuilder: "{{name}}Service", + methodNameBuilder: (operation) => { + // @ts-expect-error + let name: string = operation.name + // @ts-expect-error + const service: string = operation.service + + if (service && name.toLowerCase().startsWith(service.toLowerCase())) { + name = name.slice(service.length) + } + + return name.charAt(0).toLowerCase() + name.slice(1) + }, + }, + { + name: "@hey-api/schemas", + type: "json", + }, + ], +}) diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000000..66a15782be --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,9214 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@chakra-ui/react": "^3.27.0", + "@emotion/react": "^11.14.0", + "@tanstack/react-query": "^5.90.2", + "@tanstack/react-query-devtools": "^5.90.2", + "@tanstack/react-router": "^1.131.50", + "axios": "1.12.2", + "form-data": "4.0.4", + "next-themes": "^0.4.6", + "react": "^19.1.1", + "react-dom": "^19.2.0", + "react-error-boundary": "^6.0.0", + "react-hook-form": "7.62.0", + "react-icons": "^5.5.0" + }, + "devDependencies": { + "@biomejs/biome": "^2.2.4", + "@hey-api/openapi-ts": "0.73.0", + "@playwright/test": "1.56.1", + "@tanstack/router-devtools": "^1.131.42", + "@tanstack/router-plugin": "^1.133.15", + "@types/node": "^24.5.2", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.2.1", + "@vitejs/plugin-react-swc": "^4.1.0", + "dotenv": "^17.2.2", + "typescript": "^5.2.2", + "vite": "^7.1.11" + } + }, + "node_modules/@ark-ui/react": { + "version": "5.24.1", + "resolved": "https://registry.npmjs.org/@ark-ui/react/-/react-5.24.1.tgz", + "integrity": "sha512-Czx6pLRJzs8G9t8XCvBlizd1aGRC7KOyUGJgK5a1vUDz8WhAALUUm66yslhs7GfU4/jJX8mS73FwStgtK0znAg==", + "dependencies": { + "@internationalized/date": "3.9.0", + "@zag-js/accordion": "1.24.1", + "@zag-js/anatomy": "1.24.1", + "@zag-js/angle-slider": "1.24.1", + "@zag-js/async-list": "1.24.1", + "@zag-js/auto-resize": "1.24.1", + "@zag-js/avatar": "1.24.1", + "@zag-js/carousel": "1.24.1", + "@zag-js/checkbox": "1.24.1", + "@zag-js/clipboard": "1.24.1", + "@zag-js/collapsible": "1.24.1", + "@zag-js/collection": "1.24.1", + "@zag-js/color-picker": "1.24.1", + "@zag-js/color-utils": "1.24.1", + "@zag-js/combobox": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/date-picker": "1.24.1", + "@zag-js/date-utils": "1.24.1", + "@zag-js/dialog": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/editable": "1.24.1", + "@zag-js/file-upload": "1.24.1", + "@zag-js/file-utils": "1.24.1", + "@zag-js/floating-panel": "1.24.1", + "@zag-js/focus-trap": "1.24.1", + "@zag-js/highlight-word": "1.24.1", + "@zag-js/hover-card": "1.24.1", + "@zag-js/i18n-utils": "1.24.1", + "@zag-js/json-tree-utils": "1.24.1", + "@zag-js/listbox": "1.24.1", + "@zag-js/menu": "1.24.1", + "@zag-js/number-input": "1.24.1", + "@zag-js/pagination": "1.24.1", + "@zag-js/password-input": "1.24.1", + "@zag-js/pin-input": "1.24.1", + "@zag-js/popover": "1.24.1", + "@zag-js/presence": "1.24.1", + "@zag-js/progress": "1.24.1", + "@zag-js/qr-code": "1.24.1", + "@zag-js/radio-group": "1.24.1", + "@zag-js/rating-group": "1.24.1", + "@zag-js/react": "1.24.1", + "@zag-js/scroll-area": "1.24.1", + "@zag-js/select": "1.24.1", + "@zag-js/signature-pad": "1.24.1", + "@zag-js/slider": "1.24.1", + "@zag-js/splitter": "1.24.1", + "@zag-js/steps": "1.24.1", + "@zag-js/switch": "1.24.1", + "@zag-js/tabs": "1.24.1", + "@zag-js/tags-input": "1.24.1", + "@zag-js/timer": "1.24.1", + "@zag-js/toast": "1.24.1", + "@zag-js/toggle": "1.24.1", + "@zag-js/toggle-group": "1.24.1", + "@zag-js/tooltip": "1.24.1", + "@zag-js/tour": "1.24.1", + "@zag-js/tree-view": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", + "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.2.4.tgz", + "integrity": "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg==", + "dev": true, + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.2.4", + "@biomejs/cli-darwin-x64": "2.2.4", + "@biomejs/cli-linux-arm64": "2.2.4", + "@biomejs/cli-linux-arm64-musl": "2.2.4", + "@biomejs/cli-linux-x64": "2.2.4", + "@biomejs/cli-linux-x64-musl": "2.2.4", + "@biomejs/cli-win32-arm64": "2.2.4", + "@biomejs/cli-win32-x64": "2.2.4" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.4.tgz", + "integrity": "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.4.tgz", + "integrity": "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.4.tgz", + "integrity": "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.4.tgz", + "integrity": "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.4.tgz", + "integrity": "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.4.tgz", + "integrity": "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.4.tgz", + "integrity": "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.4.tgz", + "integrity": "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@chakra-ui/react": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-3.27.0.tgz", + "integrity": "sha512-M1WTAErI2cYM/PB4h5Kf5CCAg70g3HCzVvTEhcf5ty8QrG6QybPf3RdWSpBlIy7qpjuEnQYpHLxM0jnFLArBgA==", + "dependencies": { + "@ark-ui/react": "^5.24.1", + "@emotion/is-prop-valid": "^1.3.1", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@pandacss/is-valid-prop": "^0.54.0", + "csstype": "^3.1.3", + "fast-safe-stringify": "^2.1.1" + }, + "peerDependencies": { + "@emotion/react": ">=11", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "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.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", + "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", + "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", + "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", + "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", + "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", + "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", + "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", + "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", + "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", + "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", + "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", + "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", + "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", + "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", + "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", + "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", + "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", + "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", + "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", + "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", + "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", + "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", + "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", + "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", + "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" + }, + "node_modules/@hey-api/json-schema-ref-parser": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.0.6.tgz", + "integrity": "sha512-yktiFZoWPtEW8QKS65eqKwA5MTKp88CyiL8q72WynrBs/73SAaxlSWlA2zW/DZlywZ5hX1OYzrCC0wFdvO9c2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + } + }, + "node_modules/@hey-api/openapi-ts": { + "version": "0.73.0", + "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.73.0.tgz", + "integrity": "sha512-sUscR3OIGW0k9U//28Cu6BTp3XaogWMDORj9H+5Du9E5AvTT7LZbCEDvkLhebFOPkp2cZAQfd66HiZsiwssBcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hey-api/json-schema-ref-parser": "1.0.6", + "ansi-colors": "4.1.3", + "c12": "2.0.1", + "color-support": "1.1.3", + "commander": "13.0.0", + "handlebars": "4.7.8", + "open": "10.1.2" + }, + "bin": { + "openapi-ts": "bin/index.cjs" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=22.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "typescript": "^5.5.3" + } + }, + "node_modules/@internationalized/date": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.9.0.tgz", + "integrity": "sha512-yaN3brAnHRD+4KyyOsJyk49XUvj2wtbNACSqg0bz3u8t2VuzhC8Q5dfRnrSxjnnbDb+ienBnkn1TzQfE154vyg==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/number": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.5.tgz", + "integrity": "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@pandacss/is-valid-prop": { + "version": "0.54.0", + "resolved": "https://registry.npmjs.org/@pandacss/is-valid-prop/-/is-valid-prop-0.54.0.tgz", + "integrity": "sha512-UhRgg1k9VKRCBAHl+XUK3lvN0k9bYifzYGZOqajDid4L1DyU813A1L0ZwN4iV9WX5TX3PfUugqtgG9LnIeFGBQ==" + }, + "node_modules/@playwright/test": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.35", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.35.tgz", + "integrity": "sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz", + "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz", + "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz", + "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz", + "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz", + "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz", + "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz", + "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz", + "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz", + "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz", + "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz", + "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz", + "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz", + "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz", + "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz", + "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz", + "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz", + "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz", + "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz", + "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz", + "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz", + "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/core": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", + "integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.24" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.13.5", + "@swc/core-darwin-x64": "1.13.5", + "@swc/core-linux-arm-gnueabihf": "1.13.5", + "@swc/core-linux-arm64-gnu": "1.13.5", + "@swc/core-linux-arm64-musl": "1.13.5", + "@swc/core-linux-x64-gnu": "1.13.5", + "@swc/core-linux-x64-musl": "1.13.5", + "@swc/core-win32-arm64-msvc": "1.13.5", + "@swc/core-win32-ia32-msvc": "1.13.5", + "@swc/core-win32-x64-msvc": "1.13.5" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.5.tgz", + "integrity": "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.5.tgz", + "integrity": "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.5.tgz", + "integrity": "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.5.tgz", + "integrity": "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.5.tgz", + "integrity": "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.5.tgz", + "integrity": "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.5.tgz", + "integrity": "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.5.tgz", + "integrity": "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.5.tgz", + "integrity": "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.5.tgz", + "integrity": "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tanstack/history": { + "version": "1.133.3", + "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.133.3.tgz", + "integrity": "sha512-zFQnGdX0S4g5xRuS+95iiEXM+qlGvYG7ksmOKx7LaMv60lDWa0imR8/24WwXXvBWJT1KnwVdZcjvhCwz9IiJCw==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz", + "integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.90.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.90.1.tgz", + "integrity": "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.2.tgz", + "integrity": "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==", + "dependencies": { + "@tanstack/query-core": "5.90.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.90.2.tgz", + "integrity": "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ==", + "dependencies": { + "@tanstack/query-devtools": "5.90.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.90.2", + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-router": { + "version": "1.133.15", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.133.15.tgz", + "integrity": "sha512-3gQitqq/5lL//KSv9Ro34Fw7xak2ZQcPbR7x6bu5X4W0v97xTE7+bMbBS5UAg9zXTq0FNyB124GabgyBgeQ0NA==", + "dependencies": { + "@tanstack/history": "1.133.3", + "@tanstack/react-store": "^0.7.0", + "@tanstack/router-core": "1.133.15", + "isbot": "^5.1.22", + "tiny-invariant": "^1.3.3", + "tiny-warning": "^1.0.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + } + }, + "node_modules/@tanstack/react-router-devtools": { + "version": "1.131.42", + "resolved": "https://registry.npmjs.org/@tanstack/react-router-devtools/-/react-router-devtools-1.131.42.tgz", + "integrity": "sha512-7pymFB1CCimRHot2Zp0ZekQjd1iN812V88n9NLPSeiv9sVRtRVIaLphJjDeudx1NNgkfSJPx2lOhz6K38cuZog==", + "dev": true, + "dependencies": { + "@tanstack/router-devtools-core": "1.131.42" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-router": "^1.131.41", + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + } + }, + "node_modules/@tanstack/react-store": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.4.tgz", + "integrity": "sha512-DyG1e5Qz/c1cNLt/NdFbCA7K1QGuFXQYT6EfUltYMJoQ4LzBOGnOl5IjuxepNcRtmIKkGpmdMzdFZEkevgU9bQ==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.7.4", + "use-sync-external-store": "^1.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/router-core": { + "version": "1.133.15", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.133.15.tgz", + "integrity": "sha512-ZWAmoFcgi27Ojv2FH3Dq3D6Vt73LswdTnA1tyHShNWQf7wOMH/VKKB9JxiXJqpLTK4NJqpnUp/x0/3nvmdrIqg==", + "dependencies": { + "@tanstack/history": "1.133.3", + "@tanstack/store": "^0.7.0", + "cookie-es": "^2.0.0", + "seroval": "^1.3.2", + "seroval-plugins": "^1.3.2", + "tiny-invariant": "^1.3.3", + "tiny-warning": "^1.0.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/router-devtools": { + "version": "1.131.42", + "resolved": "https://registry.npmjs.org/@tanstack/router-devtools/-/router-devtools-1.131.42.tgz", + "integrity": "sha512-iWJzr4aN/IOsDSaF/kysM7tPSYj89hnzcWMKNuYN9redIwHgg7rNZ4toKhfNWYNfzxdhKwL9/Yvpf7bDemyc+Q==", + "dev": true, + "dependencies": { + "@tanstack/react-router-devtools": "1.131.42", + "clsx": "^2.1.1", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-router": "^1.131.41", + "csstype": "^3.0.10", + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + }, + "peerDependenciesMeta": { + "csstype": { + "optional": true + } + } + }, + "node_modules/@tanstack/router-devtools-core": { + "version": "1.131.42", + "resolved": "https://registry.npmjs.org/@tanstack/router-devtools-core/-/router-devtools-core-1.131.42.tgz", + "integrity": "sha512-o8jKTiwXcUSjmkozcMjIw1yhjVYeXcuQO7DtfgjKW3B85iveH6VzYK+bGEVU7wmLNMuUSe2eI/7RBzJ6a5+MCA==", + "dev": true, + "dependencies": { + "clsx": "^2.1.1", + "goober": "^2.1.16", + "solid-js": "^1.9.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/router-core": "^1.131.41", + "csstype": "^3.0.10", + "solid-js": ">=1.9.5", + "tiny-invariant": "^1.3.3" + }, + "peerDependenciesMeta": { + "csstype": { + "optional": true + } + } + }, + "node_modules/@tanstack/router-generator": { + "version": "1.133.15", + "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.133.15.tgz", + "integrity": "sha512-TXI07UzV5t1j1LeJ2eOErV9TxvzBRx2oSCEmkVaWMXaGKuQL7I4VB9e9w15ylHnvCO2Z/4DgIhUVF6h9/ZS3Mw==", + "dev": true, + "dependencies": { + "@tanstack/router-core": "1.133.15", + "@tanstack/router-utils": "1.133.3", + "@tanstack/virtual-file-routes": "1.133.3", + "prettier": "^3.5.0", + "recast": "^0.23.11", + "source-map": "^0.7.4", + "tsx": "^4.19.2", + "zod": "^3.24.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/router-generator/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@tanstack/router-plugin": { + "version": "1.133.15", + "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.133.15.tgz", + "integrity": "sha512-c3m7Pfuth/TXiRol0OpTw+cJyE7RxJpiMXDLooCiZgRDu2VhyXaanPLuuti9vyZiVdSrVZTQ7tJBFABymWbX5w==", + "dev": true, + "dependencies": { + "@babel/core": "^7.27.7", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.7", + "@babel/types": "^7.27.7", + "@tanstack/router-core": "1.133.15", + "@tanstack/router-generator": "1.133.15", + "@tanstack/router-utils": "1.133.3", + "@tanstack/virtual-file-routes": "1.133.3", + "babel-dead-code-elimination": "^1.0.10", + "chokidar": "^3.6.0", + "unplugin": "^2.1.2", + "zod": "^3.24.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@rsbuild/core": ">=1.0.2", + "@tanstack/react-router": "^1.133.15", + "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", + "vite-plugin-solid": "^2.11.8", + "webpack": ">=5.92.0" + }, + "peerDependenciesMeta": { + "@rsbuild/core": { + "optional": true + }, + "@tanstack/react-router": { + "optional": true + }, + "vite": { + "optional": true + }, + "vite-plugin-solid": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/@tanstack/router-plugin/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@tanstack/router-plugin/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@tanstack/router-plugin/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/@tanstack/router-utils": { + "version": "1.133.3", + "resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.133.3.tgz", + "integrity": "sha512-miPFlt0aG6ID5VDolYuRXgLS7cofvbZGMvHwf2Wmyxjo6GLp/kxxpkQrfM4T1I5cwjwYZZAQmdUKbVHwFZz9sQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/parser": "^7.27.5", + "@babel/preset-typescript": "^7.27.1", + "ansis": "^4.1.0", + "diff": "^8.0.2", + "pathe": "^2.0.3", + "tinyglobby": "^0.2.15" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/router-utils/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, + "node_modules/@tanstack/store": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.4.tgz", + "integrity": "sha512-F1XqZQici1Aq6WigEfcxJSml92nW+85Om8ElBMokPNg5glCYVOmPkZGIQeieYFxcPiKTfwo0MTOQpUyJtwncrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/virtual-file-routes": { + "version": "1.133.3", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.133.3.tgz", + "integrity": "sha512-6d2AP9hAjEi8mcIew2RkxBX+wClH1xedhfaYhs8fUiX+V2Cedk7RBD9E9ww2z6BGUYD8Es4fS0OIrzXZWHKGhw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "dev": true, + "dependencies": { + "undici-types": "~7.12.0" + } + }, + "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/react": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "dev": true, + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz", + "integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==", + "dev": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.1.0.tgz", + "integrity": "sha512-Ff690TUck0Anlh7wdIcnsVMhofeEVgm44Y4OYdeeEEPSKyZHzDI9gfVBvySEhDfXtBp8tLCbfsVKPWEMEjq8/g==", + "dev": true, + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.35", + "@swc/core": "^1.13.5" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/@zag-js/accordion": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/accordion/-/accordion-1.24.1.tgz", + "integrity": "sha512-JOlmXjO+1tTlyeZ93S+chIlV8uDr8fodj3/XCjLFHc/G116O8cN18KG0Ug9pImy1vT2Kkwb9Ag9QOTyUAXM3PA==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/anatomy": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/anatomy/-/anatomy-1.24.1.tgz", + "integrity": "sha512-mRkpetNjnjgvdyEX880AOjhMhcgdRMLjOM+aEgoDRnhultC4im+nriNoCShJLeVpwsRrEQCU7YVXO4mZaqWUMg==" + }, + "node_modules/@zag-js/angle-slider": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/angle-slider/-/angle-slider-1.24.1.tgz", + "integrity": "sha512-pcWIpVZDMbujMK0nFaKa0wd7uGkP4E5D7x8cmvoiKMT4E1vZpg2kZeN9qmdnhum9ye7nb80IPKhcDl9C0JuSLw==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/rect-utils": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/aria-hidden": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/aria-hidden/-/aria-hidden-1.24.1.tgz", + "integrity": "sha512-R/a80ZjITZi4rotN7Q9+RTCYCdmJZf3rZi9bObczbR7h5j5GSsjikByUjksWAYzPvFxQxBWTs4GqlCI9dU2f9A==" + }, + "node_modules/@zag-js/async-list": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/async-list/-/async-list-1.24.1.tgz", + "integrity": "sha512-EZE3wORLOhMtT1tiDA0kTHrtY7XNkOoNyn5jCs8Ec1GfqIHSRzQB+2jt+wPIBwUhDcgQksXgOy91s/i9XfQe1g==", + "dependencies": { + "@zag-js/core": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/auto-resize": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/auto-resize/-/auto-resize-1.24.1.tgz", + "integrity": "sha512-OH1VTeObddMiN2PUK+7SpkPV8Znlkdq+10odmbbe9K2MZPh352RNcPYytIZTWT0X4/4czhn2MTU6IhZ2lZp2hw==", + "dependencies": { + "@zag-js/dom-query": "1.24.1" + } + }, + "node_modules/@zag-js/avatar": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/avatar/-/avatar-1.24.1.tgz", + "integrity": "sha512-zYGUdkxsMoN8OAFYYCZBrsQx++kjWEBdYZew4en9g8vw7yonNjzywtfF/Vd3Dv6mUZ2r5JtaltbK/qp4aBdZvg==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/carousel": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/carousel/-/carousel-1.24.1.tgz", + "integrity": "sha512-7WGlFtF4JoIK4kduiFgucdTe9eD+884d9BF9Sh308MlpiL0KZnO3l3Pyq58yi4R0KUTy7zILLGSsUesifVAuEA==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/scroll-snap": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/checkbox": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/checkbox/-/checkbox-1.24.1.tgz", + "integrity": "sha512-eU/RKaO44Tgt1iTGg26M2nUd12p+gTuq2rNjqVuPfN3dvRzYNi5rGKk6yTQI2T4DH4D+fDMz6gUneiBuGcVoJA==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/focus-visible": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/clipboard": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/clipboard/-/clipboard-1.24.1.tgz", + "integrity": "sha512-GfmjjiEDS9NB6Wo/ThbbzO10BgOYzTSeG00a/pJ5QpvSgvOCz+oLV5NBQHOd8XjOw0e0GQEyfsif0i6wQExSIQ==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/collapsible": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/collapsible/-/collapsible-1.24.1.tgz", + "integrity": "sha512-U6AP4nE6jwMC3kirFQmOL9i3CSfp8mJqb+Gv3opbClpjqCa8hn9v4PNiimKmd0Qr3kynuVRpAshaUaLeg33YiA==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/collection": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/collection/-/collection-1.24.1.tgz", + "integrity": "sha512-aWNDI0iZ5Wb8vCZLJWPjRQOK5/B2wvhhR1+pYaScxZfWy2das2DVKam8tnR0p1GrRfBi/kZNaCXtvM1ZNPjlOQ==", + "dependencies": { + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/color-picker": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/color-picker/-/color-picker-1.24.1.tgz", + "integrity": "sha512-vLW11JrySJR5fGeXXdmlCJuNm7yE0Tsx/SjkX0WBnrPC4PYaGfiwF7LT59bs5XsQp65kEaIca6mw/J0Bouc8Sw==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/color-utils": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/color-utils": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/color-utils/-/color-utils-1.24.1.tgz", + "integrity": "sha512-8KPTa3I9+WbDLrYPH5knEYMW3CjAC20ikosdrgYshGTFIPuqinAnsxD7H0fZO4I+jSjuhtIyNQuvgwJar9A2rg==", + "dependencies": { + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/combobox": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/combobox/-/combobox-1.24.1.tgz", + "integrity": "sha512-BhjQOL/Ssr5lQLPCyEersCqOqllFlNuR8nvQOgl1u8Y0EaZR+ZPQbgXum6kE5AuH3SlcY9+1kDK1ZLswOagL1Q==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/aria-hidden": "1.24.1", + "@zag-js/collection": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/core": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/core/-/core-1.24.1.tgz", + "integrity": "sha512-0e7QdxBaY9PMHQfDY/Xu/7MKyRxNsriNscpkZI7L4MHMGPmxdfedGBpteI3gFfqWsdJ5NvvpqxdLUwkbYk5Q5A==", + "dependencies": { + "@zag-js/dom-query": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/date-picker": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/date-picker/-/date-picker-1.24.1.tgz", + "integrity": "sha512-8jLv074sGJQw4L+5YTDv7l2bwb1x9E7YhvklCffhf/7OzW7RB/ELkljFhmjueuJp7W/sD4xhJyigjp/mDEg1XA==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/date-utils": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/live-region": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + }, + "peerDependencies": { + "@internationalized/date": ">=3.0.0" + } + }, + "node_modules/@zag-js/date-utils": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/date-utils/-/date-utils-1.24.1.tgz", + "integrity": "sha512-Rgll6P4Imq479WxH3uMvwQri4o4lF2cxWX2Hka/W7Nhv1DhPBnmfBw30INyWPXzx5agEVzKdGX/br8MU5DV33Q==", + "peerDependencies": { + "@internationalized/date": ">=3.0.0" + } + }, + "node_modules/@zag-js/dialog": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/dialog/-/dialog-1.24.1.tgz", + "integrity": "sha512-ITzOoXBC92vIkhNvxM0GMMKwboLLk7hSU9dsplk/X9bpX+fQywgc6d5O4I7WHCMmgUWI5y3/aWjqsWATWwufWg==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/aria-hidden": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/focus-trap": "1.24.1", + "@zag-js/remove-scroll": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/dismissable": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/dismissable/-/dismissable-1.24.1.tgz", + "integrity": "sha512-Oca+nbwaqHGt0rmkKfmpExwL+kVYLbVi6fxhzHP1WBrip//IUThoTrPH/gqB51o1DT1z/VNE+8BhWhsHSgkQfw==", + "dependencies": { + "@zag-js/dom-query": "1.24.1", + "@zag-js/interact-outside": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/dom-query": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/dom-query/-/dom-query-1.24.1.tgz", + "integrity": "sha512-ww3tS5hrB2s6ywGtjMjSOajP19CnQOH0IAGgzjE+lbvDD+ZroXWn9O3Z/v2kTfKNwZFQ4TOb8oSymuSRQsFOYg==", + "dependencies": { + "@zag-js/types": "1.24.1" + } + }, + "node_modules/@zag-js/editable": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/editable/-/editable-1.24.1.tgz", + "integrity": "sha512-SV8X7jd95ZAx4VnlhoEcbAiW8jhoGkPf7L0JFB2KWX+NFacEVCKGQpDjZpdzD6j7C10750v3blbkjr6iyzeIqw==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/interact-outside": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/file-upload": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/file-upload/-/file-upload-1.24.1.tgz", + "integrity": "sha512-Un0+qDlkoC93pf7/Nvq9DBVKR6PBKybbNE/En/PC4XLJybK448bY85UuEdBPgXEoR6hIGA3t8NdeHZ+PUoZXIw==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/file-utils": "1.24.1", + "@zag-js/i18n-utils": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/file-utils": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/file-utils/-/file-utils-1.24.1.tgz", + "integrity": "sha512-ydMct0iyd4uPxf+NP4gfyPq1gJlvW29WWIm5ez9El9L+z5tDBhXYNc73s2kSdDBKXkO4fp6Mwoqbz/wZOw99/Q==", + "dependencies": { + "@zag-js/i18n-utils": "1.24.1" + } + }, + "node_modules/@zag-js/floating-panel": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/floating-panel/-/floating-panel-1.24.1.tgz", + "integrity": "sha512-qVVtnKCQE2C//0q7utRvpfRKsZedL8gnSqwHDX4ie8nKmLLSLn6jDGuAzxrscsGPHEjCOru9NlTHlAAMtB3ybQ==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/rect-utils": "1.24.1", + "@zag-js/store": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/focus-trap": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/focus-trap/-/focus-trap-1.24.1.tgz", + "integrity": "sha512-cpgYWWaiKx9eycm4Mahv6Dng5+CbDiTtyz/gnbZUv6sqcM4b9N+UqdmBdWYPLHV4gZYrzuO+X4P1C/Ew/rA+xg==", + "dependencies": { + "@zag-js/dom-query": "1.24.1" + } + }, + "node_modules/@zag-js/focus-visible": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/focus-visible/-/focus-visible-1.24.1.tgz", + "integrity": "sha512-HzUf8cRl5tbIil6rVe24CxC3s1pdFGpfYSt5NyaFoFd0HuWhobp+De1kVUvlLU0DDUU6Kgw6DB1w8APEPzb8gg==", + "dependencies": { + "@zag-js/dom-query": "1.24.1" + } + }, + "node_modules/@zag-js/highlight-word": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/highlight-word/-/highlight-word-1.24.1.tgz", + "integrity": "sha512-paDF/sWKDMMclpCzrG60vD4/AFQ3EOu2lzQxl7S21uD/B8Rir4w1CkxK/9+cm1Bu7mj4mkR4t+VJxycEZ7YuIw==" + }, + "node_modules/@zag-js/hover-card": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/hover-card/-/hover-card-1.24.1.tgz", + "integrity": "sha512-zXTcLEb8YOFoEjDMsMcxqidRDN2fY0C94j+XdZYj5eZtKBIgbyCyAjvZrEu9yyPqqrXCNwYU0fTFjac3t9IV4g==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/i18n-utils": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/i18n-utils/-/i18n-utils-1.24.1.tgz", + "integrity": "sha512-dI9M73FTJcE40s/TPBLLKsypmBoMNe5NoRSBW64PWdmn0fCq65qcAUMgwQ0MVenh4oofoDYyffl8pIStr8T1tA==", + "dependencies": { + "@zag-js/dom-query": "1.24.1" + } + }, + "node_modules/@zag-js/interact-outside": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/interact-outside/-/interact-outside-1.24.1.tgz", + "integrity": "sha512-xKyGT295WVrlJaOPCVBrundlXqL4YEvl36SHNSi7EZs/AYpzxR/aBtnFCRN1/7nWvdqvfGs7ya0kl/ly0H7VBg==", + "dependencies": { + "@zag-js/dom-query": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/json-tree-utils": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/json-tree-utils/-/json-tree-utils-1.24.1.tgz", + "integrity": "sha512-TWVg+Y4fLr9o0YaB3OnX4xmV91Te/vzRwnNKntsz3GIWJ5fLNngg4hm3E+eaYnJIlKMHrvJv4T/UB4IGYUF+EQ==" + }, + "node_modules/@zag-js/listbox": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/listbox/-/listbox-1.24.1.tgz", + "integrity": "sha512-fTJ125SWVZ+NxgkT6s8LWpdJQMeADk9Lm+Ur1pi0mZnRCmuHI3nwPkg1dfqynjVyrKs6P8wBmUxt3hlr2cc6TQ==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/collection": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/focus-visible": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/live-region": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/live-region/-/live-region-1.24.1.tgz", + "integrity": "sha512-A/55dOyRhfdgVtCBP05Uf2UGz/58H0TMWP69GdVYM4uADtfCLNPy6yxHAt9p334qJsWicg/YWSzBdEAVTThNag==" + }, + "node_modules/@zag-js/menu": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/menu/-/menu-1.24.1.tgz", + "integrity": "sha512-XPNQbkIxSbNuYNLLQZlgXbj6Ptn2XHT5BXkUSw2hSbIg35S7Lq8gckiZVtxmUiX8zbv7krTBSD7zThSnwx1TOA==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/rect-utils": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/number-input": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/number-input/-/number-input-1.24.1.tgz", + "integrity": "sha512-F5nX0VvuRmSxddJ8byHYp4OSHLU1C5Fv1rT4L1AnSXud8q6C+zCy4Vy8772pUKNobZf0q8Ru4SgnOe5TQcvRpg==", + "dependencies": { + "@internationalized/number": "3.6.5", + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/pagination": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/pagination/-/pagination-1.24.1.tgz", + "integrity": "sha512-IO9Q5SiYmk00pjJAD18qFjOkpN1qb9iSeuX6A9Bdo8sMBFSigI6c7tGo1MPYGENma3b+aX7LbUpt8hYFufqUow==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/password-input": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/password-input/-/password-input-1.24.1.tgz", + "integrity": "sha512-TWgTRNsaAZ6IE1QmCQKhPY6uSRPDGjgdxGSpG7wOuYsbxHw/hD3v5sUAhAo9teIL0wV8COZIh6hyG2UAAgT2kg==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/pin-input": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/pin-input/-/pin-input-1.24.1.tgz", + "integrity": "sha512-ytJK/1ekU06VmOpe7KdSkIQ3If+fffrA/EpbktZBuRepsz80QHB64+X6QQ6H1lEMbLWPNZ0TuFPaYhFfqH7cTQ==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/popover": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/popover/-/popover-1.24.1.tgz", + "integrity": "sha512-auNy7/5/VMeNUYbKfcvSz7OHkbrUWdODtA6gB/d/weAxvEHyMSk0+Ms4c5lmN8KDChrBAPJs4CfKSPv1U4I4zw==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/aria-hidden": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/focus-trap": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/remove-scroll": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/popper": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/popper/-/popper-1.24.1.tgz", + "integrity": "sha512-VWbOjBy/haIDmXhwfyMT1rRcQhSfYmPX67YzQwLA7863kXkoTH1r9fR+1f9uq3VuXQLhw2Cg/lkSzlkg9TIp+g==", + "dependencies": { + "@floating-ui/dom": "1.7.4", + "@zag-js/dom-query": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/presence": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/presence/-/presence-1.24.1.tgz", + "integrity": "sha512-MMcw4iOsGdSGM3hmvd0gcMuk1X9rE/xE3Ndm113vc+lkhk93COiuJPz1ZpyBb8l1CIJwlZ5nnRpx4Lx8Do6aNQ==", + "dependencies": { + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1" + } + }, + "node_modules/@zag-js/progress": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/progress/-/progress-1.24.1.tgz", + "integrity": "sha512-ocp6zkl5Y3sVMzPVIRLZtqtDfMkc365JYIrOUsdUqwJMvZJhSP1IbsbtIJS1ycOaHfLdK27E//GVyjxA7SHGhw==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/qr-code": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/qr-code/-/qr-code-1.24.1.tgz", + "integrity": "sha512-Hy722PNwLs1tnXFQkTqtrEILypZcUDiC8YdvGn57mmmvPGtZdAzhs4G8ghoP9ahJ02ztREjIt8Qnmct344fALA==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1", + "proxy-memoize": "3.0.1", + "uqr": "0.1.2" + } + }, + "node_modules/@zag-js/radio-group": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/radio-group/-/radio-group-1.24.1.tgz", + "integrity": "sha512-49S+nmaZzjf98206VeevmfTNTf+WjLveKCOGz5SVWPX3R8maZJgka1ZlIDuWlnRK1JfL+4Ls10/ZxAk3HrI7sg==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/focus-visible": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/rating-group": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/rating-group/-/rating-group-1.24.1.tgz", + "integrity": "sha512-EGGObQDmulon5N9s5ElGZv9yQmky10s7ps7wyVgW1+vJTsWr8gaoFMJwf6nbXOsUjqW8iDuzsF68Rel9CgjxIQ==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/react": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/react/-/react-1.24.1.tgz", + "integrity": "sha512-oiaiuR7FKVHOEJtzoYZ2QBQ5+J/j086eebhLCIWkh2ie6QBJM73LHsMUxfZp2D2G1is8EoyUhrH3v2MPMlYMXg==", + "dependencies": { + "@zag-js/core": "1.24.1", + "@zag-js/store": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@zag-js/rect-utils": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/rect-utils/-/rect-utils-1.24.1.tgz", + "integrity": "sha512-6JkVq71feW9Yyt7Pynyf199ugDFVgRT+jPpg2ECRHgY2oHvn5atBP3PA1uM2cx7ZydiajnBgk4n1ePnGYD2xNw==" + }, + "node_modules/@zag-js/remove-scroll": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/remove-scroll/-/remove-scroll-1.24.1.tgz", + "integrity": "sha512-SAK3ZsnDUcJve5q3OHsMjrl0JOW9sv1fGbBFXyyid9Uu8s79LMh7EZw2na5jXDNzdMWmk1Euu82OaZSlLl9Kew==", + "dependencies": { + "@zag-js/dom-query": "1.24.1" + } + }, + "node_modules/@zag-js/scroll-area": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/scroll-area/-/scroll-area-1.24.1.tgz", + "integrity": "sha512-eRZKs6Yyl8Zp+YkIxzr1QsgRDDsNMxXshwpIzt/L5xK+EV34mv760FOkX/unG/WxQ1Z0gBogPm9ZY53/m4bhJA==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/scroll-snap": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/scroll-snap/-/scroll-snap-1.24.1.tgz", + "integrity": "sha512-Co/NlccX4XDg6OzQeRgv8bANbsCkMog1FZ0BveN8+2Mso/svOLVkB6UGswWZk/DyqY8DlxvfZAdPltmQpu5h8w==", + "dependencies": { + "@zag-js/dom-query": "1.24.1" + } + }, + "node_modules/@zag-js/select": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/select/-/select-1.24.1.tgz", + "integrity": "sha512-boU5m3Qd//EGe1M2i4a2SbCXQpcPP9Ewe6DvjEpOhxP+dwdbZzDrtRBdZ4ByhMJ+1bT5B6TqsfvsQHhAI0LunA==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/collection": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/signature-pad": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/signature-pad/-/signature-pad-1.24.1.tgz", + "integrity": "sha512-CRTcefUGMwdhxqmB8yGkHU3gweMfXw0CCoMc0LhMmla12hMJOBi+mpMVaBJnQHYGSG8uFUh2IKdPbe2Vtp4T3Q==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1", + "perfect-freehand": "^1.2.2" + } + }, + "node_modules/@zag-js/slider": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/slider/-/slider-1.24.1.tgz", + "integrity": "sha512-HClZBKcT+9tihZArRNRj35YOIUbztCcyYzggYYIrK4+OFD0RLYihA+yBO4hxs7xZVenzma9i0pc6q/Vo4z2tvA==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/splitter": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/splitter/-/splitter-1.24.1.tgz", + "integrity": "sha512-UUqiCD0T8kfgm/vRTY1QrPlrpxbzxqZ+8QvysUchnibmStetkHnuzAXC4ZD9jlJbToqzE4p1eLOiWGaVXRdB/Q==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/steps": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/steps/-/steps-1.24.1.tgz", + "integrity": "sha512-njL1SMKef0JfYzw5KUhpeVuzOtgBjSxVUwDrPR9s095WUCUiOYlxzqummg3VBY8IDuT/pS/K6LDSY11YCRzeNw==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/store": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/store/-/store-1.24.1.tgz", + "integrity": "sha512-iVl+NX2CcxEDLL3hrj31mqSqBZYBqHEBqa/Z7FwKVoTImMQ1AabMF5XPreTtB8KFbaVJlNlM6D5qngDPpVj/xw==", + "dependencies": { + "proxy-compare": "3.0.1" + } + }, + "node_modules/@zag-js/switch": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/switch/-/switch-1.24.1.tgz", + "integrity": "sha512-RI2bG2AtsQ4ci8T7RA3XVSjd9urpNQXIwEatpa8cw9GCWFI421rt4Xcab5jy/IOu6VzXl6pwh11/cWAC/PBYCw==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/focus-visible": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/tabs": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/tabs/-/tabs-1.24.1.tgz", + "integrity": "sha512-RjdW4opxhvCWTwHoCqq+lfNCthiyPu376hto6j4Ybl/UN3UFTV4zfTbwbMbAH7dyqj8m1nkKxidLaO0Yhx3zZA==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/tags-input": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/tags-input/-/tags-input-1.24.1.tgz", + "integrity": "sha512-HY1ebBZE2j3/fuzfKw4z/44S9WWe50auMWLlFg47j6zVBcyNdXEeMO1OvvfyfQFJOcvOKXGxW8Hi4MXGxLWqmA==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/auto-resize": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/interact-outside": "1.24.1", + "@zag-js/live-region": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/timer": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/timer/-/timer-1.24.1.tgz", + "integrity": "sha512-cjD8+I8CgSugsj5DI+kqzgvuQ2vYeArRdjO3iSjB4AjR+j08W8NKZvr7aawhYq636vrE9LeJGbxxZ3DBV12ELw==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/toast": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/toast/-/toast-1.24.1.tgz", + "integrity": "sha512-gmHv65EYdypfMoF9WYIp7Y8z6XN5tebXEdjIWF8bJBaqW5zPn2VLdUYpfXv7wrHW2YtSTnF/xtgIhJ7MIX7HxA==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/toggle": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/toggle/-/toggle-1.24.1.tgz", + "integrity": "sha512-dMN9Q4XFqr7jPlUZsLCFdUc1rtW88FzUaXcFVaeNCy8y8XGc+MG9AJJqjBiBL9EUeeR+LIp8yUIhJQEEDBm0kw==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/toggle-group": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/toggle-group/-/toggle-group-1.24.1.tgz", + "integrity": "sha512-GVBay9XzmXjp1GgAmHUMpeYq3iMMevH+n0TyC0NcRe00prAEL9S4/q9pVy0P33PIOa20dxcvQ/Q3Tf+n5PFQcg==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/tooltip": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/tooltip/-/tooltip-1.24.1.tgz", + "integrity": "sha512-gdD5C9AF6JD8LC6mxXzUGWjnHqY3MS7ZvtNx/nuNGJAqKCD32dPT73fuv0up1UVh1yJhX4IrXg3H6q52Pm+jPw==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/focus-visible": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/tour": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/tour/-/tour-1.24.1.tgz", + "integrity": "sha512-e+UR8xauKyRhE6tA8gRsR1GuOn1QGjj2YAmtRC8lIb5tD+QrGCPy0jX2xBeR7M7eY1IPSSyi0gCUGE2CbaRK8Q==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/focus-trap": "1.24.1", + "@zag-js/interact-outside": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/tree-view": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/tree-view/-/tree-view-1.24.1.tgz", + "integrity": "sha512-HXCoqW6j2RunFxaIVRevgRTrRUEP05lpdOvc1Smzne7sC2mczwIqN68Vei6e83gRhXSF80v6Fc4TcHdPiW6wJA==", + "dependencies": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/collection": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "node_modules/@zag-js/types": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/types/-/types-1.24.1.tgz", + "integrity": "sha512-XyINtxe5JK7A+RtTmBdCQElNoElDiTw6NSWpjKZGRAXXGU9HIZ9JIFeaS77uq1aVs0JhAOFwqJiPs2NJzaYHLA==", + "dependencies": { + "csstype": "3.1.3" + } + }, + "node_modules/@zag-js/utils": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/utils/-/utils-1.24.1.tgz", + "integrity": "sha512-4nU9lfFlLLW/4T+/HaP+HdHYFeWvacxSVcccv0JSf+ZTC110IldV48kZELP+wFg9xDL/jCPPjlRtO1K64EIwgA==" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "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.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-dead-code-elimination": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.10.tgz", + "integrity": "sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" + } + }, + "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/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/c12": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/c12/-/c12-2.0.1.tgz", + "integrity": "sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.1", + "confbox": "^0.1.7", + "defu": "^6.1.4", + "dotenv": "^16.4.5", + "giget": "^1.2.3", + "jiti": "^2.3.0", + "mlly": "^1.7.1", + "ohash": "^1.1.4", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/caniuse-lite": { + "version": "1.0.30001741", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", + "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "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/commander": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.0.0.tgz", + "integrity": "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "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/cookie-es": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz", + "integrity": "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==" + }, + "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/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true, + "license": "MIT" + }, + "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/destr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", + "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", + "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.214", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.214.tgz", + "integrity": "sha512-TpvUNdha+X3ybfU78NoQatKvQEm1oq3lf2QbnmCEdw+Bd9RuIAY+hJTvq1avzHM0f7EJfnH3vbCnbzKzisc/9Q==", + "dev": true, + "license": "ISC" + }, + "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/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", + "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.3", + "@esbuild/android-arm": "0.25.3", + "@esbuild/android-arm64": "0.25.3", + "@esbuild/android-x64": "0.25.3", + "@esbuild/darwin-arm64": "0.25.3", + "@esbuild/darwin-x64": "0.25.3", + "@esbuild/freebsd-arm64": "0.25.3", + "@esbuild/freebsd-x64": "0.25.3", + "@esbuild/linux-arm": "0.25.3", + "@esbuild/linux-arm64": "0.25.3", + "@esbuild/linux-ia32": "0.25.3", + "@esbuild/linux-loong64": "0.25.3", + "@esbuild/linux-mips64el": "0.25.3", + "@esbuild/linux-ppc64": "0.25.3", + "@esbuild/linux-riscv64": "0.25.3", + "@esbuild/linux-s390x": "0.25.3", + "@esbuild/linux-x64": "0.25.3", + "@esbuild/netbsd-arm64": "0.25.3", + "@esbuild/netbsd-x64": "0.25.3", + "@esbuild/openbsd-arm64": "0.25.3", + "@esbuild/openbsd-x64": "0.25.3", + "@esbuild/sunos-x64": "0.25.3", + "@esbuild/win32-arm64": "0.25.3", + "@esbuild/win32-ia32": "0.25.3", + "@esbuild/win32-x64": "0.25.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-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/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.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.3.tgz", + "integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.3", + "nypm": "^0.3.8", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "tar": "^6.2.0" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "dev": true, + "peerDependencies": { + "csstype": "^3.0.10" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "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/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.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/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-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isbot": { + "version": "5.1.30", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.30.tgz", + "integrity": "sha512-3wVJEonAns1OETX83uWsk5IAne2S5zfDcntD2hbtU23LelSqNXzXs9zKjMPOLMzroCgIjCfjYAEHrd2D6FOkiA==", + "license": "Unlicense", + "engines": { + "node": ">=18" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.0.tgz", + "integrity": "sha512-H5UpaUI+aHOqZXlYOaFP/8AzKsg+guWu+Pr3Y8i7+Y3zr1aXAvCvTAQ1RxSc6oVD8R8c7brgNtTVP91E7upH/g==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru-cache/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "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/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mlly": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.3.tgz", + "integrity": "sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "pathe": "^1.1.2", + "pkg-types": "^1.2.1", + "ufo": "^1.5.4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "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/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", + "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz", + "integrity": "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.12.tgz", + "integrity": "sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "execa": "^8.0.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/ohash": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz", + "integrity": "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "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-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "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/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/perfect-freehand": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.2.2.tgz", + "integrity": "sha512-eh31l019WICQ03pkF3FSzHxB8n07ItqIQ++G5UV8JX0zVOXzgTGCqnRR0jJ2h9U8/2uW4W4mtGJELt9kEV0CFQ==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.1.tgz", + "integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.2", + "pathe": "^1.1.2" + } + }, + "node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "dev": true, + "license": "Apache-2.0", + "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, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proxy-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz", + "integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==" + }, + "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/proxy-memoize": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/proxy-memoize/-/proxy-memoize-3.0.1.tgz", + "integrity": "sha512-VDdG/VYtOgdGkWJx7y0o7p+zArSf2383Isci8C+BP3YXgMYDoPd3cCBjw0JdWb6YBb9sFiOPbAADDVTPJnh+9g==", + "dependencies": { + "proxy-compare": "^3.0.0" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-error-boundary": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.0.0.tgz", + "integrity": "sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, + "node_modules/react-hook-form": { + "version": "7.62.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", + "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "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/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "dev": true, + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "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/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz", + "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.50.1", + "@rollup/rollup-android-arm64": "4.50.1", + "@rollup/rollup-darwin-arm64": "4.50.1", + "@rollup/rollup-darwin-x64": "4.50.1", + "@rollup/rollup-freebsd-arm64": "4.50.1", + "@rollup/rollup-freebsd-x64": "4.50.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", + "@rollup/rollup-linux-arm-musleabihf": "4.50.1", + "@rollup/rollup-linux-arm64-gnu": "4.50.1", + "@rollup/rollup-linux-arm64-musl": "4.50.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", + "@rollup/rollup-linux-ppc64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-musl": "4.50.1", + "@rollup/rollup-linux-s390x-gnu": "4.50.1", + "@rollup/rollup-linux-x64-gnu": "4.50.1", + "@rollup/rollup-linux-x64-musl": "4.50.1", + "@rollup/rollup-openharmony-arm64": "4.50.1", + "@rollup/rollup-win32-arm64-msvc": "4.50.1", + "@rollup/rollup-win32-ia32-msvc": "4.50.1", + "@rollup/rollup-win32-x64-msvc": "4.50.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/seroval": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", + "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.3.tgz", + "integrity": "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/solid-js": { + "version": "1.9.9", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.9.tgz", + "integrity": "sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA==", + "dev": true, + "dependencies": { + "csstype": "^3.1.0", + "seroval": "~1.3.0", + "seroval-plugins": "~1.3.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.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.20.5", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", + "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "dev": true, + "license": "MIT" + }, + "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": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "dev": true + }, + "node_modules/unplugin": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.10.tgz", + "integrity": "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uqr": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.2.tgz", + "integrity": "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==" + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "7.1.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", + "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", + "dev": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/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/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + }, + "dependencies": { + "@ark-ui/react": { + "version": "5.24.1", + "resolved": "https://registry.npmjs.org/@ark-ui/react/-/react-5.24.1.tgz", + "integrity": "sha512-Czx6pLRJzs8G9t8XCvBlizd1aGRC7KOyUGJgK5a1vUDz8WhAALUUm66yslhs7GfU4/jJX8mS73FwStgtK0znAg==", + "requires": { + "@internationalized/date": "3.9.0", + "@zag-js/accordion": "1.24.1", + "@zag-js/anatomy": "1.24.1", + "@zag-js/angle-slider": "1.24.1", + "@zag-js/async-list": "1.24.1", + "@zag-js/auto-resize": "1.24.1", + "@zag-js/avatar": "1.24.1", + "@zag-js/carousel": "1.24.1", + "@zag-js/checkbox": "1.24.1", + "@zag-js/clipboard": "1.24.1", + "@zag-js/collapsible": "1.24.1", + "@zag-js/collection": "1.24.1", + "@zag-js/color-picker": "1.24.1", + "@zag-js/color-utils": "1.24.1", + "@zag-js/combobox": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/date-picker": "1.24.1", + "@zag-js/date-utils": "1.24.1", + "@zag-js/dialog": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/editable": "1.24.1", + "@zag-js/file-upload": "1.24.1", + "@zag-js/file-utils": "1.24.1", + "@zag-js/floating-panel": "1.24.1", + "@zag-js/focus-trap": "1.24.1", + "@zag-js/highlight-word": "1.24.1", + "@zag-js/hover-card": "1.24.1", + "@zag-js/i18n-utils": "1.24.1", + "@zag-js/json-tree-utils": "1.24.1", + "@zag-js/listbox": "1.24.1", + "@zag-js/menu": "1.24.1", + "@zag-js/number-input": "1.24.1", + "@zag-js/pagination": "1.24.1", + "@zag-js/password-input": "1.24.1", + "@zag-js/pin-input": "1.24.1", + "@zag-js/popover": "1.24.1", + "@zag-js/presence": "1.24.1", + "@zag-js/progress": "1.24.1", + "@zag-js/qr-code": "1.24.1", + "@zag-js/radio-group": "1.24.1", + "@zag-js/rating-group": "1.24.1", + "@zag-js/react": "1.24.1", + "@zag-js/scroll-area": "1.24.1", + "@zag-js/select": "1.24.1", + "@zag-js/signature-pad": "1.24.1", + "@zag-js/slider": "1.24.1", + "@zag-js/splitter": "1.24.1", + "@zag-js/steps": "1.24.1", + "@zag-js/switch": "1.24.1", + "@zag-js/tabs": "1.24.1", + "@zag-js/tags-input": "1.24.1", + "@zag-js/timer": "1.24.1", + "@zag-js/toast": "1.24.1", + "@zag-js/toggle": "1.24.1", + "@zag-js/toggle-group": "1.24.1", + "@zag-js/tooltip": "1.24.1", + "@zag-js/tour": "1.24.1", + "@zag-js/tree-view": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "requires": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true + }, + "@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "dependencies": { + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "requires": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "requires": { + "@babel/types": "^7.27.3" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.3", + "semver": "^6.3.1" + } + }, + "@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==" + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "requires": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + } + }, + "@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "requires": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + } + }, + "@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "requires": { + "@babel/types": "^7.27.1" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true + }, + "@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "requires": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + } + }, + "@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==" + }, + "@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==" + }, + "@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true + }, + "@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "requires": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + } + }, + "@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "requires": { + "@babel/types": "^7.28.4" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-typescript": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", + "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + } + }, + "@babel/preset-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1" + } + }, + "@babel/runtime": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + } + }, + "@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + } + }, + "@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "requires": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + } + }, + "@biomejs/biome": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.2.4.tgz", + "integrity": "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg==", + "dev": true, + "requires": { + "@biomejs/cli-darwin-arm64": "2.2.4", + "@biomejs/cli-darwin-x64": "2.2.4", + "@biomejs/cli-linux-arm64": "2.2.4", + "@biomejs/cli-linux-arm64-musl": "2.2.4", + "@biomejs/cli-linux-x64": "2.2.4", + "@biomejs/cli-linux-x64-musl": "2.2.4", + "@biomejs/cli-win32-arm64": "2.2.4", + "@biomejs/cli-win32-x64": "2.2.4" + } + }, + "@biomejs/cli-darwin-arm64": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.4.tgz", + "integrity": "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA==", + "dev": true, + "optional": true + }, + "@biomejs/cli-darwin-x64": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.4.tgz", + "integrity": "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg==", + "dev": true, + "optional": true + }, + "@biomejs/cli-linux-arm64": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.4.tgz", + "integrity": "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw==", + "dev": true, + "optional": true + }, + "@biomejs/cli-linux-arm64-musl": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.4.tgz", + "integrity": "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ==", + "dev": true, + "optional": true + }, + "@biomejs/cli-linux-x64": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.4.tgz", + "integrity": "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ==", + "dev": true, + "optional": true + }, + "@biomejs/cli-linux-x64-musl": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.4.tgz", + "integrity": "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg==", + "dev": true, + "optional": true + }, + "@biomejs/cli-win32-arm64": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.4.tgz", + "integrity": "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ==", + "dev": true, + "optional": true + }, + "@biomejs/cli-win32-x64": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.4.tgz", + "integrity": "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg==", + "dev": true, + "optional": true + }, + "@chakra-ui/react": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-3.27.0.tgz", + "integrity": "sha512-M1WTAErI2cYM/PB4h5Kf5CCAg70g3HCzVvTEhcf5ty8QrG6QybPf3RdWSpBlIy7qpjuEnQYpHLxM0jnFLArBgA==", + "requires": { + "@ark-ui/react": "^5.24.1", + "@emotion/is-prop-valid": "^1.3.1", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@pandacss/is-valid-prop": "^0.54.0", + "csstype": "^3.1.3", + "fast-safe-stringify": "^2.1.1" + } + }, + "@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "requires": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "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.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "requires": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" + }, + "@emotion/is-prop-valid": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "requires": { + "@emotion/memoize": "^0.9.0" + } + }, + "@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, + "@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "requires": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + } + }, + "@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "requires": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" + }, + "@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" + }, + "@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "requires": {} + }, + "@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==" + }, + "@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + }, + "@esbuild/aix-ppc64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", + "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", + "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", + "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", + "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", + "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", + "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", + "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", + "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", + "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", + "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", + "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", + "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", + "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", + "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", + "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", + "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", + "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", + "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", + "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", + "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", + "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", + "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", + "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", + "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", + "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", + "dev": true, + "optional": true + }, + "@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "requires": { + "@floating-ui/utils": "^0.2.10" + } + }, + "@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "requires": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" + }, + "@hey-api/json-schema-ref-parser": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.0.6.tgz", + "integrity": "sha512-yktiFZoWPtEW8QKS65eqKwA5MTKp88CyiL8q72WynrBs/73SAaxlSWlA2zW/DZlywZ5hX1OYzrCC0wFdvO9c2w==", + "dev": true, + "requires": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21" + } + }, + "@hey-api/openapi-ts": { + "version": "0.73.0", + "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.73.0.tgz", + "integrity": "sha512-sUscR3OIGW0k9U//28Cu6BTp3XaogWMDORj9H+5Du9E5AvTT7LZbCEDvkLhebFOPkp2cZAQfd66HiZsiwssBcQ==", + "dev": true, + "requires": { + "@hey-api/json-schema-ref-parser": "1.0.6", + "ansi-colors": "4.1.3", + "c12": "2.0.1", + "color-support": "1.1.3", + "commander": "13.0.0", + "handlebars": "4.7.8", + "open": "10.1.2" + } + }, + "@internationalized/date": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.9.0.tgz", + "integrity": "sha512-yaN3brAnHRD+4KyyOsJyk49XUvj2wtbNACSqg0bz3u8t2VuzhC8Q5dfRnrSxjnnbDb+ienBnkn1TzQfE154vyg==", + "requires": { + "@swc/helpers": "^0.5.0" + } + }, + "@internationalized/number": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.5.tgz", + "integrity": "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==", + "requires": { + "@swc/helpers": "^0.5.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@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 + }, + "@pandacss/is-valid-prop": { + "version": "0.54.0", + "resolved": "https://registry.npmjs.org/@pandacss/is-valid-prop/-/is-valid-prop-0.54.0.tgz", + "integrity": "sha512-UhRgg1k9VKRCBAHl+XUK3lvN0k9bYifzYGZOqajDid4L1DyU813A1L0ZwN4iV9WX5TX3PfUugqtgG9LnIeFGBQ==" + }, + "@playwright/test": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "dev": true, + "requires": { + "playwright": "1.56.1" + } + }, + "@rolldown/pluginutils": { + "version": "1.0.0-beta.35", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.35.tgz", + "integrity": "sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg==", + "dev": true + }, + "@rollup/rollup-android-arm-eabi": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz", + "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz", + "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz", + "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz", + "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz", + "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-x64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz", + "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz", + "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-musleabihf": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz", + "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz", + "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz", + "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz", + "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-ppc64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz", + "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz", + "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-musl": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz", + "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-s390x-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz", + "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz", + "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz", + "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-openharmony-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz", + "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz", + "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz", + "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz", + "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==", + "dev": true, + "optional": true + }, + "@swc/core": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", + "integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==", + "dev": true, + "requires": { + "@swc/core-darwin-arm64": "1.13.5", + "@swc/core-darwin-x64": "1.13.5", + "@swc/core-linux-arm-gnueabihf": "1.13.5", + "@swc/core-linux-arm64-gnu": "1.13.5", + "@swc/core-linux-arm64-musl": "1.13.5", + "@swc/core-linux-x64-gnu": "1.13.5", + "@swc/core-linux-x64-musl": "1.13.5", + "@swc/core-win32-arm64-msvc": "1.13.5", + "@swc/core-win32-ia32-msvc": "1.13.5", + "@swc/core-win32-x64-msvc": "1.13.5", + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.24" + } + }, + "@swc/core-darwin-arm64": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.5.tgz", + "integrity": "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==", + "dev": true, + "optional": true + }, + "@swc/core-darwin-x64": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.5.tgz", + "integrity": "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==", + "dev": true, + "optional": true + }, + "@swc/core-linux-arm-gnueabihf": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.5.tgz", + "integrity": "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==", + "dev": true, + "optional": true + }, + "@swc/core-linux-arm64-gnu": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.5.tgz", + "integrity": "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==", + "dev": true, + "optional": true + }, + "@swc/core-linux-arm64-musl": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.5.tgz", + "integrity": "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==", + "dev": true, + "optional": true + }, + "@swc/core-linux-x64-gnu": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.5.tgz", + "integrity": "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==", + "dev": true, + "optional": true + }, + "@swc/core-linux-x64-musl": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.5.tgz", + "integrity": "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==", + "dev": true, + "optional": true + }, + "@swc/core-win32-arm64-msvc": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.5.tgz", + "integrity": "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==", + "dev": true, + "optional": true + }, + "@swc/core-win32-ia32-msvc": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.5.tgz", + "integrity": "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==", + "dev": true, + "optional": true + }, + "@swc/core-win32-x64-msvc": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.5.tgz", + "integrity": "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==", + "dev": true, + "optional": true + }, + "@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true + }, + "@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "requires": { + "tslib": "^2.8.0" + } + }, + "@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, + "requires": { + "@swc/counter": "^0.1.3" + } + }, + "@tanstack/history": { + "version": "1.133.3", + "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.133.3.tgz", + "integrity": "sha512-zFQnGdX0S4g5xRuS+95iiEXM+qlGvYG7ksmOKx7LaMv60lDWa0imR8/24WwXXvBWJT1KnwVdZcjvhCwz9IiJCw==" + }, + "@tanstack/query-core": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz", + "integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==" + }, + "@tanstack/query-devtools": { + "version": "5.90.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.90.1.tgz", + "integrity": "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==" + }, + "@tanstack/react-query": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.2.tgz", + "integrity": "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==", + "requires": { + "@tanstack/query-core": "5.90.2" + } + }, + "@tanstack/react-query-devtools": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.90.2.tgz", + "integrity": "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ==", + "requires": { + "@tanstack/query-devtools": "5.90.1" + } + }, + "@tanstack/react-router": { + "version": "1.133.15", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.133.15.tgz", + "integrity": "sha512-3gQitqq/5lL//KSv9Ro34Fw7xak2ZQcPbR7x6bu5X4W0v97xTE7+bMbBS5UAg9zXTq0FNyB124GabgyBgeQ0NA==", + "requires": { + "@tanstack/history": "1.133.3", + "@tanstack/react-store": "^0.7.0", + "@tanstack/router-core": "1.133.15", + "isbot": "^5.1.22", + "tiny-invariant": "^1.3.3", + "tiny-warning": "^1.0.3" + } + }, + "@tanstack/react-router-devtools": { + "version": "1.131.42", + "resolved": "https://registry.npmjs.org/@tanstack/react-router-devtools/-/react-router-devtools-1.131.42.tgz", + "integrity": "sha512-7pymFB1CCimRHot2Zp0ZekQjd1iN812V88n9NLPSeiv9sVRtRVIaLphJjDeudx1NNgkfSJPx2lOhz6K38cuZog==", + "dev": true, + "requires": { + "@tanstack/router-devtools-core": "1.131.42" + } + }, + "@tanstack/react-store": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.4.tgz", + "integrity": "sha512-DyG1e5Qz/c1cNLt/NdFbCA7K1QGuFXQYT6EfUltYMJoQ4LzBOGnOl5IjuxepNcRtmIKkGpmdMzdFZEkevgU9bQ==", + "requires": { + "@tanstack/store": "0.7.4", + "use-sync-external-store": "^1.5.0" + } + }, + "@tanstack/router-core": { + "version": "1.133.15", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.133.15.tgz", + "integrity": "sha512-ZWAmoFcgi27Ojv2FH3Dq3D6Vt73LswdTnA1tyHShNWQf7wOMH/VKKB9JxiXJqpLTK4NJqpnUp/x0/3nvmdrIqg==", + "requires": { + "@tanstack/history": "1.133.3", + "@tanstack/store": "^0.7.0", + "cookie-es": "^2.0.0", + "seroval": "^1.3.2", + "seroval-plugins": "^1.3.2", + "tiny-invariant": "^1.3.3", + "tiny-warning": "^1.0.3" + } + }, + "@tanstack/router-devtools": { + "version": "1.131.42", + "resolved": "https://registry.npmjs.org/@tanstack/router-devtools/-/router-devtools-1.131.42.tgz", + "integrity": "sha512-iWJzr4aN/IOsDSaF/kysM7tPSYj89hnzcWMKNuYN9redIwHgg7rNZ4toKhfNWYNfzxdhKwL9/Yvpf7bDemyc+Q==", + "dev": true, + "requires": { + "@tanstack/react-router-devtools": "1.131.42", + "clsx": "^2.1.1", + "goober": "^2.1.16" + } + }, + "@tanstack/router-devtools-core": { + "version": "1.131.42", + "resolved": "https://registry.npmjs.org/@tanstack/router-devtools-core/-/router-devtools-core-1.131.42.tgz", + "integrity": "sha512-o8jKTiwXcUSjmkozcMjIw1yhjVYeXcuQO7DtfgjKW3B85iveH6VzYK+bGEVU7wmLNMuUSe2eI/7RBzJ6a5+MCA==", + "dev": true, + "requires": { + "clsx": "^2.1.1", + "goober": "^2.1.16", + "solid-js": "^1.9.5" + } + }, + "@tanstack/router-generator": { + "version": "1.133.15", + "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.133.15.tgz", + "integrity": "sha512-TXI07UzV5t1j1LeJ2eOErV9TxvzBRx2oSCEmkVaWMXaGKuQL7I4VB9e9w15ylHnvCO2Z/4DgIhUVF6h9/ZS3Mw==", + "dev": true, + "requires": { + "@tanstack/router-core": "1.133.15", + "@tanstack/router-utils": "1.133.3", + "@tanstack/virtual-file-routes": "1.133.3", + "prettier": "^3.5.0", + "recast": "^0.23.11", + "source-map": "^0.7.4", + "tsx": "^4.19.2", + "zod": "^3.24.2" + }, + "dependencies": { + "source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true + } + } + }, + "@tanstack/router-plugin": { + "version": "1.133.15", + "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.133.15.tgz", + "integrity": "sha512-c3m7Pfuth/TXiRol0OpTw+cJyE7RxJpiMXDLooCiZgRDu2VhyXaanPLuuti9vyZiVdSrVZTQ7tJBFABymWbX5w==", + "dev": true, + "requires": { + "@babel/core": "^7.27.7", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.7", + "@babel/types": "^7.27.7", + "@tanstack/router-core": "1.133.15", + "@tanstack/router-generator": "1.133.15", + "@tanstack/router-utils": "1.133.3", + "@tanstack/virtual-file-routes": "1.133.3", + "babel-dead-code-elimination": "^1.0.10", + "chokidar": "^3.6.0", + "unplugin": "^2.1.2", + "zod": "^3.24.2" + }, + "dependencies": { + "chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + } + } + }, + "@tanstack/router-utils": { + "version": "1.133.3", + "resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.133.3.tgz", + "integrity": "sha512-miPFlt0aG6ID5VDolYuRXgLS7cofvbZGMvHwf2Wmyxjo6GLp/kxxpkQrfM4T1I5cwjwYZZAQmdUKbVHwFZz9sQ==", + "dev": true, + "requires": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/parser": "^7.27.5", + "@babel/preset-typescript": "^7.27.1", + "ansis": "^4.1.0", + "diff": "^8.0.2", + "pathe": "^2.0.3", + "tinyglobby": "^0.2.15" + }, + "dependencies": { + "pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + } + } + }, + "@tanstack/store": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.4.tgz", + "integrity": "sha512-F1XqZQici1Aq6WigEfcxJSml92nW+85Om8ElBMokPNg5glCYVOmPkZGIQeieYFxcPiKTfwo0MTOQpUyJtwncrg==" + }, + "@tanstack/virtual-file-routes": { + "version": "1.133.3", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.133.3.tgz", + "integrity": "sha512-6d2AP9hAjEi8mcIew2RkxBX+wClH1xedhfaYhs8fUiX+V2Cedk7RBD9E9ww2z6BGUYD8Es4fS0OIrzXZWHKGhw==", + "dev": true + }, + "@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "@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/node": { + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "dev": true, + "requires": { + "undici-types": "~7.12.0" + } + }, + "@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/react": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "dev": true, + "requires": { + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz", + "integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==", + "dev": true, + "requires": {} + }, + "@vitejs/plugin-react-swc": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.1.0.tgz", + "integrity": "sha512-Ff690TUck0Anlh7wdIcnsVMhofeEVgm44Y4OYdeeEEPSKyZHzDI9gfVBvySEhDfXtBp8tLCbfsVKPWEMEjq8/g==", + "dev": true, + "requires": { + "@rolldown/pluginutils": "1.0.0-beta.35", + "@swc/core": "^1.13.5" + } + }, + "@zag-js/accordion": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/accordion/-/accordion-1.24.1.tgz", + "integrity": "sha512-JOlmXjO+1tTlyeZ93S+chIlV8uDr8fodj3/XCjLFHc/G116O8cN18KG0Ug9pImy1vT2Kkwb9Ag9QOTyUAXM3PA==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/anatomy": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/anatomy/-/anatomy-1.24.1.tgz", + "integrity": "sha512-mRkpetNjnjgvdyEX880AOjhMhcgdRMLjOM+aEgoDRnhultC4im+nriNoCShJLeVpwsRrEQCU7YVXO4mZaqWUMg==" + }, + "@zag-js/angle-slider": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/angle-slider/-/angle-slider-1.24.1.tgz", + "integrity": "sha512-pcWIpVZDMbujMK0nFaKa0wd7uGkP4E5D7x8cmvoiKMT4E1vZpg2kZeN9qmdnhum9ye7nb80IPKhcDl9C0JuSLw==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/rect-utils": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/aria-hidden": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/aria-hidden/-/aria-hidden-1.24.1.tgz", + "integrity": "sha512-R/a80ZjITZi4rotN7Q9+RTCYCdmJZf3rZi9bObczbR7h5j5GSsjikByUjksWAYzPvFxQxBWTs4GqlCI9dU2f9A==" + }, + "@zag-js/async-list": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/async-list/-/async-list-1.24.1.tgz", + "integrity": "sha512-EZE3wORLOhMtT1tiDA0kTHrtY7XNkOoNyn5jCs8Ec1GfqIHSRzQB+2jt+wPIBwUhDcgQksXgOy91s/i9XfQe1g==", + "requires": { + "@zag-js/core": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/auto-resize": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/auto-resize/-/auto-resize-1.24.1.tgz", + "integrity": "sha512-OH1VTeObddMiN2PUK+7SpkPV8Znlkdq+10odmbbe9K2MZPh352RNcPYytIZTWT0X4/4czhn2MTU6IhZ2lZp2hw==", + "requires": { + "@zag-js/dom-query": "1.24.1" + } + }, + "@zag-js/avatar": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/avatar/-/avatar-1.24.1.tgz", + "integrity": "sha512-zYGUdkxsMoN8OAFYYCZBrsQx++kjWEBdYZew4en9g8vw7yonNjzywtfF/Vd3Dv6mUZ2r5JtaltbK/qp4aBdZvg==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/carousel": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/carousel/-/carousel-1.24.1.tgz", + "integrity": "sha512-7WGlFtF4JoIK4kduiFgucdTe9eD+884d9BF9Sh308MlpiL0KZnO3l3Pyq58yi4R0KUTy7zILLGSsUesifVAuEA==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/scroll-snap": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/checkbox": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/checkbox/-/checkbox-1.24.1.tgz", + "integrity": "sha512-eU/RKaO44Tgt1iTGg26M2nUd12p+gTuq2rNjqVuPfN3dvRzYNi5rGKk6yTQI2T4DH4D+fDMz6gUneiBuGcVoJA==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/focus-visible": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/clipboard": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/clipboard/-/clipboard-1.24.1.tgz", + "integrity": "sha512-GfmjjiEDS9NB6Wo/ThbbzO10BgOYzTSeG00a/pJ5QpvSgvOCz+oLV5NBQHOd8XjOw0e0GQEyfsif0i6wQExSIQ==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/collapsible": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/collapsible/-/collapsible-1.24.1.tgz", + "integrity": "sha512-U6AP4nE6jwMC3kirFQmOL9i3CSfp8mJqb+Gv3opbClpjqCa8hn9v4PNiimKmd0Qr3kynuVRpAshaUaLeg33YiA==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/collection": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/collection/-/collection-1.24.1.tgz", + "integrity": "sha512-aWNDI0iZ5Wb8vCZLJWPjRQOK5/B2wvhhR1+pYaScxZfWy2das2DVKam8tnR0p1GrRfBi/kZNaCXtvM1ZNPjlOQ==", + "requires": { + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/color-picker": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/color-picker/-/color-picker-1.24.1.tgz", + "integrity": "sha512-vLW11JrySJR5fGeXXdmlCJuNm7yE0Tsx/SjkX0WBnrPC4PYaGfiwF7LT59bs5XsQp65kEaIca6mw/J0Bouc8Sw==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/color-utils": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/color-utils": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/color-utils/-/color-utils-1.24.1.tgz", + "integrity": "sha512-8KPTa3I9+WbDLrYPH5knEYMW3CjAC20ikosdrgYshGTFIPuqinAnsxD7H0fZO4I+jSjuhtIyNQuvgwJar9A2rg==", + "requires": { + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/combobox": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/combobox/-/combobox-1.24.1.tgz", + "integrity": "sha512-BhjQOL/Ssr5lQLPCyEersCqOqllFlNuR8nvQOgl1u8Y0EaZR+ZPQbgXum6kE5AuH3SlcY9+1kDK1ZLswOagL1Q==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/aria-hidden": "1.24.1", + "@zag-js/collection": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/core": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/core/-/core-1.24.1.tgz", + "integrity": "sha512-0e7QdxBaY9PMHQfDY/Xu/7MKyRxNsriNscpkZI7L4MHMGPmxdfedGBpteI3gFfqWsdJ5NvvpqxdLUwkbYk5Q5A==", + "requires": { + "@zag-js/dom-query": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/date-picker": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/date-picker/-/date-picker-1.24.1.tgz", + "integrity": "sha512-8jLv074sGJQw4L+5YTDv7l2bwb1x9E7YhvklCffhf/7OzW7RB/ELkljFhmjueuJp7W/sD4xhJyigjp/mDEg1XA==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/date-utils": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/live-region": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/date-utils": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/date-utils/-/date-utils-1.24.1.tgz", + "integrity": "sha512-Rgll6P4Imq479WxH3uMvwQri4o4lF2cxWX2Hka/W7Nhv1DhPBnmfBw30INyWPXzx5agEVzKdGX/br8MU5DV33Q==", + "requires": {} + }, + "@zag-js/dialog": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/dialog/-/dialog-1.24.1.tgz", + "integrity": "sha512-ITzOoXBC92vIkhNvxM0GMMKwboLLk7hSU9dsplk/X9bpX+fQywgc6d5O4I7WHCMmgUWI5y3/aWjqsWATWwufWg==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/aria-hidden": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/focus-trap": "1.24.1", + "@zag-js/remove-scroll": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/dismissable": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/dismissable/-/dismissable-1.24.1.tgz", + "integrity": "sha512-Oca+nbwaqHGt0rmkKfmpExwL+kVYLbVi6fxhzHP1WBrip//IUThoTrPH/gqB51o1DT1z/VNE+8BhWhsHSgkQfw==", + "requires": { + "@zag-js/dom-query": "1.24.1", + "@zag-js/interact-outside": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/dom-query": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/dom-query/-/dom-query-1.24.1.tgz", + "integrity": "sha512-ww3tS5hrB2s6ywGtjMjSOajP19CnQOH0IAGgzjE+lbvDD+ZroXWn9O3Z/v2kTfKNwZFQ4TOb8oSymuSRQsFOYg==", + "requires": { + "@zag-js/types": "1.24.1" + } + }, + "@zag-js/editable": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/editable/-/editable-1.24.1.tgz", + "integrity": "sha512-SV8X7jd95ZAx4VnlhoEcbAiW8jhoGkPf7L0JFB2KWX+NFacEVCKGQpDjZpdzD6j7C10750v3blbkjr6iyzeIqw==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/interact-outside": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/file-upload": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/file-upload/-/file-upload-1.24.1.tgz", + "integrity": "sha512-Un0+qDlkoC93pf7/Nvq9DBVKR6PBKybbNE/En/PC4XLJybK448bY85UuEdBPgXEoR6hIGA3t8NdeHZ+PUoZXIw==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/file-utils": "1.24.1", + "@zag-js/i18n-utils": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/file-utils": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/file-utils/-/file-utils-1.24.1.tgz", + "integrity": "sha512-ydMct0iyd4uPxf+NP4gfyPq1gJlvW29WWIm5ez9El9L+z5tDBhXYNc73s2kSdDBKXkO4fp6Mwoqbz/wZOw99/Q==", + "requires": { + "@zag-js/i18n-utils": "1.24.1" + } + }, + "@zag-js/floating-panel": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/floating-panel/-/floating-panel-1.24.1.tgz", + "integrity": "sha512-qVVtnKCQE2C//0q7utRvpfRKsZedL8gnSqwHDX4ie8nKmLLSLn6jDGuAzxrscsGPHEjCOru9NlTHlAAMtB3ybQ==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/rect-utils": "1.24.1", + "@zag-js/store": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/focus-trap": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/focus-trap/-/focus-trap-1.24.1.tgz", + "integrity": "sha512-cpgYWWaiKx9eycm4Mahv6Dng5+CbDiTtyz/gnbZUv6sqcM4b9N+UqdmBdWYPLHV4gZYrzuO+X4P1C/Ew/rA+xg==", + "requires": { + "@zag-js/dom-query": "1.24.1" + } + }, + "@zag-js/focus-visible": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/focus-visible/-/focus-visible-1.24.1.tgz", + "integrity": "sha512-HzUf8cRl5tbIil6rVe24CxC3s1pdFGpfYSt5NyaFoFd0HuWhobp+De1kVUvlLU0DDUU6Kgw6DB1w8APEPzb8gg==", + "requires": { + "@zag-js/dom-query": "1.24.1" + } + }, + "@zag-js/highlight-word": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/highlight-word/-/highlight-word-1.24.1.tgz", + "integrity": "sha512-paDF/sWKDMMclpCzrG60vD4/AFQ3EOu2lzQxl7S21uD/B8Rir4w1CkxK/9+cm1Bu7mj4mkR4t+VJxycEZ7YuIw==" + }, + "@zag-js/hover-card": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/hover-card/-/hover-card-1.24.1.tgz", + "integrity": "sha512-zXTcLEb8YOFoEjDMsMcxqidRDN2fY0C94j+XdZYj5eZtKBIgbyCyAjvZrEu9yyPqqrXCNwYU0fTFjac3t9IV4g==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/i18n-utils": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/i18n-utils/-/i18n-utils-1.24.1.tgz", + "integrity": "sha512-dI9M73FTJcE40s/TPBLLKsypmBoMNe5NoRSBW64PWdmn0fCq65qcAUMgwQ0MVenh4oofoDYyffl8pIStr8T1tA==", + "requires": { + "@zag-js/dom-query": "1.24.1" + } + }, + "@zag-js/interact-outside": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/interact-outside/-/interact-outside-1.24.1.tgz", + "integrity": "sha512-xKyGT295WVrlJaOPCVBrundlXqL4YEvl36SHNSi7EZs/AYpzxR/aBtnFCRN1/7nWvdqvfGs7ya0kl/ly0H7VBg==", + "requires": { + "@zag-js/dom-query": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/json-tree-utils": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/json-tree-utils/-/json-tree-utils-1.24.1.tgz", + "integrity": "sha512-TWVg+Y4fLr9o0YaB3OnX4xmV91Te/vzRwnNKntsz3GIWJ5fLNngg4hm3E+eaYnJIlKMHrvJv4T/UB4IGYUF+EQ==" + }, + "@zag-js/listbox": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/listbox/-/listbox-1.24.1.tgz", + "integrity": "sha512-fTJ125SWVZ+NxgkT6s8LWpdJQMeADk9Lm+Ur1pi0mZnRCmuHI3nwPkg1dfqynjVyrKs6P8wBmUxt3hlr2cc6TQ==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/collection": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/focus-visible": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/live-region": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/live-region/-/live-region-1.24.1.tgz", + "integrity": "sha512-A/55dOyRhfdgVtCBP05Uf2UGz/58H0TMWP69GdVYM4uADtfCLNPy6yxHAt9p334qJsWicg/YWSzBdEAVTThNag==" + }, + "@zag-js/menu": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/menu/-/menu-1.24.1.tgz", + "integrity": "sha512-XPNQbkIxSbNuYNLLQZlgXbj6Ptn2XHT5BXkUSw2hSbIg35S7Lq8gckiZVtxmUiX8zbv7krTBSD7zThSnwx1TOA==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/rect-utils": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/number-input": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/number-input/-/number-input-1.24.1.tgz", + "integrity": "sha512-F5nX0VvuRmSxddJ8byHYp4OSHLU1C5Fv1rT4L1AnSXud8q6C+zCy4Vy8772pUKNobZf0q8Ru4SgnOe5TQcvRpg==", + "requires": { + "@internationalized/number": "3.6.5", + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/pagination": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/pagination/-/pagination-1.24.1.tgz", + "integrity": "sha512-IO9Q5SiYmk00pjJAD18qFjOkpN1qb9iSeuX6A9Bdo8sMBFSigI6c7tGo1MPYGENma3b+aX7LbUpt8hYFufqUow==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/password-input": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/password-input/-/password-input-1.24.1.tgz", + "integrity": "sha512-TWgTRNsaAZ6IE1QmCQKhPY6uSRPDGjgdxGSpG7wOuYsbxHw/hD3v5sUAhAo9teIL0wV8COZIh6hyG2UAAgT2kg==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/pin-input": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/pin-input/-/pin-input-1.24.1.tgz", + "integrity": "sha512-ytJK/1ekU06VmOpe7KdSkIQ3If+fffrA/EpbktZBuRepsz80QHB64+X6QQ6H1lEMbLWPNZ0TuFPaYhFfqH7cTQ==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/popover": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/popover/-/popover-1.24.1.tgz", + "integrity": "sha512-auNy7/5/VMeNUYbKfcvSz7OHkbrUWdODtA6gB/d/weAxvEHyMSk0+Ms4c5lmN8KDChrBAPJs4CfKSPv1U4I4zw==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/aria-hidden": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/focus-trap": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/remove-scroll": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/popper": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/popper/-/popper-1.24.1.tgz", + "integrity": "sha512-VWbOjBy/haIDmXhwfyMT1rRcQhSfYmPX67YzQwLA7863kXkoTH1r9fR+1f9uq3VuXQLhw2Cg/lkSzlkg9TIp+g==", + "requires": { + "@floating-ui/dom": "1.7.4", + "@zag-js/dom-query": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/presence": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/presence/-/presence-1.24.1.tgz", + "integrity": "sha512-MMcw4iOsGdSGM3hmvd0gcMuk1X9rE/xE3Ndm113vc+lkhk93COiuJPz1ZpyBb8l1CIJwlZ5nnRpx4Lx8Do6aNQ==", + "requires": { + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1" + } + }, + "@zag-js/progress": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/progress/-/progress-1.24.1.tgz", + "integrity": "sha512-ocp6zkl5Y3sVMzPVIRLZtqtDfMkc365JYIrOUsdUqwJMvZJhSP1IbsbtIJS1ycOaHfLdK27E//GVyjxA7SHGhw==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/qr-code": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/qr-code/-/qr-code-1.24.1.tgz", + "integrity": "sha512-Hy722PNwLs1tnXFQkTqtrEILypZcUDiC8YdvGn57mmmvPGtZdAzhs4G8ghoP9ahJ02ztREjIt8Qnmct344fALA==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1", + "proxy-memoize": "3.0.1", + "uqr": "0.1.2" + } + }, + "@zag-js/radio-group": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/radio-group/-/radio-group-1.24.1.tgz", + "integrity": "sha512-49S+nmaZzjf98206VeevmfTNTf+WjLveKCOGz5SVWPX3R8maZJgka1ZlIDuWlnRK1JfL+4Ls10/ZxAk3HrI7sg==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/focus-visible": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/rating-group": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/rating-group/-/rating-group-1.24.1.tgz", + "integrity": "sha512-EGGObQDmulon5N9s5ElGZv9yQmky10s7ps7wyVgW1+vJTsWr8gaoFMJwf6nbXOsUjqW8iDuzsF68Rel9CgjxIQ==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/react": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/react/-/react-1.24.1.tgz", + "integrity": "sha512-oiaiuR7FKVHOEJtzoYZ2QBQ5+J/j086eebhLCIWkh2ie6QBJM73LHsMUxfZp2D2G1is8EoyUhrH3v2MPMlYMXg==", + "requires": { + "@zag-js/core": "1.24.1", + "@zag-js/store": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/rect-utils": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/rect-utils/-/rect-utils-1.24.1.tgz", + "integrity": "sha512-6JkVq71feW9Yyt7Pynyf199ugDFVgRT+jPpg2ECRHgY2oHvn5atBP3PA1uM2cx7ZydiajnBgk4n1ePnGYD2xNw==" + }, + "@zag-js/remove-scroll": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/remove-scroll/-/remove-scroll-1.24.1.tgz", + "integrity": "sha512-SAK3ZsnDUcJve5q3OHsMjrl0JOW9sv1fGbBFXyyid9Uu8s79LMh7EZw2na5jXDNzdMWmk1Euu82OaZSlLl9Kew==", + "requires": { + "@zag-js/dom-query": "1.24.1" + } + }, + "@zag-js/scroll-area": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/scroll-area/-/scroll-area-1.24.1.tgz", + "integrity": "sha512-eRZKs6Yyl8Zp+YkIxzr1QsgRDDsNMxXshwpIzt/L5xK+EV34mv760FOkX/unG/WxQ1Z0gBogPm9ZY53/m4bhJA==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/scroll-snap": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/scroll-snap/-/scroll-snap-1.24.1.tgz", + "integrity": "sha512-Co/NlccX4XDg6OzQeRgv8bANbsCkMog1FZ0BveN8+2Mso/svOLVkB6UGswWZk/DyqY8DlxvfZAdPltmQpu5h8w==", + "requires": { + "@zag-js/dom-query": "1.24.1" + } + }, + "@zag-js/select": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/select/-/select-1.24.1.tgz", + "integrity": "sha512-boU5m3Qd//EGe1M2i4a2SbCXQpcPP9Ewe6DvjEpOhxP+dwdbZzDrtRBdZ4ByhMJ+1bT5B6TqsfvsQHhAI0LunA==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/collection": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/signature-pad": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/signature-pad/-/signature-pad-1.24.1.tgz", + "integrity": "sha512-CRTcefUGMwdhxqmB8yGkHU3gweMfXw0CCoMc0LhMmla12hMJOBi+mpMVaBJnQHYGSG8uFUh2IKdPbe2Vtp4T3Q==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1", + "perfect-freehand": "^1.2.2" + } + }, + "@zag-js/slider": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/slider/-/slider-1.24.1.tgz", + "integrity": "sha512-HClZBKcT+9tihZArRNRj35YOIUbztCcyYzggYYIrK4+OFD0RLYihA+yBO4hxs7xZVenzma9i0pc6q/Vo4z2tvA==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/splitter": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/splitter/-/splitter-1.24.1.tgz", + "integrity": "sha512-UUqiCD0T8kfgm/vRTY1QrPlrpxbzxqZ+8QvysUchnibmStetkHnuzAXC4ZD9jlJbToqzE4p1eLOiWGaVXRdB/Q==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/steps": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/steps/-/steps-1.24.1.tgz", + "integrity": "sha512-njL1SMKef0JfYzw5KUhpeVuzOtgBjSxVUwDrPR9s095WUCUiOYlxzqummg3VBY8IDuT/pS/K6LDSY11YCRzeNw==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/store": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/store/-/store-1.24.1.tgz", + "integrity": "sha512-iVl+NX2CcxEDLL3hrj31mqSqBZYBqHEBqa/Z7FwKVoTImMQ1AabMF5XPreTtB8KFbaVJlNlM6D5qngDPpVj/xw==", + "requires": { + "proxy-compare": "3.0.1" + } + }, + "@zag-js/switch": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/switch/-/switch-1.24.1.tgz", + "integrity": "sha512-RI2bG2AtsQ4ci8T7RA3XVSjd9urpNQXIwEatpa8cw9GCWFI421rt4Xcab5jy/IOu6VzXl6pwh11/cWAC/PBYCw==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/focus-visible": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/tabs": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/tabs/-/tabs-1.24.1.tgz", + "integrity": "sha512-RjdW4opxhvCWTwHoCqq+lfNCthiyPu376hto6j4Ybl/UN3UFTV4zfTbwbMbAH7dyqj8m1nkKxidLaO0Yhx3zZA==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/tags-input": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/tags-input/-/tags-input-1.24.1.tgz", + "integrity": "sha512-HY1ebBZE2j3/fuzfKw4z/44S9WWe50auMWLlFg47j6zVBcyNdXEeMO1OvvfyfQFJOcvOKXGxW8Hi4MXGxLWqmA==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/auto-resize": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/interact-outside": "1.24.1", + "@zag-js/live-region": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/timer": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/timer/-/timer-1.24.1.tgz", + "integrity": "sha512-cjD8+I8CgSugsj5DI+kqzgvuQ2vYeArRdjO3iSjB4AjR+j08W8NKZvr7aawhYq636vrE9LeJGbxxZ3DBV12ELw==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/toast": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/toast/-/toast-1.24.1.tgz", + "integrity": "sha512-gmHv65EYdypfMoF9WYIp7Y8z6XN5tebXEdjIWF8bJBaqW5zPn2VLdUYpfXv7wrHW2YtSTnF/xtgIhJ7MIX7HxA==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/toggle": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/toggle/-/toggle-1.24.1.tgz", + "integrity": "sha512-dMN9Q4XFqr7jPlUZsLCFdUc1rtW88FzUaXcFVaeNCy8y8XGc+MG9AJJqjBiBL9EUeeR+LIp8yUIhJQEEDBm0kw==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/toggle-group": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/toggle-group/-/toggle-group-1.24.1.tgz", + "integrity": "sha512-GVBay9XzmXjp1GgAmHUMpeYq3iMMevH+n0TyC0NcRe00prAEL9S4/q9pVy0P33PIOa20dxcvQ/Q3Tf+n5PFQcg==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/tooltip": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/tooltip/-/tooltip-1.24.1.tgz", + "integrity": "sha512-gdD5C9AF6JD8LC6mxXzUGWjnHqY3MS7ZvtNx/nuNGJAqKCD32dPT73fuv0up1UVh1yJhX4IrXg3H6q52Pm+jPw==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/focus-visible": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/tour": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/tour/-/tour-1.24.1.tgz", + "integrity": "sha512-e+UR8xauKyRhE6tA8gRsR1GuOn1QGjj2YAmtRC8lIb5tD+QrGCPy0jX2xBeR7M7eY1IPSSyi0gCUGE2CbaRK8Q==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dismissable": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/focus-trap": "1.24.1", + "@zag-js/interact-outside": "1.24.1", + "@zag-js/popper": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/tree-view": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/tree-view/-/tree-view-1.24.1.tgz", + "integrity": "sha512-HXCoqW6j2RunFxaIVRevgRTrRUEP05lpdOvc1Smzne7sC2mczwIqN68Vei6e83gRhXSF80v6Fc4TcHdPiW6wJA==", + "requires": { + "@zag-js/anatomy": "1.24.1", + "@zag-js/collection": "1.24.1", + "@zag-js/core": "1.24.1", + "@zag-js/dom-query": "1.24.1", + "@zag-js/types": "1.24.1", + "@zag-js/utils": "1.24.1" + } + }, + "@zag-js/types": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/types/-/types-1.24.1.tgz", + "integrity": "sha512-XyINtxe5JK7A+RtTmBdCQElNoElDiTw6NSWpjKZGRAXXGU9HIZ9JIFeaS77uq1aVs0JhAOFwqJiPs2NJzaYHLA==", + "requires": { + "csstype": "3.1.3" + } + }, + "@zag-js/utils": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@zag-js/utils/-/utils-1.24.1.tgz", + "integrity": "sha512-4nU9lfFlLLW/4T+/HaP+HdHYFeWvacxSVcccv0JSf+ZTC110IldV48kZELP+wFg9xDL/jCPPjlRtO1K64EIwgA==" + }, + "acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true + }, + "ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true + }, + "ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "dependencies": { + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + } + } + }, + "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 + }, + "ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "requires": { + "tslib": "^2.0.1" + } + }, + "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.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "requires": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "babel-dead-code-elimination": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.10.tgz", + "integrity": "sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==", + "dev": true, + "requires": { + "@babel/core": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" + } + }, + "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" + } + }, + "binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "browserslist": { + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + } + }, + "bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "requires": { + "run-applescript": "^7.0.0" + } + }, + "c12": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/c12/-/c12-2.0.1.tgz", + "integrity": "sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A==", + "dev": true, + "requires": { + "chokidar": "^4.0.1", + "confbox": "^0.1.7", + "defu": "^6.1.4", + "dotenv": "^16.4.5", + "giget": "^1.2.3", + "jiti": "^2.3.0", + "mlly": "^1.7.1", + "ohash": "^1.1.4", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.2.0", + "rc9": "^2.1.2" + }, + "dependencies": { + "dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true + } + } + }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "caniuse-lite": { + "version": "1.0.30001741", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", + "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", + "dev": true + }, + "chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dev": true, + "requires": { + "readdirp": "^4.0.1" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "requires": { + "consola": "^3.2.3" + } + }, + "clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true + }, + "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" + } + }, + "commander": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.0.0.tgz", + "integrity": "sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==", + "dev": true + }, + "confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true + }, + "consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "dev": true + }, + "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==" + }, + "cookie-es": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz", + "integrity": "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==" + }, + "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" + }, + "dependencies": { + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + } + } + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "requires": { + "ms": "^2.1.3" + } + }, + "default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "requires": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + } + }, + "default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true + }, + "define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true + }, + "defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "destr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", + "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==", + "dev": true + }, + "diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "dev": true + }, + "dotenv": { + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", + "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "dev": true + }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, + "electron-to-chromium": { + "version": "1.5.214", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.214.tgz", + "integrity": "sha512-TpvUNdha+X3ybfU78NoQatKvQEm1oq3lf2QbnmCEdw+Bd9RuIAY+hJTvq1avzHM0f7EJfnH3vbCnbzKzisc/9Q==", + "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" + } + }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } + }, + "es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, + "esbuild": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", + "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.25.3", + "@esbuild/android-arm": "0.25.3", + "@esbuild/android-arm64": "0.25.3", + "@esbuild/android-x64": "0.25.3", + "@esbuild/darwin-arm64": "0.25.3", + "@esbuild/darwin-x64": "0.25.3", + "@esbuild/freebsd-arm64": "0.25.3", + "@esbuild/freebsd-x64": "0.25.3", + "@esbuild/linux-arm": "0.25.3", + "@esbuild/linux-arm64": "0.25.3", + "@esbuild/linux-ia32": "0.25.3", + "@esbuild/linux-loong64": "0.25.3", + "@esbuild/linux-mips64el": "0.25.3", + "@esbuild/linux-ppc64": "0.25.3", + "@esbuild/linux-riscv64": "0.25.3", + "@esbuild/linux-s390x": "0.25.3", + "@esbuild/linux-x64": "0.25.3", + "@esbuild/netbsd-arm64": "0.25.3", + "@esbuild/netbsd-x64": "0.25.3", + "@esbuild/openbsd-arm64": "0.25.3", + "@esbuild/openbsd-x64": "0.25.3", + "@esbuild/sunos-x64": "0.25.3", + "@esbuild/win32-arm64": "0.25.3", + "@esbuild/win32-ia32": "0.25.3", + "@esbuild/win32-x64": "0.25.3" + } + }, + "escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true + }, + "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==" + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + } + }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "requires": {} + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "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.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + } + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "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==" + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, + "get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true + }, + "get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "requires": { + "resolve-pkg-maps": "^1.0.0" + } + }, + "giget": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.3.tgz", + "integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==", + "dev": true, + "requires": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.3", + "nypm": "^0.3.8", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "tar": "^6.2.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "dev": true, + "requires": {} + }, + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, + "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" + } + }, + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "requires": { + "has-symbols": "^1.0.3" + } + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "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" + } + }, + "human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true + }, + "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" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "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" + } + }, + "is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "requires": { + "is-docker": "^3.0.0" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "requires": { + "is-inside-container": "^1.0.0" + } + }, + "isbot": { + "version": "5.1.30", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.30.tgz", + "integrity": "sha512-3wVJEonAns1OETX83uWsk5IAne2S5zfDcntD2hbtU23LelSqNXzXs9zKjMPOLMzroCgIjCfjYAEHrd2D6FOkiA==" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "jiti": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.0.tgz", + "integrity": "sha512-H5UpaUI+aHOqZXlYOaFP/8AzKsg+guWu+Pr3Y8i7+Y3zr1aXAvCvTAQ1RxSc6oVD8R8c7brgNtTVP91E7upH/g==", + "dev": true + }, + "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" + } + }, + "jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==" + }, + "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==" + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "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": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + }, + "dependencies": { + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "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" + } + }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "mlly": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.3.tgz", + "integrity": "sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==", + "dev": true, + "requires": { + "acorn": "^8.14.0", + "pathe": "^1.1.2", + "pkg-types": "^1.2.1", + "ufo": "^1.5.4" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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 + }, + "next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "requires": {} + }, + "node-fetch-native": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", + "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", + "dev": true + }, + "node-releases": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz", + "integrity": "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + }, + "dependencies": { + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + } + } + }, + "nypm": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.12.tgz", + "integrity": "sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA==", + "dev": true, + "requires": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "execa": "^8.0.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" + } + }, + "ohash": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz", + "integrity": "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==", + "dev": true + }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + }, + "open": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", + "dev": true, + "requires": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + } + }, + "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-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "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==" + }, + "pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true + }, + "perfect-freehand": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.2.2.tgz", + "integrity": "sha512-eh31l019WICQ03pkF3FSzHxB8n07ItqIQ++G5UV8JX0zVOXzgTGCqnRR0jJ2h9U8/2uW4W4mtGJELt9kEV0CFQ==" + }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true + }, + "pkg-types": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.1.tgz", + "integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==", + "dev": true, + "requires": { + "confbox": "^0.1.8", + "mlly": "^1.7.2", + "pathe": "^1.1.2" + } + }, + "playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.56.1" + }, + "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.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "dev": true + }, + "postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "requires": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + } + }, + "prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true + }, + "proxy-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz", + "integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==" + }, + "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==" + }, + "proxy-memoize": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/proxy-memoize/-/proxy-memoize-3.0.1.tgz", + "integrity": "sha512-VDdG/VYtOgdGkWJx7y0o7p+zArSf2383Isci8C+BP3YXgMYDoPd3cCBjw0JdWb6YBb9sFiOPbAADDVTPJnh+9g==", + "requires": { + "proxy-compare": "^3.0.0" + } + }, + "rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dev": true, + "requires": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==" + }, + "react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "requires": { + "scheduler": "^0.27.0" + } + }, + "react-error-boundary": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.0.0.tgz", + "integrity": "sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==", + "requires": { + "@babel/runtime": "^7.12.5" + } + }, + "react-hook-form": { + "version": "7.62.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", + "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", + "requires": {} + }, + "react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "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==" + }, + "readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "dev": true + }, + "recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "dev": true, + "requires": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + } + }, + "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==" + }, + "resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true + }, + "rollup": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz", + "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.50.1", + "@rollup/rollup-android-arm64": "4.50.1", + "@rollup/rollup-darwin-arm64": "4.50.1", + "@rollup/rollup-darwin-x64": "4.50.1", + "@rollup/rollup-freebsd-arm64": "4.50.1", + "@rollup/rollup-freebsd-x64": "4.50.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", + "@rollup/rollup-linux-arm-musleabihf": "4.50.1", + "@rollup/rollup-linux-arm64-gnu": "4.50.1", + "@rollup/rollup-linux-arm64-musl": "4.50.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", + "@rollup/rollup-linux-ppc64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-musl": "4.50.1", + "@rollup/rollup-linux-s390x-gnu": "4.50.1", + "@rollup/rollup-linux-x64-gnu": "4.50.1", + "@rollup/rollup-linux-x64-musl": "4.50.1", + "@rollup/rollup-openharmony-arm64": "4.50.1", + "@rollup/rollup-win32-arm64-msvc": "4.50.1", + "@rollup/rollup-win32-ia32-msvc": "4.50.1", + "@rollup/rollup-win32-x64-msvc": "4.50.1", + "@types/estree": "1.0.8", + "fsevents": "~2.3.2" + } + }, + "run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true + }, + "scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "seroval": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", + "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==" + }, + "seroval-plugins": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.3.tgz", + "integrity": "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==", + "requires": {} + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, + "solid-js": { + "version": "1.9.9", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.9.tgz", + "integrity": "sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA==", + "dev": true, + "requires": { + "csstype": "^3.1.0", + "seroval": "~1.3.0", + "seroval-plugins": "~1.3.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.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true + }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "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==" + }, + "tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, + "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==" + }, + "tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "requires": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "tsx": { + "version": "4.20.5", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", + "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", + "dev": true, + "requires": { + "esbuild": "~0.25.0", + "fsevents": "~2.3.3", + "get-tsconfig": "^4.7.5" + } + }, + "typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true + }, + "ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "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": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "dev": true + }, + "unplugin": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.10.tgz", + "integrity": "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==", + "dev": true, + "requires": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + } + }, + "update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "requires": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + } + }, + "uqr": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.2.tgz", + "integrity": "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==" + }, + "use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "requires": {} + }, + "vite": { + "version": "7.1.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", + "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", + "dev": true, + "requires": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "fsevents": "~2.3.3", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + } + }, + "webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.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 + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000000..41357dadc6 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,42 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -p tsconfig.build.json && vite build", + "lint": "biome check --write --unsafe --no-errors-on-unmatched --files-ignore-unknown=true ./", + "preview": "vite preview", + "generate-client": "openapi-ts" + }, + "dependencies": { + "@chakra-ui/react": "^3.27.0", + "@emotion/react": "^11.14.0", + "@tanstack/react-query": "^5.90.2", + "@tanstack/react-query-devtools": "^5.90.2", + "@tanstack/react-router": "^1.131.50", + "axios": "1.12.2", + "form-data": "4.0.4", + "next-themes": "^0.4.6", + "react": "^19.1.1", + "react-dom": "^19.2.0", + "react-error-boundary": "^6.0.0", + "react-hook-form": "7.62.0", + "react-icons": "^5.5.0" + }, + "devDependencies": { + "@biomejs/biome": "^2.2.4", + "@hey-api/openapi-ts": "0.73.0", + "@playwright/test": "1.56.1", + "@tanstack/router-devtools": "^1.131.42", + "@tanstack/router-plugin": "^1.133.15", + "@types/node": "^24.5.2", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.2.1", + "@vitejs/plugin-react-swc": "^4.1.0", + "dotenv": "^17.2.2", + "typescript": "^5.2.2", + "vite": "^7.1.11" + } +} diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000000..b9d5a51246 --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,91 @@ +import { defineConfig, devices } from '@playwright/test'; +import 'dotenv/config' + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ + +/** + * 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: process.env.CI ? 'blob' : '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 new file mode 100644 index 0000000000..d3dad4bec8 --- /dev/null +++ b/frontend/public/assets/images/fastapi-logo.svg @@ -0,0 +1,51 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/frontend/public/assets/images/favicon.png b/frontend/public/assets/images/favicon.png new file mode 100644 index 0000000000..e5b7c3ada7 Binary files /dev/null and b/frontend/public/assets/images/favicon.png differ diff --git a/frontend/src/client/core/ApiError.ts b/frontend/src/client/core/ApiError.ts new file mode 100644 index 0000000000..36675d288a --- /dev/null +++ b/frontend/src/client/core/ApiError.ts @@ -0,0 +1,21 @@ +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; + } +} \ No newline at end of file diff --git a/frontend/src/client/core/ApiRequestOptions.ts b/frontend/src/client/core/ApiRequestOptions.ts new file mode 100644 index 0000000000..939a0aa4c8 --- /dev/null +++ b/frontend/src/client/core/ApiRequestOptions.ts @@ -0,0 +1,21 @@ +export type ApiRequestOptions = { + readonly body?: any; + readonly cookies?: Record; + readonly errors?: Record; + readonly formData?: Record | any[] | Blob | File; + readonly headers?: Record; + readonly mediaType?: string; + readonly method: + | 'DELETE' + | 'GET' + | 'HEAD' + | 'OPTIONS' + | 'PATCH' + | 'POST' + | 'PUT'; + readonly path?: Record; + readonly query?: Record; + readonly responseHeader?: string; + readonly responseTransformer?: (data: unknown) => Promise; + readonly url: string; +}; \ No newline at end of file diff --git a/frontend/src/client/core/ApiResult.ts b/frontend/src/client/core/ApiResult.ts new file mode 100644 index 0000000000..4c58e39138 --- /dev/null +++ b/frontend/src/client/core/ApiResult.ts @@ -0,0 +1,7 @@ +export type ApiResult = { + readonly body: TData; + readonly ok: boolean; + readonly status: number; + readonly statusText: string; + readonly url: string; +}; \ No newline at end of file diff --git a/frontend/src/client/core/CancelablePromise.ts b/frontend/src/client/core/CancelablePromise.ts new file mode 100644 index 0000000000..ccc082e8f2 --- /dev/null +++ b/frontend/src/client/core/CancelablePromise.ts @@ -0,0 +1,126 @@ +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; + } +} \ No newline at end of file diff --git a/frontend/src/client/core/OpenAPI.ts b/frontend/src/client/core/OpenAPI.ts new file mode 100644 index 0000000000..74f92b4085 --- /dev/null +++ b/frontend/src/client/core/OpenAPI.ts @@ -0,0 +1,57 @@ +import type { AxiosRequestConfig, AxiosResponse } from 'axios'; +import type { ApiRequestOptions } from './ApiRequestOptions'; + +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): void { + const index = this._fns.indexOf(fn); + if (index !== -1) { + this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)]; + } + } + + use(fn: Middleware): void { + 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; + 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, + TOKEN: undefined, + USERNAME: undefined, + VERSION: '0.1.0', + WITH_CREDENTIALS: false, + interceptors: { + request: new Interceptors(), + response: new Interceptors(), + }, +}; \ No newline at end of file diff --git a/frontend/src/client/core/request.ts b/frontend/src/client/core/request.ts new file mode 100644 index 0000000000..ecc2e393cd --- /dev/null +++ b/frontend/src/client/core/request.ts @@ -0,0 +1,347 @@ +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 (value instanceof Date) { + append(key, value.toISOString()); + } else 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([ + // @ts-ignore + resolve(options, config.TOKEN), + // @ts-ignore + resolve(options, config.USERNAME), + // @ts-ignore + resolve(options, config.PASSWORD), + // @ts-ignore + 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); + + let transformedBody = responseBody; + if (options.responseTransformer && isSuccess(response.status)) { + transformedBody = await options.responseTransformer(responseBody) + } + + const result: ApiResult = { + url, + ok: isSuccess(response.status), + status: response.status, + statusText: response.statusText, + body: responseHeader ?? transformedBody, + }; + + catchErrorCodes(options, result); + + resolve(result.body); + } + } catch (error) { + reject(error); + } + }); +}; \ No newline at end of file diff --git a/frontend/src/client/index.ts b/frontend/src/client/index.ts new file mode 100644 index 0000000000..50a1dd734c --- /dev/null +++ b/frontend/src/client/index.ts @@ -0,0 +1,6 @@ +// This file is auto-generated by @hey-api/openapi-ts +export { ApiError } from './core/ApiError'; +export { CancelablePromise, CancelError } from './core/CancelablePromise'; +export { OpenAPI, type OpenAPIConfig } from './core/OpenAPI'; +export * from './sdk.gen'; +export * from './types.gen'; \ No newline at end of file diff --git a/frontend/src/client/schemas.gen.ts b/frontend/src/client/schemas.gen.ts new file mode 100644 index 0000000000..a924713d37 --- /dev/null +++ b/frontend/src/client/schemas.gen.ts @@ -0,0 +1,526 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export const Body_login_login_access_tokenSchema = { + properties: { + grant_type: { + anyOf: [ + { + type: 'string', + pattern: 'password' + }, + { + type: 'null' + } + ], + title: 'Grant Type' + }, + username: { + type: 'string', + title: 'Username' + }, + password: { + type: 'string', + title: 'Password' + }, + scope: { + type: 'string', + title: 'Scope', + default: '' + }, + client_id: { + anyOf: [ + { + type: 'string' + }, + { + type: 'null' + } + ], + title: 'Client Id' + }, + client_secret: { + anyOf: [ + { + type: 'string' + }, + { + type: 'null' + } + ], + title: 'Client Secret' + } + }, + type: 'object', + required: ['username', 'password'], + title: 'Body_login-login_access_token' +} as const; + +export const HTTPValidationErrorSchema = { + properties: { + detail: { + items: { + '$ref': '#/components/schemas/ValidationError' + }, + type: 'array', + title: 'Detail' + } + }, + type: 'object', + title: 'HTTPValidationError' +} as const; + +export const ItemCreateSchema = { + properties: { + title: { + type: 'string', + maxLength: 255, + minLength: 1, + title: 'Title' + }, + description: { + anyOf: [ + { + type: 'string', + maxLength: 255 + }, + { + type: 'null' + } + ], + title: 'Description' + } + }, + type: 'object', + required: ['title'], + title: 'ItemCreate' +} as const; + +export const ItemPublicSchema = { + properties: { + title: { + type: 'string', + maxLength: 255, + minLength: 1, + title: 'Title' + }, + description: { + anyOf: [ + { + type: 'string', + maxLength: 255 + }, + { + type: 'null' + } + ], + title: 'Description' + }, + id: { + type: 'string', + format: 'uuid', + title: 'Id' + }, + owner_id: { + type: 'string', + format: 'uuid', + title: 'Owner Id' + } + }, + type: 'object', + required: ['title', 'id', 'owner_id'], + title: 'ItemPublic' +} as const; + +export const ItemUpdateSchema = { + properties: { + title: { + anyOf: [ + { + type: 'string', + maxLength: 255, + minLength: 1 + }, + { + type: 'null' + } + ], + title: 'Title' + }, + description: { + anyOf: [ + { + type: 'string', + maxLength: 255 + }, + { + type: 'null' + } + ], + title: 'Description' + } + }, + type: 'object', + title: 'ItemUpdate' +} as const; + +export const ItemsPublicSchema = { + properties: { + data: { + items: { + '$ref': '#/components/schemas/ItemPublic' + }, + type: 'array', + title: 'Data' + }, + count: { + type: 'integer', + title: 'Count' + } + }, + type: 'object', + required: ['data', 'count'], + title: 'ItemsPublic' +} as const; + +export const MessageSchema = { + properties: { + message: { + type: 'string', + title: 'Message' + } + }, + type: 'object', + required: ['message'], + title: 'Message' +} as const; + +export const NewPasswordSchema = { + properties: { + token: { + type: 'string', + title: 'Token' + }, + new_password: { + type: 'string', + maxLength: 128, + minLength: 8, + title: 'New Password' + } + }, + type: 'object', + required: ['token', 'new_password'], + title: 'NewPassword' +} as const; + +export const PrivateUserCreateSchema = { + properties: { + email: { + type: 'string', + title: 'Email' + }, + password: { + type: 'string', + title: 'Password' + }, + full_name: { + type: 'string', + title: 'Full Name' + }, + is_verified: { + type: 'boolean', + title: 'Is Verified', + default: false + } + }, + type: 'object', + required: ['email', 'password', 'full_name'], + title: 'PrivateUserCreate' +} as const; + +export const TokenSchema = { + properties: { + access_token: { + type: 'string', + title: 'Access Token' + }, + token_type: { + type: 'string', + title: 'Token Type', + default: 'bearer' + } + }, + type: 'object', + required: ['access_token'], + title: 'Token' +} as const; + +export const UpdatePasswordSchema = { + properties: { + current_password: { + type: 'string', + maxLength: 128, + minLength: 8, + title: 'Current Password' + }, + new_password: { + type: 'string', + maxLength: 128, + minLength: 8, + title: 'New Password' + } + }, + type: 'object', + required: ['current_password', 'new_password'], + title: 'UpdatePassword' +} as const; + +export const UserCreateSchema = { + properties: { + email: { + type: 'string', + maxLength: 255, + format: 'email', + title: 'Email' + }, + is_active: { + type: 'boolean', + title: 'Is Active', + default: true + }, + is_superuser: { + type: 'boolean', + title: 'Is Superuser', + default: false + }, + full_name: { + anyOf: [ + { + type: 'string', + maxLength: 255 + }, + { + type: 'null' + } + ], + title: 'Full Name' + }, + password: { + type: 'string', + maxLength: 128, + minLength: 8, + title: 'Password' + } + }, + type: 'object', + required: ['email', 'password'], + title: 'UserCreate' +} as const; + +export const UserPublicSchema = { + properties: { + email: { + type: 'string', + maxLength: 255, + format: 'email', + title: 'Email' + }, + is_active: { + type: 'boolean', + title: 'Is Active', + default: true + }, + is_superuser: { + type: 'boolean', + title: 'Is Superuser', + default: false + }, + full_name: { + anyOf: [ + { + type: 'string', + maxLength: 255 + }, + { + type: 'null' + } + ], + title: 'Full Name' + }, + id: { + type: 'string', + format: 'uuid', + title: 'Id' + } + }, + type: 'object', + required: ['email', 'id'], + title: 'UserPublic' +} as const; + +export const UserRegisterSchema = { + properties: { + email: { + type: 'string', + maxLength: 255, + format: 'email', + title: 'Email' + }, + password: { + type: 'string', + maxLength: 128, + minLength: 8, + title: 'Password' + }, + full_name: { + anyOf: [ + { + type: 'string', + maxLength: 255 + }, + { + type: 'null' + } + ], + title: 'Full Name' + } + }, + type: 'object', + required: ['email', 'password'], + title: 'UserRegister' +} as const; + +export const UserUpdateSchema = { + properties: { + email: { + anyOf: [ + { + type: 'string', + maxLength: 255, + format: 'email' + }, + { + type: 'null' + } + ], + title: 'Email' + }, + is_active: { + type: 'boolean', + title: 'Is Active', + default: true + }, + is_superuser: { + type: 'boolean', + title: 'Is Superuser', + default: false + }, + full_name: { + anyOf: [ + { + type: 'string', + maxLength: 255 + }, + { + type: 'null' + } + ], + title: 'Full Name' + }, + password: { + anyOf: [ + { + type: 'string', + maxLength: 128, + minLength: 8 + }, + { + type: 'null' + } + ], + title: 'Password' + } + }, + type: 'object', + title: 'UserUpdate' +} as const; + +export const UserUpdateMeSchema = { + properties: { + full_name: { + anyOf: [ + { + type: 'string', + maxLength: 255 + }, + { + type: 'null' + } + ], + title: 'Full Name' + }, + email: { + anyOf: [ + { + type: 'string', + maxLength: 255, + format: 'email' + }, + { + type: 'null' + } + ], + title: 'Email' + } + }, + type: 'object', + title: 'UserUpdateMe' +} as const; + +export const UsersPublicSchema = { + properties: { + data: { + items: { + '$ref': '#/components/schemas/UserPublic' + }, + type: 'array', + title: 'Data' + }, + count: { + type: 'integer', + title: 'Count' + } + }, + type: 'object', + required: ['data', 'count'], + title: 'UsersPublic' +} as const; + +export const ValidationErrorSchema = { + properties: { + loc: { + items: { + anyOf: [ + { + type: 'string' + }, + { + type: 'integer' + } + ] + }, + type: 'array', + title: 'Location' + }, + msg: { + type: 'string', + title: 'Message' + }, + type: { + type: 'string', + title: 'Error Type' + } + }, + type: 'object', + required: ['loc', 'msg', 'type'], + title: 'ValidationError' +} as const; \ No newline at end of file diff --git a/frontend/src/client/sdk.gen.ts b/frontend/src/client/sdk.gen.ts new file mode 100644 index 0000000000..ba79e3f726 --- /dev/null +++ b/frontend/src/client/sdk.gen.ts @@ -0,0 +1,468 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { CancelablePromise } from './core/CancelablePromise'; +import { OpenAPI } from './core/OpenAPI'; +import { request as __request } from './core/request'; +import type { ItemsReadItemsData, ItemsReadItemsResponse, ItemsCreateItemData, ItemsCreateItemResponse, ItemsReadItemData, ItemsReadItemResponse, ItemsUpdateItemData, ItemsUpdateItemResponse, ItemsDeleteItemData, ItemsDeleteItemResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, PrivateCreateUserData, PrivateCreateUserResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse } from './types.gen'; + +export class ItemsService { + /** + * Read Items + * Retrieve items. + * @param data The data for the request. + * @param data.skip + * @param data.limit + * @returns ItemsPublic Successful Response + * @throws ApiError + */ + public static readItems(data: ItemsReadItemsData = {}): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/items/', + query: { + skip: data.skip, + limit: data.limit + }, + errors: { + 422: 'Validation Error' + } + }); + } + + /** + * Create Item + * Create new item. + * @param data The data for the request. + * @param data.requestBody + * @returns ItemPublic Successful Response + * @throws ApiError + */ + public static createItem(data: ItemsCreateItemData): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/items/', + body: data.requestBody, + mediaType: 'application/json', + errors: { + 422: 'Validation Error' + } + }); + } + + /** + * Read Item + * Get item by ID. + * @param data The data for the request. + * @param data.id + * @returns ItemPublic Successful Response + * @throws ApiError + */ + public static readItem(data: ItemsReadItemData): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/items/{id}', + path: { + id: data.id + }, + errors: { + 422: 'Validation Error' + } + }); + } + + /** + * Update Item + * Update an item. + * @param data The data for the request. + * @param data.id + * @param data.requestBody + * @returns ItemPublic Successful Response + * @throws ApiError + */ + public static updateItem(data: ItemsUpdateItemData): CancelablePromise { + return __request(OpenAPI, { + method: 'PUT', + url: '/api/v1/items/{id}', + path: { + id: data.id + }, + body: data.requestBody, + mediaType: 'application/json', + errors: { + 422: 'Validation Error' + } + }); + } + + /** + * Delete Item + * Delete an item. + * @param data The data for the request. + * @param data.id + * @returns Message Successful Response + * @throws ApiError + */ + public static deleteItem(data: ItemsDeleteItemData): CancelablePromise { + return __request(OpenAPI, { + method: 'DELETE', + url: '/api/v1/items/{id}', + path: { + id: data.id + }, + errors: { + 422: 'Validation Error' + } + }); + } +} + +export class LoginService { + /** + * Login Access Token + * OAuth2 compatible token login, get an access token for future requests + * @param data The data for the request. + * @param data.formData + * @returns Token Successful Response + * @throws ApiError + */ + public static loginAccessToken(data: LoginLoginAccessTokenData): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/login/access-token', + formData: data.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 + * @param data The data for the request. + * @param data.email + * @returns Message Successful Response + * @throws ApiError + */ + public static recoverPassword(data: LoginRecoverPasswordData): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/password-recovery/{email}', + path: { + email: data.email + }, + errors: { + 422: 'Validation Error' + } + }); + } + + /** + * Reset Password + * Reset password + * @param data The data for the request. + * @param data.requestBody + * @returns Message Successful Response + * @throws ApiError + */ + public static resetPassword(data: LoginResetPasswordData): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/reset-password/', + body: data.requestBody, + mediaType: 'application/json', + errors: { + 422: 'Validation Error' + } + }); + } + + /** + * Recover Password Html Content + * HTML Content for Password Recovery + * @param data The data for the request. + * @param data.email + * @returns string Successful Response + * @throws ApiError + */ + public static recoverPasswordHtmlContent(data: LoginRecoverPasswordHtmlContentData): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/password-recovery-html-content/{email}', + path: { + email: data.email + }, + errors: { + 422: 'Validation Error' + } + }); + } +} + +export class PrivateService { + /** + * Create User + * Create a new user. + * @param data The data for the request. + * @param data.requestBody + * @returns UserPublic Successful Response + * @throws ApiError + */ + public static createUser(data: PrivateCreateUserData): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/private/users/', + body: data.requestBody, + mediaType: 'application/json', + errors: { + 422: 'Validation Error' + } + }); + } +} + +export class UsersService { + /** + * Read Users + * Retrieve users. + * @param data The data for the request. + * @param data.skip + * @param data.limit + * @returns UsersPublic Successful Response + * @throws ApiError + */ + public static readUsers(data: UsersReadUsersData = {}): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/users/', + query: { + skip: data.skip, + limit: data.limit + }, + errors: { + 422: 'Validation Error' + } + }); + } + + /** + * Create User + * Create new user. + * @param data The data for the request. + * @param data.requestBody + * @returns UserPublic Successful Response + * @throws ApiError + */ + public static createUser(data: UsersCreateUserData): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/users/', + body: data.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. + * @param data The data for the request. + * @param data.requestBody + * @returns UserPublic Successful Response + * @throws ApiError + */ + public static updateUserMe(data: UsersUpdateUserMeData): CancelablePromise { + return __request(OpenAPI, { + method: 'PATCH', + url: '/api/v1/users/me', + body: data.requestBody, + mediaType: 'application/json', + errors: { + 422: 'Validation Error' + } + }); + } + + /** + * Update Password Me + * Update own password. + * @param data The data for the request. + * @param data.requestBody + * @returns Message Successful Response + * @throws ApiError + */ + public static updatePasswordMe(data: UsersUpdatePasswordMeData): CancelablePromise { + return __request(OpenAPI, { + method: 'PATCH', + url: '/api/v1/users/me/password', + body: data.requestBody, + mediaType: 'application/json', + errors: { + 422: 'Validation Error' + } + }); + } + + /** + * Register User + * Create new user without the need to be logged in. + * @param data The data for the request. + * @param data.requestBody + * @returns UserPublic Successful Response + * @throws ApiError + */ + public static registerUser(data: UsersRegisterUserData): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/users/signup', + body: data.requestBody, + mediaType: 'application/json', + errors: { + 422: 'Validation Error' + } + }); + } + + /** + * Read User By Id + * Get a specific user by id. + * @param data The data for the request. + * @param data.userId + * @returns UserPublic Successful Response + * @throws ApiError + */ + public static readUserById(data: UsersReadUserByIdData): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/users/{user_id}', + path: { + user_id: data.userId + }, + errors: { + 422: 'Validation Error' + } + }); + } + + /** + * Update User + * Update a user. + * @param data The data for the request. + * @param data.userId + * @param data.requestBody + * @returns UserPublic Successful Response + * @throws ApiError + */ + public static updateUser(data: UsersUpdateUserData): CancelablePromise { + return __request(OpenAPI, { + method: 'PATCH', + url: '/api/v1/users/{user_id}', + path: { + user_id: data.userId + }, + body: data.requestBody, + mediaType: 'application/json', + errors: { + 422: 'Validation Error' + } + }); + } + + /** + * Delete User + * Delete a user. + * @param data The data for the request. + * @param data.userId + * @returns Message Successful Response + * @throws ApiError + */ + public static deleteUser(data: UsersDeleteUserData): CancelablePromise { + return __request(OpenAPI, { + method: 'DELETE', + url: '/api/v1/users/{user_id}', + path: { + user_id: data.userId + }, + errors: { + 422: 'Validation Error' + } + }); + } +} + +export class UtilsService { + /** + * Test Email + * Test emails. + * @param data The data for the request. + * @param data.emailTo + * @returns Message Successful Response + * @throws ApiError + */ + public static testEmail(data: UtilsTestEmailData): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/utils/test-email/', + query: { + email_to: data.emailTo + }, + errors: { + 422: 'Validation Error' + } + }); + } + + /** + * Health Check + * @returns boolean Successful Response + * @throws ApiError + */ + public static healthCheck(): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/utils/health-check/' + }); + } +} \ No newline at end of file diff --git a/frontend/src/client/types.gen.ts b/frontend/src/client/types.gen.ts new file mode 100644 index 0000000000..e5cf34c34c --- /dev/null +++ b/frontend/src/client/types.gen.ts @@ -0,0 +1,234 @@ +// This file is auto-generated by @hey-api/openapi-ts + +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 ItemsPublic = { + data: Array; + count: number; +}; + +export type ItemUpdate = { + title?: (string | null); + description?: (string | null); +}; + +export type Message = { + message: string; +}; + +export type NewPassword = { + token: string; + new_password: string; +}; + +export type PrivateUserCreate = { + email: string; + password: string; + full_name: string; + is_verified?: boolean; +}; + +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 UsersPublic = { + data: Array; + count: number; +}; + +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 ValidationError = { + loc: Array<(string | number)>; + msg: string; + type: string; +}; + +export type ItemsReadItemsData = { + limit?: number; + skip?: number; +}; + +export type ItemsReadItemsResponse = (ItemsPublic); + +export type ItemsCreateItemData = { + requestBody: ItemCreate; +}; + +export type ItemsCreateItemResponse = (ItemPublic); + +export type ItemsReadItemData = { + id: string; +}; + +export type ItemsReadItemResponse = (ItemPublic); + +export type ItemsUpdateItemData = { + id: string; + requestBody: ItemUpdate; +}; + +export type ItemsUpdateItemResponse = (ItemPublic); + +export type ItemsDeleteItemData = { + id: string; +}; + +export type ItemsDeleteItemResponse = (Message); + +export type LoginLoginAccessTokenData = { + formData: Body_login_login_access_token; +}; + +export type LoginLoginAccessTokenResponse = (Token); + +export type LoginTestTokenResponse = (UserPublic); + +export type LoginRecoverPasswordData = { + email: string; +}; + +export type LoginRecoverPasswordResponse = (Message); + +export type LoginResetPasswordData = { + requestBody: NewPassword; +}; + +export type LoginResetPasswordResponse = (Message); + +export type LoginRecoverPasswordHtmlContentData = { + email: string; +}; + +export type LoginRecoverPasswordHtmlContentResponse = (string); + +export type PrivateCreateUserData = { + requestBody: PrivateUserCreate; +}; + +export type PrivateCreateUserResponse = (UserPublic); + +export type UsersReadUsersData = { + limit?: number; + skip?: number; +}; + +export type UsersReadUsersResponse = (UsersPublic); + +export type UsersCreateUserData = { + requestBody: UserCreate; +}; + +export type UsersCreateUserResponse = (UserPublic); + +export type UsersReadUserMeResponse = (UserPublic); + +export type UsersDeleteUserMeResponse = (Message); + +export type UsersUpdateUserMeData = { + requestBody: UserUpdateMe; +}; + +export type UsersUpdateUserMeResponse = (UserPublic); + +export type UsersUpdatePasswordMeData = { + requestBody: UpdatePassword; +}; + +export type UsersUpdatePasswordMeResponse = (Message); + +export type UsersRegisterUserData = { + requestBody: UserRegister; +}; + +export type UsersRegisterUserResponse = (UserPublic); + +export type UsersReadUserByIdData = { + userId: string; +}; + +export type UsersReadUserByIdResponse = (UserPublic); + +export type UsersUpdateUserData = { + requestBody: UserUpdate; + userId: string; +}; + +export type UsersUpdateUserResponse = (UserPublic); + +export type UsersDeleteUserData = { + userId: string; +}; + +export type UsersDeleteUserResponse = (Message); + +export type UtilsTestEmailData = { + emailTo: string; +}; + +export type UtilsTestEmailResponse = (Message); + +export type UtilsHealthCheckResponse = (boolean); \ No newline at end of file diff --git a/frontend/src/components/Admin/AddUser.tsx b/frontend/src/components/Admin/AddUser.tsx new file mode 100644 index 0000000000..2276b43c24 --- /dev/null +++ b/frontend/src/components/Admin/AddUser.tsx @@ -0,0 +1,225 @@ +import { + Button, + DialogActionTrigger, + DialogTitle, + Flex, + Input, + Text, + VStack, +} from "@chakra-ui/react" +import { useMutation, useQueryClient } from "@tanstack/react-query" +import { useState } from "react" +import { Controller, type SubmitHandler, useForm } from "react-hook-form" +import { FaPlus } from "react-icons/fa" +import { type UserCreate, UsersService } from "@/client" +import type { ApiError } from "@/client/core/ApiError" +import useCustomToast from "@/hooks/useCustomToast" +import { emailPattern, handleError } from "@/utils" +import { Checkbox } from "../ui/checkbox" +import { + DialogBody, + DialogCloseTrigger, + DialogContent, + DialogFooter, + DialogHeader, + DialogRoot, + DialogTrigger, +} from "../ui/dialog" +import { Field } from "../ui/field" + +interface UserCreateForm extends UserCreate { + confirm_password: string +} + +const AddUser = () => { + const [isOpen, setIsOpen] = useState(false) + const queryClient = useQueryClient() + const { showSuccessToast } = useCustomToast() + const { + control, + register, + handleSubmit, + reset, + getValues, + formState: { errors, isValid, 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: () => { + showSuccessToast("User created successfully.") + reset() + setIsOpen(false) + }, + onError: (err: ApiError) => { + handleError(err) + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }) + }, + }) + + const onSubmit: SubmitHandler = (data) => { + mutation.mutate(data) + } + + return ( + setIsOpen(open)} + > + + + + +
+ + Add User + + + + Fill in the form below to add a new user to the system. + + + + + + + + + + + + + + + + + value === getValues().password || + "The passwords do not match", + })} + placeholder="Password" + type="password" + /> + + + + + ( + + field.onChange(checked)} + > + Is superuser? + + + )} + /> + ( + + field.onChange(checked)} + > + Is active? + + + )} + /> + + + + + + + + + +
+ +
+
+ ) +} + +export default AddUser diff --git a/frontend/src/components/Admin/DeleteUser.tsx b/frontend/src/components/Admin/DeleteUser.tsx new file mode 100644 index 0000000000..f3e7db3173 --- /dev/null +++ b/frontend/src/components/Admin/DeleteUser.tsx @@ -0,0 +1,104 @@ +import { Button, DialogTitle, Text } from "@chakra-ui/react" +import { useMutation, useQueryClient } from "@tanstack/react-query" +import { useState } from "react" +import { useForm } from "react-hook-form" +import { FiTrash2 } from "react-icons/fi" + +import { UsersService } from "@/client" +import { + DialogActionTrigger, + DialogBody, + DialogCloseTrigger, + DialogContent, + DialogFooter, + DialogHeader, + DialogRoot, + DialogTrigger, +} from "@/components/ui/dialog" +import useCustomToast from "@/hooks/useCustomToast" + +const DeleteUser = ({ id }: { id: string }) => { + const [isOpen, setIsOpen] = useState(false) + const queryClient = useQueryClient() + const { showSuccessToast, showErrorToast } = useCustomToast() + const { + handleSubmit, + formState: { isSubmitting }, + } = useForm() + + const deleteUser = async (id: string) => { + await UsersService.deleteUser({ userId: id }) + } + + const mutation = useMutation({ + mutationFn: deleteUser, + onSuccess: () => { + showSuccessToast("The user was deleted successfully") + setIsOpen(false) + }, + onError: () => { + showErrorToast("An error occurred while deleting the user") + }, + onSettled: () => { + queryClient.invalidateQueries() + }, + }) + + const onSubmit = async () => { + mutation.mutate(id) + } + + return ( + setIsOpen(open)} + > + + + + +
+ + Delete User + + + + All items associated with this user will also be{" "} + permanently deleted. Are you sure? You will not + be able to undo this action. + + + + + + + + + + + +
+
+ ) +} + +export default DeleteUser diff --git a/frontend/src/components/Admin/EditUser.tsx b/frontend/src/components/Admin/EditUser.tsx new file mode 100644 index 0000000000..545f334a51 --- /dev/null +++ b/frontend/src/components/Admin/EditUser.tsx @@ -0,0 +1,215 @@ +import { + Button, + DialogActionTrigger, + DialogRoot, + DialogTrigger, + Flex, + Input, + Text, + VStack, +} from "@chakra-ui/react" +import { useMutation, useQueryClient } from "@tanstack/react-query" +import { useState } from "react" +import { Controller, type SubmitHandler, useForm } from "react-hook-form" +import { FaExchangeAlt } from "react-icons/fa" + +import { type UserPublic, UsersService, type UserUpdate } from "@/client" +import type { ApiError } from "@/client/core/ApiError" +import useCustomToast from "@/hooks/useCustomToast" +import { emailPattern, handleError } from "@/utils" +import { Checkbox } from "../ui/checkbox" +import { + DialogBody, + DialogCloseTrigger, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../ui/dialog" +import { Field } from "../ui/field" + +interface EditUserProps { + user: UserPublic +} + +interface UserUpdateForm extends UserUpdate { + confirm_password?: string +} + +const EditUser = ({ user }: EditUserProps) => { + const [isOpen, setIsOpen] = useState(false) + const queryClient = useQueryClient() + const { showSuccessToast } = useCustomToast() + const { + control, + register, + handleSubmit, + reset, + getValues, + formState: { errors, isSubmitting }, + } = useForm({ + mode: "onBlur", + criteriaMode: "all", + defaultValues: user, + }) + + const mutation = useMutation({ + mutationFn: (data: UserUpdateForm) => + UsersService.updateUser({ userId: user.id, requestBody: data }), + onSuccess: () => { + showSuccessToast("User updated successfully.") + reset() + setIsOpen(false) + }, + onError: (err: ApiError) => { + handleError(err) + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }) + }, + }) + + const onSubmit: SubmitHandler = async (data) => { + if (data.password === "") { + data.password = undefined + } + mutation.mutate(data) + } + + return ( + setIsOpen(open)} + > + + + + +
+ + Edit User + + + Update the user details below. + + + + + + + + + + + + + + + + value === getValues().password || + "The passwords do not match", + })} + placeholder="Password" + type="password" + /> + + + + + ( + + field.onChange(checked)} + > + Is superuser? + + + )} + /> + ( + + field.onChange(checked)} + > + Is active? + + + )} + /> + + + + + + + + + + + +
+
+ ) +} + +export default EditUser diff --git a/frontend/src/components/Common/ItemActionsMenu.tsx b/frontend/src/components/Common/ItemActionsMenu.tsx new file mode 100644 index 0000000000..18e424fdd4 --- /dev/null +++ b/frontend/src/components/Common/ItemActionsMenu.tsx @@ -0,0 +1,26 @@ +import { IconButton } from "@chakra-ui/react" +import { BsThreeDotsVertical } from "react-icons/bs" +import type { ItemPublic } from "@/client" +import DeleteItem from "../Items/DeleteItem" +import EditItem from "../Items/EditItem" +import { MenuContent, MenuRoot, MenuTrigger } from "../ui/menu" + +interface ItemActionsMenuProps { + item: ItemPublic +} + +export const ItemActionsMenu = ({ item }: ItemActionsMenuProps) => { + return ( + + + + + + + + + + + + ) +} diff --git a/frontend/src/components/Common/Navbar.tsx b/frontend/src/components/Common/Navbar.tsx new file mode 100644 index 0000000000..7e952e005e --- /dev/null +++ b/frontend/src/components/Common/Navbar.tsx @@ -0,0 +1,32 @@ +import { Flex, Image, useBreakpointValue } from "@chakra-ui/react" +import { Link } from "@tanstack/react-router" + +import Logo from "/assets/images/fastapi-logo.svg" +import UserMenu from "./UserMenu" + +function Navbar() { + const display = useBreakpointValue({ base: "none", md: "flex" }) + + return ( + + + Logo + + + + + + ) +} + +export default Navbar diff --git a/frontend/src/components/Common/NotFound.tsx b/frontend/src/components/Common/NotFound.tsx new file mode 100644 index 0000000000..9e4f18528e --- /dev/null +++ b/frontend/src/components/Common/NotFound.tsx @@ -0,0 +1,44 @@ +import { Button, Center, Flex, Text } from "@chakra-ui/react" +import { Link } from "@tanstack/react-router" + +const NotFound = () => { + return ( + + + + + 404 + + + Oops! + + + + + + The page you are looking for was not found. + +
+ + + +
+
+ ) +} + +export default NotFound diff --git a/frontend/src/components/Common/Sidebar.tsx b/frontend/src/components/Common/Sidebar.tsx new file mode 100644 index 0000000000..8437634f47 --- /dev/null +++ b/frontend/src/components/Common/Sidebar.tsx @@ -0,0 +1,97 @@ +import { Box, Flex, IconButton, Text } from "@chakra-ui/react" +import { useQueryClient } from "@tanstack/react-query" +import { useState } from "react" +import { FaBars } from "react-icons/fa" +import { FiLogOut } from "react-icons/fi" + +import type { UserPublic } from "@/client" +import useAuth from "@/hooks/useAuth" +import { + DrawerBackdrop, + DrawerBody, + DrawerCloseTrigger, + DrawerContent, + DrawerRoot, + DrawerTrigger, +} from "../ui/drawer" +import SidebarItems from "./SidebarItems" + +const Sidebar = () => { + const queryClient = useQueryClient() + const currentUser = queryClient.getQueryData(["currentUser"]) + const { logout } = useAuth() + const [open, setOpen] = useState(false) + + return ( + <> + {/* Mobile */} + setOpen(e.open)} + > + + + + + + + + + + + + setOpen(false)} /> + { + logout() + }} + alignItems="center" + gap={4} + px={4} + py={2} + > + + Log Out + + + {currentUser?.email && ( + + Logged in as: {currentUser.email} + + )} + + + + + + + {/* Desktop */} + + + + + + + + ) +} + +export default Sidebar diff --git a/frontend/src/components/Common/SidebarItems.tsx b/frontend/src/components/Common/SidebarItems.tsx new file mode 100644 index 0000000000..13f71495f5 --- /dev/null +++ b/frontend/src/components/Common/SidebarItems.tsx @@ -0,0 +1,61 @@ +import { Box, Flex, Icon, Text } from "@chakra-ui/react" +import { useQueryClient } from "@tanstack/react-query" +import { Link as RouterLink } from "@tanstack/react-router" +import { FiBriefcase, FiHome, FiSettings, FiUsers } from "react-icons/fi" +import type { IconType } from "react-icons/lib" + +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 +} + +interface Item { + icon: IconType + title: string + path: string +} + +const SidebarItems = ({ onClose }: SidebarItemsProps) => { + const queryClient = useQueryClient() + const currentUser = queryClient.getQueryData(["currentUser"]) + + const finalItems: Item[] = currentUser?.is_superuser + ? [...items, { icon: FiUsers, title: "Admin", path: "/admin" }] + : items + + const listItems = finalItems.map(({ icon, title, path }) => ( + + + + {title} + + + )) + + return ( + <> + + Menu + + {listItems} + + ) +} + +export default SidebarItems diff --git a/frontend/src/components/Common/UserActionsMenu.tsx b/frontend/src/components/Common/UserActionsMenu.tsx new file mode 100644 index 0000000000..882ecb4dd8 --- /dev/null +++ b/frontend/src/components/Common/UserActionsMenu.tsx @@ -0,0 +1,27 @@ +import { IconButton } from "@chakra-ui/react" +import { BsThreeDotsVertical } from "react-icons/bs" +import type { UserPublic } from "@/client" +import DeleteUser from "../Admin/DeleteUser" +import EditUser from "../Admin/EditUser" +import { MenuContent, MenuRoot, MenuTrigger } from "../ui/menu" + +interface UserActionsMenuProps { + user: UserPublic + disabled?: boolean +} + +export const UserActionsMenu = ({ user, disabled }: UserActionsMenuProps) => { + return ( + + + + + + + + + + + + ) +} diff --git a/frontend/src/components/Common/UserMenu.tsx b/frontend/src/components/Common/UserMenu.tsx new file mode 100644 index 0000000000..9db1df944f --- /dev/null +++ b/frontend/src/components/Common/UserMenu.tsx @@ -0,0 +1,59 @@ +import { Box, Button, Flex, Text } 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" +import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from "../ui/menu" + +const UserMenu = () => { + const { user, logout } = useAuth() + + const handleLogout = async () => { + logout() + } + + return ( + <> + {/* Desktop */} + + + + + + + + + + + My Profile + + + + + + Log Out + + + + + + ) +} + +export default UserMenu diff --git a/frontend/src/components/Items/AddItem.tsx b/frontend/src/components/Items/AddItem.tsx new file mode 100644 index 0000000000..5a377b952a --- /dev/null +++ b/frontend/src/components/Items/AddItem.tsx @@ -0,0 +1,143 @@ +import { + Button, + DialogActionTrigger, + DialogTitle, + Input, + Text, + VStack, +} from "@chakra-ui/react" +import { useMutation, useQueryClient } from "@tanstack/react-query" +import { useState } from "react" +import { type SubmitHandler, useForm } from "react-hook-form" +import { FaPlus } from "react-icons/fa" + +import { type ItemCreate, ItemsService } from "@/client" +import type { ApiError } from "@/client/core/ApiError" +import useCustomToast from "@/hooks/useCustomToast" +import { handleError } from "@/utils" +import { + DialogBody, + DialogCloseTrigger, + DialogContent, + DialogFooter, + DialogHeader, + DialogRoot, + DialogTrigger, +} from "../ui/dialog" +import { Field } from "../ui/field" + +const AddItem = () => { + const [isOpen, setIsOpen] = useState(false) + const queryClient = useQueryClient() + const { showSuccessToast } = useCustomToast() + const { + register, + handleSubmit, + reset, + formState: { errors, isValid, isSubmitting }, + } = useForm({ + mode: "onBlur", + criteriaMode: "all", + defaultValues: { + title: "", + description: "", + }, + }) + + const mutation = useMutation({ + mutationFn: (data: ItemCreate) => + ItemsService.createItem({ requestBody: data }), + onSuccess: () => { + showSuccessToast("Item created successfully.") + reset() + setIsOpen(false) + }, + onError: (err: ApiError) => { + handleError(err) + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ["items"] }) + }, + }) + + const onSubmit: SubmitHandler = (data) => { + mutation.mutate(data) + } + + return ( + setIsOpen(open)} + > + + + + +
+ + Add Item + + + Fill in the details to add a new item. + + + + + + + + + + + + + + + + + +
+ +
+
+ ) +} + +export default AddItem diff --git a/frontend/src/components/Items/DeleteItem.tsx b/frontend/src/components/Items/DeleteItem.tsx new file mode 100644 index 0000000000..ea3b7fdc7e --- /dev/null +++ b/frontend/src/components/Items/DeleteItem.tsx @@ -0,0 +1,104 @@ +import { Button, DialogTitle, Text } from "@chakra-ui/react" +import { useMutation, useQueryClient } from "@tanstack/react-query" +import { useState } from "react" +import { useForm } from "react-hook-form" +import { FiTrash2 } from "react-icons/fi" + +import { ItemsService } from "@/client" +import { + DialogActionTrigger, + DialogBody, + DialogCloseTrigger, + DialogContent, + DialogFooter, + DialogHeader, + DialogRoot, + DialogTrigger, +} from "@/components/ui/dialog" +import useCustomToast from "@/hooks/useCustomToast" + +const DeleteItem = ({ id }: { id: string }) => { + const [isOpen, setIsOpen] = useState(false) + const queryClient = useQueryClient() + const { showSuccessToast, showErrorToast } = useCustomToast() + const { + handleSubmit, + formState: { isSubmitting }, + } = useForm() + + const deleteItem = async (id: string) => { + await ItemsService.deleteItem({ id: id }) + } + + const mutation = useMutation({ + mutationFn: deleteItem, + onSuccess: () => { + showSuccessToast("The item was deleted successfully") + setIsOpen(false) + }, + onError: () => { + showErrorToast("An error occurred while deleting the item") + }, + onSettled: () => { + queryClient.invalidateQueries() + }, + }) + + const onSubmit = async () => { + mutation.mutate(id) + } + + return ( + setIsOpen(open)} + > + + + + + +
+ + + Delete Item + + + + This item will be permanently deleted. Are you sure? You will not + be able to undo this action. + + + + + + + + + + +
+
+ ) +} + +export default DeleteItem diff --git a/frontend/src/components/Items/EditItem.tsx b/frontend/src/components/Items/EditItem.tsx new file mode 100644 index 0000000000..e23c92b422 --- /dev/null +++ b/frontend/src/components/Items/EditItem.tsx @@ -0,0 +1,149 @@ +import { + Button, + ButtonGroup, + DialogActionTrigger, + Input, + Text, + VStack, +} from "@chakra-ui/react" +import { useMutation, useQueryClient } from "@tanstack/react-query" +import { useState } from "react" +import { type SubmitHandler, useForm } from "react-hook-form" +import { FaExchangeAlt } from "react-icons/fa" + +import { type ApiError, type ItemPublic, ItemsService } from "@/client" +import useCustomToast from "@/hooks/useCustomToast" +import { handleError } from "@/utils" +import { + DialogBody, + DialogCloseTrigger, + DialogContent, + DialogFooter, + DialogHeader, + DialogRoot, + DialogTitle, + DialogTrigger, +} from "../ui/dialog" +import { Field } from "../ui/field" + +interface EditItemProps { + item: ItemPublic +} + +interface ItemUpdateForm { + title: string + description?: string +} + +const EditItem = ({ item }: EditItemProps) => { + const [isOpen, setIsOpen] = useState(false) + const queryClient = useQueryClient() + const { showSuccessToast } = useCustomToast() + const { + register, + handleSubmit, + reset, + formState: { errors, isSubmitting }, + } = useForm({ + mode: "onBlur", + criteriaMode: "all", + defaultValues: { + ...item, + description: item.description ?? undefined, + }, + }) + + const mutation = useMutation({ + mutationFn: (data: ItemUpdateForm) => + ItemsService.updateItem({ id: item.id, requestBody: data }), + onSuccess: () => { + showSuccessToast("Item updated successfully.") + reset() + setIsOpen(false) + }, + onError: (err: ApiError) => { + handleError(err) + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ["items"] }) + }, + }) + + const onSubmit: SubmitHandler = async (data) => { + mutation.mutate(data) + } + + return ( + setIsOpen(open)} + > + + + + +
+ + Edit Item + + + Update the item details below. + + + + + + + + + + + + + + + + + + + +
+ +
+
+ ) +} + +export default EditItem diff --git a/frontend/src/components/Pending/PendingItems.tsx b/frontend/src/components/Pending/PendingItems.tsx new file mode 100644 index 0000000000..0afc50477d --- /dev/null +++ b/frontend/src/components/Pending/PendingItems.tsx @@ -0,0 +1,35 @@ +import { Table } from "@chakra-ui/react" +import { SkeletonText } from "../ui/skeleton" + +const PendingItems = () => ( + + + + ID + Title + Description + Actions + + + + {[...Array(5)].map((_, index) => ( + + + + + + + + + + + + + + + ))} + + +) + +export default PendingItems diff --git a/frontend/src/components/Pending/PendingUsers.tsx b/frontend/src/components/Pending/PendingUsers.tsx new file mode 100644 index 0000000000..c7ac1c73ec --- /dev/null +++ b/frontend/src/components/Pending/PendingUsers.tsx @@ -0,0 +1,39 @@ +import { Table } from "@chakra-ui/react" +import { SkeletonText } from "../ui/skeleton" + +const PendingUsers = () => ( + + + + Full name + Email + Role + Status + Actions + + + + {[...Array(5)].map((_, index) => ( + + + + + + + + + + + + + + + + + + ))} + + +) + +export default PendingUsers diff --git a/frontend/src/components/UserSettings/Appearance.tsx b/frontend/src/components/UserSettings/Appearance.tsx new file mode 100644 index 0000000000..679f424a9d --- /dev/null +++ b/frontend/src/components/UserSettings/Appearance.tsx @@ -0,0 +1,29 @@ +import { Container, Heading, Stack } from "@chakra-ui/react" +import { useTheme } from "next-themes" + +import { Radio, RadioGroup } from "@/components/ui/radio" + +const Appearance = () => { + const { theme, setTheme } = useTheme() + + return ( + + + Appearance + + + setTheme(e.value ?? "system")} + value={theme} + colorPalette="teal" + > + + System + Light Mode + Dark Mode + + + + ) +} +export default Appearance diff --git a/frontend/src/components/UserSettings/ChangePassword.tsx b/frontend/src/components/UserSettings/ChangePassword.tsx new file mode 100644 index 0000000000..8cd0b541b4 --- /dev/null +++ b/frontend/src/components/UserSettings/ChangePassword.tsx @@ -0,0 +1,80 @@ +import { Box, Button, Container, Heading, VStack } from "@chakra-ui/react" +import { useMutation } from "@tanstack/react-query" +import { type SubmitHandler, useForm } from "react-hook-form" +import { FiLock } from "react-icons/fi" + +import { type ApiError, type UpdatePassword, UsersService } from "@/client" +import useCustomToast from "@/hooks/useCustomToast" +import { confirmPasswordRules, handleError, passwordRules } from "@/utils" +import { PasswordInput } from "../ui/password-input" + +interface UpdatePasswordForm extends UpdatePassword { + confirm_password: string +} + +const ChangePassword = () => { + const { showSuccessToast } = 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: () => { + showSuccessToast("Password updated successfully.") + reset() + }, + onError: (err: ApiError) => { + handleError(err) + }, + }) + + const onSubmit: SubmitHandler = async (data) => { + mutation.mutate(data) + } + + return ( + + + Change Password + + + + } + {...register("current_password", passwordRules())} + placeholder="Current Password" + errors={errors} + /> + } + {...register("new_password", passwordRules())} + placeholder="New Password" + errors={errors} + /> + } + {...register("confirm_password", confirmPasswordRules(getValues))} + placeholder="Confirm Password" + errors={errors} + /> + + + + + ) +} +export default ChangePassword diff --git a/frontend/src/components/UserSettings/DeleteAccount.tsx b/frontend/src/components/UserSettings/DeleteAccount.tsx new file mode 100644 index 0000000000..5800c98fe1 --- /dev/null +++ b/frontend/src/components/UserSettings/DeleteAccount.tsx @@ -0,0 +1,19 @@ +import { Container, Heading, Text } from "@chakra-ui/react" + +import DeleteConfirmation from "./DeleteConfirmation" + +const DeleteAccount = () => { + 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 new file mode 100644 index 0000000000..3885a570d7 --- /dev/null +++ b/frontend/src/components/UserSettings/DeleteConfirmation.tsx @@ -0,0 +1,107 @@ +import { Button, ButtonGroup, Text } from "@chakra-ui/react" +import { useMutation, useQueryClient } from "@tanstack/react-query" +import { useState } from "react" +import { useForm } from "react-hook-form" + +import { type ApiError, UsersService } from "@/client" +import { + DialogActionTrigger, + DialogBody, + DialogCloseTrigger, + DialogContent, + DialogFooter, + DialogHeader, + DialogRoot, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import useAuth from "@/hooks/useAuth" +import useCustomToast from "@/hooks/useCustomToast" +import { handleError } from "@/utils" + +const DeleteConfirmation = () => { + const [isOpen, setIsOpen] = useState(false) + const queryClient = useQueryClient() + const { showSuccessToast } = useCustomToast() + const { + handleSubmit, + formState: { isSubmitting }, + } = useForm() + const { logout } = useAuth() + + const mutation = useMutation({ + mutationFn: () => UsersService.deleteUserMe(), + onSuccess: () => { + showSuccessToast("Your account has been successfully deleted") + setIsOpen(false) + logout() + }, + onError: (err: ApiError) => { + handleError(err) + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ["currentUser"] }) + }, + }) + + const onSubmit = async () => { + mutation.mutate() + } + + return ( + setIsOpen(open)} + > + + + + + +
+ + + 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 new file mode 100644 index 0000000000..c9e64693c1 --- /dev/null +++ b/frontend/src/components/UserSettings/UserInformation.tsx @@ -0,0 +1,148 @@ +import { + Box, + Button, + Container, + Flex, + Heading, + Input, + Text, +} 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, + UsersService, + type UserUpdateMe, +} from "@/client" +import useAuth from "@/hooks/useAuth" +import useCustomToast from "@/hooks/useCustomToast" +import { emailPattern, handleError } from "@/utils" +import { Field } from "../ui/field" + +const UserInformation = () => { + const queryClient = useQueryClient() + const { showSuccessToast } = 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: () => { + showSuccessToast("User updated successfully.") + }, + onError: (err: ApiError) => { + handleError(err) + }, + onSettled: () => { + queryClient.invalidateQueries() + }, + }) + + const onSubmit: SubmitHandler = async (data) => { + mutation.mutate(data) + } + + const onCancel = () => { + reset() + toggleEditMode() + } + + return ( + + + User Information + + + + {editMode ? ( + + ) : ( + + {currentUser?.full_name || "N/A"} + + )} + + + {editMode ? ( + + ) : ( + + {currentUser?.email} + + )} + + + + {editMode && ( + + )} + + + + ) +} + +export default UserInformation diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000000..21d5f4b550 --- /dev/null +++ b/frontend/src/components/ui/button.tsx @@ -0,0 +1,40 @@ +import type { ButtonProps as ChakraButtonProps } from "@chakra-ui/react" +import { + AbsoluteCenter, + Button as ChakraButton, + Span, + Spinner, +} from "@chakra-ui/react" +import * as React from "react" + +interface ButtonLoadingProps { + loading?: boolean + loadingText?: React.ReactNode +} + +export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps {} + +export const Button = React.forwardRef( + function Button(props, ref) { + const { loading, disabled, loadingText, children, ...rest } = props + return ( + + {loading && !loadingText ? ( + <> + + + + {children} + + ) : loading && loadingText ? ( + <> + + {loadingText} + + ) : ( + children + )} + + ) + }, +) diff --git a/frontend/src/components/ui/checkbox.tsx b/frontend/src/components/ui/checkbox.tsx new file mode 100644 index 0000000000..2a27c2ffb3 --- /dev/null +++ b/frontend/src/components/ui/checkbox.tsx @@ -0,0 +1,25 @@ +import { Checkbox as ChakraCheckbox } from "@chakra-ui/react" +import * as React from "react" + +export interface CheckboxProps extends ChakraCheckbox.RootProps { + icon?: React.ReactNode + inputProps?: React.InputHTMLAttributes + rootRef?: React.Ref +} + +export const Checkbox = React.forwardRef( + function Checkbox(props, ref) { + const { icon, children, inputProps, rootRef, ...rest } = props + return ( + + + + {icon || } + + {children != null && ( + {children} + )} + + ) + }, +) diff --git a/frontend/src/components/ui/close-button.tsx b/frontend/src/components/ui/close-button.tsx new file mode 100644 index 0000000000..94af488598 --- /dev/null +++ b/frontend/src/components/ui/close-button.tsx @@ -0,0 +1,17 @@ +import type { ButtonProps } from "@chakra-ui/react" +import { IconButton as ChakraIconButton } from "@chakra-ui/react" +import * as React from "react" +import { LuX } from "react-icons/lu" + +export type CloseButtonProps = ButtonProps + +export const CloseButton = React.forwardRef< + HTMLButtonElement, + CloseButtonProps +>(function CloseButton(props, ref) { + return ( + + {props.children ?? } + + ) +}) diff --git a/frontend/src/components/ui/color-mode.tsx b/frontend/src/components/ui/color-mode.tsx new file mode 100644 index 0000000000..f93feabca9 --- /dev/null +++ b/frontend/src/components/ui/color-mode.tsx @@ -0,0 +1,107 @@ +"use client" + +import type { IconButtonProps, SpanProps } from "@chakra-ui/react" +import { ClientOnly, IconButton, Skeleton, Span } from "@chakra-ui/react" +import { ThemeProvider, useTheme } from "next-themes" +import type { ThemeProviderProps } from "next-themes" +import * as React from "react" +import { LuMoon, LuSun } from "react-icons/lu" + +export interface ColorModeProviderProps extends ThemeProviderProps {} + +export function ColorModeProvider(props: ColorModeProviderProps) { + return ( + + ) +} + +export type ColorMode = "light" | "dark" + +export interface UseColorModeReturn { + colorMode: ColorMode + setColorMode: (colorMode: ColorMode) => void + toggleColorMode: () => void +} + +export function useColorMode(): UseColorModeReturn { + const { resolvedTheme, setTheme } = useTheme() + const toggleColorMode = () => { + setTheme(resolvedTheme === "dark" ? "light" : "dark") + } + return { + colorMode: resolvedTheme as ColorMode, + setColorMode: setTheme, + toggleColorMode, + } +} + +export function useColorModeValue(light: T, dark: T) { + const { colorMode } = useColorMode() + return colorMode === "dark" ? dark : light +} + +export function ColorModeIcon() { + const { colorMode } = useColorMode() + return colorMode === "dark" ? : +} + +interface ColorModeButtonProps extends Omit {} + +export const ColorModeButton = React.forwardRef< + HTMLButtonElement, + ColorModeButtonProps +>(function ColorModeButton(props, ref) { + const { toggleColorMode } = useColorMode() + return ( + }> + + + + + ) +}) + +export const LightMode = React.forwardRef( + function LightMode(props, ref) { + return ( + + ) + }, +) + +export const DarkMode = React.forwardRef( + function DarkMode(props, ref) { + return ( + + ) + }, +) diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000000..1a038373ac --- /dev/null +++ b/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,62 @@ +import { Dialog as ChakraDialog, Portal } from "@chakra-ui/react" +import * as React from "react" +import { CloseButton } from "./close-button" + +interface DialogContentProps extends ChakraDialog.ContentProps { + portalled?: boolean + portalRef?: React.RefObject + backdrop?: boolean +} + +export const DialogContent = React.forwardRef< + HTMLDivElement, + DialogContentProps +>(function DialogContent(props, ref) { + const { + children, + portalled = true, + portalRef, + backdrop = true, + ...rest + } = props + + return ( + + {backdrop && } + + + {children} + + + + ) +}) + +export const DialogCloseTrigger = React.forwardRef< + HTMLButtonElement, + ChakraDialog.CloseTriggerProps +>(function DialogCloseTrigger(props, ref) { + return ( + + + {props.children} + + + ) +}) + +export const DialogRoot = ChakraDialog.Root +export const DialogFooter = ChakraDialog.Footer +export const DialogHeader = ChakraDialog.Header +export const DialogBody = ChakraDialog.Body +export const DialogBackdrop = ChakraDialog.Backdrop +export const DialogTitle = ChakraDialog.Title +export const DialogDescription = ChakraDialog.Description +export const DialogTrigger = ChakraDialog.Trigger +export const DialogActionTrigger = ChakraDialog.ActionTrigger diff --git a/frontend/src/components/ui/drawer.tsx b/frontend/src/components/ui/drawer.tsx new file mode 100644 index 0000000000..7b0dab3b9d --- /dev/null +++ b/frontend/src/components/ui/drawer.tsx @@ -0,0 +1,52 @@ +import { Drawer as ChakraDrawer, Portal } from "@chakra-ui/react" +import * as React from "react" +import { CloseButton } from "./close-button" + +interface DrawerContentProps extends ChakraDrawer.ContentProps { + portalled?: boolean + portalRef?: React.RefObject + offset?: ChakraDrawer.ContentProps["padding"] +} + +export const DrawerContent = React.forwardRef< + HTMLDivElement, + DrawerContentProps +>(function DrawerContent(props, ref) { + const { children, portalled = true, portalRef, offset, ...rest } = props + return ( + + + + {children} + + + + ) +}) + +export const DrawerCloseTrigger = React.forwardRef< + HTMLButtonElement, + ChakraDrawer.CloseTriggerProps +>(function DrawerCloseTrigger(props, ref) { + return ( + + + + ) +}) + +export const DrawerTrigger = ChakraDrawer.Trigger +export const DrawerRoot = ChakraDrawer.Root +export const DrawerFooter = ChakraDrawer.Footer +export const DrawerHeader = ChakraDrawer.Header +export const DrawerBody = ChakraDrawer.Body +export const DrawerBackdrop = ChakraDrawer.Backdrop +export const DrawerDescription = ChakraDrawer.Description +export const DrawerTitle = ChakraDrawer.Title +export const DrawerActionTrigger = ChakraDrawer.ActionTrigger diff --git a/frontend/src/components/ui/field.tsx b/frontend/src/components/ui/field.tsx new file mode 100644 index 0000000000..dd3b66f100 --- /dev/null +++ b/frontend/src/components/ui/field.tsx @@ -0,0 +1,33 @@ +import { Field as ChakraField } from "@chakra-ui/react" +import * as React from "react" + +export interface FieldProps extends Omit { + label?: React.ReactNode + helperText?: React.ReactNode + errorText?: React.ReactNode + optionalText?: React.ReactNode +} + +export const Field = React.forwardRef( + function Field(props, ref) { + const { label, children, helperText, errorText, optionalText, ...rest } = + props + return ( + + {label && ( + + {label} + + + )} + {children} + {helperText && ( + {helperText} + )} + {errorText && ( + {errorText} + )} + + ) + }, +) diff --git a/frontend/src/components/ui/input-group.tsx b/frontend/src/components/ui/input-group.tsx new file mode 100644 index 0000000000..5d8fb32ad2 --- /dev/null +++ b/frontend/src/components/ui/input-group.tsx @@ -0,0 +1,53 @@ +import type { BoxProps, InputElementProps } from "@chakra-ui/react" +import { Group, InputElement } from "@chakra-ui/react" +import * as React from "react" + +export interface InputGroupProps extends BoxProps { + startElementProps?: InputElementProps + endElementProps?: InputElementProps + startElement?: React.ReactNode + endElement?: React.ReactNode + children: React.ReactElement + startOffset?: InputElementProps["paddingStart"] + endOffset?: InputElementProps["paddingEnd"] +} + +export const InputGroup = React.forwardRef( + function InputGroup(props, ref) { + const { + startElement, + startElementProps, + endElement, + endElementProps, + children, + startOffset = "6px", + endOffset = "6px", + ...rest + } = props + + const child = + React.Children.only>(children) + + return ( + + {startElement && ( + + {startElement} + + )} + {React.cloneElement(child, { + ...(startElement && { + ps: `calc(var(--input-height) - ${startOffset})`, + }), + ...(endElement && { pe: `calc(var(--input-height) - ${endOffset})` }), + ...children.props, + })} + {endElement && ( + + {endElement} + + )} + + ) + }, +) diff --git a/frontend/src/components/ui/link-button.tsx b/frontend/src/components/ui/link-button.tsx new file mode 100644 index 0000000000..defa1c3776 --- /dev/null +++ b/frontend/src/components/ui/link-button.tsx @@ -0,0 +1,12 @@ +"use client" + +import type { HTMLChakraProps, RecipeProps } from "@chakra-ui/react" +import { createRecipeContext } from "@chakra-ui/react" + +export interface LinkButtonProps + extends HTMLChakraProps<"a", RecipeProps<"button">> {} + +const { withContext } = createRecipeContext({ key: "button" }) + +// Replace "a" with your framework's link component +export const LinkButton = withContext("a") diff --git a/frontend/src/components/ui/menu.tsx b/frontend/src/components/ui/menu.tsx new file mode 100644 index 0000000000..08e5db64f8 --- /dev/null +++ b/frontend/src/components/ui/menu.tsx @@ -0,0 +1,112 @@ +"use client" + +import { AbsoluteCenter, Menu as ChakraMenu, Portal } from "@chakra-ui/react" +import * as React from "react" +import { LuCheck, LuChevronRight } from "react-icons/lu" + +interface MenuContentProps extends ChakraMenu.ContentProps { + portalled?: boolean + portalRef?: React.RefObject +} + +export const MenuContent = React.forwardRef( + function MenuContent(props, ref) { + const { portalled = true, portalRef, ...rest } = props + return ( + + + + + + ) + }, +) + +export const MenuArrow = React.forwardRef< + HTMLDivElement, + ChakraMenu.ArrowProps +>(function MenuArrow(props, ref) { + return ( + + + + ) +}) + +export const MenuCheckboxItem = React.forwardRef< + HTMLDivElement, + ChakraMenu.CheckboxItemProps +>(function MenuCheckboxItem(props, ref) { + return ( + + + + + + + {props.children} + + ) +}) + +export const MenuRadioItem = React.forwardRef< + HTMLDivElement, + ChakraMenu.RadioItemProps +>(function MenuRadioItem(props, ref) { + const { children, ...rest } = props + return ( + + + + + + + {children} + + ) +}) + +export const MenuItemGroup = React.forwardRef< + HTMLDivElement, + ChakraMenu.ItemGroupProps +>(function MenuItemGroup(props, ref) { + const { title, children, ...rest } = props + return ( + + {title && ( + + {title} + + )} + {children} + + ) +}) + +export interface MenuTriggerItemProps extends ChakraMenu.ItemProps { + startIcon?: React.ReactNode +} + +export const MenuTriggerItem = React.forwardRef< + HTMLDivElement, + MenuTriggerItemProps +>(function MenuTriggerItem(props, ref) { + const { startIcon, children, ...rest } = props + return ( + + {startIcon} + {children} + + + ) +}) + +export const MenuRadioItemGroup = ChakraMenu.RadioItemGroup +export const MenuContextTrigger = ChakraMenu.ContextTrigger +export const MenuRoot = ChakraMenu.Root +export const MenuSeparator = ChakraMenu.Separator + +export const MenuItem = ChakraMenu.Item +export const MenuItemText = ChakraMenu.ItemText +export const MenuItemCommand = ChakraMenu.ItemCommand +export const MenuTrigger = ChakraMenu.Trigger diff --git a/frontend/src/components/ui/pagination.tsx b/frontend/src/components/ui/pagination.tsx new file mode 100644 index 0000000000..706e2da854 --- /dev/null +++ b/frontend/src/components/ui/pagination.tsx @@ -0,0 +1,211 @@ +"use client" + +import type { ButtonProps, TextProps } from "@chakra-ui/react" +import { + Button, + Pagination as ChakraPagination, + IconButton, + Text, + createContext, + usePaginationContext, +} from "@chakra-ui/react" +import * as React from "react" +import { + HiChevronLeft, + HiChevronRight, + HiMiniEllipsisHorizontal, +} from "react-icons/hi2" +import { LinkButton } from "./link-button" + +interface ButtonVariantMap { + current: ButtonProps["variant"] + default: ButtonProps["variant"] + ellipsis: ButtonProps["variant"] +} + +type PaginationVariant = "outline" | "solid" | "subtle" + +interface ButtonVariantContext { + size: ButtonProps["size"] + variantMap: ButtonVariantMap + getHref?: (page: number) => string +} + +const [RootPropsProvider, useRootProps] = createContext({ + name: "RootPropsProvider", +}) + +export interface PaginationRootProps + extends Omit { + size?: ButtonProps["size"] + variant?: PaginationVariant + getHref?: (page: number) => string +} + +const variantMap: Record = { + outline: { default: "ghost", ellipsis: "plain", current: "outline" }, + solid: { default: "outline", ellipsis: "outline", current: "solid" }, + subtle: { default: "ghost", ellipsis: "plain", current: "subtle" }, +} + +export const PaginationRoot = React.forwardRef< + HTMLDivElement, + PaginationRootProps +>(function PaginationRoot(props, ref) { + const { size = "sm", variant = "outline", getHref, ...rest } = props + return ( + + + + ) +}) + +export const PaginationEllipsis = React.forwardRef< + HTMLDivElement, + ChakraPagination.EllipsisProps +>(function PaginationEllipsis(props, ref) { + const { size, variantMap } = useRootProps() + return ( + + + + ) +}) + +export const PaginationItem = React.forwardRef< + HTMLButtonElement, + ChakraPagination.ItemProps +>(function PaginationItem(props, ref) { + const { page } = usePaginationContext() + const { size, variantMap, getHref } = useRootProps() + + const current = page === props.value + const variant = current ? variantMap.current : variantMap.default + + if (getHref) { + return ( + + {props.value} + + ) + } + + return ( + + + + ) +}) + +export const PaginationPrevTrigger = React.forwardRef< + HTMLButtonElement, + ChakraPagination.PrevTriggerProps +>(function PaginationPrevTrigger(props, ref) { + const { size, variantMap, getHref } = useRootProps() + const { previousPage } = usePaginationContext() + + if (getHref) { + return ( + + + + ) + } + + return ( + + + + + + ) +}) + +export const PaginationNextTrigger = React.forwardRef< + HTMLButtonElement, + ChakraPagination.NextTriggerProps +>(function PaginationNextTrigger(props, ref) { + const { size, variantMap, getHref } = useRootProps() + const { nextPage } = usePaginationContext() + + if (getHref) { + return ( + + + + ) + } + + return ( + + + + + + ) +}) + +export const PaginationItems = (props: React.HTMLAttributes) => { + return ( + + {({ pages }) => + pages.map((page, index) => { + return page.type === "ellipsis" ? ( + + ) : ( + + ) + }) + } + + ) +} + +interface PageTextProps extends TextProps { + format?: "short" | "compact" | "long" +} + +export const PaginationPageText = React.forwardRef< + HTMLParagraphElement, + PageTextProps +>(function PaginationPageText(props, ref) { + const { format = "compact", ...rest } = props + const { page, totalPages, pageRange, count } = usePaginationContext() + const content = React.useMemo(() => { + if (format === "short") return `${page} / ${totalPages}` + if (format === "compact") return `${page} of ${totalPages}` + return `${pageRange.start + 1} - ${Math.min( + pageRange.end, + count, + )} of ${count}` + }, [format, page, totalPages, pageRange, count]) + + return ( + + {content} + + ) +}) diff --git a/frontend/src/components/ui/password-input.tsx b/frontend/src/components/ui/password-input.tsx new file mode 100644 index 0000000000..de99c85d6e --- /dev/null +++ b/frontend/src/components/ui/password-input.tsx @@ -0,0 +1,162 @@ +"use client" + +import type { + ButtonProps, + GroupProps, + InputProps, + StackProps, +} from "@chakra-ui/react" +import { + Box, + HStack, + IconButton, + Input, + Stack, + mergeRefs, + useControllableState, +} from "@chakra-ui/react" +import { forwardRef, useRef } from "react" +import { FiEye, FiEyeOff } from "react-icons/fi" +import { Field } from "./field" +import { InputGroup } from "./input-group" + +export interface PasswordVisibilityProps { + defaultVisible?: boolean + visible?: boolean + onVisibleChange?: (visible: boolean) => void + visibilityIcon?: { on: React.ReactNode; off: React.ReactNode } +} + +export interface PasswordInputProps + extends InputProps, + PasswordVisibilityProps { + rootProps?: GroupProps + startElement?: React.ReactNode + type: string + errors: any +} + +export const PasswordInput = forwardRef( + function PasswordInput(props, ref) { + const { + rootProps, + defaultVisible, + visible: visibleProp, + onVisibleChange, + visibilityIcon = { on: , off: }, + startElement, + type, + errors, + ...rest + } = props + + const [visible, setVisible] = useControllableState({ + value: visibleProp, + defaultValue: defaultVisible || false, + onChange: onVisibleChange, + }) + + const inputRef = useRef(null) + + return ( + + { + if (rest.disabled) return + if (e.button !== 0) return + e.preventDefault() + setVisible(!visible) + }} + > + {visible ? visibilityIcon.off : visibilityIcon.on} + + } + {...rootProps} + > + + + + ) + }, +) + +const VisibilityTrigger = forwardRef( + function VisibilityTrigger(props, ref) { + return ( + + ) + }, +) + +interface PasswordStrengthMeterProps extends StackProps { + max?: number + value: number +} + +export const PasswordStrengthMeter = forwardRef< + HTMLDivElement, + PasswordStrengthMeterProps +>(function PasswordStrengthMeter(props, ref) { + const { max = 4, value, ...rest } = props + + const percent = (value / max) * 100 + const { label, colorPalette } = getColorPalette(percent) + + return ( + + + {Array.from({ length: max }).map((_, index) => ( + + ))} + + {label && {label}} + + ) +}) + +function getColorPalette(percent: number) { + switch (true) { + case percent < 33: + return { label: "Low", colorPalette: "red" } + case percent < 66: + return { label: "Medium", colorPalette: "orange" } + default: + return { label: "High", colorPalette: "green" } + } +} diff --git a/frontend/src/components/ui/provider.tsx b/frontend/src/components/ui/provider.tsx new file mode 100644 index 0000000000..6725536909 --- /dev/null +++ b/frontend/src/components/ui/provider.tsx @@ -0,0 +1,18 @@ +"use client" + +import { ChakraProvider } from "@chakra-ui/react" +import { type PropsWithChildren } from "react" +import { system } from "../../theme" +import { ColorModeProvider } from "./color-mode" +import { Toaster } from "./toaster" + +export function CustomProvider(props: PropsWithChildren) { + return ( + + + {props.children} + + + + ) +} diff --git a/frontend/src/components/ui/radio.tsx b/frontend/src/components/ui/radio.tsx new file mode 100644 index 0000000000..b3919d08c8 --- /dev/null +++ b/frontend/src/components/ui/radio.tsx @@ -0,0 +1,24 @@ +import { RadioGroup as ChakraRadioGroup } from "@chakra-ui/react" +import * as React from "react" + +export interface RadioProps extends ChakraRadioGroup.ItemProps { + rootRef?: React.Ref + inputProps?: React.InputHTMLAttributes +} + +export const Radio = React.forwardRef( + function Radio(props, ref) { + const { children, inputProps, rootRef, ...rest } = props + return ( + + + + {children && ( + {children} + )} + + ) + }, +) + +export const RadioGroup = ChakraRadioGroup.Root diff --git a/frontend/src/components/ui/skeleton.tsx b/frontend/src/components/ui/skeleton.tsx new file mode 100644 index 0000000000..4f2c25be57 --- /dev/null +++ b/frontend/src/components/ui/skeleton.tsx @@ -0,0 +1,47 @@ +import type { + SkeletonProps as ChakraSkeletonProps, + CircleProps, +} from "@chakra-ui/react" +import { Skeleton as ChakraSkeleton, Circle, Stack } from "@chakra-ui/react" +import * as React from "react" + +export interface SkeletonCircleProps extends ChakraSkeletonProps { + size?: CircleProps["size"] +} + +export const SkeletonCircle = React.forwardRef< + HTMLDivElement, + SkeletonCircleProps +>(function SkeletonCircle(props, ref) { + const { size, ...rest } = props + return ( + + + + ) +}) + +export interface SkeletonTextProps extends ChakraSkeletonProps { + noOfLines?: number +} + +export const SkeletonText = React.forwardRef( + function SkeletonText(props, ref) { + const { noOfLines = 3, gap, ...rest } = props + return ( + + {Array.from({ length: noOfLines }).map((_, index) => ( + + ))} + + ) + }, +) + +export const Skeleton = ChakraSkeleton diff --git a/frontend/src/components/ui/toaster.tsx b/frontend/src/components/ui/toaster.tsx new file mode 100644 index 0000000000..baac2e7406 --- /dev/null +++ b/frontend/src/components/ui/toaster.tsx @@ -0,0 +1,43 @@ +"use client" + +import { + Toaster as ChakraToaster, + Portal, + Spinner, + Stack, + Toast, + createToaster, +} from "@chakra-ui/react" + +export const toaster = createToaster({ + placement: "top-end", + pauseOnPageIdle: true, +}) + +export const Toaster = () => { + return ( + + + {(toast) => ( + + {toast.type === "loading" ? ( + + ) : ( + + )} + + {toast.title && {toast.title}} + {toast.description && ( + {toast.description} + )} + + {toast.action && ( + {toast.action.label} + )} + {toast.meta?.closable && } + + )} + + + ) +} diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts new file mode 100644 index 0000000000..5344493d49 --- /dev/null +++ b/frontend/src/hooks/useAuth.ts @@ -0,0 +1,77 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { useNavigate } from "@tanstack/react-router" +import { useState } from "react" + +import { + type Body_login_login_access_token as AccessToken, + type ApiError, + LoginService, + type UserPublic, + type UserRegister, + UsersService, +} from "@/client" +import { handleError } from "@/utils" + +const isLoggedIn = () => { + return localStorage.getItem("access_token") !== null +} + +const useAuth = () => { + const [error, setError] = useState(null) + const navigate = useNavigate() + const queryClient = useQueryClient() + const { data: user } = useQuery({ + queryKey: ["currentUser"], + queryFn: UsersService.readUserMe, + enabled: isLoggedIn(), + }) + + const signUpMutation = useMutation({ + mutationFn: (data: UserRegister) => + UsersService.registerUser({ requestBody: data }), + + onSuccess: () => { + navigate({ to: "/login" }) + }, + onError: (err: ApiError) => { + handleError(err) + }, + 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) => { + handleError(err) + }, + }) + + const logout = () => { + localStorage.removeItem("access_token") + navigate({ to: "/login" }) + } + + return { + signUpMutation, + loginMutation, + logout, + user, + error, + resetError: () => setError(null), + } +} + +export { isLoggedIn } +export default useAuth diff --git a/frontend/src/hooks/useCustomToast.ts b/frontend/src/hooks/useCustomToast.ts new file mode 100644 index 0000000000..fb04623516 --- /dev/null +++ b/frontend/src/hooks/useCustomToast.ts @@ -0,0 +1,25 @@ +"use client" + +import { toaster } from "@/components/ui/toaster" + +const useCustomToast = () => { + const showSuccessToast = (description: string) => { + toaster.create({ + title: "Success!", + description, + type: "success", + }) + } + + const showErrorToast = (description: string) => { + toaster.create({ + title: "Something went wrong!", + description, + type: "error", + }) + } + + return { showSuccessToast, showErrorToast } +} + +export default useCustomToast diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000000..77f648090d --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,49 @@ +import { + MutationCache, + QueryCache, + QueryClient, + QueryClientProvider, +} from "@tanstack/react-query" +import { createRouter, RouterProvider } from "@tanstack/react-router" +import { StrictMode } from "react" +import ReactDOM from "react-dom/client" +import { ApiError, OpenAPI } from "./client" +import { CustomProvider } from "./components/ui/provider" +import { routeTree } from "./routeTree.gen" + +OpenAPI.BASE = import.meta.env.VITE_API_URL +OpenAPI.TOKEN = async () => { + return localStorage.getItem("access_token") || "" +} + +const handleApiError = (error: Error) => { + if (error instanceof ApiError && [401, 403].includes(error.status)) { + localStorage.removeItem("access_token") + window.location.href = "/login" + } +} +const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: handleApiError, + }), + mutationCache: new MutationCache({ + onError: handleApiError, + }), +}) + +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 new file mode 100644 index 0000000000..8849130b4c --- /dev/null +++ b/frontend/src/routeTree.gen.ts @@ -0,0 +1,235 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as SignupRouteImport } from './routes/signup' +import { Route as ResetPasswordRouteImport } from './routes/reset-password' +import { Route as RecoverPasswordRouteImport } from './routes/recover-password' +import { Route as LoginRouteImport } from './routes/login' +import { Route as LayoutRouteImport } from './routes/_layout' +import { Route as LayoutIndexRouteImport } from './routes/_layout/index' +import { Route as LayoutSettingsRouteImport } from './routes/_layout/settings' +import { Route as LayoutItemsRouteImport } from './routes/_layout/items' +import { Route as LayoutAdminRouteImport } from './routes/_layout/admin' + +const SignupRoute = SignupRouteImport.update({ + id: '/signup', + path: '/signup', + getParentRoute: () => rootRouteImport, +} as any) +const ResetPasswordRoute = ResetPasswordRouteImport.update({ + id: '/reset-password', + path: '/reset-password', + getParentRoute: () => rootRouteImport, +} as any) +const RecoverPasswordRoute = RecoverPasswordRouteImport.update({ + id: '/recover-password', + path: '/recover-password', + getParentRoute: () => rootRouteImport, +} as any) +const LoginRoute = LoginRouteImport.update({ + id: '/login', + path: '/login', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutRoute = LayoutRouteImport.update({ + id: '/_layout', + getParentRoute: () => rootRouteImport, +} as any) +const LayoutIndexRoute = LayoutIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => LayoutRoute, +} as any) +const LayoutSettingsRoute = LayoutSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => LayoutRoute, +} as any) +const LayoutItemsRoute = LayoutItemsRouteImport.update({ + id: '/items', + path: '/items', + getParentRoute: () => LayoutRoute, +} as any) +const LayoutAdminRoute = LayoutAdminRouteImport.update({ + id: '/admin', + path: '/admin', + getParentRoute: () => LayoutRoute, +} as any) + +export interface FileRoutesByFullPath { + '/login': typeof LoginRoute + '/recover-password': typeof RecoverPasswordRoute + '/reset-password': typeof ResetPasswordRoute + '/signup': typeof SignupRoute + '/admin': typeof LayoutAdminRoute + '/items': typeof LayoutItemsRoute + '/settings': typeof LayoutSettingsRoute + '/': typeof LayoutIndexRoute +} +export interface FileRoutesByTo { + '/login': typeof LoginRoute + '/recover-password': typeof RecoverPasswordRoute + '/reset-password': typeof ResetPasswordRoute + '/signup': typeof SignupRoute + '/admin': typeof LayoutAdminRoute + '/items': typeof LayoutItemsRoute + '/settings': typeof LayoutSettingsRoute + '/': typeof LayoutIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/_layout': typeof LayoutRouteWithChildren + '/login': typeof LoginRoute + '/recover-password': typeof RecoverPasswordRoute + '/reset-password': typeof ResetPasswordRoute + '/signup': typeof SignupRoute + '/_layout/admin': typeof LayoutAdminRoute + '/_layout/items': typeof LayoutItemsRoute + '/_layout/settings': typeof LayoutSettingsRoute + '/_layout/': typeof LayoutIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/login' + | '/recover-password' + | '/reset-password' + | '/signup' + | '/admin' + | '/items' + | '/settings' + | '/' + fileRoutesByTo: FileRoutesByTo + to: + | '/login' + | '/recover-password' + | '/reset-password' + | '/signup' + | '/admin' + | '/items' + | '/settings' + | '/' + id: + | '__root__' + | '/_layout' + | '/login' + | '/recover-password' + | '/reset-password' + | '/signup' + | '/_layout/admin' + | '/_layout/items' + | '/_layout/settings' + | '/_layout/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + LayoutRoute: typeof LayoutRouteWithChildren + LoginRoute: typeof LoginRoute + RecoverPasswordRoute: typeof RecoverPasswordRoute + ResetPasswordRoute: typeof ResetPasswordRoute + SignupRoute: typeof SignupRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/signup': { + id: '/signup' + path: '/signup' + fullPath: '/signup' + preLoaderRoute: typeof SignupRouteImport + parentRoute: typeof rootRouteImport + } + '/reset-password': { + id: '/reset-password' + path: '/reset-password' + fullPath: '/reset-password' + preLoaderRoute: typeof ResetPasswordRouteImport + parentRoute: typeof rootRouteImport + } + '/recover-password': { + id: '/recover-password' + path: '/recover-password' + fullPath: '/recover-password' + preLoaderRoute: typeof RecoverPasswordRouteImport + parentRoute: typeof rootRouteImport + } + '/login': { + id: '/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginRouteImport + parentRoute: typeof rootRouteImport + } + '/_layout': { + id: '/_layout' + path: '' + fullPath: '' + preLoaderRoute: typeof LayoutRouteImport + parentRoute: typeof rootRouteImport + } + '/_layout/': { + id: '/_layout/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof LayoutIndexRouteImport + parentRoute: typeof LayoutRoute + } + '/_layout/settings': { + id: '/_layout/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof LayoutSettingsRouteImport + parentRoute: typeof LayoutRoute + } + '/_layout/items': { + id: '/_layout/items' + path: '/items' + fullPath: '/items' + preLoaderRoute: typeof LayoutItemsRouteImport + parentRoute: typeof LayoutRoute + } + '/_layout/admin': { + id: '/_layout/admin' + path: '/admin' + fullPath: '/admin' + preLoaderRoute: typeof LayoutAdminRouteImport + parentRoute: typeof LayoutRoute + } + } +} + +interface LayoutRouteChildren { + LayoutAdminRoute: typeof LayoutAdminRoute + LayoutItemsRoute: typeof LayoutItemsRoute + LayoutSettingsRoute: typeof LayoutSettingsRoute + LayoutIndexRoute: typeof LayoutIndexRoute +} + +const LayoutRouteChildren: LayoutRouteChildren = { + LayoutAdminRoute: LayoutAdminRoute, + LayoutItemsRoute: LayoutItemsRoute, + LayoutSettingsRoute: LayoutSettingsRoute, + LayoutIndexRoute: LayoutIndexRoute, +} + +const LayoutRouteWithChildren = + LayoutRoute._addFileChildren(LayoutRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + LayoutRoute: LayoutRouteWithChildren, + LoginRoute: LoginRoute, + RecoverPasswordRoute: RecoverPasswordRoute, + ResetPasswordRoute: ResetPasswordRoute, + SignupRoute: SignupRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx new file mode 100644 index 0000000000..ab74696101 --- /dev/null +++ b/frontend/src/routes/__root.tsx @@ -0,0 +1,34 @@ +import { createRootRoute, Outlet } 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 new file mode 100644 index 0000000000..2514324271 --- /dev/null +++ b/frontend/src/routes/_layout.tsx @@ -0,0 +1,33 @@ +import { Flex } from "@chakra-ui/react" +import { createFileRoute, Outlet, redirect } from "@tanstack/react-router" + +import Navbar from "@/components/Common/Navbar" +import Sidebar from "@/components/Common/Sidebar" +import { isLoggedIn } from "@/hooks/useAuth" + +export const Route = createFileRoute("/_layout")({ + component: Layout, + beforeLoad: async () => { + if (!isLoggedIn()) { + throw redirect({ + to: "/login", + }) + } + }, +}) + +function Layout() { + return ( + + + + + + + + + + ) +} + +export default Layout diff --git a/frontend/src/routes/_layout/admin.tsx b/frontend/src/routes/_layout/admin.tsx new file mode 100644 index 0000000000..706fd58dc9 --- /dev/null +++ b/frontend/src/routes/_layout/admin.tsx @@ -0,0 +1,129 @@ +import { Badge, Container, Flex, Heading, Table } from "@chakra-ui/react" +import { useQuery, useQueryClient } from "@tanstack/react-query" +import { createFileRoute, useNavigate } from "@tanstack/react-router" +import { z } from "zod" + +import { type UserPublic, UsersService } from "@/client" +import AddUser from "@/components/Admin/AddUser" +import { UserActionsMenu } from "@/components/Common/UserActionsMenu" +import PendingUsers from "@/components/Pending/PendingUsers" +import { + PaginationItems, + PaginationNextTrigger, + PaginationPrevTrigger, + PaginationRoot, +} from "@/components/ui/pagination.tsx" + +const usersSearchSchema = z.object({ + page: z.number().catch(1), +}) + +const PER_PAGE = 5 + +function getUsersQueryOptions({ page }: { page: number }) { + return { + queryFn: () => + UsersService.readUsers({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }), + queryKey: ["users", { page }], + } +} + +export const Route = createFileRoute("/_layout/admin")({ + component: Admin, + validateSearch: (search) => usersSearchSchema.parse(search), +}) + +function UsersTable() { + const queryClient = useQueryClient() + const currentUser = queryClient.getQueryData(["currentUser"]) + const navigate = useNavigate({ from: Route.fullPath }) + const { page } = Route.useSearch() + + const { data, isLoading, isPlaceholderData } = useQuery({ + ...getUsersQueryOptions({ page }), + placeholderData: (prevData) => prevData, + }) + + const setPage = (page: number) => { + navigate({ + to: "/admin", + search: (prev) => ({ ...prev, page }), + }) + } + + const users = data?.data.slice(0, PER_PAGE) ?? [] + const count = data?.count ?? 0 + + if (isLoading) { + return + } + + return ( + <> + + + + Full name + Email + Role + Status + Actions + + + + {users?.map((user) => ( + + + {user.full_name || "N/A"} + {currentUser?.id === user.id && ( + + You + + )} + + + {user.email} + + + {user.is_superuser ? "Superuser" : "User"} + + {user.is_active ? "Active" : "Inactive"} + + + + + ))} + + + + setPage(page)} + > + + + + + + + + + ) +} + +function Admin() { + return ( + + + Users Management + + + + + + ) +} diff --git a/frontend/src/routes/_layout/index.tsx b/frontend/src/routes/_layout/index.tsx new file mode 100644 index 0000000000..66e32e0b30 --- /dev/null +++ b/frontend/src/routes/_layout/index.tsx @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000000..487ede9138 --- /dev/null +++ b/frontend/src/routes/_layout/items.tsx @@ -0,0 +1,146 @@ +import { + Container, + EmptyState, + Flex, + Heading, + Table, + VStack, +} from "@chakra-ui/react" +import { useQuery } from "@tanstack/react-query" +import { createFileRoute, useNavigate } from "@tanstack/react-router" +import { FiSearch } from "react-icons/fi" +import { z } from "zod" + +import { ItemsService } from "@/client" +import { ItemActionsMenu } from "@/components/Common/ItemActionsMenu" +import AddItem from "@/components/Items/AddItem" +import PendingItems from "@/components/Pending/PendingItems" +import { + PaginationItems, + PaginationNextTrigger, + PaginationPrevTrigger, + PaginationRoot, +} from "@/components/ui/pagination.tsx" + +const itemsSearchSchema = z.object({ + page: z.number().catch(1), +}) + +const PER_PAGE = 5 + +function getItemsQueryOptions({ page }: { page: number }) { + return { + queryFn: () => + ItemsService.readItems({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }), + queryKey: ["items", { page }], + } +} + +export const Route = createFileRoute("/_layout/items")({ + component: Items, + validateSearch: (search) => itemsSearchSchema.parse(search), +}) + +function ItemsTable() { + const navigate = useNavigate({ from: Route.fullPath }) + const { page } = Route.useSearch() + + const { data, isLoading, isPlaceholderData } = useQuery({ + ...getItemsQueryOptions({ page }), + placeholderData: (prevData) => prevData, + }) + + const setPage = (page: number) => { + navigate({ + to: "/items", + search: (prev) => ({ ...prev, page }), + }) + } + + const items = data?.data.slice(0, PER_PAGE) ?? [] + const count = data?.count ?? 0 + + if (isLoading) { + return + } + + if (items.length === 0) { + return ( + + + + + + + You don't have any items yet + + Add a new item to get started + + + + + ) + } + + return ( + <> + + + + ID + Title + Description + Actions + + + + {items?.map((item) => ( + + + {item.id} + + + {item.title} + + + {item.description || "N/A"} + + + + + + ))} + + + + setPage(page)} + > + + + + + + + + + ) +} + +function Items() { + return ( + + + Items Management + + + + + ) +} diff --git a/frontend/src/routes/_layout/settings.tsx b/frontend/src/routes/_layout/settings.tsx new file mode 100644 index 0000000000..3a544acb8b --- /dev/null +++ b/frontend/src/routes/_layout/settings.tsx @@ -0,0 +1,53 @@ +import { Container, Heading, Tabs } from "@chakra-ui/react" +import { createFileRoute } from "@tanstack/react-router" + +import Appearance from "@/components/UserSettings/Appearance" +import ChangePassword from "@/components/UserSettings/ChangePassword" +import DeleteAccount from "@/components/UserSettings/DeleteAccount" +import UserInformation from "@/components/UserSettings/UserInformation" +import useAuth from "@/hooks/useAuth" + +const tabsConfig = [ + { value: "my-profile", title: "My profile", component: UserInformation }, + { value: "password", title: "Password", component: ChangePassword }, + { value: "appearance", title: "Appearance", component: Appearance }, + { value: "danger-zone", title: "Danger zone", component: DeleteAccount }, +] + +export const Route = createFileRoute("/_layout/settings")({ + component: UserSettings, +}) + +function UserSettings() { + const { user: currentUser } = useAuth() + const finalTabs = currentUser?.is_superuser + ? tabsConfig.slice(0, 3) + : tabsConfig + + if (!currentUser) { + return null + } + + return ( + + + User Settings + + + + + {finalTabs.map((tab) => ( + + {tab.title} + + ))} + + {finalTabs.map((tab) => ( + + + + ))} + + + ) +} diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx new file mode 100644 index 0000000000..2e0539d9d9 --- /dev/null +++ b/frontend/src/routes/login.tsx @@ -0,0 +1,112 @@ +import { Container, Image, Input, Text } from "@chakra-ui/react" +import { + createFileRoute, + Link as RouterLink, + redirect, +} from "@tanstack/react-router" +import { type SubmitHandler, useForm } from "react-hook-form" +import { FiLock, FiMail } from "react-icons/fi" + +import type { Body_login_login_access_token as AccessToken } from "@/client" +import { Button } from "@/components/ui/button" +import { Field } from "@/components/ui/field" +import { InputGroup } from "@/components/ui/input-group" +import { PasswordInput } from "@/components/ui/password-input" +import useAuth, { isLoggedIn } from "@/hooks/useAuth" +import Logo from "/assets/images/fastapi-logo.svg" +import { emailPattern, passwordRules } from "../utils" + +export const Route = createFileRoute("/login")({ + component: Login, + beforeLoad: async () => { + if (isLoggedIn()) { + throw redirect({ + to: "/", + }) + } + }, +}) + +function Login() { + 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 + + }> + + + + } + {...register("password", passwordRules())} + placeholder="Password" + errors={errors} + /> + + 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 new file mode 100644 index 0000000000..084fbdd73a --- /dev/null +++ b/frontend/src/routes/recover-password.tsx @@ -0,0 +1,94 @@ +import { Container, 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 { FiMail } from "react-icons/fi" + +import { type ApiError, LoginService } from "@/client" +import { Button } from "@/components/ui/button" +import { Field } from "@/components/ui/field" +import { InputGroup } from "@/components/ui/input-group" +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 { showSuccessToast } = useCustomToast() + + const recoverPassword = async (data: FormData) => { + await LoginService.recoverPassword({ + email: data.email, + }) + } + + const mutation = useMutation({ + mutationFn: recoverPassword, + onSuccess: () => { + showSuccessToast("Password recovery email sent successfully.") + reset() + }, + onError: (err: ApiError) => { + handleError(err) + }, + }) + + const onSubmit: SubmitHandler = async (data) => { + mutation.mutate(data) + } + + return ( + + + Password Recovery + + + A password recovery email will be sent to the registered account. + + + }> + + + + + + ) +} diff --git a/frontend/src/routes/reset-password.tsx b/frontend/src/routes/reset-password.tsx new file mode 100644 index 0000000000..f55f49e287 --- /dev/null +++ b/frontend/src/routes/reset-password.tsx @@ -0,0 +1,106 @@ +import { Container, Heading, 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 { FiLock } from "react-icons/fi" + +import { type ApiError, LoginService, type NewPassword } from "@/client" +import { Button } from "@/components/ui/button" +import { PasswordInput } from "@/components/ui/password-input" +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 { showSuccessToast } = 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: () => { + showSuccessToast("Password updated successfully.") + reset() + navigate({ to: "/login" }) + }, + onError: (err: ApiError) => { + handleError(err) + }, + }) + + const onSubmit: SubmitHandler = async (data) => { + mutation.mutate(data) + } + + return ( + + + Reset Password + + + Please enter your new password and confirm it to reset your password. + + } + type="new_password" + errors={errors} + {...register("new_password", passwordRules())} + placeholder="New Password" + /> + } + type="confirm_password" + errors={errors} + {...register("confirm_password", confirmPasswordRules(getValues))} + placeholder="Confirm Password" + /> + + + ) +} diff --git a/frontend/src/routes/signup.tsx b/frontend/src/routes/signup.tsx new file mode 100644 index 0000000000..6e7890c485 --- /dev/null +++ b/frontend/src/routes/signup.tsx @@ -0,0 +1,132 @@ +import { Container, Flex, Image, Input, Text } from "@chakra-ui/react" +import { + createFileRoute, + Link as RouterLink, + redirect, +} from "@tanstack/react-router" +import { type SubmitHandler, useForm } from "react-hook-form" +import { FiLock, FiUser } from "react-icons/fi" + +import type { UserRegister } from "@/client" +import { Button } from "@/components/ui/button" +import { Field } from "@/components/ui/field" +import { InputGroup } from "@/components/ui/input-group" +import { PasswordInput } from "@/components/ui/password-input" +import useAuth, { isLoggedIn } from "@/hooks/useAuth" +import { confirmPasswordRules, emailPattern, passwordRules } from "@/utils" +import Logo from "/assets/images/fastapi-logo.svg" + +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 + + }> + + + + + + }> + + + + } + {...register("password", passwordRules())} + placeholder="Password" + errors={errors} + /> + } + {...register("confirm_password", confirmPasswordRules(getValues))} + placeholder="Confirm Password" + errors={errors} + /> + + + Already have an account?{" "} + + Log In + + + + + ) +} + +export default SignUp diff --git a/frontend/src/theme.tsx b/frontend/src/theme.tsx new file mode 100644 index 0000000000..e7f2e60f66 --- /dev/null +++ b/frontend/src/theme.tsx @@ -0,0 +1,31 @@ +import { createSystem, defaultConfig } from "@chakra-ui/react" +import { buttonRecipe } from "./theme/button.recipe" + +export const system = createSystem(defaultConfig, { + globalCss: { + html: { + fontSize: "16px", + }, + body: { + fontSize: "0.875rem", + margin: 0, + padding: 0, + }, + ".main-link": { + color: "ui.main", + fontWeight: "bold", + }, + }, + theme: { + tokens: { + colors: { + ui: { + main: { value: "#009688" }, + }, + }, + }, + recipes: { + button: buttonRecipe, + }, + }, +}) diff --git a/frontend/src/theme/button.recipe.ts b/frontend/src/theme/button.recipe.ts new file mode 100644 index 0000000000..766ca29b9a --- /dev/null +++ b/frontend/src/theme/button.recipe.ts @@ -0,0 +1,21 @@ +import { defineRecipe } from "@chakra-ui/react" + +export const buttonRecipe = defineRecipe({ + base: { + fontWeight: "bold", + display: "flex", + alignItems: "center", + justifyContent: "center", + colorPalette: "teal", + }, + variants: { + variant: { + ghost: { + bg: "transparent", + _hover: { + bg: "gray.100", + }, + }, + }, + }, +}) diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts new file mode 100644 index 0000000000..ce1d184f9b --- /dev/null +++ b/frontend/src/utils.ts @@ -0,0 +1,55 @@ +import type { ApiError } from "./client" +import useCustomToast from "./hooks/useCustomToast" + +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) => { + const { showErrorToast } = useCustomToast() + const errDetail = (err.body as any)?.detail + let errorMessage = errDetail || "Something went wrong." + if (Array.isArray(errDetail) && errDetail.length > 0) { + errorMessage = errDetail[0].msg + } + showErrorToast(errorMessage) +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000000..b54b4c9828 --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/frontend/tests/auth.setup.ts b/frontend/tests/auth.setup.ts new file mode 100644 index 0000000000..3882f4f810 --- /dev/null +++ b/frontend/tests/auth.setup.ts @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000000..188cb367e3 --- /dev/null +++ b/frontend/tests/config.ts @@ -0,0 +1,21 @@ +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 new file mode 100644 index 0000000000..b639fd7998 --- /dev/null +++ b/frontend/tests/login.spec.ts @@ -0,0 +1,127 @@ +import { expect, type Page, 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") +}) + +test("Redirects to /login when token is wrong", async ({ page }) => { + await page.goto("/settings") + await page.evaluate(() => { + localStorage.setItem("access_token", "invalid_token") + }) + await page.goto("/settings") + await page.waitForURL("/login") + await expect(page).toHaveURL("/login") +}) diff --git a/frontend/tests/reset-password.spec.ts b/frontend/tests/reset-password.spec.ts new file mode 100644 index 0000000000..6c7096f33e --- /dev/null +++ b/frontend/tests/reset-password.spec.ts @@ -0,0 +1,125 @@ +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( + `${process.env.MAILCATCHER_HOST}/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.getByPlaceholder("New Password").fill(newPassword) + await page.getByPlaceholder("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.getByPlaceholder("New Password").fill(password) + await page.getByPlaceholder("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( + `${process.env.MAILCATCHER_HOST}/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.getByPlaceholder("New Password").fill(weakPassword) + await page.getByPlaceholder("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 new file mode 100644 index 0000000000..750edb1830 --- /dev/null +++ b/frontend/tests/sign-up.spec.ts @@ -0,0 +1,169 @@ +import { expect, type Page, 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("Confirm 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, "Confirm 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 new file mode 100644 index 0000000000..bd344d3dda --- /dev/null +++ b/frontend/tests/user-settings.spec.ts @@ -0,0 +1,330 @@ +import { expect, test } from "@playwright/test" +import { firstSuperuser, firstSuperuserPassword } from "./config.ts" +import { createUser } from "./utils/privateApi.ts" +import { randomEmail, randomPassword } from "./utils/random" +import { logInUser, logOutUser } 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 email = randomEmail() + const updatedName = "Test User 2" + const password = randomPassword() + + await createUser({ 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 email = randomEmail() + const updatedEmail = randomEmail() + const password = randomPassword() + + await createUser({ 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 email = randomEmail() + const password = randomPassword() + const invalidEmail = "" + + await createUser({ 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 email = randomEmail() + const password = randomPassword() + const updatedName = "Test User" + + const user = await createUser({ 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(user.full_name as string, { exact: true }), + ).toBeVisible() + }) + + test("Cancel edit action restores original email", async ({ page }) => { + const email = randomEmail() + const password = randomPassword() + const updatedEmail = randomEmail() + + await createUser({ 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 email = randomEmail() + const password = randomPassword() + const NewPassword = randomPassword() + + await createUser({ email, password }) + + // Log in the user + await logInUser(page, email, password) + + await page.goto("/settings") + await page.getByRole("tab", { name: "Password" }).click() + await page.getByPlaceholder("Current Password").fill(password) + await page.getByPlaceholder("New Password").fill(NewPassword) + await page.getByPlaceholder("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 email = randomEmail() + const password = randomPassword() + const weakPassword = "weak" + + await createUser({ email, password }) + + // Log in the user + await logInUser(page, email, password) + + await page.goto("/settings") + await page.getByRole("tab", { name: "Password" }).click() + await page.getByPlaceholder("Current Password").fill(password) + await page.getByPlaceholder("New Password").fill(weakPassword) + await page.getByPlaceholder("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 email = randomEmail() + const password = randomPassword() + const newPassword = randomPassword() + const confirmPassword = randomPassword() + + await createUser({ email, password }) + + // Log in the user + await logInUser(page, email, password) + + await page.goto("/settings") + await page.getByRole("tab", { name: "Password" }).click() + await page.getByPlaceholder("Current Password").fill(password) + await page.getByPlaceholder("New Password").fill(newPassword) + await page.getByPlaceholder("Confirm Password").fill(confirmPassword) + await page.getByLabel("Password", { exact: true }).locator("form").click() + await expect(page.getByText("The passwords do not match")).toBeVisible() + }) + + test("Current password and new password are the same", async ({ page }) => { + const email = randomEmail() + const password = randomPassword() + + await createUser({ email, password }) + + // Log in the user + await logInUser(page, email, password) + + await page.goto("/settings") + await page.getByRole("tab", { name: "Password" }).click() + await page.getByPlaceholder("Current Password").fill(password) + await page.getByPlaceholder("New Password").fill(password) + await page.getByPlaceholder("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 and vice versa", async ({ + page, +}) => { + await page.goto("/settings") + await page.getByRole("tab", { name: "Appearance" }).click() + + // Ensure the initial state is light mode + if ( + await page.evaluate(() => + document.documentElement.classList.contains("dark"), + ) + ) { + await page + .locator("label") + .filter({ hasText: "Light Mode" }) + .locator("span") + .first() + .click() + } + + let isLightMode = await page.evaluate(() => + document.documentElement.classList.contains("light"), + ) + expect(isLightMode).toBe(true) + + await page + .locator("label") + .filter({ hasText: "Dark Mode" }) + .locator("span") + .first() + .click() + const isDarkMode = await page.evaluate(() => + document.documentElement.classList.contains("dark"), + ) + expect(isDarkMode).toBe(true) + + await page + .locator("label") + .filter({ hasText: "Light Mode" }) + .locator("span") + .first() + .click() + isLightMode = await page.evaluate(() => + document.documentElement.classList.contains("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() + + // Ensure the initial state is light mode + if ( + await page.evaluate(() => + document.documentElement.classList.contains("dark"), + ) + ) { + await page + .locator("label") + .filter({ hasText: "Light Mode" }) + .locator("span") + .first() + .click() + } + + const isLightMode = await page.evaluate(() => + document.documentElement.classList.contains("light"), + ) + expect(isLightMode).toBe(true) + + await page + .locator("label") + .filter({ hasText: "Dark Mode" }) + .locator("span") + .first() + .click() + let isDarkMode = await page.evaluate(() => + document.documentElement.classList.contains("dark"), + ) + expect(isDarkMode).toBe(true) + + await logOutUser(page) + await logInUser(page, firstSuperuser, firstSuperuserPassword) + + isDarkMode = await page.evaluate(() => + document.documentElement.classList.contains("dark"), + ) + expect(isDarkMode).toBe(true) +}) diff --git a/frontend/tests/utils/mailcatcher.ts b/frontend/tests/utils/mailcatcher.ts new file mode 100644 index 0000000000..8e6f78b2c8 --- /dev/null +++ b/frontend/tests/utils/mailcatcher.ts @@ -0,0 +1,62 @@ +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(`${process.env.MAILCATCHER_HOST}/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/privateApi.ts b/frontend/tests/utils/privateApi.ts new file mode 100644 index 0000000000..b6fa0afe67 --- /dev/null +++ b/frontend/tests/utils/privateApi.ts @@ -0,0 +1,22 @@ +// Note: the `PrivateService` is only available when generating the client +// for local environments +import { OpenAPI, PrivateService } from "../../src/client" + +OpenAPI.BASE = `${process.env.VITE_API_URL}` + +export const createUser = async ({ + email, + password, +}: { + email: string + password: string +}) => { + return await PrivateService.createUser({ + requestBody: { + email, + password, + is_verified: true, + full_name: "Test User", + }, + }) +} diff --git a/frontend/tests/utils/random.ts b/frontend/tests/utils/random.ts new file mode 100644 index 0000000000..d96f0833ce --- /dev/null +++ b/frontend/tests/utils/random.ts @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000000..4e31fe0ff7 --- /dev/null +++ b/frontend/tests/utils/user.ts @@ -0,0 +1,35 @@ +import { expect, type Page } 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("Confirm Password").fill(password) + await page.getByRole("button", { name: "Sign Up" }).click() + 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.build.json b/frontend/tsconfig.build.json new file mode 100644 index 0000000000..13bd2efc21 --- /dev/null +++ b/frontend/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["tests/**/*.ts"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000000..bb73d7ef9f --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,30 @@ +{ + "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, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src", "tests", "playwright.config.ts"], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000000..42872c59f5 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "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 new file mode 100644 index 0000000000..997b7dd7fc --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,20 @@ +import path from "node:path" +import { tanstackRouter } from "@tanstack/router-plugin/vite" +import react from "@vitejs/plugin-react-swc" +import { defineConfig } from "vite" + +// https://vitejs.dev/config/ +export default defineConfig({ + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + plugins: [ + tanstackRouter({ + target: "react", + autoCodeSplitting: true, + }), + react(), + ], +}) diff --git a/img/dashboard-create.png b/img/dashboard-create.png new file mode 100644 index 0000000000..a394141f7b Binary files /dev/null and b/img/dashboard-create.png differ diff --git a/img/dashboard-dark.png b/img/dashboard-dark.png new file mode 100644 index 0000000000..51040a157b Binary files /dev/null and b/img/dashboard-dark.png differ diff --git a/img/dashboard-items.png b/img/dashboard-items.png new file mode 100644 index 0000000000..f50e2e834e Binary files /dev/null and b/img/dashboard-items.png differ diff --git a/img/dashboard-user-settings.png b/img/dashboard-user-settings.png new file mode 100644 index 0000000000..8da2e21df7 Binary files /dev/null and b/img/dashboard-user-settings.png differ diff --git a/img/dashboard.png b/img/dashboard.png index ab3f86357c..0f034d691b 100644 Binary files a/img/dashboard.png and b/img/dashboard.png differ diff --git a/img/github-social-preview.png b/img/github-social-preview.png new file mode 100644 index 0000000000..f1dc5959fb Binary files /dev/null and b/img/github-social-preview.png differ diff --git a/img/github-social-preview.svg b/img/github-social-preview.svg new file mode 100644 index 0000000000..4b7a75760e --- /dev/null +++ b/img/github-social-preview.svg @@ -0,0 +1,100 @@ + + + + + + + + image/svg+xml + + + + + + + + + + FastAPI + + Full Stack + Template + diff --git a/img/login.png b/img/login.png index b731bd238f..66e3a7202f 100644 Binary files a/img/login.png and b/img/login.png differ diff --git a/img/redoc.png b/img/redoc.png deleted file mode 100644 index 09243741d0..0000000000 Binary files a/img/redoc.png and /dev/null differ diff --git a/release-notes.md b/release-notes.md new file mode 100644 index 0000000000..40b2caccf3 --- /dev/null +++ b/release-notes.md @@ -0,0 +1,678 @@ +# Release Notes + +## Latest Changes + +### Fixes + +* πŸ› Fix `parse_cors` function to be consistent for both empty string and empty list. PR [#1672](https://github.com/fastapi/full-stack-fastapi-template/pull/1672) by [@rolkotaki](https://github.com/rolkotaki). +* πŸ› Close sidebar drawer on user selection. PR [#1515](https://github.com/fastapi/full-stack-fastapi-template/pull/1515) by [@dtellz](https://github.com/dtellz). +* πŸ› Fix required password validation when editing user fields. PR [#1508](https://github.com/fastapi/full-stack-fastapi-template/pull/1508) by [@jpizquierdo](https://github.com/jpizquierdo). + +### Refactors + +* ♻️ Update password max length. PR [#1447](https://github.com/fastapi/full-stack-fastapi-template/pull/1447) by [@michaelAlvarino](https://github.com/michaelAlvarino). +* 🚚 Move backend tests outside the `app` directory. PR [#1862](https://github.com/fastapi/full-stack-fastapi-template/pull/1862) by [@YuriiMotov](https://github.com/YuriiMotov). +* ✨ Add ImportMetaEnv and ImportMeta interfaces for Vite environment variables. PR [#1860](https://github.com/fastapi/full-stack-fastapi-template/pull/1860) by [@alejsdev](https://github.com/alejsdev). +* πŸ”§ Update `tsconfig.json` and fix errors. PR [#1859](https://github.com/fastapi/full-stack-fastapi-template/pull/1859) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Remove disabled attribute from Save button in ChangePassword component. PR [#1844](https://github.com/fastapi/full-stack-fastapi-template/pull/1844) by [@alejsdev](https://github.com/alejsdev). +* πŸ‘·πŸ»β€β™€οΈ Update CI for client generation. PR [#1573](https://github.com/fastapi/full-stack-fastapi-template/pull/1573) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Remove redundant field in inherited class. PR [#1520](https://github.com/fastapi/full-stack-fastapi-template/pull/1520) by [@tzway](https://github.com/tzway). +* 🎨 Add minor UI tweaks in Skeletons and other components. PR [#1507](https://github.com/fastapi/full-stack-fastapi-template/pull/1507) by [@alejsdev](https://github.com/alejsdev). +* 🎨 Add minor UI tweaks. PR [#1506](https://github.com/fastapi/full-stack-fastapi-template/pull/1506) by [@alejsdev](https://github.com/alejsdev). + +### Upgrades + +* ⬆ Bump @types/react from 19.1.12 to 19.1.13 in /frontend. PR [#1888](https://github.com/fastapi/full-stack-fastapi-template/pull/1888) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @tanstack/router-plugin from 1.131.41 to 1.131.43 in /frontend. PR [#1887](https://github.com/fastapi/full-stack-fastapi-template/pull/1887) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pydantic from 2.11.7 to 2.11.9 in /backend. PR [#1891](https://github.com/fastapi/full-stack-fastapi-template/pull/1891) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @chakra-ui/react from 3.26.0 to 3.27.0 in /frontend. PR [#1890](https://github.com/fastapi/full-stack-fastapi-template/pull/1890) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump axios from 1.12.0 to 1.12.2 in /frontend. PR [#1889](https://github.com/fastapi/full-stack-fastapi-template/pull/1889) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @types/node from 24.3.1 to 24.4.0 in /frontend. PR [#1886](https://github.com/fastapi/full-stack-fastapi-template/pull/1886) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @tanstack/router-devtools from 1.131.41 to 1.131.42 in /frontend. PR [#1881](https://github.com/fastapi/full-stack-fastapi-template/pull/1881) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @tanstack/router-plugin from 1.131.39 to 1.131.41 in /frontend. PR [#1879](https://github.com/fastapi/full-stack-fastapi-template/pull/1879) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @tanstack/react-query-devtools from 5.87.3 to 5.87.4 in /frontend. PR [#1876](https://github.com/fastapi/full-stack-fastapi-template/pull/1876) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump axios from 1.11.0 to 1.12.0 in /frontend. PR [#1878](https://github.com/fastapi/full-stack-fastapi-template/pull/1878) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @tanstack/router-devtools from 1.131.40 to 1.131.41 in /frontend. PR [#1877](https://github.com/fastapi/full-stack-fastapi-template/pull/1877) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @tanstack/react-router from 1.131.40 to 1.131.41 in /frontend. PR [#1875](https://github.com/fastapi/full-stack-fastapi-template/pull/1875) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @tanstack/router-devtools from 1.131.36 to 1.131.37 in /frontend. PR [#1871](https://github.com/fastapi/full-stack-fastapi-template/pull/1871) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @tanstack/router-plugin from 1.131.36 to 1.131.37 in /frontend. PR [#1870](https://github.com/fastapi/full-stack-fastapi-template/pull/1870) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @tanstack/react-query from 5.87.1 to 5.87.4 in /frontend. PR [#1868](https://github.com/fastapi/full-stack-fastapi-template/pull/1868) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @biomejs/biome from 2.2.3 to 2.2.4 in /frontend. PR [#1869](https://github.com/fastapi/full-stack-fastapi-template/pull/1869) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @tanstack/react-router from 1.131.36 to 1.131.37 in /frontend. PR [#1872](https://github.com/fastapi/full-stack-fastapi-template/pull/1872) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆️ Upgrade Biome to the latest version. PR [#1861](https://github.com/fastapi/full-stack-fastapi-template/pull/1861) by [@alejsdev](https://github.com/alejsdev). +* ⬆️ Update TansTack Router dependencies. PR [#1853](https://github.com/fastapi/full-stack-fastapi-template/pull/1853) by [@alejsdev](https://github.com/alejsdev). +* ⬆️ Bump @tanstack/react-query from 5.28.14 to 5.87.1. PR [#1852](https://github.com/fastapi/full-stack-fastapi-template/pull/1852) by [@alejsdev](https://github.com/alejsdev). +* ⬆ Bump @chakra-ui/react from 3.8.0 to 3.26.0 in /frontend. PR [#1796](https://github.com/fastapi/full-stack-fastapi-template/pull/1796) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆️ Update @hey-api/openapi-ts dependency version and update dependabot config. PR [#1845](https://github.com/fastapi/full-stack-fastapi-template/pull/1845) by [@alejsdev](https://github.com/alejsdev). +* ⬆️ Update Playwright. PR [#1793](https://github.com/fastapi/full-stack-fastapi-template/pull/1793) by [@alejsdev](https://github.com/alejsdev). +* ⬆️ Upgrade React and related dependencies. PR [#1843](https://github.com/fastapi/full-stack-fastapi-template/pull/1843) by [@alejsdev](https://github.com/alejsdev). + +### Docs + +* ✏️ Fix small typo in `deployment.md`. PR [#1679](https://github.com/fastapi/full-stack-fastapi-template/pull/1679) by [@cassmtnr](https://github.com/cassmtnr). + +### Internal + +* ⬆ Bump @vitejs/plugin-react-swc from 4.0.1 to 4.1.0 in /frontend. PR [#1897](https://github.com/fastapi/full-stack-fastapi-template/pull/1897) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump playwright from v1.55.0-noble to v1.56.1-noble in /frontend. PR [#1943](https://github.com/fastapi/full-stack-fastapi-template/pull/1943) by [@dependabot[bot]](https://github.com/apps/dependabot). +* πŸ”§ Configure reminder for `waiting` label in `issue-manager`. PR [#1939](https://github.com/fastapi/full-stack-fastapi-template/pull/1939) by [@YuriiMotov](https://github.com/YuriiMotov). +* ⬆ Bump vite from 7.1.9 to 7.1.11 in /frontend. PR [#1949](https://github.com/fastapi/full-stack-fastapi-template/pull/1949) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pydantic from 2.11.10 to 2.12.3 in /backend. PR [#1947](https://github.com/fastapi/full-stack-fastapi-template/pull/1947) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump react-dom and @types/react-dom in /frontend. PR [#1934](https://github.com/fastapi/full-stack-fastapi-template/pull/1934) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump alembic from 1.16.5 to 1.17.0 in /backend. PR [#1935](https://github.com/fastapi/full-stack-fastapi-template/pull/1935) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/setup-node from 5 to 6. PR [#1937](https://github.com/fastapi/full-stack-fastapi-template/pull/1937) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @tanstack/router-plugin from 1.132.41 to 1.133.15 in /frontend. PR [#1946](https://github.com/fastapi/full-stack-fastapi-template/pull/1946) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump astral-sh/setup-uv from 6 to 7. PR [#1925](https://github.com/fastapi/full-stack-fastapi-template/pull/1925) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump vite from 7.1.7 to 7.1.9 in /frontend. PR [#1919](https://github.com/fastapi/full-stack-fastapi-template/pull/1919) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @tanstack/router-plugin from 1.131.44 to 1.132.41 in /frontend. PR [#1920](https://github.com/fastapi/full-stack-fastapi-template/pull/1920) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @tanstack/react-query-devtools from 5.87.4 to 5.90.2 in /frontend. PR [#1921](https://github.com/fastapi/full-stack-fastapi-template/pull/1921) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pydantic from 2.11.9 to 2.11.10 in /backend. PR [#1922](https://github.com/fastapi/full-stack-fastapi-template/pull/1922) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump tiangolo/issue-manager from 0.5.1 to 0.6.0. PR [#1912](https://github.com/fastapi/full-stack-fastapi-template/pull/1912) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @types/react from 19.1.13 to 19.1.15 in /frontend. PR [#1906](https://github.com/fastapi/full-stack-fastapi-template/pull/1906) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pydantic-settings from 2.10.1 to 2.11.0 in /backend. PR [#1907](https://github.com/fastapi/full-stack-fastapi-template/pull/1907) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @tanstack/react-query from 5.90.1 to 5.90.2 in /frontend. PR [#1905](https://github.com/fastapi/full-stack-fastapi-template/pull/1905) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @types/node from 24.4.0 to 24.5.2 in /frontend. PR [#1903](https://github.com/fastapi/full-stack-fastapi-template/pull/1903) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump vite from 7.1.5 to 7.1.7 in /frontend. PR [#1893](https://github.com/fastapi/full-stack-fastapi-template/pull/1893) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @tanstack/react-query from 5.87.4 to 5.90.1 in /frontend. PR [#1896](https://github.com/fastapi/full-stack-fastapi-template/pull/1896) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @tanstack/react-router from 1.131.44 to 1.131.50 in /frontend. PR [#1894](https://github.com/fastapi/full-stack-fastapi-template/pull/1894) by [@dependabot[bot]](https://github.com/apps/dependabot). +* πŸ”§ Update dependabot intervals for uv and npm dependencies to weekly. PR [#1880](https://github.com/fastapi/full-stack-fastapi-template/pull/1880) by [@alejsdev](https://github.com/alejsdev). +* ⬆ Bump pydantic from 2.9.2 to 2.11.7 in /backend. PR [#1864](https://github.com/fastapi/full-stack-fastapi-template/pull/1864) by [@dependabot[bot]](https://github.com/apps/dependabot). +* πŸ”§ Update coverage configuration and simplify test script. PR [#1867](https://github.com/fastapi/full-stack-fastapi-template/pull/1867) by [@alejsdev](https://github.com/alejsdev). +* πŸ”§ Add T201 rule to ruff linting configuration to disallow print statements. PR [#1865](https://github.com/fastapi/full-stack-fastapi-template/pull/1865) by [@alejsdev](https://github.com/alejsdev). +* ⬆ Bump @tanstack/react-query-devtools from 5.87.1 to 5.87.3 in /frontend. PR [#1863](https://github.com/fastapi/full-stack-fastapi-template/pull/1863) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump vite from 6.3.4 to 7.1.5 in /frontend. PR [#1857](https://github.com/fastapi/full-stack-fastapi-template/pull/1857) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @types/node from 22.15.3 to 24.3.1 in /frontend. PR [#1854](https://github.com/fastapi/full-stack-fastapi-template/pull/1854) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @vitejs/plugin-react-swc from 3.9.0 to 4.0.1 in /frontend. PR [#1856](https://github.com/fastapi/full-stack-fastapi-template/pull/1856) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump axios from 1.9.0 to 1.11.0 in /frontend. PR [#1855](https://github.com/fastapi/full-stack-fastapi-template/pull/1855) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump alembic from 1.15.2 to 1.16.5 in /backend. PR [#1847](https://github.com/fastapi/full-stack-fastapi-template/pull/1847) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump email-validator from 2.2.0 to 2.3.0 in /backend. PR [#1850](https://github.com/fastapi/full-stack-fastapi-template/pull/1850) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pydantic-settings from 2.9.1 to 2.10.1 in /backend. PR [#1851](https://github.com/fastapi/full-stack-fastapi-template/pull/1851) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump react-error-boundary from 5.0.0 to 6.0.0 in /frontend. PR [#1849](https://github.com/fastapi/full-stack-fastapi-template/pull/1849) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @tanstack/react-query-devtools from 5.74.9 to 5.87.1 in /frontend. PR [#1848](https://github.com/fastapi/full-stack-fastapi-template/pull/1848) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump dotenv from 16.4.5 to 17.2.2 in /frontend. PR [#1846](https://github.com/fastapi/full-stack-fastapi-template/pull/1846) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump node from 20 to 24 in /frontend. PR [#1621](https://github.com/fastapi/full-stack-fastapi-template/pull/1621) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/labeler from 5 to 6. PR [#1839](https://github.com/fastapi/full-stack-fastapi-template/pull/1839) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/setup-python from 5 to 6. PR [#1835](https://github.com/fastapi/full-stack-fastapi-template/pull/1835) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/setup-node from 4 to 5. PR [#1836](https://github.com/fastapi/full-stack-fastapi-template/pull/1836) by [@dependabot[bot]](https://github.com/apps/dependabot). +* πŸ‘· Detect and label merge conflicts on PRs automatically. PR [#1838](https://github.com/fastapi/full-stack-fastapi-template/pull/1838) by [@svlandeg](https://github.com/svlandeg). +* πŸ”§ Add frontend linter pre-commit hook. PR [#1791](https://github.com/fastapi/full-stack-fastapi-template/pull/1791) by [@alexrockhill](https://github.com/alexrockhill). +* ⬆ Bump form-data from 4.0.2 to 4.0.4 in /frontend. PR [#1725](https://github.com/fastapi/full-stack-fastapi-template/pull/1725) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/checkout from 4 to 5. PR [#1768](https://github.com/fastapi/full-stack-fastapi-template/pull/1768) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/download-artifact from 4 to 5. PR [#1754](https://github.com/fastapi/full-stack-fastapi-template/pull/1754) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump tiangolo/latest-changes from 0.3.2 to 0.4.0. PR [#1744](https://github.com/fastapi/full-stack-fastapi-template/pull/1744) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump bcrypt from 4.0.1 to 4.3.0 in /backend. PR [#1601](https://github.com/fastapi/full-stack-fastapi-template/pull/1601) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump react-error-boundary from 4.0.13 to 5.0.0 in /frontend. PR [#1602](https://github.com/fastapi/full-stack-fastapi-template/pull/1602) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump vite from 6.3.3 to 6.3.4 in /frontend. PR [#1608](https://github.com/fastapi/full-stack-fastapi-template/pull/1608) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @playwright/test from 1.45.2 to 1.52.0 in /frontend. PR [#1586](https://github.com/fastapi/full-stack-fastapi-template/pull/1586) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pydantic-settings from 2.5.2 to 2.9.1 in /backend. PR [#1589](https://github.com/fastapi/full-stack-fastapi-template/pull/1589) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump next-themes from 0.4.4 to 0.4.6 in /frontend. PR [#1598](https://github.com/fastapi/full-stack-fastapi-template/pull/1598) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @types/node from 20.10.5 to 22.15.3 in /frontend. PR [#1599](https://github.com/fastapi/full-stack-fastapi-template/pull/1599) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @tanstack/react-query-devtools from 5.28.14 to 5.74.9 in /frontend. PR [#1597](https://github.com/fastapi/full-stack-fastapi-template/pull/1597) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump sqlmodel from 0.0.22 to 0.0.24 in /backend. PR [#1596](https://github.com/fastapi/full-stack-fastapi-template/pull/1596) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump python-multipart from 0.0.10 to 0.0.20 in /backend. PR [#1595](https://github.com/fastapi/full-stack-fastapi-template/pull/1595) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump alembic from 1.13.2 to 1.15.2 in /backend. PR [#1594](https://github.com/fastapi/full-stack-fastapi-template/pull/1594) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump postgres from 12 to 17. PR [#1580](https://github.com/fastapi/full-stack-fastapi-template/pull/1580) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump axios from 1.8.2 to 1.9.0 in /frontend. PR [#1592](https://github.com/fastapi/full-stack-fastapi-template/pull/1592) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump react-icons from 5.4.0 to 5.5.0 in /frontend. PR [#1581](https://github.com/fastapi/full-stack-fastapi-template/pull/1581) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump jinja2 from 3.1.4 to 3.1.6 in /backend. PR [#1591](https://github.com/fastapi/full-stack-fastapi-template/pull/1591) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pyjwt from 2.9.0 to 2.10.1 in /backend. PR [#1588](https://github.com/fastapi/full-stack-fastapi-template/pull/1588) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump httpx from 0.27.2 to 0.28.1 in /backend. PR [#1587](https://github.com/fastapi/full-stack-fastapi-template/pull/1587) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump form-data from 4.0.0 to 4.0.2 in /frontend. PR [#1578](https://github.com/fastapi/full-stack-fastapi-template/pull/1578) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump @biomejs/biome from 1.6.1 to 1.9.4 in /frontend. PR [#1582](https://github.com/fastapi/full-stack-fastapi-template/pull/1582) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆️ Update Dependabot configuration to target the backend directory for Python uv updates. PR [#1577](https://github.com/fastapi/full-stack-fastapi-template/pull/1577) by [@alejsdev](https://github.com/alejsdev). +* πŸ”§ Update Dependabot config. PR [#1576](https://github.com/fastapi/full-stack-fastapi-template/pull/1576) by [@alejsdev](https://github.com/alejsdev). +* Bump @babel/runtime from 7.23.9 to 7.27.0 in /frontend. PR [#1570](https://github.com/fastapi/full-stack-fastapi-template/pull/1570) by [@dependabot[bot]](https://github.com/apps/dependabot). +* Bump esbuild, @vitejs/plugin-react-swc and vite in /frontend. PR [#1571](https://github.com/fastapi/full-stack-fastapi-template/pull/1571) by [@dependabot[bot]](https://github.com/apps/dependabot). +* Bump axios from 1.7.4 to 1.8.2 in /frontend. PR [#1568](https://github.com/fastapi/full-stack-fastapi-template/pull/1568) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump astral-sh/setup-uv from 5 to 6. PR [#1566](https://github.com/fastapi/full-stack-fastapi-template/pull/1566) by [@dependabot[bot]](https://github.com/apps/dependabot). +* πŸ”§ Add npm and docker package ecosystems to Dependabot configuration. PR [#1535](https://github.com/fastapi/full-stack-fastapi-template/pull/1535) by [@alejsdev](https://github.com/alejsdev). + +## 0.8.0 + +### Features + +* πŸ›‚ Migrate to Chakra UI v3 . PR [#1496](https://github.com/fastapi/full-stack-fastapi-template/pull/1496) by [@alejsdev](https://github.com/alejsdev). +* ✨ Add private, local only, API for usage in E2E tests. PR [#1429](https://github.com/fastapi/full-stack-fastapi-template/pull/1429) by [@patrick91](https://github.com/patrick91). +* ✨ Migrate to latest openapi-ts. PR [#1430](https://github.com/fastapi/full-stack-fastapi-template/pull/1430) by [@patrick91](https://github.com/patrick91). + +### Fixes + +* πŸ§‘β€πŸ”§ Replace correct value for 'htmlFor'. PR [#1456](https://github.com/fastapi/full-stack-fastapi-template/pull/1456) by [@wesenbergg](https://github.com/wesenbergg). + +### Refactors + +* ♻️ Redirect the user to `login` if we get 401/403. PR [#1501](https://github.com/fastapi/full-stack-fastapi-template/pull/1501) by [@alejsdev](https://github.com/alejsdev). +* πŸ› Refactor reset password test to create normal user instead of using super user. PR [#1499](https://github.com/fastapi/full-stack-fastapi-template/pull/1499) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Replace email types from `str` to `EmailStr` in `config.py`. PR [#1492](https://github.com/fastapi/full-stack-fastapi-template/pull/1492) by [@jpizquierdo](https://github.com/jpizquierdo). +* πŸ”§ Remove unused context from router creation. PR [#1498](https://github.com/fastapi/full-stack-fastapi-template/pull/1498) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Remove redundant item deletion code leveraging cascade delete. PR [#1481](https://github.com/fastapi/full-stack-fastapi-template/pull/1481) by [@nauanbek](https://github.com/nauanbek). +* ✏️ Fix a couple of spelling mistakes. PR [#1485](https://github.com/fastapi/full-stack-fastapi-template/pull/1485) by [@rjmunro](https://github.com/rjmunro). +* 🎨 Move `prefix` and `tags` to routers. PR [#1439](https://github.com/fastapi/full-stack-fastapi-template/pull/1439) by [@patrick91](https://github.com/patrick91). +* ♻️ Remove modify id script in favor of openapi-ts config. PR [#1434](https://github.com/fastapi/full-stack-fastapi-template/pull/1434) by [@patrick91](https://github.com/patrick91). +* πŸ‘· Improve Playwright CI speed: sharding (parallel runs), run in Docker to use cache, use env vars. PR [#1405](https://github.com/fastapi/full-stack-fastapi-template/pull/1405) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Add PaginationFooter component. PR [#1381](https://github.com/fastapi/full-stack-fastapi-template/pull/1381) by [@saltie2193](https://github.com/saltie2193). +* ♻️ Refactored code to use encryption algorithm name from settings for consistency. PR [#1160](https://github.com/fastapi/full-stack-fastapi-template/pull/1160) by [@sameeramin](https://github.com/sameeramin). +* πŸ”Š Enable logging for email utils by default. PR [#1374](https://github.com/fastapi/full-stack-fastapi-template/pull/1374) by [@ihmily](https://github.com/ihmily). +* πŸ”§ Add `ENV PYTHONUNBUFFERED=1` to log output directly to Docker. PR [#1378](https://github.com/fastapi/full-stack-fastapi-template/pull/1378) by [@tiangolo](https://github.com/tiangolo). +* πŸ’‘ Remove unnecessary comment. PR [#1260](https://github.com/fastapi/full-stack-fastapi-template/pull/1260) by [@sebhani](https://github.com/sebhani). + +### Upgrades + +* ⬆️ Update Dockerfile to use uv version 0.5.11. PR [#1454](https://github.com/fastapi/full-stack-fastapi-template/pull/1454) by [@alejsdev](https://github.com/alejsdev). + +### Docs + +* πŸ“ Removing deprecated manual client SDK step. PR [#1494](https://github.com/fastapi/full-stack-fastapi-template/pull/1494) by [@chandy](https://github.com/chandy). +* πŸ“ Update Frontend README.md. PR [#1462](https://github.com/fastapi/full-stack-fastapi-template/pull/1462) by [@getmarkus](https://github.com/getmarkus). +* πŸ“ Update `frontend/README.md` to also remove Playwright when removing Frontend. PR [#1452](https://github.com/fastapi/full-stack-fastapi-template/pull/1452) by [@youben11](https://github.com/youben11). +* πŸ“ Update `deployment.md`, instructions to install GitHub Runner in non-root VMs. PR [#1412](https://github.com/fastapi/full-stack-fastapi-template/pull/1412) by [@tiangolo](https://github.com/tiangolo). +* πŸ“ Add MailCatcher to `development.md`. PR [#1387](https://github.com/fastapi/full-stack-fastapi-template/pull/1387) by [@tobiase](https://github.com/tobiase). + +### Internal + +* πŸ”§ Configure path alias for cleaner imports. PR [#1497](https://github.com/fastapi/full-stack-fastapi-template/pull/1497) by [@alejsdev](https://github.com/alejsdev). +* Bump vite from 5.0.13 to 5.4.14 in /frontend. PR [#1469](https://github.com/fastapi/full-stack-fastapi-template/pull/1469) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump astral-sh/setup-uv from 4 to 5. PR [#1453](https://github.com/fastapi/full-stack-fastapi-template/pull/1453) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump astral-sh/setup-uv from 3 to 4. PR [#1433](https://github.com/fastapi/full-stack-fastapi-template/pull/1433) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump tiangolo/latest-changes from 0.3.1 to 0.3.2. PR [#1418](https://github.com/fastapi/full-stack-fastapi-template/pull/1418) by [@dependabot[bot]](https://github.com/apps/dependabot). +* πŸ‘· Update issue manager workflow. PR [#1398](https://github.com/fastapi/full-stack-fastapi-template/pull/1398) by [@alejsdev](https://github.com/alejsdev). +* πŸ‘· Fix smokeshow, checkout files on CI. PR [#1395](https://github.com/fastapi/full-stack-fastapi-template/pull/1395) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Update `labeler.yml`. PR [#1388](https://github.com/fastapi/full-stack-fastapi-template/pull/1388) by [@tiangolo](https://github.com/tiangolo). +* πŸ”§ Add .auth playwright folder to `.gitignore`. PR [#1383](https://github.com/fastapi/full-stack-fastapi-template/pull/1383) by [@justin-p](https://github.com/justin-p). +* ⬆️ Bump rollup from 4.6.1 to 4.22.5 in /frontend. PR [#1379](https://github.com/fastapi/full-stack-fastapi-template/pull/1379) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump astral-sh/setup-uv from 2 to 3. PR [#1364](https://github.com/fastapi/full-stack-fastapi-template/pull/1364) by [@dependabot[bot]](https://github.com/apps/dependabot). +* πŸ‘· Update pre-commit end-of-file-fixer hook to exclude email-templates. PR [#1296](https://github.com/fastapi/full-stack-fastapi-template/pull/1296) by [@goabonga](https://github.com/goabonga). +* ⬆ Bump tiangolo/issue-manager from 0.5.0 to 0.5.1. PR [#1332](https://github.com/fastapi/full-stack-fastapi-template/pull/1332) by [@dependabot[bot]](https://github.com/apps/dependabot). +* πŸ”§ Run task by the same Python environment used to run Copier. PR [#1157](https://github.com/fastapi/full-stack-fastapi-template/pull/1157) by [@waketzheng](https://github.com/waketzheng). +* πŸ‘· Tweak generate client to error out if there are errors. PR [#1377](https://github.com/fastapi/full-stack-fastapi-template/pull/1377) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Generate and commit client only on same repo PRs, on forks, show the error. PR [#1376](https://github.com/fastapi/full-stack-fastapi-template/pull/1376) by [@tiangolo](https://github.com/tiangolo). + +## 0.7.1 + +### Highlights + +* Migrate from Poetry to [`uv`](https://github.com/astral-sh/uv). +* Simplifications and improvements for Docker Compose files, Traefik Dockerfiles. +* Make the API use its own domain `api.example.com` and the frontend use `dashboard.example.com`. This would make it easier to deploy them separately if you needed that. +* The backend and frontend on Docker Compose now listen on the same port as the local development servers, this way you can stop the Docker Compose services and run the local development servers without changing the frontend configuration. + +### Features + +* 🩺 Add DB healthcheck. PR [#1342](https://github.com/fastapi/full-stack-fastapi-template/pull/1342) by [@tiangolo](https://github.com/tiangolo). + +### Refactors + +* ♻️ Update settings to use top level `.env` file. PR [#1359](https://github.com/fastapi/full-stack-fastapi-template/pull/1359) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Migrate from Poetry to uv. PR [#1356](https://github.com/fastapi/full-stack-fastapi-template/pull/1356) by [@tiangolo](https://github.com/tiangolo). +* πŸ”₯ Remove logic for development dependencies and Jupyter, it was never documented, and I no longer use that trick. PR [#1355](https://github.com/fastapi/full-stack-fastapi-template/pull/1355) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Use Docker Compose `watch`. PR [#1354](https://github.com/fastapi/full-stack-fastapi-template/pull/1354) by [@tiangolo](https://github.com/tiangolo). +* πŸ”§ Use plain base official Python Docker image. PR [#1351](https://github.com/fastapi/full-stack-fastapi-template/pull/1351) by [@tiangolo](https://github.com/tiangolo). +* 🚚 Move location of scripts to simplify file structure. PR [#1352](https://github.com/fastapi/full-stack-fastapi-template/pull/1352) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Refactor prestart (migrations), move that to its own container. PR [#1350](https://github.com/fastapi/full-stack-fastapi-template/pull/1350) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Include `FRONTEND_HOST` in CORS origins by default. PR [#1348](https://github.com/fastapi/full-stack-fastapi-template/pull/1348) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Simplify domains with `api.example.com` for API and `dashboard.example.com` for frontend, improve local development with `localhost`. PR [#1344](https://github.com/fastapi/full-stack-fastapi-template/pull/1344) by [@tiangolo](https://github.com/tiangolo). +* πŸ”₯ Simplify Traefik, remove www-redirects that add complexity. PR [#1343](https://github.com/fastapi/full-stack-fastapi-template/pull/1343) by [@tiangolo](https://github.com/tiangolo). +* πŸ”₯ Enable support for Arm Docker images in Mac, remove old patch. PR [#1341](https://github.com/fastapi/full-stack-fastapi-template/pull/1341) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Remove duplicate information in the ItemCreate model. PR [#1287](https://github.com/fastapi/full-stack-fastapi-template/pull/1287) by [@jjaakko](https://github.com/jjaakko). + +### Upgrades + +* ⬆️ Upgrade FastAPI. PR [#1349](https://github.com/fastapi/full-stack-fastapi-template/pull/1349) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* πŸ’‘ Add comments to Dockerfile with uv references. PR [#1357](https://github.com/fastapi/full-stack-fastapi-template/pull/1357) by [@tiangolo](https://github.com/tiangolo). +* πŸ“ Add Email Templates to `backend/README.md`. PR [#1311](https://github.com/fastapi/full-stack-fastapi-template/pull/1311) by [@alejsdev](https://github.com/alejsdev). + +### Internal + +* πŸ‘· Do not sync labels as it overrides manually added labels. PR [#1307](https://github.com/fastapi/full-stack-fastapi-template/pull/1307) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Use uv cache on GitHub Actions. PR [#1366](https://github.com/fastapi/full-stack-fastapi-template/pull/1366) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Update GitHub Actions format. PR [#1363](https://github.com/fastapi/full-stack-fastapi-template/pull/1363) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Use `uv` for Python env to generate client. PR [#1362](https://github.com/fastapi/full-stack-fastapi-template/pull/1362) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Run tests from Python environment (with `uv`), not from Docker container. PR [#1361](https://github.com/fastapi/full-stack-fastapi-template/pull/1361) by [@tiangolo](https://github.com/tiangolo). +* πŸ”¨ Update `generate-client.sh` script, make it fail on errors, fix generation. PR [#1360](https://github.com/fastapi/full-stack-fastapi-template/pull/1360) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Add GitHub Actions workflow to lint backend apart from tests. PR [#1358](https://github.com/fastapi/full-stack-fastapi-template/pull/1358) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Improve playwright CI job. PR [#1335](https://github.com/fastapi/full-stack-fastapi-template/pull/1335) by [@patrick91](https://github.com/patrick91). +* πŸ‘· Update `issue-manager.yml`. PR [#1329](https://github.com/fastapi/full-stack-fastapi-template/pull/1329) by [@tiangolo](https://github.com/tiangolo). +* πŸ’š Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#1327](https://github.com/fastapi/full-stack-fastapi-template/pull/1327) by [@svlandeg](https://github.com/svlandeg). +* πŸ‘·πŸ» Auto-generate frontend client . PR [#1320](https://github.com/fastapi/full-stack-fastapi-template/pull/1320) by [@alejsdev](https://github.com/alejsdev). +* πŸ› Fix in `.github/labeler.yml`. PR [#1322](https://github.com/fastapi/full-stack-fastapi-template/pull/1322) by [@alejsdev](https://github.com/alejsdev). +* πŸ‘· Update `.github/labeler.yml`. PR [#1321](https://github.com/fastapi/full-stack-fastapi-template/pull/1321) by [@alejsdev](https://github.com/alejsdev). +* πŸ‘· Update `latest-changes` GitHub Action. PR [#1315](https://github.com/fastapi/full-stack-fastapi-template/pull/1315) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Update configs for labeler. PR [#1308](https://github.com/fastapi/full-stack-fastapi-template/pull/1308) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Update GitHub Action labeler to add only one label. PR [#1304](https://github.com/fastapi/full-stack-fastapi-template/pull/1304) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Bump axios from 1.6.2 to 1.7.4 in /frontend. PR [#1301](https://github.com/fastapi/full-stack-fastapi-template/pull/1301) by [@dependabot[bot]](https://github.com/apps/dependabot). +* πŸ‘· Update GitHub Action labeler dependencies. PR [#1302](https://github.com/fastapi/full-stack-fastapi-template/pull/1302) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Update GitHub Action labeler permissions. PR [#1300](https://github.com/fastapi/full-stack-fastapi-template/pull/1300) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Add GitHub Action label-checker. PR [#1299](https://github.com/fastapi/full-stack-fastapi-template/pull/1299) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Add GitHub Action labeler. PR [#1298](https://github.com/fastapi/full-stack-fastapi-template/pull/1298) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Add GitHub Action add-to-project. PR [#1297](https://github.com/fastapi/full-stack-fastapi-template/pull/1297) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Update issue-manager. PR [#1288](https://github.com/fastapi/full-stack-fastapi-template/pull/1288) by [@tiangolo](https://github.com/tiangolo). + +## 0.7.0 + +Lots of new things! 🎁 + +* E2E tests with Playwright. +* Mailcatcher configuration, to develop and test email handling. +* Pagination. +* UUIDs for database keys. +* New user sign up. +* Support for deploying to multiple environments (staging, prod). +* Many refactors and improvements. +* Several dependency upgrades. + +### Features + +* ✨ Add User Settings e2e tests. PR [#1271](https://github.com/tiangolo/full-stack-fastapi-template/pull/1271) by [@alejsdev](https://github.com/alejsdev). +* ✨ Add Reset Password e2e tests. PR [#1270](https://github.com/tiangolo/full-stack-fastapi-template/pull/1270) by [@alejsdev](https://github.com/alejsdev). +* ✨ Add Sign Up e2e tests. PR [#1268](https://github.com/tiangolo/full-stack-fastapi-template/pull/1268) by [@alejsdev](https://github.com/alejsdev). +* ✨ Add Sign Up and make `OPEN_USER_REGISTRATION=True` by default. PR [#1265](https://github.com/tiangolo/full-stack-fastapi-template/pull/1265) by [@alejsdev](https://github.com/alejsdev). +* ✨ Add Login e2e tests. PR [#1264](https://github.com/tiangolo/full-stack-fastapi-template/pull/1264) by [@alejsdev](https://github.com/alejsdev). +* ✨ Add initial setup for frontend / end-to-end tests with Playwright. PR [#1261](https://github.com/tiangolo/full-stack-fastapi-template/pull/1261) by [@alejsdev](https://github.com/alejsdev). +* ✨ Add mailcatcher configuration. PR [#1244](https://github.com/tiangolo/full-stack-fastapi-template/pull/1244) by [@patrick91](https://github.com/patrick91). +* ✨ Introduce pagination in items. PR [#1239](https://github.com/tiangolo/full-stack-fastapi-template/pull/1239) by [@patrick91](https://github.com/patrick91). +* πŸ—ƒοΈ Add max_length validation for database models and input data. PR [#1233](https://github.com/tiangolo/full-stack-fastapi-template/pull/1233) by [@estebanx64](https://github.com/estebanx64). +* ✨ Add TanStack React Query devtools in dev build. PR [#1217](https://github.com/tiangolo/full-stack-fastapi-template/pull/1217) by [@tomerb](https://github.com/tomerb). +* ✨ Add support for deploying multiple environments (staging, production) to the same server. PR [#1128](https://github.com/tiangolo/full-stack-fastapi-template/pull/1128) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Update CI GitHub Actions to allow running in private repos. PR [#1125](https://github.com/tiangolo/full-stack-fastapi-template/pull/1125) by [@tiangolo](https://github.com/tiangolo). + +### Fixes + +* πŸ› Fix welcome page to show logged-in user. PR [#1218](https://github.com/tiangolo/full-stack-fastapi-template/pull/1218) by [@tomerb](https://github.com/tomerb). +* πŸ› Fix local Traefik proxy network config to fix Gateway Timeouts. PR [#1184](https://github.com/tiangolo/full-stack-fastapi-template/pull/1184) by [@JoelGotsch](https://github.com/JoelGotsch). +* ♻️ Fix tests when first superuser password is changed in .env. PR [#1165](https://github.com/tiangolo/full-stack-fastapi-template/pull/1165) by [@billzhong](https://github.com/billzhong). +* πŸ› Fix bug when resetting password. PR [#1171](https://github.com/tiangolo/full-stack-fastapi-template/pull/1171) by [@alejsdev](https://github.com/alejsdev). +* πŸ› Fix 403 when the frontend has a directory without an index.html. PR [#1094](https://github.com/tiangolo/full-stack-fastapi-template/pull/1094) by [@tiangolo](https://github.com/tiangolo). + +### Refactors + +* 🚨 Fix Docker build warning. PR [#1283](https://github.com/tiangolo/full-stack-fastapi-template/pull/1283) by [@erip](https://github.com/erip). +* ♻️ Regenerate client to use UUID instead of id integers and update frontend. PR [#1281](https://github.com/tiangolo/full-stack-fastapi-template/pull/1281) by [@rehanabdul](https://github.com/rehanabdul). +* ♻️ Tweaks in frontend. PR [#1273](https://github.com/tiangolo/full-stack-fastapi-template/pull/1273) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Add random password util and refactor tests. PR [#1277](https://github.com/tiangolo/full-stack-fastapi-template/pull/1277) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Refactor models to use cascade delete relationships . PR [#1276](https://github.com/tiangolo/full-stack-fastapi-template/pull/1276) by [@alejsdev](https://github.com/alejsdev). +* πŸ”₯ Remove `USERS_OPEN_REGISTRATION` config, make registration enabled by default. PR [#1274](https://github.com/tiangolo/full-stack-fastapi-template/pull/1274) by [@alejsdev](https://github.com/alejsdev). +* πŸ”§ Reuse database url from config in alembic setup. PR [#1229](https://github.com/tiangolo/full-stack-fastapi-template/pull/1229) by [@patrick91](https://github.com/patrick91). +* πŸ”§ Update Playwright config and tests to use env variables. PR [#1266](https://github.com/tiangolo/full-stack-fastapi-template/pull/1266) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Edit refactor db models to use UUID's instead of integer ID's. PR [#1259](https://github.com/tiangolo/full-stack-fastapi-template/pull/1259) by [@estebanx64](https://github.com/estebanx64). +* ♻️ Update form inputs width. PR [#1263](https://github.com/tiangolo/full-stack-fastapi-template/pull/1263) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Replace deprecated utcnow() with now(timezone.utc) in utils module. PR [#1247](https://github.com/tiangolo/full-stack-fastapi-template/pull/1247) by [@jalvarezz13](https://github.com/jalvarezz13). +* 🎨 Format frontend. PR [#1262](https://github.com/tiangolo/full-stack-fastapi-template/pull/1262) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Abstraction of specific AddModal component out of the Navbar. PR [#1246](https://github.com/tiangolo/full-stack-fastapi-template/pull/1246) by [@ajbloureiro](https://github.com/ajbloureiro). +* ♻️ Update `login.tsx` to prevent error if username or password are empty. PR [#1257](https://github.com/tiangolo/full-stack-fastapi-template/pull/1257) by [@jmondaud](https://github.com/jmondaud). +* ♻️ Refactor recover password. PR [#1242](https://github.com/tiangolo/full-stack-fastapi-template/pull/1242) by [@alejsdev](https://github.com/alejsdev). +* 🎨 Format and lint . PR [#1243](https://github.com/tiangolo/full-stack-fastapi-template/pull/1243) by [@alejsdev](https://github.com/alejsdev). +* 🎨 Run biome after OpenAPI client generation. PR [#1226](https://github.com/tiangolo/full-stack-fastapi-template/pull/1226) by [@tomerb](https://github.com/tomerb). +* ♻️ Update DeleteConfirmation component to use new service. PR [#1224](https://github.com/tiangolo/full-stack-fastapi-template/pull/1224) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Update client services. PR [#1223](https://github.com/tiangolo/full-stack-fastapi-template/pull/1223) by [@alejsdev](https://github.com/alejsdev). +* βš’οΈ Add minor frontend tweaks. PR [#1210](https://github.com/tiangolo/full-stack-fastapi-template/pull/1210) by [@alejsdev](https://github.com/alejsdev). +* 🚚 Move assets to public folder. PR [#1206](https://github.com/tiangolo/full-stack-fastapi-template/pull/1206) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Refactor redirect labels to simplify removing the frontend. PR [#1208](https://github.com/tiangolo/full-stack-fastapi-template/pull/1208) by [@tiangolo](https://github.com/tiangolo). +* πŸ”’οΈ Refactor migrate from python-jose to PyJWT. PR [#1203](https://github.com/tiangolo/full-stack-fastapi-template/pull/1203) by [@estebanx64](https://github.com/estebanx64). +* πŸ”₯ Remove duplicated code. PR [#1185](https://github.com/tiangolo/full-stack-fastapi-template/pull/1185) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Add delete_user_me endpoint and corresponding test cases. PR [#1179](https://github.com/tiangolo/full-stack-fastapi-template/pull/1179) by [@alejsdev](https://github.com/alejsdev). +* βœ… Update test to add verification database records. PR [#1178](https://github.com/tiangolo/full-stack-fastapi-template/pull/1178) by [@estebanx64](https://github.com/estebanx64). +* 🚸 Use `useSuspenseQuery` to fetch members and show skeleton. PR [#1174](https://github.com/tiangolo/full-stack-fastapi-template/pull/1174) by [@patrick91](https://github.com/patrick91). +* 🎨 Format Utils. PR [#1173](https://github.com/tiangolo/full-stack-fastapi-template/pull/1173) by [@alejsdev](https://github.com/alejsdev). +* ✨ Use suspense for items page. PR [#1167](https://github.com/tiangolo/full-stack-fastapi-template/pull/1167) by [@patrick91](https://github.com/patrick91). +* 🚸 Mark login field as required. PR [#1166](https://github.com/tiangolo/full-stack-fastapi-template/pull/1166) by [@patrick91](https://github.com/patrick91). +* 🚸 Improve login. PR [#1163](https://github.com/tiangolo/full-stack-fastapi-template/pull/1163) by [@patrick91](https://github.com/patrick91). +* πŸ₯… Handle AxiosErrors in Login page. PR [#1162](https://github.com/tiangolo/full-stack-fastapi-template/pull/1162) by [@patrick91](https://github.com/patrick91). +* 🎨 Format frontend. PR [#1161](https://github.com/tiangolo/full-stack-fastapi-template/pull/1161) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Regenerate frontend client. PR [#1156](https://github.com/tiangolo/full-stack-fastapi-template/pull/1156) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Refactor rename ModelsOut to ModelsPublic. PR [#1154](https://github.com/tiangolo/full-stack-fastapi-template/pull/1154) by [@estebanx64](https://github.com/estebanx64). +* ♻️ Migrate frontend client generation from `openapi-typescript-codegen` to `@hey-api/openapi-ts`. PR [#1151](https://github.com/tiangolo/full-stack-fastapi-template/pull/1151) by [@alejsdev](https://github.com/alejsdev). +* πŸ”₯ Remove unused exports and update dependencies. PR [#1146](https://github.com/tiangolo/full-stack-fastapi-template/pull/1146) by [@alejsdev](https://github.com/alejsdev). +* πŸ”§ Update sentry dns initialization following the environment settings. PR [#1145](https://github.com/tiangolo/full-stack-fastapi-template/pull/1145) by [@estebanx64](https://github.com/estebanx64). +* ♻️ Refactor and tweaks, rename `UserCreateOpen` to `UserRegister` and others. PR [#1143](https://github.com/tiangolo/full-stack-fastapi-template/pull/1143) by [@alejsdev](https://github.com/alejsdev). +* 🎨 Format imports. PR [#1140](https://github.com/tiangolo/full-stack-fastapi-template/pull/1140) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Refactor and remove `React.FC`. PR [#1139](https://github.com/tiangolo/full-stack-fastapi-template/pull/1139) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Add email pattern and refactor in frontend. PR [#1138](https://github.com/tiangolo/full-stack-fastapi-template/pull/1138) by [@alejsdev](https://github.com/alejsdev). +* πŸ₯… Set up Sentry for FastAPI applications. PR [#1136](https://github.com/tiangolo/full-stack-fastapi-template/pull/1136) by [@estebanx64](https://github.com/estebanx64). +* πŸ”₯ Remove deprecated Docker Compose version key. PR [#1129](https://github.com/tiangolo/full-stack-fastapi-template/pull/1129) by [@tiangolo](https://github.com/tiangolo). +* 🎨 Format with Biome . PR [#1097](https://github.com/tiangolo/full-stack-fastapi-template/pull/1097) by [@alejsdev](https://github.com/alejsdev). +* 🎨 Update quote style in biome formatter. PR [#1095](https://github.com/tiangolo/full-stack-fastapi-template/pull/1095) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Replace ESLint and Prettier with Biome to format and lint frontend. PR [#719](https://github.com/tiangolo/full-stack-fastapi-template/pull/719) by [@santigandolfo](https://github.com/santigandolfo). +* 🎨 Replace buttons styling for variants for consistency. PR [#722](https://github.com/tiangolo/full-stack-fastapi-template/pull/722) by [@alejsdev](https://github.com/alejsdev). +* πŸ› οΈ Improve `modify-openapi-operationids.js`. PR [#720](https://github.com/tiangolo/full-stack-fastapi-template/pull/720) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Replace pytest-mock with unittest.mock and remove pytest-cov. PR [#717](https://github.com/tiangolo/full-stack-fastapi-template/pull/717) by [@estebanx64](https://github.com/estebanx64). +* πŸ› οΈ Minor changes in frontend. PR [#715](https://github.com/tiangolo/full-stack-fastapi-template/pull/715) by [@alejsdev](https://github.com/alejsdev). +* β™» Update Docker image to prevent errors in M1 Macs. PR [#710](https://github.com/tiangolo/full-stack-fastapi-template/pull/710) by [@dudil](https://github.com/dudil). +* ✏ Fix typo in variable names in `backend/app/api/routes/items.py` and `backend/app/api/routes/users.py`. PR [#711](https://github.com/tiangolo/full-stack-fastapi-template/pull/711) by [@disrupted](https://github.com/disrupted). + +### Upgrades + +* ⬆️ Update SQLModel to version `>=0.0.21`. PR [#1275](https://github.com/tiangolo/full-stack-fastapi-template/pull/1275) by [@alejsdev](https://github.com/alejsdev). +* ⬆️ Upgrade Traefik. PR [#1241](https://github.com/tiangolo/full-stack-fastapi-template/pull/1241) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Bump requests from 2.31.0 to 2.32.0 in /backend. PR [#1211](https://github.com/tiangolo/full-stack-fastapi-template/pull/1211) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆️ Bump jinja2 from 3.1.3 to 3.1.4 in /backend. PR [#1196](https://github.com/tiangolo/full-stack-fastapi-template/pull/1196) by [@dependabot[bot]](https://github.com/apps/dependabot). +* Bump gunicorn from 21.2.0 to 22.0.0 in /backend. PR [#1176](https://github.com/tiangolo/full-stack-fastapi-template/pull/1176) by [@dependabot[bot]](https://github.com/apps/dependabot). +* Bump idna from 3.6 to 3.7 in /backend. PR [#1168](https://github.com/tiangolo/full-stack-fastapi-template/pull/1168) by [@dependabot[bot]](https://github.com/apps/dependabot). +* πŸ†™ Update React Query to TanStack Query. PR [#1153](https://github.com/tiangolo/full-stack-fastapi-template/pull/1153) by [@patrick91](https://github.com/patrick91). +* Bump vite from 5.0.12 to 5.0.13 in /frontend. PR [#1149](https://github.com/tiangolo/full-stack-fastapi-template/pull/1149) by [@dependabot[bot]](https://github.com/apps/dependabot). +* Bump follow-redirects from 1.15.5 to 1.15.6 in /frontend. PR [#734](https://github.com/tiangolo/full-stack-fastapi-template/pull/734) by [@dependabot[bot]](https://github.com/apps/dependabot). + +### Docs + +* πŸ“ Update links from tiangolo repo to fastapi org repo. PR [#1285](https://github.com/fastapi/full-stack-fastapi-template/pull/1285) by [@tiangolo](https://github.com/tiangolo). +* πŸ“ Add End-to-End Testing with Playwright to frontend `README.md`. PR [#1279](https://github.com/tiangolo/full-stack-fastapi-template/pull/1279) by [@alejsdev](https://github.com/alejsdev). +* πŸ“ Update release-notes.md. PR [#1220](https://github.com/tiangolo/full-stack-fastapi-template/pull/1220) by [@alejsdev](https://github.com/alejsdev). +* ✏️ Update `README.md`. PR [#1205](https://github.com/tiangolo/full-stack-fastapi-template/pull/1205) by [@Craz1k0ek](https://github.com/Craz1k0ek). +* ✏️ Fix Adminer URL in `deployment.md`. PR [#1194](https://github.com/tiangolo/full-stack-fastapi-template/pull/1194) by [@PhilippWu](https://github.com/PhilippWu). +* πŸ“ Add `Enabling Open User Registration` to backend docs. PR [#1191](https://github.com/tiangolo/full-stack-fastapi-template/pull/1191) by [@alejsdev](https://github.com/alejsdev). +* πŸ“ Update release-notes.md. PR [#1164](https://github.com/tiangolo/full-stack-fastapi-template/pull/1164) by [@alejsdev](https://github.com/alejsdev). +* πŸ“ Update `README.md`. PR [#716](https://github.com/tiangolo/full-stack-fastapi-template/pull/716) by [@alejsdev](https://github.com/alejsdev). +* πŸ“ Update instructions to clone for a private repo, including updates. PR [#1127](https://github.com/tiangolo/full-stack-fastapi-template/pull/1127) by [@tiangolo](https://github.com/tiangolo). +* πŸ“ Add docs about CI keys, LATEST_CHANGES and SMOKESHOW_AUTH_KEY. PR [#1126](https://github.com/tiangolo/full-stack-fastapi-template/pull/1126) by [@tiangolo](https://github.com/tiangolo). +* ✏️ Fix file path in `backend/README.md` when not wanting to use migrations. PR [#1116](https://github.com/tiangolo/full-stack-fastapi-template/pull/1116) by [@leonlowitzki](https://github.com/leonlowitzki). +* πŸ“ Add documentation for pre-commit and code linting. PR [#718](https://github.com/tiangolo/full-stack-fastapi-template/pull/718) by [@estebanx64](https://github.com/estebanx64). +* πŸ“ Fix localhost URLs in `development.md`. PR [#1099](https://github.com/tiangolo/full-stack-fastapi-template/pull/1099) by [@efonte](https://github.com/efonte). +* ✏ Update header titles for consistency. PR [#708](https://github.com/tiangolo/full-stack-fastapi-template/pull/708) by [@codesmith-emmy](https://github.com/codesmith-emmy). +* πŸ“ Update `README.md`, dark mode screenshot position. PR [#706](https://github.com/tiangolo/full-stack-fastapi-template/pull/706) by [@alejsdev](https://github.com/alejsdev). + +### Internal + +* πŸ”§ Update deploy workflows to exclude the main repository. PR [#1284](https://github.com/tiangolo/full-stack-fastapi-template/pull/1284) by [@alejsdev](https://github.com/alejsdev). +* πŸ‘· Update issue-manager.yml GitHub Action permissions. PR [#1278](https://github.com/tiangolo/full-stack-fastapi-template/pull/1278) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Bump setuptools from 69.1.1 to 70.0.0 in /backend. PR [#1255](https://github.com/tiangolo/full-stack-fastapi-template/pull/1255) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆️ Bump certifi from 2024.2.2 to 2024.7.4 in /backend. PR [#1250](https://github.com/tiangolo/full-stack-fastapi-template/pull/1250) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆️ Bump urllib3 from 2.2.1 to 2.2.2 in /backend. PR [#1235](https://github.com/tiangolo/full-stack-fastapi-template/pull/1235) by [@dependabot[bot]](https://github.com/apps/dependabot). +* πŸ”§ Ignore `src/routeTree.gen.ts` in biome. PR [#1175](https://github.com/tiangolo/full-stack-fastapi-template/pull/1175) by [@patrick91](https://github.com/patrick91). +* πŸ‘· Update Smokeshow download artifact GitHub Action. PR [#1198](https://github.com/tiangolo/full-stack-fastapi-template/pull/1198) by [@tiangolo](https://github.com/tiangolo). +* πŸ”§ Update Node.js version in `.nvmrc`. PR [#1192](https://github.com/tiangolo/full-stack-fastapi-template/pull/1192) by [@alejsdev](https://github.com/alejsdev). +* πŸ”₯ Remove ESLint and Prettier from pre-commit config. PR [#1096](https://github.com/tiangolo/full-stack-fastapi-template/pull/1096) by [@alejsdev](https://github.com/alejsdev). +* πŸ”§ Update mypy config to ignore .venv directories. PR [#1155](https://github.com/tiangolo/full-stack-fastapi-template/pull/1155) by [@tiangolo](https://github.com/tiangolo). +* 🚨 Enable `ARG001` to prevent unused arguments. PR [#1152](https://github.com/tiangolo/full-stack-fastapi-template/pull/1152) by [@patrick91](https://github.com/patrick91). +* πŸ”₯ Remove isort configuration, since we use Ruff now. PR [#1144](https://github.com/tiangolo/full-stack-fastapi-template/pull/1144) by [@patrick91](https://github.com/patrick91). +* πŸ”§ Update pre-commit config to exclude generated client folder. PR [#1150](https://github.com/tiangolo/full-stack-fastapi-template/pull/1150) by [@alejsdev](https://github.com/alejsdev). +* πŸ”§ Change `.nvmrc` format. PR [#1148](https://github.com/tiangolo/full-stack-fastapi-template/pull/1148) by [@patrick91](https://github.com/patrick91). +* 🎨 Ignore alembic from ruff lint and format. PR [#1131](https://github.com/tiangolo/full-stack-fastapi-template/pull/1131) by [@estebanx64](https://github.com/estebanx64). +* πŸ”§ Add GitHub templates for discussions and issues, and security policy. PR [#1105](https://github.com/tiangolo/full-stack-fastapi-template/pull/1105) by [@alejsdev](https://github.com/alejsdev). +* ⬆ Bump dawidd6/action-download-artifact from 3.1.2 to 3.1.4. PR [#1103](https://github.com/tiangolo/full-stack-fastapi-template/pull/1103) by [@dependabot[bot]](https://github.com/apps/dependabot). +* πŸ”§ Add Biome to pre-commit config. PR [#1098](https://github.com/tiangolo/full-stack-fastapi-template/pull/1098) by [@alejsdev](https://github.com/alejsdev). +* πŸ”₯ Delete leftover celery file. PR [#727](https://github.com/tiangolo/full-stack-fastapi-template/pull/727) by [@dr-neptune](https://github.com/dr-neptune). +* βš™οΈ Update pre-commit config with Prettier and ESLint. PR [#714](https://github.com/tiangolo/full-stack-fastapi-template/pull/714) by [@alejsdev](https://github.com/alejsdev). + +## 0.6.0 + +Latest FastAPI, Pydantic, SQLModel πŸš€ + +Brand new frontend with React, TS, Vite, Chakra UI, TanStack Query/Router, generated client/SDK 🎨 + +CI/CD - GitHub Actions πŸ€– + +Test cov > 90% βœ… + +### Features + +* ✨ Adopt SQLModel, create models, start using it. PR [#559](https://github.com/tiangolo/full-stack-fastapi-template/pull/559) by [@tiangolo](https://github.com/tiangolo). +* ✨ Upgrade items router with new SQLModel models, simplified logic, and new FastAPI Annotated dependencies. PR [#560](https://github.com/tiangolo/full-stack-fastapi-template/pull/560) by [@tiangolo](https://github.com/tiangolo). +* ✨ Migrate from pgAdmin to Adminer. PR [#692](https://github.com/tiangolo/full-stack-fastapi-template/pull/692) by [@tiangolo](https://github.com/tiangolo). +* ✨ Add support for setting `POSTGRES_PORT`. PR [#333](https://github.com/tiangolo/full-stack-fastapi-template/pull/333) by [@uepoch](https://github.com/uepoch). +* ⬆ Upgrade Flower version and command. PR [#447](https://github.com/tiangolo/full-stack-fastapi-template/pull/447) by [@maurob](https://github.com/maurob). +* 🎨 Improve styles. PR [#673](https://github.com/tiangolo/full-stack-fastapi-template/pull/673) by [@alejsdev](https://github.com/alejsdev). +* 🎨 Update theme. PR [#666](https://github.com/tiangolo/full-stack-fastapi-template/pull/666) by [@alejsdev](https://github.com/alejsdev). +* πŸ‘· Add continuous deployment and refactors needed for it. PR [#667](https://github.com/tiangolo/full-stack-fastapi-template/pull/667) by [@tiangolo](https://github.com/tiangolo). +* ✨ Create endpoint to show password recovery email content and update email template. PR [#664](https://github.com/tiangolo/full-stack-fastapi-template/pull/664) by [@alejsdev](https://github.com/alejsdev). +* 🎨 Format with Prettier. PR [#646](https://github.com/tiangolo/full-stack-fastapi-template/pull/646) by [@alejsdev](https://github.com/alejsdev). +* βœ… Add tests to raise coverage to at least 90% and fix recover password logic. PR [#632](https://github.com/tiangolo/full-stack-fastapi-template/pull/632) by [@estebanx64](https://github.com/estebanx64). +* βš™οΈ Add Prettier and ESLint config with pre-commit. PR [#640](https://github.com/tiangolo/full-stack-fastapi-template/pull/640) by [@alejsdev](https://github.com/alejsdev). +* πŸ‘· Add coverage with Smokeshow to CI and badge. PR [#638](https://github.com/tiangolo/full-stack-fastapi-template/pull/638) by [@estebanx64](https://github.com/estebanx64). +* ✨ Migrate to TanStack Query (React Query) and TanStack Router. PR [#637](https://github.com/tiangolo/full-stack-fastapi-template/pull/637) by [@alejsdev](https://github.com/alejsdev). +* βœ… Add setup and teardown database for tests. PR [#626](https://github.com/tiangolo/full-stack-fastapi-template/pull/626) by [@estebanx64](https://github.com/estebanx64). +* ✨ Update new-frontend client. PR [#625](https://github.com/tiangolo/full-stack-fastapi-template/pull/625) by [@alejsdev](https://github.com/alejsdev). +* ✨ Add password reset functionality. PR [#624](https://github.com/tiangolo/full-stack-fastapi-template/pull/624) by [@alejsdev](https://github.com/alejsdev). +* ✨ Add private/public routing. PR [#621](https://github.com/tiangolo/full-stack-fastapi-template/pull/621) by [@alejsdev](https://github.com/alejsdev). +* πŸ”§ Add VS Code debug configs. PR [#620](https://github.com/tiangolo/full-stack-fastapi-template/pull/620) by [@tiangolo](https://github.com/tiangolo). +* ✨ Add `Not Found` page. PR [#595](https://github.com/tiangolo/full-stack-fastapi-template/pull/595) by [@alejsdev](https://github.com/alejsdev). +* ✨ Add new pages, components, panels, modals, and theme; refactor and improvements in existing components. PR [#593](https://github.com/tiangolo/full-stack-fastapi-template/pull/593) by [@alejsdev](https://github.com/alejsdev). +* ✨ Support delete own account and other tweaks. PR [#614](https://github.com/tiangolo/full-stack-fastapi-template/pull/614) by [@alejsdev](https://github.com/alejsdev). +* ✨ Restructure folders, allow editing of users/items, and implement other refactors and improvements. PR [#603](https://github.com/tiangolo/full-stack-fastapi-template/pull/603) by [@alejsdev](https://github.com/alejsdev). +* ✨ Add Copier, migrate from Cookiecutter, in a way that supports using the project as is, forking or cloning it. PR [#612](https://github.com/tiangolo/full-stack-fastapi-template/pull/612) by [@tiangolo](https://github.com/tiangolo). +* βž• Replace black, isort, flake8, autoflake with ruff and upgrade mypy. PR [#610](https://github.com/tiangolo/full-stack-fastapi-template/pull/610) by [@tiangolo](https://github.com/tiangolo). +* β™» Refactor items and services endpoints to return count and data, and add CI tests. PR [#599](https://github.com/tiangolo/full-stack-fastapi-template/pull/599) by [@estebanx64](https://github.com/estebanx64). +* ✨ Add support for updating items and upgrade SQLModel to 0.0.16 (which supports model object updates). PR [#601](https://github.com/tiangolo/full-stack-fastapi-template/pull/601) by [@tiangolo](https://github.com/tiangolo). +* ✨ Add dark mode to new-frontend and conditional sidebar items. PR [#600](https://github.com/tiangolo/full-stack-fastapi-template/pull/600) by [@alejsdev](https://github.com/alejsdev). +* ✨ Migrate to RouterProvider and other refactors . PR [#598](https://github.com/tiangolo/full-stack-fastapi-template/pull/598) by [@alejsdev](https://github.com/alejsdev). +* ✨ Add delete_user; refactor delete_item. PR [#594](https://github.com/tiangolo/full-stack-fastapi-template/pull/594) by [@alejsdev](https://github.com/alejsdev). +* ✨ Add state store to new frontend. PR [#592](https://github.com/tiangolo/full-stack-fastapi-template/pull/592) by [@alejsdev](https://github.com/alejsdev). +* ✨ Add form validation to Admin, Items and Login. PR [#616](https://github.com/tiangolo/full-stack-fastapi-template/pull/616) by [@alejsdev](https://github.com/alejsdev). +* ✨ Add Sidebar to new frontend. PR [#587](https://github.com/tiangolo/full-stack-fastapi-template/pull/587) by [@alejsdev](https://github.com/alejsdev). +* ✨ Add Login to new frontend. PR [#585](https://github.com/tiangolo/full-stack-fastapi-template/pull/585) by [@alejsdev](https://github.com/alejsdev). +* ✨ Include schemas in generated frontend client. PR [#584](https://github.com/tiangolo/full-stack-fastapi-template/pull/584) by [@alejsdev](https://github.com/alejsdev). +* ✨ Regenerate frontend client with recent changes. PR [#575](https://github.com/tiangolo/full-stack-fastapi-template/pull/575) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Refactor API in `utils.py`. PR [#573](https://github.com/tiangolo/full-stack-fastapi-template/pull/573) by [@alejsdev](https://github.com/alejsdev). +* ✨ Update code for login API. PR [#571](https://github.com/tiangolo/full-stack-fastapi-template/pull/571) by [@tiangolo](https://github.com/tiangolo). +* ✨ Add client in frontend and client generation. PR [#569](https://github.com/tiangolo/full-stack-fastapi-template/pull/569) by [@alejsdev](https://github.com/alejsdev). +* 🐳 Set up Docker config for new-frontend. PR [#564](https://github.com/tiangolo/full-stack-fastapi-template/pull/564) by [@alejsdev](https://github.com/alejsdev). +* ✨ Set up new frontend with Vite, TypeScript and React. PR [#563](https://github.com/tiangolo/full-stack-fastapi-template/pull/563) by [@alejsdev](https://github.com/alejsdev). +* πŸ“Œ Add NodeJS version management and instructions. PR [#551](https://github.com/tiangolo/full-stack-fastapi-template/pull/551) by [@alejsdev](https://github.com/alejsdev). +* Add consistent errors for env vars not set. PR [#200](https://github.com/tiangolo/full-stack-fastapi-template/pull/200). +* Upgrade Traefik to version 2, keeping in sync with DockerSwarm.rocks. PR [#199](https://github.com/tiangolo/full-stack-fastapi-template/pull/199). +* Run tests with `TestClient`. PR [#160](https://github.com/tiangolo/full-stack-fastapi-template/pull/160). + +### Fixes + +* πŸ› Fix copier to handle string vars with spaces in quotes. PR [#631](https://github.com/tiangolo/full-stack-fastapi-template/pull/631) by [@estebanx64](https://github.com/estebanx64). +* πŸ› Fix allowing a user to update the email to the same email they already have. PR [#696](https://github.com/tiangolo/full-stack-fastapi-template/pull/696) by [@alejsdev](https://github.com/alejsdev). +* πŸ› Set up Sentry only when used. PR [#671](https://github.com/tiangolo/full-stack-fastapi-template/pull/671) by [@tiangolo](https://github.com/tiangolo). +* πŸ”₯ Remove unnecessary validation. PR [#662](https://github.com/tiangolo/full-stack-fastapi-template/pull/662) by [@alejsdev](https://github.com/alejsdev). +* πŸ› Fix bug when editing own user. PR [#651](https://github.com/tiangolo/full-stack-fastapi-template/pull/651) by [@alejsdev](https://github.com/alejsdev). +* πŸ› Add `onClose` to `SidebarItems`. PR [#589](https://github.com/tiangolo/full-stack-fastapi-template/pull/589) by [@alejsdev](https://github.com/alejsdev). +* πŸ› Fix positional argument bug in `init_db.py`. PR [#562](https://github.com/tiangolo/full-stack-fastapi-template/pull/562) by [@alejsdev](https://github.com/alejsdev). +* πŸ“Œ Fix flower Docker image, pin version. PR [#396](https://github.com/tiangolo/full-stack-fastapi-template/pull/396) by [@sanggusti](https://github.com/sanggusti). +* πŸ› Fix Celery worker command. PR [#443](https://github.com/tiangolo/full-stack-fastapi-template/pull/443) by [@bechtold](https://github.com/bechtold). +* πŸ› Fix Poetry installation in Dockerfile and upgrade Python version and packages to fix Docker build. PR [#480](https://github.com/tiangolo/full-stack-fastapi-template/pull/480) by [@little7Li](https://github.com/little7Li). + +### Refactors + +* πŸ”§ Add missing dotenv variables. PR [#554](https://github.com/tiangolo/full-stack-fastapi-template/pull/554) by [@tiangolo](https://github.com/tiangolo). +* βͺ Revert "βš™οΈ Add Prettier and ESLint config with pre-commit". PR [#644](https://github.com/tiangolo/full-stack-fastapi-template/pull/644) by [@alejsdev](https://github.com/alejsdev). +* πŸ™ˆ Add .prettierignore and include client folder. PR [#648](https://github.com/tiangolo/full-stack-fastapi-template/pull/648) by [@alejsdev](https://github.com/alejsdev). +* 🏷️ Add mypy to the GitHub Action for tests and fixed types in the whole project. PR [#655](https://github.com/tiangolo/full-stack-fastapi-template/pull/655) by [@estebanx64](https://github.com/estebanx64). +* πŸ”’οΈ Ensure the default values of "changethis" are not deployed. PR [#698](https://github.com/tiangolo/full-stack-fastapi-template/pull/698) by [@tiangolo](https://github.com/tiangolo). +* β—€ Revert "πŸ“Έ Rename Dashboard to Home and update screenshots". PR [#697](https://github.com/tiangolo/full-stack-fastapi-template/pull/697) by [@alejsdev](https://github.com/alejsdev). +* πŸ“Έ Rename Dashboard to Home and update screenshots. PR [#693](https://github.com/tiangolo/full-stack-fastapi-template/pull/693) by [@alejsdev](https://github.com/alejsdev). +* πŸ› Fixed items count when retrieving data for all items by user. PR [#695](https://github.com/tiangolo/full-stack-fastapi-template/pull/695) by [@estebanx64](https://github.com/estebanx64). +* πŸ”₯ Remove Celery and Flower, they are currently not used nor recommended. PR [#694](https://github.com/tiangolo/full-stack-fastapi-template/pull/694) by [@tiangolo](https://github.com/tiangolo). +* βœ… Add test for deleting user without privileges. PR [#690](https://github.com/tiangolo/full-stack-fastapi-template/pull/690) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Refactor user update. PR [#689](https://github.com/tiangolo/full-stack-fastapi-template/pull/689) by [@alejsdev](https://github.com/alejsdev). +* πŸ“Œ Add Poetry lock to git. PR [#685](https://github.com/tiangolo/full-stack-fastapi-template/pull/685) by [@tiangolo](https://github.com/tiangolo). +* 🎨 Adjust color and spacing. PR [#684](https://github.com/tiangolo/full-stack-fastapi-template/pull/684) by [@alejsdev](https://github.com/alejsdev). +* πŸ‘· Avoid creating unnecessary *.pyc files with PYTHONDONTWRITEBYTECODE=1. PR [#677](https://github.com/tiangolo/full-stack-fastapi-template/pull/677) by [@estebanx64](https://github.com/estebanx64). +* πŸ”§ Add `SMTP_SSL` option for older SMTP servers. PR [#365](https://github.com/tiangolo/full-stack-fastapi-template/pull/365) by [@Metrea](https://github.com/Metrea). +* ♻️ Refactor logic to allow running pytest tests locally. PR [#683](https://github.com/tiangolo/full-stack-fastapi-template/pull/683) by [@tiangolo](https://github.com/tiangolo). +* β™» Update error messages. PR [#417](https://github.com/tiangolo/full-stack-fastapi-template/pull/417) by [@qu3vipon](https://github.com/qu3vipon). +* πŸ”§ Add a default Flower password. PR [#682](https://github.com/tiangolo/full-stack-fastapi-template/pull/682) by [@tiangolo](https://github.com/tiangolo). +* πŸ”§ Update VS Code debug config. PR [#676](https://github.com/tiangolo/full-stack-fastapi-template/pull/676) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Refactor code structure for tests. PR [#674](https://github.com/tiangolo/full-stack-fastapi-template/pull/674) by [@tiangolo](https://github.com/tiangolo). +* πŸ”§ Set TanStack Router devtools only in dev mode. PR [#668](https://github.com/tiangolo/full-stack-fastapi-template/pull/668) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Refactor email logic to allow re-using util functions for testing and development. PR [#663](https://github.com/tiangolo/full-stack-fastapi-template/pull/663) by [@tiangolo](https://github.com/tiangolo). +* πŸ’¬ Improve Delete Account description and confirmation. PR [#661](https://github.com/tiangolo/full-stack-fastapi-template/pull/661) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Refactor email templates. PR [#659](https://github.com/tiangolo/full-stack-fastapi-template/pull/659) by [@alejsdev](https://github.com/alejsdev). +* πŸ“ Update deployment files and docs. PR [#660](https://github.com/tiangolo/full-stack-fastapi-template/pull/660) by [@tiangolo](https://github.com/tiangolo). +* πŸ”₯ Remove unused schemas. PR [#656](https://github.com/tiangolo/full-stack-fastapi-template/pull/656) by [@alejsdev](https://github.com/alejsdev). +* πŸ”₯ Remove old frontend. PR [#649](https://github.com/tiangolo/full-stack-fastapi-template/pull/649) by [@tiangolo](https://github.com/tiangolo). +* β™» Move project source files to top level from src, update Sentry dependency. PR [#630](https://github.com/tiangolo/full-stack-fastapi-template/pull/630) by [@estebanx64](https://github.com/estebanx64). +* β™» Refactor Python folder tree. PR [#629](https://github.com/tiangolo/full-stack-fastapi-template/pull/629) by [@estebanx64](https://github.com/estebanx64). +* ♻️ Refactor old CRUD utils and tests. PR [#622](https://github.com/tiangolo/full-stack-fastapi-template/pull/622) by [@alejsdev](https://github.com/alejsdev). +* πŸ”§ Update .env to allow local debug for the backend. PR [#618](https://github.com/tiangolo/full-stack-fastapi-template/pull/618) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Refactor and update CORS, remove trailing slash from new Pydantic v2. PR [#617](https://github.com/tiangolo/full-stack-fastapi-template/pull/617) by [@tiangolo](https://github.com/tiangolo). +* 🎨 Format files with pre-commit and Ruff. PR [#611](https://github.com/tiangolo/full-stack-fastapi-template/pull/611) by [@tiangolo](https://github.com/tiangolo). +* 🚚 Refactor and simplify backend file structure. PR [#609](https://github.com/tiangolo/full-stack-fastapi-template/pull/609) by [@tiangolo](https://github.com/tiangolo). +* πŸ”₯ Clean up old files no longer relevant. PR [#608](https://github.com/tiangolo/full-stack-fastapi-template/pull/608) by [@tiangolo](https://github.com/tiangolo). +* β™» Re-structure Docker Compose files, discard Docker Swarm specific logic. PR [#607](https://github.com/tiangolo/full-stack-fastapi-template/pull/607) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Refactor update endpoints and regenerate client for new-frontend. PR [#602](https://github.com/tiangolo/full-stack-fastapi-template/pull/602) by [@alejsdev](https://github.com/alejsdev). +* ✨ Add Layout to App. PR [#588](https://github.com/tiangolo/full-stack-fastapi-template/pull/588) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Re-enable user update path operations for frontend client generation. PR [#574](https://github.com/tiangolo/full-stack-fastapi-template/pull/574) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Remove type ignores and add `response_model`. PR [#572](https://github.com/tiangolo/full-stack-fastapi-template/pull/572) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Refactor Users API and dependencies. PR [#561](https://github.com/tiangolo/full-stack-fastapi-template/pull/561) by [@alejsdev](https://github.com/alejsdev). +* ♻️ Refactor frontend Docker build setup, use plain NodeJS, use custom Nginx config, fix build for old Vue. PR [#555](https://github.com/tiangolo/full-stack-fastapi-template/pull/555) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Refactor project generation, discard cookiecutter, use plain git/clone/fork. PR [#553](https://github.com/tiangolo/full-stack-fastapi-template/pull/553) by [@tiangolo](https://github.com/tiangolo). +* Refactor backend: + * Simplify configs for tools and format to better support editor integration. + * Add mypy configurations and plugins. + * Add types to all the codebase. + * Update types for SQLAlchemy models with plugin. + * Update and refactor CRUD utils. + * Refactor DB sessions to use dependencies with `yield`. + * Refactor dependencies, security, CRUD, models, schemas, etc. To simplify code and improve autocompletion. + * Change from PyJWT to Python-JOSE as it supports additional use cases. + * Fix JWT tokens using user email/ID as the subject in `sub`. + * PR [#158](https://github.com/tiangolo/full-stack-fastapi-template/pull/158). +* Simplify `docker-compose.*.yml` files, refactor deployment to reduce config files. PR [#153](https://github.com/tiangolo/full-stack-fastapi-template/pull/153). +* Simplify env var files, merge to a single `.env` file. PR [#151](https://github.com/tiangolo/full-stack-fastapi-template/pull/151). + +### Upgrades + +* πŸ“Œ Upgrade Poetry lock dependencies. PR [#702](https://github.com/tiangolo/full-stack-fastapi-template/pull/702) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Upgrade Python version and dependencies. PR [#558](https://github.com/tiangolo/full-stack-fastapi-template/pull/558) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump tiangolo/issue-manager from 0.2.0 to 0.5.0. PR [#591](https://github.com/tiangolo/full-stack-fastapi-template/pull/591) by [@dependabot[bot]](https://github.com/apps/dependabot). +* Bump follow-redirects from 1.15.3 to 1.15.5 in /frontend. PR [#654](https://github.com/tiangolo/full-stack-fastapi-template/pull/654) by [@dependabot[bot]](https://github.com/apps/dependabot). +* Bump vite from 5.0.4 to 5.0.12 in /frontend. PR [#653](https://github.com/tiangolo/full-stack-fastapi-template/pull/653) by [@dependabot[bot]](https://github.com/apps/dependabot). +* Bump fastapi from 0.104.1 to 0.109.1 in /backend. PR [#687](https://github.com/tiangolo/full-stack-fastapi-template/pull/687) by [@dependabot[bot]](https://github.com/apps/dependabot). +* Bump python-multipart from 0.0.6 to 0.0.7 in /backend. PR [#686](https://github.com/tiangolo/full-stack-fastapi-template/pull/686) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Add `uvicorn[standard]` to include `watchgod` and `uvloop`. PR [#438](https://github.com/tiangolo/full-stack-fastapi-template/pull/438) by [@alonme](https://github.com/alonme). +* ⬆ Upgrade code to support pydantic V2. PR [#615](https://github.com/tiangolo/full-stack-fastapi-template/pull/615) by [@estebanx64](https://github.com/estebanx64). + +### Docs + +* πŸ¦‡ Add dark mode to `README.md`. PR [#703](https://github.com/tiangolo/full-stack-fastapi-template/pull/703) by [@alejsdev](https://github.com/alejsdev). +* 🍱 Update GitHub image. PR [#701](https://github.com/tiangolo/full-stack-fastapi-template/pull/701) by [@tiangolo](https://github.com/tiangolo). +* 🍱 Add GitHub image. PR [#700](https://github.com/tiangolo/full-stack-fastapi-template/pull/700) by [@tiangolo](https://github.com/tiangolo). +* 🚚 Rename project to Full Stack FastAPI Template. PR [#699](https://github.com/tiangolo/full-stack-fastapi-template/pull/699) by [@tiangolo](https://github.com/tiangolo). +* πŸ“ Update `README.md`. PR [#691](https://github.com/tiangolo/full-stack-fastapi-template/pull/691) by [@alejsdev](https://github.com/alejsdev). +* ✏ Fix typo in `development.md`. PR [#309](https://github.com/tiangolo/full-stack-fastapi-template/pull/309) by [@graue70](https://github.com/graue70). +* πŸ“ Add docs for wildcard domains. PR [#681](https://github.com/tiangolo/full-stack-fastapi-template/pull/681) by [@tiangolo](https://github.com/tiangolo). +* πŸ“ Add the required GitHub Actions secrets to docs. PR [#679](https://github.com/tiangolo/full-stack-fastapi-template/pull/679) by [@tiangolo](https://github.com/tiangolo). +* πŸ“ Update `README.md` and `deployment.md`. PR [#678](https://github.com/tiangolo/full-stack-fastapi-template/pull/678) by [@alejsdev](https://github.com/alejsdev). +* πŸ“ Update frontend `README.md`. PR [#675](https://github.com/tiangolo/full-stack-fastapi-template/pull/675) by [@alejsdev](https://github.com/alejsdev). +* πŸ“ Update deployment docs to use a different directory for traefik-public. PR [#670](https://github.com/tiangolo/full-stack-fastapi-template/pull/670) by [@tiangolo](https://github.com/tiangolo). +* πŸ“Έ Add new screenshots . PR [#657](https://github.com/tiangolo/full-stack-fastapi-template/pull/657) by [@alejsdev](https://github.com/alejsdev). +* πŸ“ Refactor README into separate README.md files for backend, frontend, deployment, development. PR [#639](https://github.com/tiangolo/full-stack-fastapi-template/pull/639) by [@tiangolo](https://github.com/tiangolo). +* πŸ“ Update README. PR [#628](https://github.com/tiangolo/full-stack-fastapi-template/pull/628) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Update GitHub Action latest-changes and move release notes to independent file. PR [#619](https://github.com/tiangolo/full-stack-fastapi-template/pull/619) by [@tiangolo](https://github.com/tiangolo). +* πŸ“ Update internal README and referred files. PR [#613](https://github.com/tiangolo/full-stack-fastapi-template/pull/613) by [@tiangolo](https://github.com/tiangolo). +* πŸ“ Update README with in construction notice. PR [#552](https://github.com/tiangolo/full-stack-fastapi-template/pull/552) by [@tiangolo](https://github.com/tiangolo). +* Add docs about reporting test coverage in HTML. PR [#161](https://github.com/tiangolo/full-stack-fastapi-template/pull/161). +* Add docs about removing the frontend, for an API-only app. PR [#156](https://github.com/tiangolo/full-stack-fastapi-template/pull/156). + +### Internal + +* πŸ‘· Add Lint to GitHub Actions outside of tests. PR [#688](https://github.com/tiangolo/full-stack-fastapi-template/pull/688) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump dawidd6/action-download-artifact from 2.28.0 to 3.1.2. PR [#643](https://github.com/tiangolo/full-stack-fastapi-template/pull/643) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/upload-artifact from 3 to 4. PR [#642](https://github.com/tiangolo/full-stack-fastapi-template/pull/642) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/setup-python from 4 to 5. PR [#641](https://github.com/tiangolo/full-stack-fastapi-template/pull/641) by [@dependabot[bot]](https://github.com/apps/dependabot). +* πŸ‘· Tweak test GitHub Action names. PR [#672](https://github.com/tiangolo/full-stack-fastapi-template/pull/672) by [@tiangolo](https://github.com/tiangolo). +* πŸ”§ Add `.gitattributes` file to ensure LF endings for `.sh` files. PR [#658](https://github.com/tiangolo/full-stack-fastapi-template/pull/658) by [@estebanx64](https://github.com/estebanx64). +* 🚚 Move new-frontend to frontend. PR [#652](https://github.com/tiangolo/full-stack-fastapi-template/pull/652) by [@alejsdev](https://github.com/alejsdev). +* πŸ”§ Add script for ESLint. PR [#650](https://github.com/tiangolo/full-stack-fastapi-template/pull/650) by [@alejsdev](https://github.com/alejsdev). +* βš™οΈ Add Prettier config. PR [#647](https://github.com/tiangolo/full-stack-fastapi-template/pull/647) by [@alejsdev](https://github.com/alejsdev). +* πŸ”§ Update pre-commit config. PR [#645](https://github.com/tiangolo/full-stack-fastapi-template/pull/645) by [@alejsdev](https://github.com/alejsdev). +* πŸ‘· Add dependabot. PR [#547](https://github.com/tiangolo/full-stack-fastapi-template/pull/547) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Fix latest-changes GitHub Action token, strike 2. PR [#546](https://github.com/tiangolo/full-stack-fastapi-template/pull/546) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Fix latest-changes GitHub Action token config. PR [#545](https://github.com/tiangolo/full-stack-fastapi-template/pull/545) by [@tiangolo](https://github.com/tiangolo). +* πŸ‘· Add latest-changes GitHub Action. PR [#544](https://github.com/tiangolo/full-stack-fastapi-template/pull/544) by [@tiangolo](https://github.com/tiangolo). +* Update issue-manager. PR [#211](https://github.com/tiangolo/full-stack-fastapi-template/pull/211). +* Add [GitHub Sponsors](https://github.com/sponsors/tiangolo) button. PR [#201](https://github.com/tiangolo/full-stack-fastapi-template/pull/201). +* Simplify scripts and development, update docs and configs. PR [#155](https://github.com/tiangolo/full-stack-fastapi-template/pull/155). + +## 0.5.0 + +* Make the Traefik public network a fixed default of `traefik-public` as done in DockerSwarm.rocks, to simplify development and iteration of the project generator. PR [#150](https://github.com/tiangolo/full-stack-fastapi-template/pull/150). +* Update to PostgreSQL 12. PR [#148](https://github.com/tiangolo/full-stack-fastapi-template/pull/148). by [@RCheese](https://github.com/RCheese). +* Use Poetry for package management. Initial PR [#144](https://github.com/tiangolo/full-stack-fastapi-template/pull/144) by [@RCheese](https://github.com/RCheese). +* Fix Windows line endings for shell scripts after project generation with Cookiecutter hooks. PR [#149](https://github.com/tiangolo/full-stack-fastapi-template/pull/149). +* Upgrade Vue CLI to version 4. PR [#120](https://github.com/tiangolo/full-stack-fastapi-template/pull/120) by [@br3ndonland](https://github.com/br3ndonland). +* Remove duplicate `login` tag. PR [#135](https://github.com/tiangolo/full-stack-fastapi-template/pull/135) by [@Nonameentered](https://github.com/Nonameentered). +* Fix showing email in dashboard when there's no user's full name. PR [#129](https://github.com/tiangolo/full-stack-fastapi-template/pull/129) by [@rlonka](https://github.com/rlonka). +* Format code with Black and Flake8. PR [#121](https://github.com/tiangolo/full-stack-fastapi-template/pull/121) by [@br3ndonland](https://github.com/br3ndonland). +* Simplify SQLAlchemy Base class. PR [#117](https://github.com/tiangolo/full-stack-fastapi-template/pull/117) by [@airibarne](https://github.com/airibarne). +* Update CRUD utils for users, handling password hashing. PR [#106](https://github.com/tiangolo/full-stack-fastapi-template/pull/106) by [@mocsar](https://github.com/mocsar). +* Use `.` instead of `source` for interoperability. PR [#98](https://github.com/tiangolo/full-stack-fastapi-template/pull/98) by [@gucharbon](https://github.com/gucharbon). +* Use Pydantic's `BaseSettings` for settings/configs and env vars. PR [#87](https://github.com/tiangolo/full-stack-fastapi-template/pull/87) by [@StephenBrown2](https://github.com/StephenBrown2). +* Remove `package-lock.json` to let everyone lock their own versions (depending on OS, etc). +* Simplify Traefik service labels PR [#139](https://github.com/tiangolo/full-stack-fastapi-template/pull/139). +* Add email validation. PR [#40](https://github.com/tiangolo/full-stack-fastapi-template/pull/40) by [@kedod](https://github.com/kedod). +* Fix typo in README. PR [#83](https://github.com/tiangolo/full-stack-fastapi-template/pull/83) by [@ashears](https://github.com/ashears). +* Fix typo in README. PR [#80](https://github.com/tiangolo/full-stack-fastapi-template/pull/80) by [@abjoker](https://github.com/abjoker). +* Fix function name `read_item` and response code. PR [#74](https://github.com/tiangolo/full-stack-fastapi-template/pull/74) by [@jcaguirre89](https://github.com/jcaguirre89). +* Fix typo in comment. PR [#70](https://github.com/tiangolo/full-stack-fastapi-template/pull/70) by [@daniel-butler](https://github.com/daniel-butler). +* Fix Flower Docker configuration. PR [#37](https://github.com/tiangolo/full-stack-fastapi-template/pull/37) by [@dmontagu](https://github.com/dmontagu). +* Add new CRUD utils based on DB and Pydantic models. Initial PR [#23](https://github.com/tiangolo/full-stack-fastapi-template/pull/23) by [@ebreton](https://github.com/ebreton). +* Add normal user testing Pytest fixture. PR [#20](https://github.com/tiangolo/full-stack-fastapi-template/pull/20) by [@ebreton](https://github.com/ebreton). + +## 0.4.0 + +* Fix security on resetting a password. Receive token as body, not query. PR [#34](https://github.com/tiangolo/full-stack-fastapi-template/pull/34). + +* Fix security on resetting a password. Receive it as body, not query. PR [#33](https://github.com/tiangolo/full-stack-fastapi-template/pull/33) by [@dmontagu](https://github.com/dmontagu). + +* Fix SQLAlchemy class lookup on initialization. PR [#29](https://github.com/tiangolo/full-stack-fastapi-template/pull/29) by [@ebreton](https://github.com/ebreton). + +* Fix SQLAlchemy operation errors on database restart. PR [#32](https://github.com/tiangolo/full-stack-fastapi-template/pull/32) by [@ebreton](https://github.com/ebreton). + +* Fix locations of scripts in generated README. PR [#19](https://github.com/tiangolo/full-stack-fastapi-template/pull/19) by [@ebreton](https://github.com/ebreton). + +* Forward arguments from script to `pytest` inside container. PR [#17](https://github.com/tiangolo/full-stack-fastapi-template/pull/17) by [@ebreton](https://github.com/ebreton). + +* Update development scripts. + +* Read Alembic configs from env vars. PR #9 by @ebreton. + +* Create DB Item objects from all Pydantic model's fields. + +* Update Jupyter Lab installation and util script/environment variable for local development. + +## 0.3.0 + +* PR #14: + * Update CRUD utils to use types better. + * Simplify Pydantic model names, from `UserInCreate` to `UserCreate`, etc. + * Upgrade packages. + * Add new generic "Items" models, crud utils, endpoints, and tests. To facilitate re-using them to create new functionality. As they are simple and generic (not like Users), it's easier to copy-paste and adapt them to each use case. + * Update endpoints/*path operations* to simplify code and use new utilities, prefix and tags in `include_router`. + * Update testing utils. + * Update linting rules, relax vulture to reduce false positives. + * Update migrations to include new Items. + * Update project README.md with tips about how to start with backend. + +* Upgrade Python to 3.7 as Celery is now compatible too. PR #10 by @ebreton. + +## 0.2.2 + +* Fix frontend hijacking /docs in development. Using latest https://github.com/tiangolo/node-frontend with custom Nginx configs in frontend. PR #6. + +## 0.2.1 + +* Fix documentation for *path operation* to get user by ID. PR #4 by @mpclarkson in FastAPI. + +* Set `/start-reload.sh` as a command override for development by default. + +* Update generated README. + +## 0.2.0 + +**PR #2**: + +* Simplify and update backend `Dockerfile`s. +* Refactor and simplify backend code, improve naming, imports, modules and "namespaces". +* Improve and simplify Vuex integration with TypeScript accessors. +* Standardize frontend components layout, buttons order, etc. +* Add local development scripts (to develop this project generator itself). +* Add logs to startup modules to detect errors early. +* Improve FastAPI dependency utilities, to simplify and reduce code (to require a superuser). + +## 0.1.2 + +* Fix path operation to update self-user, set parameters as body payload. + +## 0.1.1 + +Several bug fixes since initial publication, including: + +* Order of path operations for users. +* Frontend sending login data in the correct format. +* Add https://localhost variants to CORS. diff --git a/{{cookiecutter.project_slug}}/scripts/build-push.sh b/scripts/build-push.sh similarity index 100% rename from {{cookiecutter.project_slug}}/scripts/build-push.sh rename to scripts/build-push.sh diff --git a/{{cookiecutter.project_slug}}/scripts/build.sh b/scripts/build.sh similarity index 100% rename from {{cookiecutter.project_slug}}/scripts/build.sh rename to scripts/build.sh diff --git a/{{cookiecutter.project_slug}}/scripts/deploy.sh b/scripts/deploy.sh similarity index 88% rename from {{cookiecutter.project_slug}}/scripts/deploy.sh rename to scripts/deploy.sh index 55a86ee94c..99faa96bf7 100644 --- a/{{cookiecutter.project_slug}}/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -4,7 +4,6 @@ set -e DOMAIN=${DOMAIN?Variable not set} \ -TRAEFIK_TAG=${TRAEFIK_TAG?Variable not set} \ STACK_NAME=${STACK_NAME?Variable not set} \ TAG=${TAG?Variable not set} \ docker-compose \ diff --git a/scripts/dev-fsfp-back.sh b/scripts/dev-fsfp-back.sh deleted file mode 100644 index 95e9b78102..0000000000 --- a/scripts/dev-fsfp-back.sh +++ /dev/null @@ -1,22 +0,0 @@ -#! /usr/bin/env bash - -# Run this script from outside the project, to integrate a dev-fsfp project with changes and review modifications - -# Exit in case of error -set -e - -if [ ! -d ./full-stack-fastapi-postgresql ] ; then - echo "Run this script from outside the project, to integrate a sibling dev-fsfp project with changes and review modifications" - exit 1 -fi - -if [ $(uname -s) = "Linux" ]; then - echo "Remove __pycache__ files" - sudo find ./dev-fsfp/ -type d -name __pycache__ -exec rm -r {} \+ -fi - -rm -rf ./full-stack-fastapi-postgresql/\{\{cookiecutter.project_slug\}\}/* - -rsync -a --exclude=node_modules ./dev-fsfp/* ./full-stack-fastapi-postgresql/\{\{cookiecutter.project_slug\}\}/ - -rsync -a ./dev-fsfp/{.env,.gitignore,.gitlab-ci.yml} ./full-stack-fastapi-postgresql/\{\{cookiecutter.project_slug\}\}/ diff --git a/scripts/dev-fsfp.sh b/scripts/dev-fsfp.sh deleted file mode 100644 index 9afbe30b15..0000000000 --- a/scripts/dev-fsfp.sh +++ /dev/null @@ -1,13 +0,0 @@ -#! /usr/bin/env bash - -# Exit in case of error -set -e - -if [ ! -d ./full-stack-fastapi-postgresql ] ; then - echo "Run this script from outside the project, to generate a sibling dev-fsfp project with independent git" - exit 1 -fi - -rm -rf ./dev-fsfp - -cookiecutter --no-input -f ./full-stack-fastapi-postgresql project_name="Dev FSFP" diff --git a/scripts/dev-link.sh b/scripts/dev-link.sh deleted file mode 100644 index 3b59f9d52a..0000000000 --- a/scripts/dev-link.sh +++ /dev/null @@ -1,34 +0,0 @@ -#! /usr/bin/env bash - -# Exit in case of error -set -e - -# Run this from the root of the project to generate a dev-link project -# It will contain a link to each of the files of the generator, except for -# .env and frontend/.env, that will be the generated ones -# This allows developing with a live stack while keeping the same source code -# Without having to generate dev-fsfp and integrating back all the files - -rm -rf dev-link -mkdir -p tmp-dev-link/frontend - -cookiecutter --no-input -f ./ project_name="Dev Link" - -mv ./dev-link/.env ./tmp-dev-link/ -mv ./dev-link/frontend/.env ./tmp-dev-link/frontend/ - -rm -rf ./dev-link/ -mkdir -p ./dev-link/ - -cd ./dev-link/ - -for f in ../\{\{cookiecutter.project_slug\}\}/* ; do - ln -s "$f" ./ -done - -cd .. - -mv ./tmp-dev-link/.env ./dev-link/ -mv ./tmp-dev-link/frontend/.env ./dev-link/frontend/ - -rm -rf ./tmp-dev-link diff --git a/scripts/discard-dev-files.sh b/scripts/discard-dev-files.sh deleted file mode 100644 index 7a07a70bb3..0000000000 --- a/scripts/discard-dev-files.sh +++ /dev/null @@ -1,13 +0,0 @@ -#! /usr/bin/env bash - -set -e - -rm -rf \{\{cookiecutter.project_slug\}\}/.git -rm -rf \{\{cookiecutter.project_slug\}\}/backend/app/poetry.lock -rm -rf \{\{cookiecutter.project_slug\}\}/frontend/node_modules -rm -rf \{\{cookiecutter.project_slug\}\}/frontend/dist -git checkout \{\{cookiecutter.project_slug\}\}/README.md -git checkout \{\{cookiecutter.project_slug\}\}/.gitlab-ci.yml -git checkout \{\{cookiecutter.project_slug\}\}/cookiecutter-config-file.yml -git checkout \{\{cookiecutter.project_slug\}\}/.env -git checkout \{\{cookiecutter.project_slug\}\}/frontend/.env diff --git a/scripts/generate-client.sh b/scripts/generate-client.sh new file mode 100644 index 0000000000..c4f85df84b --- /dev/null +++ b/scripts/generate-client.sh @@ -0,0 +1,11 @@ +#! /usr/bin/env bash + +set -e +set -x + +cd backend +python -c "import app.main; import json; print(json.dumps(app.main.app.openapi()))" > ../openapi.json +cd .. +mv openapi.json frontend/ +cd frontend +npm run generate-client \ No newline at end of file diff --git a/scripts/generate_cookiecutter_config.py b/scripts/generate_cookiecutter_config.py deleted file mode 100644 index 12a9a6ed4e..0000000000 --- a/scripts/generate_cookiecutter_config.py +++ /dev/null @@ -1,20 +0,0 @@ -import json -from collections import OrderedDict -import oyaml as yaml -from pathlib import Path -cookie_path = Path('./cookiecutter.json') -out_path = Path('./{{cookiecutter.project_slug}}/cookiecutter-config-file.yml') - -with open(cookie_path) as f: - cookie_config = json.load(f) -config_out = OrderedDict() - -for key, value in cookie_config.items(): - if key.startswith('_'): - config_out[key] = value - else: - config_out[key] = '{{ cookiecutter.' + key + ' }}' -config_out['_template'] = './' - -with open(out_path, 'w') as out_f: - out_f.write(yaml.dump({'default_context': config_out}, line_break=None, width=200)) diff --git a/{{cookiecutter.project_slug}}/scripts/test-local.sh b/scripts/test-local.sh similarity index 84% rename from {{cookiecutter.project_slug}}/scripts/test-local.sh rename to scripts/test-local.sh index 4c180f21d3..7f2fa9fbce 100644 --- a/{{cookiecutter.project_slug}}/scripts/test-local.sh +++ b/scripts/test-local.sh @@ -12,4 +12,4 @@ fi docker-compose build docker-compose up -d -docker-compose exec -T backend bash /app/tests-start.sh "$@" +docker-compose exec -T backend bash scripts/tests-start.sh "$@" diff --git a/scripts/test.sh b/scripts/test.sh index 36cc535fa8..6dabef7471 100644 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,16 +1,11 @@ -#! /usr/bin/env bash +#! /usr/bin/env sh # Exit in case of error set -e +set -x -# Run this from the root of the project - -rm -rf ./testing-project - -cookiecutter --no-input -f ./ project_name="Testing Project" - -cd ./testing-project - -bash ./scripts/test.sh "$@" - -cd ../ +docker compose build +docker compose down -v --remove-orphans # Remove possibly previous broken stacks left hanging after an error +docker compose up -d +docker compose exec -T backend bash scripts/tests-start.sh "$@" +docker compose down -v --remove-orphans diff --git a/{{cookiecutter.project_slug}}/.env b/{{cookiecutter.project_slug}}/.env deleted file mode 100644 index f6540eeff7..0000000000 --- a/{{cookiecutter.project_slug}}/.env +++ /dev/null @@ -1,45 +0,0 @@ -DOMAIN=localhost -# DOMAIN=local.dockertoolbox.tiangolo.com -# DOMAIN=localhost.tiangolo.com -# DOMAIN=dev.{{cookiecutter.domain_main}} - -STACK_NAME={{cookiecutter.docker_swarm_stack_name_main}} - -TRAEFIK_PUBLIC_NETWORK=traefik-public -TRAEFIK_TAG={{cookiecutter.traefik_constraint_tag}} -TRAEFIK_PUBLIC_TAG={{cookiecutter.traefik_public_constraint_tag}} - -DOCKER_IMAGE_BACKEND={{cookiecutter.docker_image_backend}} -DOCKER_IMAGE_CELERYWORKER={{cookiecutter.docker_image_celeryworker}} -DOCKER_IMAGE_FRONTEND={{cookiecutter.docker_image_frontend}} - -# Backend -BACKEND_CORS_ORIGINS={{cookiecutter.backend_cors_origins}} -PROJECT_NAME={{cookiecutter.project_name}} -SECRET_KEY={{cookiecutter.secret_key}} -FIRST_SUPERUSER={{cookiecutter.first_superuser}} -FIRST_SUPERUSER_PASSWORD={{cookiecutter.first_superuser_password}} -SMTP_TLS=True -SMTP_PORT={{cookiecutter.smtp_port}} -SMTP_HOST={{cookiecutter.smtp_host}} -SMTP_USER={{cookiecutter.smtp_user}} -SMTP_PASSWORD={{cookiecutter.smtp_password}} -EMAILS_FROM_EMAIL={{cookiecutter.smtp_emails_from_email}} - -USERS_OPEN_REGISTRATION=False - -SENTRY_DSN={{cookiecutter.sentry_dsn}} - -# Flower -FLOWER_BASIC_AUTH={{cookiecutter.flower_auth}} - -# Postgres -POSTGRES_SERVER=db -POSTGRES_USER=postgres -POSTGRES_PASSWORD={{cookiecutter.postgres_password}} -POSTGRES_DB=app - -# PgAdmin -PGADMIN_LISTEN_PORT=5050 -PGADMIN_DEFAULT_EMAIL={{cookiecutter.pgadmin_default_user}} -PGADMIN_DEFAULT_PASSWORD={{cookiecutter.pgadmin_default_user_password}} diff --git a/{{cookiecutter.project_slug}}/.gitignore b/{{cookiecutter.project_slug}}/.gitignore deleted file mode 100755 index 1230031ec4..0000000000 --- a/{{cookiecutter.project_slug}}/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.vscode -.mypy_cache -docker-stack.yml diff --git a/{{cookiecutter.project_slug}}/.gitlab-ci.yml b/{{cookiecutter.project_slug}}/.gitlab-ci.yml deleted file mode 100644 index cd730c1b80..0000000000 --- a/{{cookiecutter.project_slug}}/.gitlab-ci.yml +++ /dev/null @@ -1,74 +0,0 @@ -image: tiangolo/docker-with-compose - -before_script: - - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY - - pip install docker-auto-labels - -stages: - - test - - build - - deploy - -tests: - stage: test - script: - - sh ./scripts/test.sh - tags: - - build - - test - -build-stag: - stage: build - script: - - TAG=stag FRONTEND_ENV=staging sh ./scripts/build-push.sh - only: - - master - tags: - - build - - test - -build-prod: - stage: build - script: - - TAG=prod FRONTEND_ENV=production sh ./scripts/build-push.sh - only: - - production - tags: - - build - - test - -deploy-stag: - stage: deploy - script: - - > - DOMAIN={{cookiecutter.domain_staging}} - TRAEFIK_TAG={{cookiecutter.traefik_constraint_tag_staging}} - STACK_NAME={{cookiecutter.docker_swarm_stack_name_staging}} - TAG=stag - sh ./scripts/deploy.sh - environment: - name: staging - url: https://{{cookiecutter.domain_staging}} - only: - - master - tags: - - swarm - - stag - -deploy-prod: - stage: deploy - script: - - > - DOMAIN={{cookiecutter.domain_main}} - TRAEFIK_TAG={{cookiecutter.traefik_constraint_tag}} - STACK_NAME={{cookiecutter.docker_swarm_stack_name_main}} - TAG=prod - sh ./scripts/deploy.sh - environment: - name: production - url: https://{{cookiecutter.domain_main}} - only: - - production - tags: - - swarm - - prod diff --git a/{{cookiecutter.project_slug}}/README.md b/{{cookiecutter.project_slug}}/README.md deleted file mode 100644 index 5594dd546d..0000000000 --- a/{{cookiecutter.project_slug}}/README.md +++ /dev/null @@ -1,833 +0,0 @@ -# {{cookiecutter.project_name}} - -## Backend Requirements - -* [Docker](https://www.docker.com/). -* [Docker Compose](https://docs.docker.com/compose/install/). -* [Poetry](https://python-poetry.org/) for Python package and environment management. - -## Frontend Requirements - -* Node.js (with `npm`). - -## Backend local development - -* Start the stack with Docker Compose: - -```bash -docker-compose up -d -``` - -* Now you can open your browser and interact with these URLs: - -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 - -Alternative automatic documentation with ReDoc (from the OpenAPI backend): http://localhost/redoc - -PGAdmin, PostgreSQL web administration: http://localhost:5050 - -Flower, administration of Celery tasks: http://localhost:5555 - -Traefik UI, to see how the routes are being handled by the proxy: http://localhost:8090 - -**Note**: The first time you start your stack, it might take a minute for it to be ready. While the backend waits for the database to be ready and configures everything. You can check the logs to monitor it. - -To check the logs, run: - -```bash -docker-compose logs -``` - -To check the logs of a specific service, add the name of the service, e.g.: - -```bash -docker-compose logs backend -``` - -If your Docker is not running in `localhost` (the URLs above wouldn't work) check the sections below on **Development with Docker Toolbox** and **Development with a custom IP**. - -## Backend local development, additional details - -### General workflow - -By default, the dependencies are managed with [Poetry](https://python-poetry.org/), go there and install it. - -From `./backend/app/` you can install all the dependencies with: - -```console -$ poetry install -``` - -Then you can start a shell session with the new environment with: - -```console -$ poetry shell -``` - -Next, open your editor at `./backend/app/` (instead of the project root: `./`), so that you see an `./app/` directory with your code inside. That way, your editor will be able to find all the imports, etc. Make sure your editor uses the environment you just created with Poetry. - -Modify or add SQLAlchemy models in `./backend/app/app/models/`, Pydantic schemas in `./backend/app/app/schemas/`, API endpoints in `./backend/app/app/api/`, CRUD (Create, Read, Update, Delete) utils in `./backend/app/app/crud/`. The easiest might be to copy the ones for Items (models, endpoints, and CRUD utils) and update them to your needs. - -Add and modify tasks to the Celery worker in `./backend/app/app/worker.py`. - -If you need to install any additional package to the worker, add it to the file `./backend/app/celeryworker.dockerfile`. - -### Docker Compose Override - -During development, you can change Docker Compose settings that will only affect the local development environment, in the file `docker-compose.override.yml`. - -The changes to that file only affect the local development environment, not the production environment. So, you can add "temporary" changes that help the development workflow. - -For example, the directory with the backend code is mounted as a Docker "host volume", mapping the code you change live to the directory inside the container. That allows you to test your changes right away, without having to build the Docker image again. It should only be done during development, for production, you should build the Docker image with a recent version of the backend code. But during development, it allows you to iterate very fast. - -There is also a command override that runs `/start-reload.sh` (included in the base image) instead of the default `/start.sh` (also included in the base image). It starts a single server process (instead of multiple, as would be for production) and reloads the process whenever the code changes. Have in mind that if you have a syntax error and save the Python file, it will break and exit, and the container will stop. After that, you can restart the container by fixing the error and running again: - -```console -$ docker-compose up -d -``` - -There is also a commented out `command` override, you can uncomment it and comment the default one. It makes the backend container run a process that does "nothing", but keeps the container alive. That allows you to get inside your running container and execute commands inside, for example a Python interpreter to test installed dependencies, or start the development server that reloads when it detects changes, or start a Jupyter Notebook session. - -To get inside the container with a `bash` session you can start the stack with: - -```console -$ docker-compose up -d -``` - -and then `exec` inside the running container: - -```console -$ 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. - -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 -$ DOMAIN=backend sh ./scripts/test.sh -``` - -The file `./scripts/test.sh` has the commands to generate a testing `docker-stack.yml` file, start the stack and test it. - -The tests run with Pytest, modify and add tests to `./backend/app/app/tests/`. - -If you use GitLab CI the tests will run automatically. - -#### Local tests - -Start the stack with this command: - -```Bash -DOMAIN=backend sh ./scripts/test-local.sh -``` -The `./backend/app` directory is mounted as a "host volume" inside the docker container (set in the file `docker-compose.dev.volumes.yml`). -You can rerun the test on live code: - -```Bash -docker-compose exec backend /app/tests-start.sh -``` - -#### 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 /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 - -Because the test scripts forward arguments to `pytest`, you can enable test coverage HTML report generation by passing `--cov-report=html`. - -To run the local tests with coverage HTML reports: - -```Bash -DOMAIN=backend sh ./scripts/test-local.sh --cov-report=html -``` - -To run the tests in a running stack with coverage HTML reports: - -```bash -docker-compose exec backend bash /app/tests-start.sh --cov-report=html -``` - -### Live development with Python Jupyter Notebooks - -If you know about Python [Jupyter Notebooks](http://jupyter.org/), you can take advantage of them during local development. - -The `docker-compose.override.yml` file sends a variable `env` with a value `dev` to the build process of the Docker image (during local development) and the `Dockerfile` has steps to then install and configure Jupyter inside your Docker container. - -So, you can enter into the running Docker container: - -```bash -docker-compose exec backend bash -``` - -And use the environment variable `$JUPYTER` to run a Jupyter Notebook with everything configured to listen on the public port (so that you can use it from your browser). - -It will output something like: - -```console -root@73e0ec1f1ae6:/app# $JUPYTER -[I 12:02:09.975 NotebookApp] Writing notebook server cookie secret to /root/.local/share/jupyter/runtime/notebook_cookie_secret -[I 12:02:10.317 NotebookApp] Serving notebooks from local directory: /app -[I 12:02:10.317 NotebookApp] The Jupyter Notebook is running at: -[I 12:02:10.317 NotebookApp] http://(73e0ec1f1ae6 or 127.0.0.1):8888/?token=f20939a41524d021fbfc62b31be8ea4dd9232913476f4397 -[I 12:02:10.317 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation). -[W 12:02:10.317 NotebookApp] No web browser found: could not locate runnable browser. -[C 12:02:10.317 NotebookApp] - - Copy/paste this URL into your browser when you connect for the first time, - to login with a token: - http://(73e0ec1f1ae6 or 127.0.0.1):8888/?token=f20939a41524d021fbfc62b31be8ea4dd9232913476f4397 -``` - -you can copy that URL and modify the "host" to be `localhost` or the domain you are using for development (e.g. `local.dockertoolbox.tiangolo.com`), in the case above, it would be, e.g.: - -``` -http://localhost:8888/token=f20939a41524d021fbfc62b31be8ea4dd9232913476f4397 -``` - - and then open it in your browser. - -You will have a full Jupyter Notebook running inside your container that has direct access to your database by the container name (`db`), etc. So, you can just run sections of your backend code directly, for example with [VS Code Python Jupyter Interactive Window](https://code.visualstudio.com/docs/python/jupyter-support-py) or [Hydrogen](https://github.com/nteract/hydrogen). - -### 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 -``` - -* If you created a new model in `./backend/app/app/models/`, make sure to import it in `./backend/app/app/db/base.py`, that Python module (`base.py`) that imports all the models will be used by Alembic. - -* After changing a model (for example, adding a column), inside the container, create a revision, e.g.: - -```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 -``` - -If you don't want to use migrations at all, uncomment the line in the file at `./backend/app/app/db/init_db.py` with: - -```python -Base.metadata.create_all(bind=engine) -``` - -and comment the line in the file `prestart.sh` that contains: - -```console -$ 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. - -### Development with Docker Toolbox - -If you are using **Docker Toolbox** in Windows or macOS instead of **Docker for Windows** or **Docker for Mac**, Docker will be running in a VirtualBox Virtual Machine, and it will have a local IP different than `127.0.0.1`, which is the IP address for `localhost` in your machine. - -The address of your Docker Toolbox virtual machine would probably be `192.168.99.100` (that is the default). - -As this is a common case, the domain `local.dockertoolbox.tiangolo.com` points to that (private) IP, just to help with development (actually `dockertoolbox.tiangolo.com` and all its subdomains point to that IP). That way, you can start the stack in Docker Toolbox, and use that domain for development. You will be able to open that URL in Chrome and it will communicate with your local Docker Toolbox directly as if it was a cloud server, including CORS (Cross Origin Resource Sharing). - -If you used the default CORS enabled domains while generating the project, `local.dockertoolbox.tiangolo.com` was configured to be allowed. If you didn't, you will need to add it to the list in the variable `BACKEND_CORS_ORIGINS` in the `.env` file. - -To configure it in your stack, follow the section **Change the development "domain"** below, using the domain `local.dockertoolbox.tiangolo.com`. - -After performing those steps you should be able to open: http://local.dockertoolbox.tiangolo.com and it will be server by your stack in your Docker Toolbox virtual machine. - -Check all the corresponding available URLs in the section at the end. - -### Development in `localhost` with a custom domain - -You might want to use something different than `localhost` as the domain. For example, if you are having problems with cookies that need a subdomain, and Chrome is not allowing you to use `localhost`. - -In that case, you have two options: you could use the instructions to modify your system `hosts` file with the instructions below in **Development with a custom IP** or you can just use `localhost.tiangolo.com`, it is set up to point to `localhost` (to the IP `127.0.0.1`) and all its subdomains too. And as it is an actual domain, the browsers will store the cookies you set during development, etc. - -If you used the default CORS enabled domains while generating the project, `localhost.tiangolo.com` was configured to be allowed. If you didn't, you will need to add it to the list in the variable `BACKEND_CORS_ORIGINS` in the `.env` file. - -To configure it in your stack, follow the section **Change the development "domain"** below, using the domain `localhost.tiangolo.com`. - -After performing those steps you should be able to open: http://localhost.tiangolo.com and it will be server by your stack in `localhost`. - -Check all the corresponding available URLs in the section at the end. - -### Development with a custom IP - -If you are running Docker in an IP address different than `127.0.0.1` (`localhost`) and `192.168.99.100` (the default of Docker Toolbox), you will need to perform some additional steps. That will be the case if you are running a custom Virtual Machine, a secondary Docker Toolbox or your Docker is located in a different machine in your network. - -In that case, you will need to use a fake local domain (`dev.{{cookiecutter.domain_main}}`) and make your computer think that the domain is is served by the custom IP (e.g. `192.168.99.150`). - -If you used the default CORS enabled domains, `dev.{{cookiecutter.domain_main}}` was configured to be allowed. If you want a custom one, you need to add it to the list in the variable `BACKEND_CORS_ORIGINS` in the `.env` file. - -* Open your `hosts` file with administrative privileges using a text editor: - * **Note for Windows**: If you are in Windows, open the main Windows menu, search for "notepad", right click on it, and select the option "open as Administrator" or similar. Then click the "File" menu, "Open file", go to the directory `c:\Windows\System32\Drivers\etc\`, select the option to show "All files" instead of only "Text (.txt) files", and open the `hosts` file. - * **Note for Mac and Linux**: Your `hosts` file is probably located at `/etc/hosts`, you can edit it in a terminal running `sudo nano /etc/hosts`. - -* Additional to the contents it might have, add a new line with the custom IP (e.g. `192.168.99.150`) a space character, and your fake local domain: `dev.{{cookiecutter.domain_main}}`. - -The new line might look like: - -``` -192.168.99.100 dev.{{cookiecutter.domain_main}} -``` - -* Save the file. - * **Note for Windows**: Make sure you save the file as "All files", without an extension of `.txt`. By default, Windows tries to add the extension. Make sure the file is saved as is, without extension. - -...that will make your computer think that the fake local domain is served by that custom IP, and when you open that URL in your browser, it will talk directly to your locally running server when it is asked to go to `dev.{{cookiecutter.domain_main}}` and think that it is a remote server while it is actually running in your computer. - -To configure it in your stack, follow the section **Change the development "domain"** below, using the domain `dev.{{cookiecutter.domain_main}}`. - -After performing those steps you should be able to open: http://dev.{{cookiecutter.domain_main}} and it will be server by your stack in `localhost`. - -Check all the corresponding available URLs in the section at the end. - -### Change the development "domain" - -If you need to use your local stack with a different domain than `localhost`, you need to make sure the domain you use points to the IP where your stack is set up. See the different ways to achieve that in the sections above (i.e. using Docker Toolbox with `local.dockertoolbox.tiangolo.com`, using `localhost.tiangolo.com` or using `dev.{{cookiecutter.domain_main}}`). - -To simplify your Docker Compose setup, for example, so that the API docs (Swagger UI) knows where is your API, you should let it know you are using that domain for development. You will need to edit 1 line in 2 files. - -* Open the file located at `./.env`. It would have a line like: - -``` -DOMAIN=localhost -``` - -* Change it to the domain you are going to use, e.g.: - -``` -DOMAIN=localhost.tiangolo.com -``` - -That variable will be used by the Docker Compose files. - -* Now open the file located at `./frontend/.env`. It would have a line like: - -``` -VUE_APP_DOMAIN_DEV=localhost -``` - -* Change that line to the domain you are going to use, e.g.: - -``` -VUE_APP_DOMAIN_DEV=localhost.tiangolo.com -``` - -That variable will make your frontend communicate with that domain when interacting with your backend API, when the other variable `VUE_APP_ENV` is set to `development`. - -After changing the two lines, you can re-start your stack with: - -```bash -docker-compose up -d -``` - -and check all the corresponding available URLs in the section at the end. - -## Frontend development - -* Enter the `frontend` directory, install the NPM packages and start the live server using the `npm` scripts: - -```bash -cd frontend -npm install -npm run serve -``` - -Then open your browser at http://localhost:8080 - -Notice that this live server is not running inside Docker, it is 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 compiling 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. - -If you have Vue CLI installed, you can also run `vue ui` to control, configure, serve, and analyze your application using a nice local web user interface. - -If you are only developing the frontend (e.g. other team members are developing the backend) and there is a staging environment already deployed, you can make your local development code use that staging API instead of a full local Docker Compose stack. - -To do that, modify the file `./frontend/.env`, there's a section with: - -``` -VUE_APP_ENV=development -# VUE_APP_ENV=staging -``` - -* Switch the comment, to: - -``` -# VUE_APP_ENV=development -VUE_APP_ENV=staging -``` - -### 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` -* `.gitlab-ci.yml` -* `./scripts/*.sh` - -But it would be only to clean them up, leaving them won't really have any effect either way. - -## Deployment - -You can deploy the stack to a Docker Swarm mode cluster with a main Traefik proxy, set up using the ideas from DockerSwarm.rocks, to get automatic HTTPS certificates, etc. - -And you can use CI (continuous integration) systems to do it automatically. - -But you have to configure a couple things first. - -### Traefik network - -This stack expects the public Traefik network to be named `traefik-public`, just as in the tutorials in DockerSwarm.rocks. - -If you need to use a different Traefik public network name, update it in the `docker-compose.yml` files, in the section: - -```YAML -networks: - traefik-public: - external: true -``` - -Change `traefik-public` to the name of the used Traefik network. And then update it in the file `.env`: - -```bash -TRAEFIK_PUBLIC_NETWORK=traefik-public -``` - -### Persisting Docker named volumes - -You need to make sure that each service (Docker container) that uses a volume is always deployed to the same Docker "node" in the cluster, that way it will preserve the data. Otherwise, it could be deployed to a different node each time, and each time the volume would be created in that new node before starting the service. As a result, it would look like your service was starting from scratch every time, losing all the previous data. - -That's specially important for a service running a database. But the same problem would apply if you were saving files in your main backend service (for example, if those files were uploaded by your users, or if they were created by your system). - -To solve that, you can put constraints in the services that use one or more data volumes (like databases) to make them be deployed to a Docker node with a specific label. And of course, you need to have that label assigned to one (only one) of your nodes. - -#### Adding services with volumes - -For each service that uses a volume (databases, services with uploaded files, etc) you should have a label constraint in your `docker-compose.yml` file. - -To make sure that your labels are unique per volume per stack (for example, that they are not the same for `prod` and `stag`) you should prefix them with the name of your stack and then use the same name of the volume. - -Then you need to have those constraints in your `docker-compose.yml` file for the services that need to be fixed with each volume. - -To be able to use different environments, like `prod` and `stag`, you should pass the name of the stack as an environment variable. Like: - -```bash -STACK_NAME={{cookiecutter.docker_swarm_stack_name_staging}} sh ./scripts/deploy.sh -``` - -To use and expand that environment variable inside the `docker-compose.yml` files you can add the constraints to the services like: - -```yaml -version: '3' -services: - db: - volumes: - - 'app-db-data:/var/lib/postgresql/data/pgdata' - deploy: - placement: - constraints: - - node.labels.${STACK_NAME?Variable not set}.app-db-data == true -``` - -note the `${STACK_NAME?Variable not set}`. In the script `./scripts/deploy.sh`, the `docker-compose.yml` would be converted, and saved to a file `docker-stack.yml` containing: - -```yaml -version: '3' -services: - db: - volumes: - - 'app-db-data:/var/lib/postgresql/data/pgdata' - deploy: - placement: - constraints: - - node.labels.{{cookiecutter.docker_swarm_stack_name_main}}.app-db-data == true -``` - -**Note**: The `${STACK_NAME?Variable not set}` means "use the environment variable `STACK_NAME`, but if it is not set, show an error `Variable not set`". - -If you add more volumes to your stack, you need to make sure you add the corresponding constraints to the services that use that named volume. - -Then you have to create those labels in some nodes in your Docker Swarm mode cluster. You can use `docker-auto-labels` to do it automatically. - -#### `docker-auto-labels` - -You can use [`docker-auto-labels`](https://github.com/tiangolo/docker-auto-labels) to automatically read the placement constraint labels in your Docker stack (Docker Compose file) and assign them to a random Docker node in your Swarm mode cluster if those labels don't exist yet. - -To do that, you can install `docker-auto-labels`: - -```bash -pip install docker-auto-labels -``` - -And then run it passing your `docker-stack.yml` file as a parameter: - -```bash -docker-auto-labels docker-stack.yml -``` - -You can run that command every time you deploy, right before deploying, as it doesn't modify anything if the required labels already exist. - -#### (Optionally) adding labels manually - -If you don't want to use `docker-auto-labels` or for any reason you want to manually assign the constraint labels to specific nodes in your Docker Swarm mode cluster, you can do the following: - -* First, connect via SSH to your Docker Swarm mode cluster. - -* Then check the available nodes with: - -```console -$ docker node ls - - -// you would see an output like: - -ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS -nfa3d4df2df34as2fd34230rm * dog.example.com Ready Active Reachable -2c2sd2342asdfasd42342304e cat.example.com Ready Active Leader -c4sdf2342asdfasd4234234ii snake.example.com Ready Active Reachable -``` - -then chose a node from the list. For example, `dog.example.com`. - -* Add the label to that node. Use as label the name of the stack you are deploying followed by a dot (`.`) followed by the named volume, and as value, just `true`, e.g.: - -```bash -docker node update --label-add {{cookiecutter.docker_swarm_stack_name_main}}.app-db-data=true dog.example.com -``` - -* Then you need to do the same for each stack version you have. For example, for staging you could do: - -```bash -docker node update --label-add {{cookiecutter.docker_swarm_stack_name_staging}}.app-db-data=true cat.example.com -``` - -### Deploy to a Docker Swarm mode cluster - -There are 3 steps: - -1. **Build** your app images -2. Optionally, **push** your custom images to a Docker Registry -3. **Deploy** your stack - ---- - -Here are the steps in detail: - -1. **Build your app images** - -* Set these environment variables, right before the next command: - * `TAG=prod` - * `FRONTEND_ENV=production` -* Use the provided `scripts/build.sh` file with those environment variables: - -```bash -TAG=prod FRONTEND_ENV=production bash ./scripts/build.sh -``` - -2. **Optionally, push your images to a Docker Registry** - -**Note**: if the deployment Docker Swarm mode "cluster" has more than one server, you will have to push the images to a registry or build the images in each server, so that when each of the servers in your cluster tries to start the containers it can get the Docker images for them, pulling them from a Docker Registry or because it has them already built locally. - -If you are using a registry and pushing your images, you can omit running the previous script and instead using this one, in a single shot. - -* Set these environment variables: - * `TAG=prod` - * `FRONTEND_ENV=production` -* Use the provided `scripts/build-push.sh` file with those environment variables: - -```bash -TAG=prod FRONTEND_ENV=production bash ./scripts/build-push.sh -``` - -3. **Deploy your stack** - -* Set these environment variables: - * `DOMAIN={{cookiecutter.domain_main}}` - * `TRAEFIK_TAG={{cookiecutter.traefik_constraint_tag}}` - * `STACK_NAME={{cookiecutter.docker_swarm_stack_name_main}}` - * `TAG=prod` -* Use the provided `scripts/deploy.sh` file with those environment variables: - -```bash -DOMAIN={{cookiecutter.domain_main}} \ -TRAEFIK_TAG={{cookiecutter.traefik_constraint_tag}} \ -STACK_NAME={{cookiecutter.docker_swarm_stack_name_main}} \ -TAG=prod \ -bash ./scripts/deploy.sh -``` - ---- - -If you change your mind and, for example, want to deploy everything to a different domain, you only have to change the `DOMAIN` environment variable in the previous commands. If you wanted to add a different version / environment of your stack, like "`preproduction`", you would only have to set `TAG=preproduction` in your command and update these other environment variables accordingly. And it would all work, that way you could have different environments and deployments of the same app in the same cluster. - -#### Deployment Technical Details - -Building and pushing is done with the `docker-compose.yml` file, using the `docker-compose` command. The file `docker-compose.yml` uses the file `.env` with default environment variables. And the scripts set some additional environment variables as well. - -The deployment requires using `docker stack` instead of `docker-swarm`, and it can't read environment variables or `.env` files. Because of that, the `deploy.sh` script generates a file `docker-stack.yml` with the configurations from `docker-compose.yml` and injecting the environment variables in it. And then uses it to deploy the stack. - -You can do the process by hand based on those same scripts if you wanted. The general structure is like this: - -```bash -# Use the environment variables passed to this script, as TAG and FRONTEND_ENV -# And re-create those variables as environment variables for the next command -TAG=${TAG?Variable not set} \ -# Set the environment variable FRONTEND_ENV to the same value passed to this script with -# a default value of "production" if nothing else was passed -FRONTEND_ENV=${FRONTEND_ENV-production?Variable not set} \ -# The actual comand that does the work: docker-compose -docker-compose \ -# Pass the file that should be used, setting explicitly docker-compose.yml avoids the -# default of also using docker-compose.override.yml --f docker-compose.yml \ -# Use the docker-compose sub command named "config", it just uses the docker-compose.yml -# file passed to it and prints their combined contents -# Put those contents in a file "docker-stack.yml", with ">" -config > docker-stack.yml - -# The previous only generated a docker-stack.yml file, -# but didn't do anything with it yet - -# docker-auto-labels makes sure the labels used for constraints exist in the cluster -docker-auto-labels docker-stack.yml - -# Now this command uses that same file to deploy it -docker stack deploy -c docker-stack.yml --with-registry-auth "${STACK_NAME?Variable not set}" -``` - -### Continuous Integration / Continuous Delivery - -If you use GitLab CI, the included `.gitlab-ci.yml` can automatically deploy it. You may need to update it according to your GitLab configurations. - -If you use any other CI / CD provider, you can base your deployment from that `.gitlab-ci.yml` file, as all the actual script steps are performed in `bash` scripts that you can easily re-use. - -GitLab CI is configured assuming 2 environments following GitLab flow: - -* `prod` (production) from the `production` branch. -* `stag` (staging) from the `master` branch. - -If you need to add more environments, for example, you could imagine using a client-approved `preprod` branch, you can just copy the configurations in `.gitlab-ci.yml` for `stag` and rename the corresponding variables. The Docker Compose file and environment variables are configured to support as many environments as you need, so that you only need to modify `.gitlab-ci.yml` (or whichever CI system configuration you are using). - -## Docker Compose files and env vars - -There is a main `docker-compose.yml` file with all the configurations that apply to the whole stack, it is used automatically by `docker-compose`. - -And there's also a `docker-compose.override.yml` with overrides for development, for example to mount the source code as a volume. It is used automatically by `docker-compose` to apply overrides on top of `docker-compose.yml`. - -These Docker Compose files use the `.env` file containing configurations to be injected as environment variables in the containers. - -They also use some additional configurations taken from environment variables set in the scripts before calling the `docker-compose` command. - -It is all designed to support several "stages", like development, building, testing, and deployment. Also, allowing the deployment to different environments like staging and production (and you can add more environments very easily). - -They are designed to have the minimum repetition of code and configurations, so that if you need to change something, you have to change it in the minimum amount of places. That's why files use environment variables that get auto-expanded. That way, if for example, you want to use a different domain, you can call the `docker-compose` command with a different `DOMAIN` environment variable instead of having to change the domain in several places inside the Docker Compose files. - -Also, if you want to have another deployment environment, say `preprod`, you just have to change environment variables, but you can keep using the same Docker Compose files. - -### The .env file - -The `.env` file is the one that contains all your configurations, generated keys and passwords, etc. - -Depending on your workflow, you could want to exclude it from Git, for example if your project is public. In that case, you would have to make sure to set up a way for your CI tools to obtain it while building or deploying your project. - -One way to do it could be to add each environment variable to your CI/CD system, and updating the `docker-compose.yml` file to read that specific env var instead of reading the `.env` file. - -## URLs - -These are the URLs that will be used and generated by the project. - -### Production URLs - -Production URLs, from the branch `production`. - -Frontend: https://{{cookiecutter.domain_main}} - -Backend: https://{{cookiecutter.domain_main}}/api/ - -Automatic Interactive Docs (Swagger UI): https://{{cookiecutter.domain_main}}/docs - -Automatic Alternative Docs (ReDoc): https://{{cookiecutter.domain_main}}/redoc - -PGAdmin: https://pgadmin.{{cookiecutter.domain_main}} - -Flower: https://flower.{{cookiecutter.domain_main}} - -### Staging URLs - -Staging URLs, from the branch `master`. - -Frontend: https://{{cookiecutter.domain_staging}} - -Backend: https://{{cookiecutter.domain_staging}}/api/ - -Automatic Interactive Docs (Swagger UI): https://{{cookiecutter.domain_staging}}/docs - -Automatic Alternative Docs (ReDoc): https://{{cookiecutter.domain_staging}}/redoc - -PGAdmin: https://pgadmin.{{cookiecutter.domain_staging}} - -Flower: https://flower.{{cookiecutter.domain_staging}} - -### Development URLs - -Development URLs, for local development. - -Frontend: http://localhost - -Backend: http://localhost/api/ - -Automatic Interactive Docs (Swagger UI): https://localhost/docs - -Automatic Alternative Docs (ReDoc): https://localhost/redoc - -PGAdmin: http://localhost:5050 - -Flower: http://localhost:5555 - -Traefik UI: http://localhost:8090 - -### Development with Docker Toolbox URLs - -Development URLs, for local development. - -Frontend: http://local.dockertoolbox.tiangolo.com - -Backend: http://local.dockertoolbox.tiangolo.com/api/ - -Automatic Interactive Docs (Swagger UI): https://local.dockertoolbox.tiangolo.com/docs - -Automatic Alternative Docs (ReDoc): https://local.dockertoolbox.tiangolo.com/redoc - -PGAdmin: http://local.dockertoolbox.tiangolo.com:5050 - -Flower: http://local.dockertoolbox.tiangolo.com:5555 - -Traefik UI: http://local.dockertoolbox.tiangolo.com:8090 - -### Development with a custom IP URLs - -Development URLs, for local development. - -Frontend: http://dev.{{cookiecutter.domain_main}} - -Backend: http://dev.{{cookiecutter.domain_main}}/api/ - -Automatic Interactive Docs (Swagger UI): https://dev.{{cookiecutter.domain_main}}/docs - -Automatic Alternative Docs (ReDoc): https://dev.{{cookiecutter.domain_main}}/redoc - -PGAdmin: http://dev.{{cookiecutter.domain_main}}:5050 - -Flower: http://dev.{{cookiecutter.domain_main}}:5555 - -Traefik UI: http://dev.{{cookiecutter.domain_main}}:8090 - -### Development in localhost with a custom domain URLs - -Development URLs, for local development. - -Frontend: http://localhost.tiangolo.com - -Backend: http://localhost.tiangolo.com/api/ - -Automatic Interactive Docs (Swagger UI): https://localhost.tiangolo.com/docs - -Automatic Alternative Docs (ReDoc): https://localhost.tiangolo.com/redoc - -PGAdmin: http://localhost.tiangolo.com:5050 - -Flower: http://localhost.tiangolo.com:5555 - -Traefik UI: http://localhost.tiangolo.com:8090 - -## Project generation and updating, or re-generating - -This project was generated using https://github.com/tiangolo/full-stack-fastapi-postgresql with: - -```bash -pip install cookiecutter -cookiecutter https://github.com/tiangolo/full-stack-fastapi-postgresql -``` - -You can check the variables used during generation in the file `cookiecutter-config-file.yml`. - -You can generate the project again with the same configurations used the first time. - -That would be useful if, for example, the project generator (`tiangolo/full-stack-fastapi-postgresql`) was updated and you wanted to integrate or review the changes. - -You could generate a new project with the same configurations as this one in a parallel directory. And compare the differences between the two, without having to overwrite your current code but being able to use the same variables used for your current project. - -To achieve that, the generated project includes the file `cookiecutter-config-file.yml` with the current variables used. - -You can use that file while generating a new project to reuse all those variables. - -For example, run: - -```console -$ cookiecutter --config-file ./cookiecutter-config-file.yml --output-dir ../project-copy https://github.com/tiangolo/full-stack-fastapi-postgresql -``` - -That will use the file `cookiecutter-config-file.yml` in the current directory (in this project) to generate a new project inside a sibling directory `project-copy`. diff --git a/{{cookiecutter.project_slug}}/backend/.gitignore b/{{cookiecutter.project_slug}}/backend/.gitignore deleted file mode 100644 index 529e8d581f..0000000000 --- a/{{cookiecutter.project_slug}}/backend/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -__pycache__ -app.egg-info diff --git a/{{cookiecutter.project_slug}}/backend/app/.flake8 b/{{cookiecutter.project_slug}}/backend/app/.flake8 deleted file mode 100644 index 710dc9c030..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/.flake8 +++ /dev/null @@ -1,3 +0,0 @@ -[flake8] -max-line-length = 88 -exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache diff --git a/{{cookiecutter.project_slug}}/backend/app/.gitignore b/{{cookiecutter.project_slug}}/backend/app/.gitignore deleted file mode 100644 index f511683016..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.mypy_cache -.coverage -htmlcov diff --git a/{{cookiecutter.project_slug}}/backend/app/alembic/README b/{{cookiecutter.project_slug}}/backend/app/alembic/README deleted file mode 100755 index 98e4f9c44e..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/alembic/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/backend/app/alembic/versions/d4867f3a4c0a_first_revision.py b/{{cookiecutter.project_slug}}/backend/app/alembic/versions/d4867f3a4c0a_first_revision.py deleted file mode 100644 index a43bf9d2fd..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/alembic/versions/d4867f3a4c0a_first_revision.py +++ /dev/null @@ -1,59 +0,0 @@ -"""First revision - -Revision ID: d4867f3a4c0a -Revises: -Create Date: 2019-04-17 13:53:32.978401 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = "d4867f3a4c0a" -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "user", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("full_name", sa.String(), nullable=True), - sa.Column("email", sa.String(), nullable=True), - sa.Column("hashed_password", sa.String(), nullable=True), - sa.Column("is_active", sa.Boolean(), nullable=True), - sa.Column("is_superuser", sa.Boolean(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True) - op.create_index(op.f("ix_user_full_name"), "user", ["full_name"], unique=False) - op.create_index(op.f("ix_user_id"), "user", ["id"], unique=False) - op.create_table( - "item", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("title", sa.String(), nullable=True), - sa.Column("description", sa.String(), nullable=True), - sa.Column("owner_id", sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(["owner_id"], ["user.id"],), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_item_description"), "item", ["description"], unique=False) - op.create_index(op.f("ix_item_id"), "item", ["id"], unique=False) - op.create_index(op.f("ix_item_title"), "item", ["title"], unique=False) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f("ix_item_title"), table_name="item") - op.drop_index(op.f("ix_item_id"), table_name="item") - op.drop_index(op.f("ix_item_description"), table_name="item") - op.drop_table("item") - op.drop_index(op.f("ix_user_id"), table_name="user") - op.drop_index(op.f("ix_user_full_name"), table_name="user") - op.drop_index(op.f("ix_user_email"), table_name="user") - op.drop_table("user") - # ### end Alembic commands ### diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/api.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/api.py deleted file mode 100644 index 2163017268..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/api.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import APIRouter - -from app.api.api_v1.endpoints import items, login, users, utils - -api_router = APIRouter() -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"]) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/items.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/items.py deleted file mode 100644 index e88885cd80..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/items.py +++ /dev/null @@ -1,99 +0,0 @@ -from typing import Any, List - -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session - -from app import crud, models, schemas -from app.api import deps - -router = APIRouter() - - -@router.get("/", response_model=List[schemas.Item]) -def read_items( - db: Session = Depends(deps.get_db), - skip: int = 0, - limit: int = 100, - current_user: models.User = Depends(deps.get_current_active_user), -) -> Any: - """ - Retrieve items. - """ - if crud.user.is_superuser(current_user): - items = crud.item.get_multi(db, skip=skip, limit=limit) - else: - items = crud.item.get_multi_by_owner( - db=db, owner_id=current_user.id, skip=skip, limit=limit - ) - return items - - -@router.post("/", response_model=schemas.Item) -def create_item( - *, - db: Session = Depends(deps.get_db), - item_in: schemas.ItemCreate, - current_user: models.User = Depends(deps.get_current_active_user), -) -> Any: - """ - Create new item. - """ - item = crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=current_user.id) - return item - - -@router.put("/{id}", response_model=schemas.Item) -def update_item( - *, - db: Session = Depends(deps.get_db), - id: int, - item_in: schemas.ItemUpdate, - current_user: models.User = Depends(deps.get_current_active_user), -) -> Any: - """ - Update an item. - """ - item = crud.item.get(db=db, id=id) - if not item: - raise HTTPException(status_code=404, detail="Item not found") - if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id): - raise HTTPException(status_code=400, detail="Not enough permissions") - item = crud.item.update(db=db, db_obj=item, obj_in=item_in) - return item - - -@router.get("/{id}", response_model=schemas.Item) -def read_item( - *, - db: Session = Depends(deps.get_db), - id: int, - current_user: models.User = Depends(deps.get_current_active_user), -) -> Any: - """ - Get item by ID. - """ - item = crud.item.get(db=db, id=id) - if not item: - raise HTTPException(status_code=404, detail="Item not found") - if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id): - raise HTTPException(status_code=400, detail="Not enough permissions") - return item - - -@router.delete("/{id}", response_model=schemas.Item) -def delete_item( - *, - db: Session = Depends(deps.get_db), - id: int, - current_user: models.User = Depends(deps.get_current_active_user), -) -> Any: - """ - Delete an item. - """ - item = crud.item.get(db=db, id=id) - if not item: - raise HTTPException(status_code=404, detail="Item not found") - if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id): - raise HTTPException(status_code=400, detail="Not enough permissions") - item = crud.item.remove(db=db, id=id) - return item diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py deleted file mode 100644 index 4dc3a9b248..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py +++ /dev/null @@ -1,96 +0,0 @@ -from datetime import timedelta -from typing import Any - -from fastapi import APIRouter, Body, Depends, HTTPException -from fastapi.security import OAuth2PasswordRequestForm -from sqlalchemy.orm import Session - -from app import crud, models, schemas -from app.api import deps -from app.core import security -from app.core.config import settings -from app.core.security import get_password_hash -from app.utils import ( - generate_password_reset_token, - send_reset_password_email, - verify_password_reset_token, -) - -router = APIRouter() - - -@router.post("/login/access-token", response_model=schemas.Token) -def login_access_token( - db: Session = Depends(deps.get_db), form_data: OAuth2PasswordRequestForm = Depends() -) -> Any: - """ - OAuth2 compatible token login, get an access token for future requests - """ - user = crud.user.authenticate( - db, email=form_data.username, password=form_data.password - ) - if not user: - raise HTTPException(status_code=400, detail="Incorrect email or password") - elif not crud.user.is_active(user): - raise HTTPException(status_code=400, detail="Inactive user") - access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) - return { - "access_token": security.create_access_token( - user.id, expires_delta=access_token_expires - ), - "token_type": "bearer", - } - - -@router.post("/login/test-token", response_model=schemas.User) -def test_token(current_user: models.User = Depends(deps.get_current_user)) -> Any: - """ - Test access token - """ - return current_user - - -@router.post("/password-recovery/{email}", response_model=schemas.Msg) -def recover_password(email: str, db: Session = Depends(deps.get_db)) -> Any: - """ - Password Recovery - """ - user = crud.user.get_by_email(db, email=email) - - if not user: - raise HTTPException( - status_code=404, - detail="The user with this username does not exist in the system.", - ) - password_reset_token = generate_password_reset_token(email=email) - send_reset_password_email( - email_to=user.email, email=email, token=password_reset_token - ) - return {"msg": "Password recovery email sent"} - - -@router.post("/reset-password/", response_model=schemas.Msg) -def reset_password( - token: str = Body(...), - new_password: str = Body(...), - db: Session = Depends(deps.get_db), -) -> Any: - """ - Reset password - """ - email = verify_password_reset_token(token) - if not email: - raise HTTPException(status_code=400, detail="Invalid token") - user = crud.user.get_by_email(db, email=email) - if not user: - raise HTTPException( - status_code=404, - detail="The user with this username does not exist in the system.", - ) - elif not crud.user.is_active(user): - raise HTTPException(status_code=400, detail="Inactive user") - hashed_password = get_password_hash(new_password) - user.hashed_password = hashed_password - db.add(user) - db.commit() - return {"msg": "Password updated successfully"} diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py deleted file mode 100644 index c8f89b63d8..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py +++ /dev/null @@ -1,153 +0,0 @@ -from typing import Any, List - -from fastapi import APIRouter, Body, Depends, HTTPException -from fastapi.encoders import jsonable_encoder -from pydantic.networks import EmailStr -from sqlalchemy.orm import Session - -from app import crud, models, schemas -from app.api import deps -from app.core.config import settings -from app.utils import send_new_account_email - -router = APIRouter() - - -@router.get("/", response_model=List[schemas.User]) -def read_users( - db: Session = Depends(deps.get_db), - skip: int = 0, - limit: int = 100, - current_user: models.User = Depends(deps.get_current_active_superuser), -) -> Any: - """ - Retrieve users. - """ - users = crud.user.get_multi(db, skip=skip, limit=limit) - return users - - -@router.post("/", response_model=schemas.User) -def create_user( - *, - db: Session = Depends(deps.get_db), - user_in: schemas.UserCreate, - current_user: models.User = Depends(deps.get_current_active_superuser), -) -> Any: - """ - Create new user. - """ - user = crud.user.get_by_email(db, email=user_in.email) - if user: - raise HTTPException( - status_code=400, - detail="The user with this username already exists in the system.", - ) - user = crud.user.create(db, obj_in=user_in) - if settings.EMAILS_ENABLED and user_in.email: - send_new_account_email( - email_to=user_in.email, username=user_in.email, password=user_in.password - ) - return user - - -@router.put("/me", response_model=schemas.User) -def update_user_me( - *, - db: Session = Depends(deps.get_db), - password: str = Body(None), - full_name: str = Body(None), - email: EmailStr = Body(None), - current_user: models.User = Depends(deps.get_current_active_user), -) -> Any: - """ - Update own user. - """ - current_user_data = jsonable_encoder(current_user) - user_in = schemas.UserUpdate(**current_user_data) - if password is not None: - user_in.password = password - if full_name is not None: - user_in.full_name = full_name - if email is not None: - user_in.email = email - user = crud.user.update(db, db_obj=current_user, obj_in=user_in) - return user - - -@router.get("/me", response_model=schemas.User) -def read_user_me( - db: Session = Depends(deps.get_db), - current_user: models.User = Depends(deps.get_current_active_user), -) -> Any: - """ - Get current user. - """ - return current_user - - -@router.post("/open", response_model=schemas.User) -def create_user_open( - *, - db: Session = Depends(deps.get_db), - password: str = Body(...), - email: EmailStr = Body(...), - full_name: str = Body(None), -) -> Any: - """ - Create new user without the need to be logged in. - """ - if not settings.USERS_OPEN_REGISTRATION: - raise HTTPException( - status_code=403, - detail="Open user registration is forbidden on this server", - ) - user = crud.user.get_by_email(db, email=email) - if user: - raise HTTPException( - status_code=400, - detail="The user with this username already exists in the system", - ) - user_in = schemas.UserCreate(password=password, email=email, full_name=full_name) - user = crud.user.create(db, obj_in=user_in) - return user - - -@router.get("/{user_id}", response_model=schemas.User) -def read_user_by_id( - user_id: int, - current_user: models.User = Depends(deps.get_current_active_user), - db: Session = Depends(deps.get_db), -) -> Any: - """ - Get a specific user by id. - """ - user = crud.user.get(db, id=user_id) - if user == current_user: - return user - if not crud.user.is_superuser(current_user): - raise HTTPException( - status_code=400, detail="The user doesn't have enough privileges" - ) - return user - - -@router.put("/{user_id}", response_model=schemas.User) -def update_user( - *, - db: Session = Depends(deps.get_db), - user_id: int, - user_in: schemas.UserUpdate, - current_user: models.User = Depends(deps.get_current_active_superuser), -) -> Any: - """ - Update a user. - """ - user = crud.user.get(db, id=user_id) - if not user: - raise HTTPException( - status_code=404, - detail="The user with this username does not exist in the system", - ) - user = crud.user.update(db, db_obj=user, obj_in=user_in) - return user diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/utils.py b/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/utils.py deleted file mode 100644 index 71fe68d0ce..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/utils.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Any - -from fastapi import APIRouter, Depends -from pydantic.networks import EmailStr - -from app import models, schemas -from app.api import deps -from app.core.celery_app import celery_app -from app.utils import send_test_email - -router = APIRouter() - - -@router.post("/test-celery/", response_model=schemas.Msg, status_code=201) -def test_celery( - msg: schemas.Msg, - current_user: models.User = Depends(deps.get_current_active_superuser), -) -> Any: - """ - Test Celery worker. - """ - celery_app.send_task("app.worker.test_celery", args=[msg.msg]) - return {"msg": "Word received"} - - -@router.post("/test-email/", response_model=schemas.Msg, status_code=201) -def test_email( - email_to: EmailStr, - current_user: models.User = Depends(deps.get_current_active_superuser), -) -> Any: - """ - Test emails. - """ - send_test_email(email_to=email_to) - return {"msg": "Test email sent"} diff --git a/{{cookiecutter.project_slug}}/backend/app/app/api/deps.py b/{{cookiecutter.project_slug}}/backend/app/app/api/deps.py deleted file mode 100644 index a0109afe84..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/api/deps.py +++ /dev/null @@ -1,61 +0,0 @@ -from typing import Generator - -from fastapi import Depends, HTTPException, status -from fastapi.security import OAuth2PasswordBearer -from jose import jwt -from pydantic import ValidationError -from sqlalchemy.orm import Session - -from app import crud, models, schemas -from app.core import security -from app.core.config import settings -from app.db.session import SessionLocal - -reusable_oauth2 = OAuth2PasswordBearer( - tokenUrl=f"{settings.API_V1_STR}/login/access-token" -) - - -def get_db() -> Generator: - try: - db = SessionLocal() - yield db - finally: - db.close() - - -def get_current_user( - db: Session = Depends(get_db), token: str = Depends(reusable_oauth2) -) -> models.User: - try: - payload = jwt.decode( - token, settings.SECRET_KEY, algorithms=[security.ALGORITHM] - ) - token_data = schemas.TokenPayload(**payload) - except (jwt.JWTError, ValidationError): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Could not validate credentials", - ) - user = crud.user.get(db, id=token_data.sub) - if not user: - raise HTTPException(status_code=404, detail="User not found") - return user - - -def get_current_active_user( - current_user: models.User = Depends(get_current_user), -) -> models.User: - if not crud.user.is_active(current_user): - raise HTTPException(status_code=400, detail="Inactive user") - return current_user - - -def get_current_active_superuser( - current_user: models.User = Depends(get_current_user), -) -> models.User: - if not crud.user.is_superuser(current_user): - raise HTTPException( - status_code=400, detail="The user doesn't have enough privileges" - ) - return current_user diff --git a/{{cookiecutter.project_slug}}/backend/app/app/celeryworker_pre_start.py b/{{cookiecutter.project_slug}}/backend/app/app/celeryworker_pre_start.py deleted file mode 100644 index 81de37134f..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/celeryworker_pre_start.py +++ /dev/null @@ -1,37 +0,0 @@ -import logging - -from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed - -from app.db.session import SessionLocal - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -max_tries = 60 * 5 # 5 minutes -wait_seconds = 1 - - -@retry( - stop=stop_after_attempt(max_tries), - wait=wait_fixed(wait_seconds), - before=before_log(logger, logging.INFO), - after=after_log(logger, logging.WARN), -) -def init() -> None: - try: - # Try to create session to check if DB is awake - db = SessionLocal() - db.execute("SELECT 1") - except Exception as e: - logger.error(e) - raise e - - -def main() -> None: - logger.info("Initializing service") - init() - logger.info("Service finished initializing") - - -if __name__ == "__main__": - main() diff --git a/{{cookiecutter.project_slug}}/backend/app/app/core/celery_app.py b/{{cookiecutter.project_slug}}/backend/app/app/core/celery_app.py deleted file mode 100644 index 0477d14589..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/core/celery_app.py +++ /dev/null @@ -1,5 +0,0 @@ -from celery import Celery - -celery_app = Celery("worker", broker="amqp://guest@queue//") - -celery_app.conf.task_routes = {"app.worker.test_celery": "main-queue"} diff --git a/{{cookiecutter.project_slug}}/backend/app/app/core/config.py b/{{cookiecutter.project_slug}}/backend/app/app/core/config.py deleted file mode 100644 index 8b07276dac..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/core/config.py +++ /dev/null @@ -1,89 +0,0 @@ -import secrets -from typing import Any, Dict, List, Optional, Union - -from pydantic import AnyHttpUrl, BaseSettings, EmailStr, HttpUrl, PostgresDsn, validator - - -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 - SERVER_NAME: str - SERVER_HOST: AnyHttpUrl - # BACKEND_CORS_ORIGINS is a JSON-formatted list of origins - # e.g: '["http://localhost", "http://localhost:4200", "http://localhost:3000", \ - # "http://localhost:8080", "http://local.dockertoolbox.tiangolo.com"]' - BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [] - - @validator("BACKEND_CORS_ORIGINS", pre=True) - def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]: - if isinstance(v, str) and not v.startswith("["): - return [i.strip() for i in v.split(",")] - elif isinstance(v, (list, str)): - return v - raise ValueError(v) - - PROJECT_NAME: str - SENTRY_DSN: Optional[HttpUrl] = None - - @validator("SENTRY_DSN", pre=True) - def sentry_dsn_can_be_blank(cls, v: str) -> Optional[str]: - if len(v) == 0: - return None - return v - - POSTGRES_SERVER: str - POSTGRES_USER: str - POSTGRES_PASSWORD: str - POSTGRES_DB: str - SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None - - @validator("SQLALCHEMY_DATABASE_URI", pre=True) - def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any: - if isinstance(v, str): - return v - return PostgresDsn.build( - scheme="postgresql", - user=values.get("POSTGRES_USER"), - password=values.get("POSTGRES_PASSWORD"), - host=values.get("POSTGRES_SERVER"), - path=f"/{values.get('POSTGRES_DB') or ''}", - ) - - SMTP_TLS: bool = True - SMTP_PORT: Optional[int] = None - SMTP_HOST: Optional[str] = None - SMTP_USER: Optional[str] = None - SMTP_PASSWORD: Optional[str] = None - EMAILS_FROM_EMAIL: Optional[EmailStr] = None - EMAILS_FROM_NAME: Optional[str] = None - - @validator("EMAILS_FROM_NAME") - def get_project_name(cls, v: Optional[str], values: Dict[str, Any]) -> str: - if not v: - return values["PROJECT_NAME"] - return v - - EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48 - EMAIL_TEMPLATES_DIR: str = "/app/app/email-templates/build" - EMAILS_ENABLED: bool = False - - @validator("EMAILS_ENABLED", pre=True) - def get_emails_enabled(cls, v: bool, values: Dict[str, Any]) -> bool: - return bool( - values.get("SMTP_HOST") - and values.get("SMTP_PORT") - and values.get("EMAILS_FROM_EMAIL") - ) - - EMAIL_TEST_USER: EmailStr = "test@example.com" # type: ignore - FIRST_SUPERUSER: EmailStr - FIRST_SUPERUSER_PASSWORD: str - USERS_OPEN_REGISTRATION: bool = False - - class Config: - case_sensitive = True - - -settings = Settings() diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/__init__.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/__init__.py deleted file mode 100644 index 40e2c673b4..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/crud/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .crud_item import item -from .crud_user import user - -# For a new basic set of CRUD operations you could just do - -# from .base import CRUDBase -# from app.models.item import Item -# from app.schemas.item import ItemCreate, ItemUpdate - -# item = CRUDBase[Item, ItemCreate, ItemUpdate](Item) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py deleted file mode 100644 index 2b6f1f10cd..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/crud/base.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union - -from fastapi.encoders import jsonable_encoder -from pydantic import BaseModel -from sqlalchemy.orm import Session - -from app.db.base_class import Base - -ModelType = TypeVar("ModelType", bound=Base) -CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) -UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) - - -class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): - def __init__(self, model: Type[ModelType]): - """ - CRUD object with default methods to Create, Read, Update, Delete (CRUD). - - **Parameters** - - * `model`: A SQLAlchemy model class - * `schema`: A Pydantic model (schema) class - """ - self.model = model - - def get(self, db: Session, id: Any) -> Optional[ModelType]: - return db.query(self.model).filter(self.model.id == id).first() - - def get_multi( - self, db: Session, *, skip: int = 0, limit: int = 100 - ) -> List[ModelType]: - return db.query(self.model).offset(skip).limit(limit).all() - - def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType: - obj_in_data = jsonable_encoder(obj_in) - db_obj = self.model(**obj_in_data) # type: ignore - db.add(db_obj) - db.commit() - db.refresh(db_obj) - return db_obj - - def update( - self, - db: Session, - *, - db_obj: ModelType, - obj_in: Union[UpdateSchemaType, Dict[str, Any]] - ) -> ModelType: - obj_data = jsonable_encoder(db_obj) - if isinstance(obj_in, dict): - update_data = obj_in - else: - update_data = obj_in.dict(exclude_unset=True) - for field in obj_data: - if field in update_data: - setattr(db_obj, field, update_data[field]) - db.add(db_obj) - db.commit() - db.refresh(db_obj) - return db_obj - - def remove(self, db: Session, *, id: int) -> ModelType: - obj = db.query(self.model).get(id) - db.delete(obj) - db.commit() - return obj diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_item.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_item.py deleted file mode 100644 index dcb87cdc5c..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_item.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import List - -from fastapi.encoders import jsonable_encoder -from sqlalchemy.orm import Session - -from app.crud.base import CRUDBase -from app.models.item import Item -from app.schemas.item import ItemCreate, ItemUpdate - - -class CRUDItem(CRUDBase[Item, ItemCreate, ItemUpdate]): - def create_with_owner( - self, db: Session, *, obj_in: ItemCreate, owner_id: int - ) -> Item: - obj_in_data = jsonable_encoder(obj_in) - db_obj = self.model(**obj_in_data, owner_id=owner_id) - db.add(db_obj) - db.commit() - db.refresh(db_obj) - return db_obj - - def get_multi_by_owner( - self, db: Session, *, owner_id: int, skip: int = 0, limit: int = 100 - ) -> List[Item]: - return ( - db.query(self.model) - .filter(Item.owner_id == owner_id) - .offset(skip) - .limit(limit) - .all() - ) - - -item = CRUDItem(Item) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_user.py b/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_user.py deleted file mode 100644 index 14525d326f..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/crud/crud_user.py +++ /dev/null @@ -1,55 +0,0 @@ -from typing import Any, Dict, Optional, Union - -from sqlalchemy.orm import Session - -from app.core.security import get_password_hash, verify_password -from app.crud.base import CRUDBase -from app.models.user import User -from app.schemas.user import UserCreate, UserUpdate - - -class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): - def get_by_email(self, db: Session, *, email: str) -> Optional[User]: - return db.query(User).filter(User.email == email).first() - - def create(self, db: Session, *, obj_in: UserCreate) -> User: - db_obj = User( - email=obj_in.email, - hashed_password=get_password_hash(obj_in.password), - full_name=obj_in.full_name, - is_superuser=obj_in.is_superuser, - ) - db.add(db_obj) - db.commit() - db.refresh(db_obj) - return db_obj - - def update( - self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]] - ) -> User: - if isinstance(obj_in, dict): - update_data = obj_in - else: - update_data = obj_in.dict(exclude_unset=True) - if update_data["password"]: - hashed_password = get_password_hash(update_data["password"]) - del update_data["password"] - update_data["hashed_password"] = hashed_password - return super().update(db, db_obj=db_obj, obj_in=update_data) - - def authenticate(self, db: Session, *, email: str, password: str) -> Optional[User]: - user = self.get_by_email(db, email=email) - if not user: - return None - if not verify_password(password, user.hashed_password): - return None - return user - - def is_active(self, user: User) -> bool: - return user.is_active - - def is_superuser(self, user: User) -> bool: - return user.is_superuser - - -user = CRUDUser(User) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/db/base.py b/{{cookiecutter.project_slug}}/backend/app/app/db/base.py deleted file mode 100644 index b7ec44fec1..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/db/base.py +++ /dev/null @@ -1,5 +0,0 @@ -# Import all the models, so that Base has them before being -# imported by Alembic -from app.db.base_class import Base # noqa -from app.models.item import Item # noqa -from app.models.user import User # noqa diff --git a/{{cookiecutter.project_slug}}/backend/app/app/db/base_class.py b/{{cookiecutter.project_slug}}/backend/app/app/db/base_class.py deleted file mode 100644 index 3b759110b6..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/db/base_class.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Any - -from sqlalchemy.ext.declarative import as_declarative, declared_attr - - -@as_declarative() -class Base: - id: Any - __name__: str - # Generate __tablename__ automatically - @declared_attr - def __tablename__(cls) -> str: - return cls.__name__.lower() diff --git a/{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py b/{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py deleted file mode 100644 index bc1dd2e06c..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py +++ /dev/null @@ -1,25 +0,0 @@ -from sqlalchemy.orm import Session - -from app import crud, schemas -from app.core.config import settings -from app.db import base # noqa: F401 - -# make sure all SQL Alchemy models are imported (app.db.base) before initializing DB -# otherwise, SQL Alchemy might fail to initialize relationships properly -# for more details: https://github.com/tiangolo/full-stack-fastapi-postgresql/issues/28 - - -def init_db(db: 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 line - # Base.metadata.create_all(bind=engine) - - user = crud.user.get_by_email(db, email=settings.FIRST_SUPERUSER) - if not user: - user_in = schemas.UserCreate( - email=settings.FIRST_SUPERUSER, - password=settings.FIRST_SUPERUSER_PASSWORD, - is_superuser=True, - ) - user = crud.user.create(db, obj_in=user_in) # noqa: F841 diff --git a/{{cookiecutter.project_slug}}/backend/app/app/db/session.py b/{{cookiecutter.project_slug}}/backend/app/app/db/session.py deleted file mode 100644 index 9edb2fa1d0..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/db/session.py +++ /dev/null @@ -1,7 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - -from app.core.config import settings - -engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/email-templates/build/new_account.html b/{{cookiecutter.project_slug}}/backend/app/app/email-templates/build/new_account.html deleted file mode 100644 index 395c7bd156..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/email-templates/build/new_account.html +++ /dev/null @@ -1,26 +0,0 @@ -

{{ project_name }} - New Account
You have a new account:
Username: {{ username }}
Password: {{ password }}
Go to Dashboard

\ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/backend/app/app/email-templates/build/reset_password.html b/{{cookiecutter.project_slug}}/backend/app/app/email-templates/build/reset_password.html deleted file mode 100644 index 7fbf368b9f..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/email-templates/build/reset_password.html +++ /dev/null @@ -1,26 +0,0 @@ -

{{ project_name }} - Password Recovery
We received a request to recover the password for user {{ username }} with email {{ email }}
Reset your password by clicking the button below:
Reset Password
Or open the following link:

The reset password link / button 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/{{cookiecutter.project_slug}}/backend/app/app/email-templates/build/test_email.html b/{{cookiecutter.project_slug}}/backend/app/app/email-templates/build/test_email.html deleted file mode 100644 index 294d576623..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/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/{{cookiecutter.project_slug}}/backend/app/app/email-templates/src/new_account.mjml b/{{cookiecutter.project_slug}}/backend/app/app/email-templates/src/new_account.mjml deleted file mode 100644 index 16c033b078..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/email-templates/src/new_account.mjml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - {{ project_name }} - New Account - You have a new account: - Username: {{ username }} - Password: {{ password }} - Go to Dashboard - - - - - diff --git a/{{cookiecutter.project_slug}}/backend/app/app/email-templates/src/reset_password.mjml b/{{cookiecutter.project_slug}}/backend/app/app/email-templates/src/reset_password.mjml deleted file mode 100644 index 4f45ea285b..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/email-templates/src/reset_password.mjml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - {{ project_name }} - Password Recovery - We received a request to recover the password for user {{ username }} - with email {{ email }} - Reset your password by clicking the button below: - Reset Password - Or open the following link: - {{ link }} - - The reset password link / button will expire in {{ valid_hours }} hours. - If you didn't request a password recovery you can disregard this email. - - - - diff --git a/{{cookiecutter.project_slug}}/backend/app/app/email-templates/src/test_email.mjml b/{{cookiecutter.project_slug}}/backend/app/app/email-templates/src/test_email.mjml deleted file mode 100644 index 5b9baa4594..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/email-templates/src/test_email.mjml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - {{ project_name }} - Test email for: {{ email }} - - - - diff --git a/{{cookiecutter.project_slug}}/backend/app/app/main.py b/{{cookiecutter.project_slug}}/backend/app/app/main.py deleted file mode 100644 index d5d0a79493..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/main.py +++ /dev/null @@ -1,21 +0,0 @@ -from fastapi import FastAPI -from starlette.middleware.cors import CORSMiddleware - -from app.api.api_v1.api import api_router -from app.core.config import settings - -app = FastAPI( - title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json" -) - -# Set all CORS enabled origins -if settings.BACKEND_CORS_ORIGINS: - app.add_middleware( - CORSMiddleware, - allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - ) - -app.include_router(api_router, prefix=settings.API_V1_STR) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/models/__init__.py b/{{cookiecutter.project_slug}}/backend/app/app/models/__init__.py deleted file mode 100644 index a9c6bdb1ef..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/models/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .item import Item -from .user import User diff --git a/{{cookiecutter.project_slug}}/backend/app/app/models/item.py b/{{cookiecutter.project_slug}}/backend/app/app/models/item.py deleted file mode 100755 index 205535e4b4..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/models/item.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import TYPE_CHECKING - -from sqlalchemy import Column, ForeignKey, Integer, String -from sqlalchemy.orm import relationship - -from app.db.base_class import Base - -if TYPE_CHECKING: - from .user import User # noqa: F401 - - -class Item(Base): - id = Column(Integer, primary_key=True, index=True) - title = Column(String, index=True) - description = Column(String, index=True) - owner_id = Column(Integer, ForeignKey("user.id")) - owner = relationship("User", back_populates="items") diff --git a/{{cookiecutter.project_slug}}/backend/app/app/models/user.py b/{{cookiecutter.project_slug}}/backend/app/app/models/user.py deleted file mode 100755 index 1e8e1a011b..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/models/user.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import TYPE_CHECKING - -from sqlalchemy import Boolean, Column, Integer, String -from sqlalchemy.orm import relationship - -from app.db.base_class import Base - -if TYPE_CHECKING: - from .item import Item # noqa: F401 - - -class User(Base): - id = Column(Integer, primary_key=True, index=True) - full_name = Column(String, index=True) - email = Column(String, unique=True, index=True, nullable=False) - hashed_password = Column(String, nullable=False) - is_active = Column(Boolean(), default=True) - is_superuser = Column(Boolean(), default=False) - items = relationship("Item", back_populates="owner") diff --git a/{{cookiecutter.project_slug}}/backend/app/app/schemas/__init__.py b/{{cookiecutter.project_slug}}/backend/app/app/schemas/__init__.py deleted file mode 100644 index 6b41593dbb..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/schemas/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .item import Item, ItemCreate, ItemInDB, ItemUpdate -from .msg import Msg -from .token import Token, TokenPayload -from .user import User, UserCreate, UserInDB, UserUpdate diff --git a/{{cookiecutter.project_slug}}/backend/app/app/schemas/item.py b/{{cookiecutter.project_slug}}/backend/app/app/schemas/item.py deleted file mode 100644 index ac992cf2b1..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/schemas/item.py +++ /dev/null @@ -1,39 +0,0 @@ -from typing import Optional - -from pydantic import BaseModel - - -# Shared properties -class ItemBase(BaseModel): - title: Optional[str] = None - description: Optional[str] = None - - -# Properties to receive on item creation -class ItemCreate(ItemBase): - title: str - - -# Properties to receive on item update -class ItemUpdate(ItemBase): - pass - - -# Properties shared by models stored in DB -class ItemInDBBase(ItemBase): - id: int - title: str - owner_id: int - - class Config: - orm_mode = True - - -# Properties to return to client -class Item(ItemInDBBase): - pass - - -# Properties properties stored in DB -class ItemInDB(ItemInDBBase): - pass diff --git a/{{cookiecutter.project_slug}}/backend/app/app/schemas/msg.py b/{{cookiecutter.project_slug}}/backend/app/app/schemas/msg.py deleted file mode 100644 index 945e0c627b..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/schemas/msg.py +++ /dev/null @@ -1,5 +0,0 @@ -from pydantic import BaseModel - - -class Msg(BaseModel): - msg: str diff --git a/{{cookiecutter.project_slug}}/backend/app/app/schemas/token.py b/{{cookiecutter.project_slug}}/backend/app/app/schemas/token.py deleted file mode 100644 index ea85b460da..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/schemas/token.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Optional - -from pydantic import BaseModel - - -class Token(BaseModel): - access_token: str - token_type: str - - -class TokenPayload(BaseModel): - sub: Optional[int] = None diff --git a/{{cookiecutter.project_slug}}/backend/app/app/schemas/user.py b/{{cookiecutter.project_slug}}/backend/app/app/schemas/user.py deleted file mode 100644 index 7f5c85ac68..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/schemas/user.py +++ /dev/null @@ -1,39 +0,0 @@ -from typing import Optional - -from pydantic import BaseModel, EmailStr - - -# Shared properties -class UserBase(BaseModel): - email: Optional[EmailStr] = None - is_active: Optional[bool] = True - is_superuser: bool = False - full_name: Optional[str] = None - - -# Properties to receive via API on creation -class UserCreate(UserBase): - email: EmailStr - password: str - - -# Properties to receive via API on update -class UserUpdate(UserBase): - password: Optional[str] = None - - -class UserInDBBase(UserBase): - id: Optional[int] = None - - class Config: - orm_mode = True - - -# Additional properties to return via API -class User(UserInDBBase): - pass - - -# Additional properties stored in DB -class UserInDB(UserInDBBase): - hashed_password: str diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/.gitignore b/{{cookiecutter.project_slug}}/backend/app/app/tests/.gitignore deleted file mode 100755 index 16d3c4dbbf..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.cache diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_celery.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_celery.py deleted file mode 100644 index 7b10a331f2..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_celery.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Dict - -from fastapi.testclient import TestClient - -from app.core.config import settings - - -def test_celery_worker_test( - client: TestClient, superuser_token_headers: Dict[str, str] -) -> None: - data = {"msg": "test"} - r = client.post( - f"{settings.API_V1_STR}/utils/test-celery/", - json=data, - headers=superuser_token_headers, - ) - response = r.json() - assert response["msg"] == "Word received" diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_items.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_items.py deleted file mode 100644 index 6d799b62d4..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_items.py +++ /dev/null @@ -1,35 +0,0 @@ -from fastapi.testclient import TestClient -from sqlalchemy.orm 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, db: Session -) -> 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, 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"] == item.id - assert content["owner_id"] == item.owner_id diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_login.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_login.py deleted file mode 100644 index fd2c65a622..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_login.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Dict - -from fastapi.testclient import TestClient - -from app.core.config import settings - - -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_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 diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_users.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_users.py deleted file mode 100644 index ba22bfe969..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_users.py +++ /dev/null @@ -1,115 +0,0 @@ -from typing import Dict - -from fastapi.testclient import TestClient -from sqlalchemy.orm import Session - -from app import crud -from app.core.config import settings -from app.schemas.user import 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, db: Session -) -> None: - 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.user.get_by_email(db, email=username) - assert user - assert user.email == created_user["email"] - - -def test_get_existing_user( - client: TestClient, superuser_token_headers: dict, db: Session -) -> None: - username = random_email() - password = random_lower_string() - user_in = UserCreate(email=username, password=password) - user = crud.user.create(db, obj_in=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.user.get_by_email(db, email=username) - assert existing_user - assert existing_user.email == api_user["email"] - - -def test_create_user_existing_username( - client: TestClient, superuser_token_headers: dict, db: Session -) -> None: - username = random_email() - # username = email - password = random_lower_string() - user_in = UserCreate(email=username, password=password) - crud.user.create(db, obj_in=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 == 400 - - -def test_retrieve_users( - client: TestClient, superuser_token_headers: dict, db: Session -) -> None: - username = random_email() - password = random_lower_string() - user_in = UserCreate(email=username, password=password) - crud.user.create(db, obj_in=user_in) - - username2 = random_email() - password2 = random_lower_string() - user_in2 = UserCreate(email=username2, password=password2) - crud.user.create(db, obj_in=user_in2) - - r = client.get(f"{settings.API_V1_STR}/users/", headers=superuser_token_headers) - all_users = r.json() - - assert len(all_users) > 1 - for item in all_users: - assert "email" in item diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py deleted file mode 100644 index 0304cb84dc..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Dict, Generator - -import pytest -from fastapi.testclient import TestClient -from sqlalchemy.orm import Session - -from app.core.config import settings -from app.db.session import SessionLocal -from app.main import app -from app.tests.utils.user import authentication_token_from_email -from app.tests.utils.utils import get_superuser_token_headers - - -@pytest.fixture(scope="session") -def db() -> Generator: - yield SessionLocal() - - -@pytest.fixture(scope="module") -def client() -> Generator: - 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/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_item.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_item.py deleted file mode 100644 index e529144ef6..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_item.py +++ /dev/null @@ -1,61 +0,0 @@ -from sqlalchemy.orm import Session - -from app import crud -from app.schemas.item import ItemCreate, ItemUpdate -from app.tests.utils.user import create_random_user -from app.tests.utils.utils import random_lower_string - - -def test_create_item(db: Session) -> None: - title = random_lower_string() - description = random_lower_string() - item_in = ItemCreate(title=title, description=description) - user = create_random_user(db) - item = crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=user.id) - assert item.title == title - assert item.description == description - assert item.owner_id == user.id - - -def test_get_item(db: Session) -> None: - title = random_lower_string() - description = random_lower_string() - item_in = ItemCreate(title=title, description=description) - user = create_random_user(db) - item = crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=user.id) - stored_item = crud.item.get(db=db, id=item.id) - assert stored_item - assert item.id == stored_item.id - assert item.title == stored_item.title - assert item.description == stored_item.description - assert item.owner_id == stored_item.owner_id - - -def test_update_item(db: Session) -> None: - title = random_lower_string() - description = random_lower_string() - item_in = ItemCreate(title=title, description=description) - user = create_random_user(db) - item = crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=user.id) - description2 = random_lower_string() - item_update = ItemUpdate(description=description2) - item2 = crud.item.update(db=db, db_obj=item, obj_in=item_update) - assert item.id == item2.id - assert item.title == item2.title - assert item2.description == description2 - assert item.owner_id == item2.owner_id - - -def test_delete_item(db: Session) -> None: - title = random_lower_string() - description = random_lower_string() - item_in = ItemCreate(title=title, description=description) - user = create_random_user(db) - item = crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=user.id) - item2 = crud.item.remove(db=db, id=item.id) - item3 = crud.item.get(db=db, id=item.id) - assert item3 is None - assert item2.id == item.id - assert item2.title == title - assert item2.description == description - assert item2.owner_id == user.id diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/__init__.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py b/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py deleted file mode 100644 index e28f967078..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Optional - -from sqlalchemy.orm import Session - -from app import crud, models -from app.schemas.item import 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, *, owner_id: Optional[int] = None) -> models.Item: - if owner_id is None: - user = create_random_user(db) - owner_id = user.id - title = random_lower_string() - description = random_lower_string() - item_in = ItemCreate(title=title, description=description, id=id) - return crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=owner_id) diff --git a/{{cookiecutter.project_slug}}/backend/app/app/utils.py b/{{cookiecutter.project_slug}}/backend/app/app/utils.py deleted file mode 100644 index b1aba6bc00..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/utils.py +++ /dev/null @@ -1,106 +0,0 @@ -import logging -from datetime import datetime, timedelta -from pathlib import Path -from typing import Any, Dict, Optional - -import emails -from emails.template import JinjaTemplate -from jose import jwt - -from app.core.config import settings - - -def send_email( - email_to: str, - subject_template: str = "", - html_template: str = "", - environment: Dict[str, Any] = {}, -) -> None: - assert settings.EMAILS_ENABLED, "no provided configuration for email variables" - message = emails.Message( - subject=JinjaTemplate(subject_template), - html=JinjaTemplate(html_template), - 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 - 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, render=environment, smtp=smtp_options) - logging.info(f"send email result: {response}") - - -def send_test_email(email_to: str) -> None: - project_name = settings.PROJECT_NAME - subject = f"{project_name} - Test email" - with open(Path(settings.EMAIL_TEMPLATES_DIR) / "test_email.html") as f: - template_str = f.read() - send_email( - email_to=email_to, - subject_template=subject, - html_template=template_str, - environment={"project_name": settings.PROJECT_NAME, "email": email_to}, - ) - - -def send_reset_password_email(email_to: str, email: str, token: str) -> None: - project_name = settings.PROJECT_NAME - subject = f"{project_name} - Password recovery for user {email}" - with open(Path(settings.EMAIL_TEMPLATES_DIR) / "reset_password.html") as f: - template_str = f.read() - server_host = settings.SERVER_HOST - link = f"{server_host}/reset-password?token={token}" - send_email( - email_to=email_to, - subject_template=subject, - html_template=template_str, - environment={ - "project_name": settings.PROJECT_NAME, - "username": email, - "email": email_to, - "valid_hours": settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS, - "link": link, - }, - ) - - -def send_new_account_email(email_to: str, username: str, password: str) -> None: - project_name = settings.PROJECT_NAME - subject = f"{project_name} - New account for user {username}" - with open(Path(settings.EMAIL_TEMPLATES_DIR) / "new_account.html") as f: - template_str = f.read() - link = settings.SERVER_HOST - send_email( - email_to=email_to, - subject_template=subject, - html_template=template_str, - environment={ - "project_name": settings.PROJECT_NAME, - "username": username, - "password": password, - "email": email_to, - "link": link, - }, - ) - - -def generate_password_reset_token(email: str) -> str: - delta = timedelta(hours=settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS) - now = datetime.utcnow() - 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) -> Optional[str]: - try: - decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) - return decoded_token["email"] - except jwt.JWTError: - return None diff --git a/{{cookiecutter.project_slug}}/backend/app/app/worker.py b/{{cookiecutter.project_slug}}/backend/app/app/worker.py deleted file mode 100644 index 5fea53c961..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/app/worker.py +++ /dev/null @@ -1,11 +0,0 @@ -from raven import Client - -from app.core.celery_app import celery_app -from app.core.config import settings - -client_sentry = Client(settings.SENTRY_DSN) - - -@celery_app.task(acks_late=True) -def test_celery(word: str) -> str: - return f"test task return {word}" diff --git a/{{cookiecutter.project_slug}}/backend/app/mypy.ini b/{{cookiecutter.project_slug}}/backend/app/mypy.ini deleted file mode 100644 index 9813db029b..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/mypy.ini +++ /dev/null @@ -1,4 +0,0 @@ -[mypy] -plugins = pydantic.mypy, sqlmypy -ignore_missing_imports = True -disallow_untyped_defs = True diff --git a/{{cookiecutter.project_slug}}/backend/app/pyproject.toml b/{{cookiecutter.project_slug}}/backend/app/pyproject.toml deleted file mode 100644 index ea4e04e198..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/pyproject.toml +++ /dev/null @@ -1,46 +0,0 @@ -[tool.poetry] -name = "app" -version = "0.1.0" -description = "" -authors = ["Admin "] - -[tool.poetry.dependencies] -python = "^3.7" -uvicorn = "^0.11.3" -fastapi = "^0.54.1" -python-multipart = "^0.0.5" -email-validator = "^1.0.5" -requests = "^2.23.0" -celery = "^4.4.2" -passlib = {extras = ["bcrypt"], version = "^1.7.2"} -tenacity = "^6.1.0" -pydantic = "^1.4" -emails = "^0.5.15" -raven = "^6.10.0" -gunicorn = "^20.0.4" -jinja2 = "^2.11.2" -psycopg2-binary = "^2.8.5" -alembic = "^1.4.2" -sqlalchemy = "^1.3.16" -pytest = "^5.4.1" -python-jose = {extras = ["cryptography"], version = "^3.1.0"} - -[tool.poetry.dev-dependencies] -mypy = "^0.770" -black = "^19.10b0" -isort = "^4.3.21" -autoflake = "^1.3.1" -flake8 = "^3.7.9" -pytest = "^5.4.1" -sqlalchemy-stubs = "^0.3" -pytest-cov = "^2.8.1" - -[tool.isort] -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -line_length = 88 -[build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" - diff --git a/{{cookiecutter.project_slug}}/backend/app/scripts/format-imports.sh b/{{cookiecutter.project_slug}}/backend/app/scripts/format-imports.sh deleted file mode 100755 index 68f70eea75..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/scripts/format-imports.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -e -set -x - -# Sort imports one per line, so autoflake can remove unused imports -isort --recursive --force-single-line-imports --apply app -sh ./scripts/format.sh diff --git a/{{cookiecutter.project_slug}}/backend/app/scripts/format.sh b/{{cookiecutter.project_slug}}/backend/app/scripts/format.sh deleted file mode 100755 index 71d9779e41..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/scripts/format.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -e -set -x - -autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place app --exclude=__init__.py -black app -isort --recursive --apply app diff --git a/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh b/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh deleted file mode 100644 index 9dc9ed427c..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -x - -mypy app -black app --check -isort --recursive --check-only app -flake8 diff --git a/{{cookiecutter.project_slug}}/backend/app/scripts/test-cov-html.sh b/{{cookiecutter.project_slug}}/backend/app/scripts/test-cov-html.sh deleted file mode 100755 index 4a1fd3b75a..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/scripts/test-cov-html.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -x - -bash scripts/test.sh --cov-report=html "${@}" diff --git a/{{cookiecutter.project_slug}}/backend/app/scripts/test.sh b/{{cookiecutter.project_slug}}/backend/app/scripts/test.sh deleted file mode 100755 index fba8e95576..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/scripts/test.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -x - -pytest --cov=app --cov-report=term-missing app/tests "${@}" diff --git a/{{cookiecutter.project_slug}}/backend/app/tests-start.sh b/{{cookiecutter.project_slug}}/backend/app/tests-start.sh deleted file mode 100644 index 099c2b3973..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/tests-start.sh +++ /dev/null @@ -1,6 +0,0 @@ -#! /usr/bin/env bash -set -e - -python /app/app/tests_pre_start.py - -bash ./scripts/test.sh "$@" diff --git a/{{cookiecutter.project_slug}}/backend/app/worker-start.sh b/{{cookiecutter.project_slug}}/backend/app/worker-start.sh deleted file mode 100644 index 172f08e6f8..0000000000 --- a/{{cookiecutter.project_slug}}/backend/app/worker-start.sh +++ /dev/null @@ -1,6 +0,0 @@ -#! /usr/bin/env bash -set -e - -python /app/app/celeryworker_pre_start.py - -celery worker -A app.worker -l info -Q main-queue -c 1 diff --git a/{{cookiecutter.project_slug}}/backend/backend.dockerfile b/{{cookiecutter.project_slug}}/backend/backend.dockerfile deleted file mode 100644 index 8c39c502af..0000000000 --- a/{{cookiecutter.project_slug}}/backend/backend.dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 - -WORKDIR /app/ - -# Install Poetry -RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \ - cd /usr/local/bin && \ - ln -s /opt/poetry/bin/poetry && \ - poetry config virtualenvs.create false - -# Copy poetry.lock* in case it doesn't exist in the repo -COPY ./app/pyproject.toml ./app/poetry.lock* /app/ - -# Allow installing dev dependencies to run tests -ARG INSTALL_DEV=false -RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --no-dev ; fi" - -# For development, Jupyter remote kernel, Hydrogen -# Using inside the container: -# jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888 -ARG INSTALL_JUPYTER=false -RUN bash -c "if [ $INSTALL_JUPYTER == 'true' ] ; then pip install jupyterlab ; fi" - -COPY ./app /app -ENV PYTHONPATH=/app diff --git a/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile b/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile deleted file mode 100644 index 4695a7b7ec..0000000000 --- a/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -FROM python:3.7 - -WORKDIR /app/ - -# Install Poetry -RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \ - cd /usr/local/bin && \ - ln -s /opt/poetry/bin/poetry && \ - poetry config virtualenvs.create false - -# Copy poetry.lock* in case it doesn't exist in the repo -COPY ./app/pyproject.toml ./app/poetry.lock* /app/ - -# Allow installing dev dependencies to run tests -ARG INSTALL_DEV=false -RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --no-dev ; fi" - -# For development, Jupyter remote kernel, Hydrogen -# Using inside the container: -# jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888 -ARG INSTALL_JUPYTER=false -RUN bash -c "if [ $INSTALL_JUPYTER == 'true' ] ; then pip install jupyterlab ; fi" - -ENV C_FORCE_ROOT=1 - -COPY ./app /app -WORKDIR /app - -ENV PYTHONPATH=/app - -COPY ./app/worker-start.sh /worker-start.sh - -RUN chmod +x /worker-start.sh - -CMD ["bash", "/worker-start.sh"] diff --git a/{{cookiecutter.project_slug}}/cookiecutter-config-file.yml b/{{cookiecutter.project_slug}}/cookiecutter-config-file.yml deleted file mode 100644 index b0e7dd2f77..0000000000 --- a/{{cookiecutter.project_slug}}/cookiecutter-config-file.yml +++ /dev/null @@ -1,30 +0,0 @@ -default_context: - project_name: '{{ cookiecutter.project_name }}' - project_slug: '{{ cookiecutter.project_slug }}' - domain_main: '{{ cookiecutter.domain_main }}' - domain_staging: '{{ cookiecutter.domain_staging }}' - docker_swarm_stack_name_main: '{{ cookiecutter.docker_swarm_stack_name_main }}' - docker_swarm_stack_name_staging: '{{ cookiecutter.docker_swarm_stack_name_staging }}' - secret_key: '{{ cookiecutter.secret_key }}' - first_superuser: '{{ cookiecutter.first_superuser }}' - first_superuser_password: '{{ cookiecutter.first_superuser_password }}' - backend_cors_origins: '{{ cookiecutter.backend_cors_origins }}' - smtp_port: '{{ cookiecutter.smtp_port }}' - smtp_host: '{{ cookiecutter.smtp_host }}' - smtp_user: '{{ cookiecutter.smtp_user }}' - smtp_password: '{{ cookiecutter.smtp_password }}' - smtp_emails_from_email: '{{ cookiecutter.smtp_emails_from_email }}' - postgres_password: '{{ cookiecutter.postgres_password }}' - pgadmin_default_user: '{{ cookiecutter.pgadmin_default_user }}' - pgadmin_default_user_password: '{{ cookiecutter.pgadmin_default_user_password }}' - traefik_constraint_tag: '{{ cookiecutter.traefik_constraint_tag }}' - traefik_constraint_tag_staging: '{{ cookiecutter.traefik_constraint_tag_staging }}' - traefik_public_constraint_tag: '{{ cookiecutter.traefik_public_constraint_tag }}' - flower_auth: '{{ cookiecutter.flower_auth }}' - sentry_dsn: '{{ cookiecutter.sentry_dsn }}' - docker_image_prefix: '{{ cookiecutter.docker_image_prefix }}' - docker_image_backend: '{{ cookiecutter.docker_image_backend }}' - docker_image_celeryworker: '{{ cookiecutter.docker_image_celeryworker }}' - docker_image_frontend: '{{ cookiecutter.docker_image_frontend }}' - _copy_without_render: [frontend/src/**/*.html, frontend/src/**/*.vue, frontend/node_modules/*, backend/app/app/email-templates/**] - _template: ./ diff --git a/{{cookiecutter.project_slug}}/docker-compose.override.yml b/{{cookiecutter.project_slug}}/docker-compose.override.yml deleted file mode 100644 index cb9b60950f..0000000000 --- a/{{cookiecutter.project_slug}}/docker-compose.override.yml +++ /dev/null @@ -1,89 +0,0 @@ -version: "3.3" -services: - - proxy: - ports: - - "80:80" - - "8090:8080" - command: - # Enable Docker in Traefik, so that it reads labels from Docker services - - --providers.docker - # Add a constraint to only use services with the label for this stack - # from the env var TRAEFIK_TAG - - --providers.docker.constraints=Label(`traefik.constraint-label-stack`, `${TRAEFIK_TAG?Variable not set}`) - # Do not expose all Docker services, only the ones explicitly exposed - - --providers.docker.exposedbydefault=false - # Disable Docker Swarm mode for local development - # - --providers.docker.swarmmode - # Enable the access log, with HTTP requests - - --accesslog - # Enable the Traefik log, for configurations and errors - - --log - # Enable the Dashboard and API - - --api - # Enable the Dashboard and API in insecure mode for local development - - --api.insecure=true - labels: - - traefik.enable=true - - traefik.http.routers.${STACK_NAME?Variable not set}-traefik-public-http.rule=Host(`${DOMAIN?Variable not set}`) - - traefik.http.services.${STACK_NAME?Variable not set}-traefik-public.loadbalancer.server.port=80 - - pgadmin: - ports: - - "5050:5050" - - flower: - ports: - - "5555:5555" - - backend: - ports: - - "8888:8888" - volumes: - - ./backend/app:/app - environment: - - JUPYTER=jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888 - - SERVER_HOST=http://${DOMAIN?Variable not set} - build: - context: ./backend - dockerfile: backend.dockerfile - args: - INSTALL_DEV: ${INSTALL_DEV-true} - INSTALL_JUPYTER: ${INSTALL_JUPYTER-true} - # command: bash -c "while true; do sleep 1; done" # Infinite loop to keep container live doing nothing - command: /start-reload.sh - labels: - - traefik.enable=true - - traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set} - - traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.rule=PathPrefix(`/api`) || PathPrefix(`/docs`) || PathPrefix(`/redoc`) - - traefik.http.services.${STACK_NAME?Variable not set}-backend.loadbalancer.server.port=80 - - celeryworker: - volumes: - - ./backend/app:/app - environment: - - RUN=celery worker -A app.worker -l info -Q main-queue -c 1 - - JUPYTER=jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888 - - SERVER_HOST=http://${DOMAIN?Variable not set} - build: - context: ./backend - dockerfile: celeryworker.dockerfile - args: - INSTALL_DEV: ${INSTALL_DEV-true} - INSTALL_JUPYTER: ${INSTALL_JUPYTER-true} - - frontend: - build: - context: ./frontend - args: - FRONTEND_ENV: dev - labels: - - traefik.enable=true - - traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set} - - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=PathPrefix(`/`) - - traefik.http.services.${STACK_NAME?Variable not set}-frontend.loadbalancer.server.port=80 - -networks: - traefik-public: - # For local dev, don't expect an external Traefik network - external: false diff --git a/{{cookiecutter.project_slug}}/docker-compose.yml b/{{cookiecutter.project_slug}}/docker-compose.yml deleted file mode 100644 index fcc48f529d..0000000000 --- a/{{cookiecutter.project_slug}}/docker-compose.yml +++ /dev/null @@ -1,200 +0,0 @@ -version: "3.3" -services: - - proxy: - image: traefik:v2.2 - networks: - - ${TRAEFIK_PUBLIC_NETWORK?Variable not set} - - default - volumes: - - /var/run/docker.sock:/var/run/docker.sock - command: - # Enable Docker in Traefik, so that it reads labels from Docker services - - --providers.docker - # Add a constraint to only use services with the label for this stack - # from the env var TRAEFIK_TAG - - --providers.docker.constraints=Label(`traefik.constraint-label-stack`, `${TRAEFIK_TAG?Variable not set}`) - # Do not expose all Docker services, only the ones explicitly exposed - - --providers.docker.exposedbydefault=false - # Enable Docker Swarm mode - - --providers.docker.swarmmode - # Enable the access log, with HTTP requests - - --accesslog - # Enable the Traefik log, for configurations and errors - - --log - # Enable the Dashboard and API - - --api - deploy: - placement: - constraints: - - node.role == manager - labels: - # Enable Traefik for this service, to make it available in the public network - - traefik.enable=true - # Use the traefik-public network (declared below) - - traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK?Variable not set} - # Use the custom label "traefik.constraint-label=traefik-public" - # This public Traefik will only use services with this label - - traefik.constraint-label=${TRAEFIK_PUBLIC_TAG?Variable not set} - # traefik-http set up only to use the middleware to redirect to https - - traefik.http.middlewares.${STACK_NAME?Variable not set}-https-redirect.redirectscheme.scheme=https - - traefik.http.middlewares.${STACK_NAME?Variable not set}-https-redirect.redirectscheme.permanent=true - # Handle host with and without "www" to redirect to only one of them - # Uses environment variable DOMAIN - # To disable www redirection remove the Host() you want to discard, here and - # below for HTTPS - - traefik.http.routers.${STACK_NAME?Variable not set}-proxy-http.rule=Host(`${DOMAIN?Variable not set}`) || Host(`www.${DOMAIN?Variable not set}`) - - traefik.http.routers.${STACK_NAME?Variable not set}-proxy-http.entrypoints=http - # traefik-https the actual router using HTTPS - - traefik.http.routers.${STACK_NAME?Variable not set}-proxy-https.rule=Host(`${DOMAIN?Variable not set}`) || Host(`www.${DOMAIN?Variable not set}`) - - traefik.http.routers.${STACK_NAME?Variable not set}-proxy-https.entrypoints=https - - traefik.http.routers.${STACK_NAME?Variable not set}-proxy-https.tls=true - # Use the "le" (Let's Encrypt) resolver created below - - traefik.http.routers.${STACK_NAME?Variable not set}-proxy-https.tls.certresolver=le - # Define the port inside of the Docker service to use - - traefik.http.services.${STACK_NAME?Variable not set}-proxy.loadbalancer.server.port=80 - # Handle domain with and without "www" to redirect to only one - # To disable www redirection remove the next line - - traefik.http.middlewares.${STACK_NAME?Variable not set}-www-redirect.redirectregex.regex=^https?://(www.)?(${DOMAIN?Variable not set})/(.*) - # Redirect a domain with www to non-www - # To disable it remove the next line - - traefik.http.middlewares.${STACK_NAME?Variable not set}-www-redirect.redirectregex.replacement=https://${DOMAIN?Variable not set}/$${3} - # Redirect a domain without www to www - # To enable it remove the previous line and uncomment the next - # - traefik.http.middlewares.${STACK_NAME}-www-redirect.redirectregex.replacement=https://www.${DOMAIN}/$${3} - # Middleware to redirect www, to disable it remove the next line - - traefik.http.routers.${STACK_NAME?Variable not set}-proxy-https.middlewares=${STACK_NAME?Variable not set}-www-redirect - # Middleware to redirect www, and redirect HTTP to HTTPS - # to disable www redirection remove the section: ${STACK_NAME?Variable not set}-www-redirect, - - traefik.http.routers.${STACK_NAME?Variable not set}-proxy-http.middlewares=${STACK_NAME?Variable not set}-www-redirect,${STACK_NAME?Variable not set}-https-redirect - - db: - image: postgres:12 - volumes: - - app-db-data:/var/lib/postgresql/data/pgdata - env_file: - - .env - environment: - - PGDATA=/var/lib/postgresql/data/pgdata - deploy: - placement: - constraints: - - node.labels.${STACK_NAME?Variable not set}.app-db-data == true - - pgadmin: - image: dpage/pgadmin4 - networks: - - ${TRAEFIK_PUBLIC_NETWORK?Variable not set} - - default - depends_on: - - db - env_file: - - .env - deploy: - labels: - - traefik.enable=true - - traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK?Variable not set} - - traefik.constraint-label=${TRAEFIK_PUBLIC_TAG?Variable not set} - - traefik.http.routers.${STACK_NAME?Variable not set}-pgadmin-http.rule=Host(`pgadmin.${DOMAIN?Variable not set}`) - - traefik.http.routers.${STACK_NAME?Variable not set}-pgadmin-http.entrypoints=http - - traefik.http.routers.${STACK_NAME?Variable not set}-pgadmin-http.middlewares=${STACK_NAME?Variable not set}-https-redirect - - traefik.http.routers.${STACK_NAME?Variable not set}-pgadmin-https.rule=Host(`pgadmin.${DOMAIN?Variable not set}`) - - traefik.http.routers.${STACK_NAME?Variable not set}-pgadmin-https.entrypoints=https - - traefik.http.routers.${STACK_NAME?Variable not set}-pgadmin-https.tls=true - - traefik.http.routers.${STACK_NAME?Variable not set}-pgadmin-https.tls.certresolver=le - - traefik.http.services.${STACK_NAME?Variable not set}-pgadmin.loadbalancer.server.port=5050 - - queue: - image: rabbitmq:3 - # Using the below image instead is required to enable the "Broker" tab in the flower UI: - # image: rabbitmq:3-management - # - # You also have to change the flower command - - flower: - image: mher/flower - networks: - - ${TRAEFIK_PUBLIC_NETWORK?Variable not set} - - default - env_file: - - .env - command: - - "--broker=amqp://guest@queue:5672//" - # For the "Broker" tab to work in the flower UI, uncomment the following command argument, - # and change the queue service's image as well - # - "--broker_api=http://guest:guest@queue:15672/api//" - deploy: - labels: - - traefik.enable=true - - traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK?Variable not set} - - traefik.constraint-label=${TRAEFIK_PUBLIC_TAG?Variable not set} - - traefik.http.routers.${STACK_NAME?Variable not set}-flower-http.rule=Host(`flower.${DOMAIN?Variable not set}`) - - traefik.http.routers.${STACK_NAME?Variable not set}-flower-http.entrypoints=http - - traefik.http.routers.${STACK_NAME?Variable not set}-flower-http.middlewares=${STACK_NAME?Variable not set}-https-redirect - - traefik.http.routers.${STACK_NAME?Variable not set}-flower-https.rule=Host(`flower.${DOMAIN?Variable not set}`) - - traefik.http.routers.${STACK_NAME?Variable not set}-flower-https.entrypoints=https - - traefik.http.routers.${STACK_NAME?Variable not set}-flower-https.tls=true - - traefik.http.routers.${STACK_NAME?Variable not set}-flower-https.tls.certresolver=le - - traefik.http.services.${STACK_NAME?Variable not set}-flower.loadbalancer.server.port=5555 - - backend: - image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}' - depends_on: - - db - env_file: - - .env - environment: - - SERVER_NAME=${DOMAIN?Variable not set} - - SERVER_HOST=https://${DOMAIN?Variable not set} - # Allow explicit env var override for tests - - SMTP_HOST=${SMTP_HOST} - build: - context: ./backend - dockerfile: backend.dockerfile - args: - INSTALL_DEV: ${INSTALL_DEV-false} - deploy: - labels: - - traefik.enable=true - - traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set} - - traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.rule=PathPrefix(`/api`) || PathPrefix(`/docs`) || PathPrefix(`/redoc`) - - traefik.http.services.${STACK_NAME?Variable not set}-backend.loadbalancer.server.port=80 - - celeryworker: - image: '${DOCKER_IMAGE_CELERYWORKER?Variable not set}:${TAG-latest}' - depends_on: - - db - - queue - env_file: - - .env - environment: - - SERVER_NAME=${DOMAIN?Variable not set} - - SERVER_HOST=https://${DOMAIN?Variable not set} - # Allow explicit env var override for tests - - SMTP_HOST=${SMTP_HOST?Variable not set} - build: - context: ./backend - dockerfile: celeryworker.dockerfile - args: - INSTALL_DEV: ${INSTALL_DEV-false} - - frontend: - image: '${DOCKER_IMAGE_FRONTEND?Variable not set}:${TAG-latest}' - build: - context: ./frontend - args: - FRONTEND_ENV: ${FRONTEND_ENV-production} - deploy: - labels: - - traefik.enable=true - - traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set} - - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=PathPrefix(`/`) - - traefik.http.services.${STACK_NAME?Variable not set}-frontend.loadbalancer.server.port=80 - -volumes: - app-db-data: - -networks: - traefik-public: - # Allow setting it to false for testing - external: ${TRAEFIK_PUBLIC_NETWORK_IS_EXTERNAL-true} diff --git a/{{cookiecutter.project_slug}}/frontend/.env b/{{cookiecutter.project_slug}}/frontend/.env deleted file mode 100644 index adaf3306e2..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/.env +++ /dev/null @@ -1,10 +0,0 @@ -VUE_APP_DOMAIN_DEV=localhost -# VUE_APP_DOMAIN_DEV=local.dockertoolbox.tiangolo.com -# VUE_APP_DOMAIN_DEV=localhost.tiangolo.com -# VUE_APP_DOMAIN_DEV=dev.{{cookiecutter.domain_main}} -VUE_APP_DOMAIN_STAG={{cookiecutter.domain_staging}} -VUE_APP_DOMAIN_PROD={{cookiecutter.domain_main}} -VUE_APP_NAME={{cookiecutter.project_name}} -VUE_APP_ENV=development -# VUE_APP_ENV=staging -# VUE_APP_ENV=production diff --git a/{{cookiecutter.project_slug}}/frontend/.gitignore b/{{cookiecutter.project_slug}}/frontend/.gitignore deleted file mode 100644 index 185e663192..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/.gitignore +++ /dev/null @@ -1,21 +0,0 @@ -.DS_Store -node_modules -/dist - -# local env files -.env.local -.env.*.local - -# Log files -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Editor directories and files -.idea -.vscode -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw* diff --git a/{{cookiecutter.project_slug}}/frontend/README.md b/{{cookiecutter.project_slug}}/frontend/README.md deleted file mode 100644 index 60e84fbb39..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# frontend - -## Project setup -``` -npm install -``` - -### Compiles and hot-reloads for development -``` -npm run serve -``` - -### Compiles and minifies for production -``` -npm run build -``` - -### Run your tests -``` -npm run test -``` - -### Lints and fixes files -``` -npm run lint -``` - -### Run your unit tests -``` -npm run test:unit -``` diff --git a/{{cookiecutter.project_slug}}/frontend/babel.config.js b/{{cookiecutter.project_slug}}/frontend/babel.config.js deleted file mode 100644 index 5902d7d136..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/babel.config.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - "presets": [ - [ - "@vue/cli-plugin-babel/preset", - { - "useBuiltIns": "entry" - } - ] - ] -} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/frontend/package.json b/{{cookiecutter.project_slug}}/frontend/package.json deleted file mode 100644 index a83c616980..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/package.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "name": "frontend", - "version": "0.1.0", - "private": true, - "scripts": { - "serve": "vue-cli-service serve", - "build": "vue-cli-service build", - "test:unit": "vue-cli-service test:unit", - "lint": "vue-cli-service lint" - }, - "dependencies": { - "@babel/polyfill": "^7.2.5", - "axios": "^0.18.0", - "core-js": "^3.4.3", - "register-service-worker": "^1.0.0", - "typesafe-vuex": "^3.1.1", - "vee-validate": "^2.1.7", - "vue": "^2.5.22", - "vue-class-component": "^6.0.0", - "vue-property-decorator": "^7.3.0", - "vue-router": "^3.0.2", - "vuetify": "^1.4.4", - "vuex": "^3.1.0" - }, - "devDependencies": { - "@types/jest": "^23.3.13", - "@vue/cli-plugin-babel": "^4.1.1", - "@vue/cli-plugin-pwa": "^4.1.1", - "@vue/cli-plugin-typescript": "^4.1.1", - "@vue/cli-plugin-unit-jest": "^4.1.1", - "@vue/cli-service": "^4.1.1", - "@vue/test-utils": "^1.0.0-beta.28", - "babel-core": "7.0.0-bridge.0", - "ts-jest": "^23.10.5", - "typescript": "^3.2.4", - "vue-cli-plugin-vuetify": "^2.0.2", - "vue-template-compiler": "^2.5.22" - }, - "postcss": { - "plugins": { - "autoprefixer": {} - } - }, - "browserslist": [ - "> 1%", - "last 2 versions", - "not ie <= 10" - ], - "jest": { - "moduleFileExtensions": [ - "js", - "jsx", - "json", - "vue", - "ts", - "tsx" - ], - "transform": { - "^.+\\.vue$": "vue-jest", - ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": "jest-transform-stub", - "^.+\\.tsx?$": "ts-jest" - }, - "moduleNameMapper": { - "^@/(.*)$": "/src/$1" - }, - "snapshotSerializers": [ - "jest-serializer-vue" - ], - "testMatch": [ - "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" - ], - "testURL": "http://localhost/" - } -} diff --git a/{{cookiecutter.project_slug}}/frontend/public/favicon.ico b/{{cookiecutter.project_slug}}/frontend/public/favicon.ico deleted file mode 100644 index c7b9a43c8c..0000000000 Binary files a/{{cookiecutter.project_slug}}/frontend/public/favicon.ico and /dev/null differ diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-192x192.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-192x192.png deleted file mode 100644 index b02aa64d97..0000000000 Binary files a/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-192x192.png and /dev/null differ diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-512x512.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-512x512.png deleted file mode 100644 index 06088b011e..0000000000 Binary files a/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-512x512.png and /dev/null differ diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-120x120.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-120x120.png deleted file mode 100644 index 1427cf6275..0000000000 Binary files a/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-120x120.png and /dev/null differ diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-152x152.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-152x152.png deleted file mode 100644 index f24d454a2e..0000000000 Binary files a/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-152x152.png and /dev/null differ diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-180x180.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-180x180.png deleted file mode 100644 index 404e192a95..0000000000 Binary files a/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-180x180.png and /dev/null differ diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-60x60.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-60x60.png deleted file mode 100644 index cf10a5602e..0000000000 Binary files a/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-60x60.png and /dev/null differ diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-76x76.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-76x76.png deleted file mode 100644 index c500769e3d..0000000000 Binary files a/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-76x76.png and /dev/null differ diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon.png deleted file mode 100644 index 03c0c5d5ec..0000000000 Binary files a/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon.png and /dev/null differ diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-16x16.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-16x16.png deleted file mode 100644 index 42af00963d..0000000000 Binary files a/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-16x16.png and /dev/null differ diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-32x32.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-32x32.png deleted file mode 100644 index 46ca04dee2..0000000000 Binary files a/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-32x32.png and /dev/null differ diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/msapplication-icon-144x144.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/msapplication-icon-144x144.png deleted file mode 100644 index 7808237a18..0000000000 Binary files a/{{cookiecutter.project_slug}}/frontend/public/img/icons/msapplication-icon-144x144.png and /dev/null differ diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/mstile-150x150.png b/{{cookiecutter.project_slug}}/frontend/public/img/icons/mstile-150x150.png deleted file mode 100644 index 3b37a43ae2..0000000000 Binary files a/{{cookiecutter.project_slug}}/frontend/public/img/icons/mstile-150x150.png and /dev/null differ diff --git a/{{cookiecutter.project_slug}}/frontend/public/img/icons/safari-pinned-tab.svg b/{{cookiecutter.project_slug}}/frontend/public/img/icons/safari-pinned-tab.svg deleted file mode 100644 index 732afd8eb0..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/public/img/icons/safari-pinned-tab.svg +++ /dev/null @@ -1,149 +0,0 @@ - - - - -Created by potrace 1.11, written by Peter Selinger 2001-2013 - - - - - diff --git a/{{cookiecutter.project_slug}}/frontend/public/index.html b/{{cookiecutter.project_slug}}/frontend/public/index.html deleted file mode 100644 index cad5aa7efa..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/public/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - <%= VUE_APP_NAME %> - - - - - - - -
- - - diff --git a/{{cookiecutter.project_slug}}/frontend/public/manifest.json b/{{cookiecutter.project_slug}}/frontend/public/manifest.json deleted file mode 100644 index 8ce10b9116..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/public/manifest.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "frontend", - "short_name": "frontend", - "icons": [ - { - "src": "/img/icons/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/img/icons/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "start_url": "/", - "display": "standalone", - "background_color": "#000000", - "theme_color": "#4DBA87" -} diff --git a/{{cookiecutter.project_slug}}/frontend/public/robots.txt b/{{cookiecutter.project_slug}}/frontend/public/robots.txt deleted file mode 100644 index eb0536286f..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/public/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Disallow: diff --git a/{{cookiecutter.project_slug}}/frontend/src/App.vue b/{{cookiecutter.project_slug}}/frontend/src/App.vue deleted file mode 100644 index 795a97c955..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/App.vue +++ /dev/null @@ -1,43 +0,0 @@ - - - diff --git a/{{cookiecutter.project_slug}}/frontend/src/api.ts b/{{cookiecutter.project_slug}}/frontend/src/api.ts deleted file mode 100644 index c24712b872..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/api.ts +++ /dev/null @@ -1,45 +0,0 @@ -import axios from 'axios'; -import { apiUrl } from '@/env'; -import { IUserProfile, IUserProfileUpdate, IUserProfileCreate } from './interfaces'; - -function authHeaders(token: string) { - return { - headers: { - Authorization: `Bearer ${token}`, - }, - }; -} - -export const api = { - async logInGetToken(username: string, password: string) { - const params = new URLSearchParams(); - params.append('username', username); - params.append('password', password); - - return axios.post(`${apiUrl}/api/v1/login/access-token`, params); - }, - async getMe(token: string) { - return axios.get(`${apiUrl}/api/v1/users/me`, authHeaders(token)); - }, - async updateMe(token: string, data: IUserProfileUpdate) { - return axios.put(`${apiUrl}/api/v1/users/me`, data, authHeaders(token)); - }, - async getUsers(token: string) { - return axios.get(`${apiUrl}/api/v1/users/`, authHeaders(token)); - }, - async updateUser(token: string, userId: number, data: IUserProfileUpdate) { - return axios.put(`${apiUrl}/api/v1/users/${userId}`, data, authHeaders(token)); - }, - async createUser(token: string, data: IUserProfileCreate) { - return axios.post(`${apiUrl}/api/v1/users/`, data, authHeaders(token)); - }, - async passwordRecovery(email: string) { - return axios.post(`${apiUrl}/api/v1/password-recovery/${email}`); - }, - async resetPassword(password: string, token: string) { - return axios.post(`${apiUrl}/api/v1/reset-password/`, { - new_password: password, - token, - }); - }, -}; diff --git a/{{cookiecutter.project_slug}}/frontend/src/assets/logo.png b/{{cookiecutter.project_slug}}/frontend/src/assets/logo.png deleted file mode 100644 index f3d2503fc2..0000000000 Binary files a/{{cookiecutter.project_slug}}/frontend/src/assets/logo.png and /dev/null differ diff --git a/{{cookiecutter.project_slug}}/frontend/src/component-hooks.ts b/{{cookiecutter.project_slug}}/frontend/src/component-hooks.ts deleted file mode 100644 index cefdc537bb..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/component-hooks.ts +++ /dev/null @@ -1,8 +0,0 @@ -import Component from 'vue-class-component'; - -// Register the router hooks with their names -Component.registerHooks([ - 'beforeRouteEnter', - 'beforeRouteLeave', - 'beforeRouteUpdate', // for vue-router 2.2+ -]); diff --git a/{{cookiecutter.project_slug}}/frontend/src/components/NotificationsManager.vue b/{{cookiecutter.project_slug}}/frontend/src/components/NotificationsManager.vue deleted file mode 100644 index 6fcffdb789..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/components/NotificationsManager.vue +++ /dev/null @@ -1,77 +0,0 @@ - - diff --git a/{{cookiecutter.project_slug}}/frontend/src/components/RouterComponent.vue b/{{cookiecutter.project_slug}}/frontend/src/components/RouterComponent.vue deleted file mode 100644 index ed986a6fda..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/components/RouterComponent.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/{{cookiecutter.project_slug}}/frontend/src/components/UploadButton.vue b/{{cookiecutter.project_slug}}/frontend/src/components/UploadButton.vue deleted file mode 100644 index 8902e949e9..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/components/UploadButton.vue +++ /dev/null @@ -1,34 +0,0 @@ - - - - - diff --git a/{{cookiecutter.project_slug}}/frontend/src/env.ts b/{{cookiecutter.project_slug}}/frontend/src/env.ts deleted file mode 100644 index b3387e69bc..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/env.ts +++ /dev/null @@ -1,14 +0,0 @@ -const env = process.env.VUE_APP_ENV; - -let envApiUrl = ''; - -if (env === 'production') { - envApiUrl = `https://${process.env.VUE_APP_DOMAIN_PROD}`; -} else if (env === 'staging') { - envApiUrl = `https://${process.env.VUE_APP_DOMAIN_STAG}`; -} else { - envApiUrl = `http://${process.env.VUE_APP_DOMAIN_DEV}`; -} - -export const apiUrl = envApiUrl; -export const appName = process.env.VUE_APP_NAME; diff --git a/{{cookiecutter.project_slug}}/frontend/src/interfaces/index.ts b/{{cookiecutter.project_slug}}/frontend/src/interfaces/index.ts deleted file mode 100644 index a1b93403cf..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/interfaces/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface IUserProfile { - email: string; - is_active: boolean; - is_superuser: boolean; - full_name: string; - id: number; -} - -export interface IUserProfileUpdate { - email?: string; - full_name?: string; - password?: string; - is_active?: boolean; - is_superuser?: boolean; -} - -export interface IUserProfileCreate { - email: string; - full_name?: string; - password?: string; - is_active?: boolean; - is_superuser?: boolean; -} diff --git a/{{cookiecutter.project_slug}}/frontend/src/main.ts b/{{cookiecutter.project_slug}}/frontend/src/main.ts deleted file mode 100644 index a844b1eab8..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/main.ts +++ /dev/null @@ -1,19 +0,0 @@ -import '@babel/polyfill'; -// Import Component hooks before component definitions -import './component-hooks'; -import Vue from 'vue'; -import './plugins/vuetify'; -import './plugins/vee-validate'; -import App from './App.vue'; -import router from './router'; -import store from '@/store'; -import './registerServiceWorker'; -import 'vuetify/dist/vuetify.min.css'; - -Vue.config.productionTip = false; - -new Vue({ - router, - store, - render: (h) => h(App), -}).$mount('#app'); diff --git a/{{cookiecutter.project_slug}}/frontend/src/plugins/vee-validate.ts b/{{cookiecutter.project_slug}}/frontend/src/plugins/vee-validate.ts deleted file mode 100644 index 9c4238f2f7..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/plugins/vee-validate.ts +++ /dev/null @@ -1,4 +0,0 @@ -import Vue from 'vue'; -import VeeValidate from 'vee-validate'; - -Vue.use(VeeValidate); diff --git a/{{cookiecutter.project_slug}}/frontend/src/plugins/vuetify.ts b/{{cookiecutter.project_slug}}/frontend/src/plugins/vuetify.ts deleted file mode 100644 index 8fdfce3a4a..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/plugins/vuetify.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Vue from 'vue'; -import Vuetify from 'vuetify'; - -Vue.use(Vuetify, { - iconfont: 'md', -}); diff --git a/{{cookiecutter.project_slug}}/frontend/src/registerServiceWorker.ts b/{{cookiecutter.project_slug}}/frontend/src/registerServiceWorker.ts deleted file mode 100644 index d3db583898..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/registerServiceWorker.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* tslint:disable:no-console */ - -import { register } from 'register-service-worker'; - -if (process.env.NODE_ENV === 'production') { - register(`${process.env.BASE_URL}service-worker.js`, { - ready() { - console.log( - 'App is being served from cache by a service worker.\n' + - 'For more details, visit https://goo.gl/AFskqB', - ); - }, - cached() { - console.log('Content has been cached for offline use.'); - }, - updated() { - console.log('New content is available; please refresh.'); - }, - offline() { - console.log('No internet connection found. App is running in offline mode.'); - }, - error(error) { - console.error('Error during service worker registration:', error); - }, - }); -} diff --git a/{{cookiecutter.project_slug}}/frontend/src/router.ts b/{{cookiecutter.project_slug}}/frontend/src/router.ts deleted file mode 100644 index b649c173ad..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/router.ts +++ /dev/null @@ -1,97 +0,0 @@ -import Vue from 'vue'; -import Router from 'vue-router'; - -import RouterComponent from './components/RouterComponent.vue'; - -Vue.use(Router); - -export default new Router({ - mode: 'history', - base: process.env.BASE_URL, - routes: [ - { - path: '/', - component: () => import(/* webpackChunkName: "start" */ './views/main/Start.vue'), - children: [ - { - path: 'login', - // route level code-splitting - // this generates a separate chunk (about.[hash].js) for this route - // which is lazy-loaded when the route is visited. - component: () => import(/* webpackChunkName: "login" */ './views/Login.vue'), - }, - { - path: 'recover-password', - component: () => import(/* webpackChunkName: "recover-password" */ './views/PasswordRecovery.vue'), - }, - { - path: 'reset-password', - component: () => import(/* webpackChunkName: "reset-password" */ './views/ResetPassword.vue'), - }, - { - path: 'main', - component: () => import(/* webpackChunkName: "main" */ './views/main/Main.vue'), - children: [ - { - path: 'dashboard', - component: () => import(/* webpackChunkName: "main-dashboard" */ './views/main/Dashboard.vue'), - }, - { - path: 'profile', - component: RouterComponent, - redirect: 'profile/view', - children: [ - { - path: 'view', - component: () => import( - /* webpackChunkName: "main-profile" */ './views/main/profile/UserProfile.vue'), - }, - { - path: 'edit', - component: () => import( - /* webpackChunkName: "main-profile-edit" */ './views/main/profile/UserProfileEdit.vue'), - }, - { - path: 'password', - component: () => import( - /* webpackChunkName: "main-profile-password" */ './views/main/profile/UserProfileEditPassword.vue'), - }, - ], - }, - { - path: 'admin', - component: () => import(/* webpackChunkName: "main-admin" */ './views/main/admin/Admin.vue'), - redirect: 'admin/users/all', - children: [ - { - path: 'users', - redirect: 'users/all', - }, - { - path: 'users/all', - component: () => import( - /* webpackChunkName: "main-admin-users" */ './views/main/admin/AdminUsers.vue'), - }, - { - path: 'users/edit/:id', - name: 'main-admin-users-edit', - component: () => import( - /* webpackChunkName: "main-admin-users-edit" */ './views/main/admin/EditUser.vue'), - }, - { - path: 'users/create', - name: 'main-admin-users-create', - component: () => import( - /* webpackChunkName: "main-admin-users-create" */ './views/main/admin/CreateUser.vue'), - }, - ], - }, - ], - }, - ], - }, - { - path: '/*', redirect: '/', - }, - ], -}); diff --git a/{{cookiecutter.project_slug}}/frontend/src/shims-tsx.d.ts b/{{cookiecutter.project_slug}}/frontend/src/shims-tsx.d.ts deleted file mode 100644 index 3b88b58292..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/shims-tsx.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Vue, { VNode } from 'vue'; - -declare global { - namespace JSX { - // tslint:disable no-empty-interface - interface Element extends VNode {} - // tslint:disable no-empty-interface - interface ElementClass extends Vue {} - interface IntrinsicElements { - [elem: string]: any; - } - } -} diff --git a/{{cookiecutter.project_slug}}/frontend/src/shims-vue.d.ts b/{{cookiecutter.project_slug}}/frontend/src/shims-vue.d.ts deleted file mode 100644 index 8f6f410263..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/shims-vue.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.vue' { - import Vue from 'vue'; - export default Vue; -} diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/admin/actions.ts b/{{cookiecutter.project_slug}}/frontend/src/store/admin/actions.ts deleted file mode 100644 index 125a08e6fd..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/admin/actions.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { api } from '@/api'; -import { ActionContext } from 'vuex'; -import { IUserProfileCreate, IUserProfileUpdate } from '@/interfaces'; -import { State } from '../state'; -import { AdminState } from './state'; -import { getStoreAccessors } from 'typesafe-vuex'; -import { commitSetUsers, commitSetUser } from './mutations'; -import { dispatchCheckApiError } from '../main/actions'; -import { commitAddNotification, commitRemoveNotification } from '../main/mutations'; - -type MainContext = ActionContext; - -export const actions = { - async actionGetUsers(context: MainContext) { - try { - const response = await api.getUsers(context.rootState.main.token); - if (response) { - commitSetUsers(context, response.data); - } - } catch (error) { - await dispatchCheckApiError(context, error); - } - }, - async actionUpdateUser(context: MainContext, payload: { id: number, user: IUserProfileUpdate }) { - try { - const loadingNotification = { content: 'saving', showProgress: true }; - commitAddNotification(context, loadingNotification); - const response = (await Promise.all([ - api.updateUser(context.rootState.main.token, payload.id, payload.user), - await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)), - ]))[0]; - commitSetUser(context, response.data); - commitRemoveNotification(context, loadingNotification); - commitAddNotification(context, { content: 'User successfully updated', color: 'success' }); - } catch (error) { - await dispatchCheckApiError(context, error); - } - }, - async actionCreateUser(context: MainContext, payload: IUserProfileCreate) { - try { - const loadingNotification = { content: 'saving', showProgress: true }; - commitAddNotification(context, loadingNotification); - const response = (await Promise.all([ - api.createUser(context.rootState.main.token, payload), - await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)), - ]))[0]; - commitSetUser(context, response.data); - commitRemoveNotification(context, loadingNotification); - commitAddNotification(context, { content: 'User successfully created', color: 'success' }); - } catch (error) { - await dispatchCheckApiError(context, error); - } - }, -}; - -const { dispatch } = getStoreAccessors(''); - -export const dispatchCreateUser = dispatch(actions.actionCreateUser); -export const dispatchGetUsers = dispatch(actions.actionGetUsers); -export const dispatchUpdateUser = dispatch(actions.actionUpdateUser); diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/admin/getters.ts b/{{cookiecutter.project_slug}}/frontend/src/store/admin/getters.ts deleted file mode 100644 index c5832ef449..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/admin/getters.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { AdminState } from './state'; -import { getStoreAccessors } from 'typesafe-vuex'; -import { State } from '../state'; - -export const getters = { - adminUsers: (state: AdminState) => state.users, - adminOneUser: (state: AdminState) => (userId: number) => { - const filteredUsers = state.users.filter((user) => user.id === userId); - if (filteredUsers.length > 0) { - return { ...filteredUsers[0] }; - } - }, -}; - -const { read } = getStoreAccessors(''); - -export const readAdminOneUser = read(getters.adminOneUser); -export const readAdminUsers = read(getters.adminUsers); diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/admin/index.ts b/{{cookiecutter.project_slug}}/frontend/src/store/admin/index.ts deleted file mode 100644 index dcaf6abbd1..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/admin/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { mutations } from './mutations'; -import { getters } from './getters'; -import { actions } from './actions'; -import { AdminState } from './state'; - -const defaultState: AdminState = { - users: [], -}; - -export const adminModule = { - state: defaultState, - mutations, - actions, - getters, -}; diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/admin/mutations.ts b/{{cookiecutter.project_slug}}/frontend/src/store/admin/mutations.ts deleted file mode 100644 index dea471d4e7..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/admin/mutations.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { IUserProfile } from '@/interfaces'; -import { AdminState } from './state'; -import { getStoreAccessors } from 'typesafe-vuex'; -import { State } from '../state'; - -export const mutations = { - setUsers(state: AdminState, payload: IUserProfile[]) { - state.users = payload; - }, - setUser(state: AdminState, payload: IUserProfile) { - const users = state.users.filter((user: IUserProfile) => user.id !== payload.id); - users.push(payload); - state.users = users; - }, -}; - -const { commit } = getStoreAccessors(''); - -export const commitSetUser = commit(mutations.setUser); -export const commitSetUsers = commit(mutations.setUsers); diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/admin/state.ts b/{{cookiecutter.project_slug}}/frontend/src/store/admin/state.ts deleted file mode 100644 index 8dfefe2f99..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/admin/state.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { IUserProfile } from '@/interfaces'; - -export interface AdminState { - users: IUserProfile[]; -} diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/index.ts b/{{cookiecutter.project_slug}}/frontend/src/store/index.ts deleted file mode 100644 index 1089971525..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Vue from 'vue'; -import Vuex, { StoreOptions } from 'vuex'; - -import { mainModule } from './main'; -import { State } from './state'; -import { adminModule } from './admin'; - -Vue.use(Vuex); - -const storeOptions: StoreOptions = { - modules: { - main: mainModule, - admin: adminModule, - }, -}; - -export const store = new Vuex.Store(storeOptions); - -export default store; diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/main/actions.ts b/{{cookiecutter.project_slug}}/frontend/src/store/main/actions.ts deleted file mode 100644 index d02c06d53c..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/main/actions.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { api } from '@/api'; -import router from '@/router'; -import { getLocalToken, removeLocalToken, saveLocalToken } from '@/utils'; -import { AxiosError } from 'axios'; -import { getStoreAccessors } from 'typesafe-vuex'; -import { ActionContext } from 'vuex'; -import { State } from '../state'; -import { - commitAddNotification, - commitRemoveNotification, - commitSetLoggedIn, - commitSetLogInError, - commitSetToken, - commitSetUserProfile, -} from './mutations'; -import { AppNotification, MainState } from './state'; - -type MainContext = ActionContext; - -export const actions = { - async actionLogIn(context: MainContext, payload: { username: string; password: string }) { - try { - const response = await api.logInGetToken(payload.username, payload.password); - const token = response.data.access_token; - if (token) { - saveLocalToken(token); - commitSetToken(context, token); - commitSetLoggedIn(context, true); - commitSetLogInError(context, false); - await dispatchGetUserProfile(context); - await dispatchRouteLoggedIn(context); - commitAddNotification(context, { content: 'Logged in', color: 'success' }); - } else { - await dispatchLogOut(context); - } - } catch (err) { - commitSetLogInError(context, true); - await dispatchLogOut(context); - } - }, - async actionGetUserProfile(context: MainContext) { - try { - const response = await api.getMe(context.state.token); - if (response.data) { - commitSetUserProfile(context, response.data); - } - } catch (error) { - await dispatchCheckApiError(context, error); - } - }, - async actionUpdateUserProfile(context: MainContext, payload) { - try { - const loadingNotification = { content: 'saving', showProgress: true }; - commitAddNotification(context, loadingNotification); - const response = (await Promise.all([ - api.updateMe(context.state.token, payload), - await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)), - ]))[0]; - commitSetUserProfile(context, response.data); - commitRemoveNotification(context, loadingNotification); - commitAddNotification(context, { content: 'Profile successfully updated', color: 'success' }); - } catch (error) { - await dispatchCheckApiError(context, error); - } - }, - async actionCheckLoggedIn(context: MainContext) { - if (!context.state.isLoggedIn) { - let token = context.state.token; - if (!token) { - const localToken = getLocalToken(); - if (localToken) { - commitSetToken(context, localToken); - token = localToken; - } - } - if (token) { - try { - const response = await api.getMe(token); - commitSetLoggedIn(context, true); - commitSetUserProfile(context, response.data); - } catch (error) { - await dispatchRemoveLogIn(context); - } - } else { - await dispatchRemoveLogIn(context); - } - } - }, - async actionRemoveLogIn(context: MainContext) { - removeLocalToken(); - commitSetToken(context, ''); - commitSetLoggedIn(context, false); - }, - async actionLogOut(context: MainContext) { - await dispatchRemoveLogIn(context); - await dispatchRouteLogOut(context); - }, - async actionUserLogOut(context: MainContext) { - await dispatchLogOut(context); - commitAddNotification(context, { content: 'Logged out', color: 'success' }); - }, - actionRouteLogOut(context: MainContext) { - if (router.currentRoute.path !== '/login') { - router.push('/login'); - } - }, - async actionCheckApiError(context: MainContext, payload: AxiosError) { - if (payload.response!.status === 401) { - await dispatchLogOut(context); - } - }, - actionRouteLoggedIn(context: MainContext) { - if (router.currentRoute.path === '/login' || router.currentRoute.path === '/') { - router.push('/main'); - } - }, - async removeNotification(context: MainContext, payload: { notification: AppNotification, timeout: number }) { - return new Promise((resolve, reject) => { - setTimeout(() => { - commitRemoveNotification(context, payload.notification); - resolve(true); - }, payload.timeout); - }); - }, - async passwordRecovery(context: MainContext, payload: { username: string }) { - const loadingNotification = { content: 'Sending password recovery email', showProgress: true }; - try { - commitAddNotification(context, loadingNotification); - const response = (await Promise.all([ - api.passwordRecovery(payload.username), - await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)), - ]))[0]; - commitRemoveNotification(context, loadingNotification); - commitAddNotification(context, { content: 'Password recovery email sent', color: 'success' }); - await dispatchLogOut(context); - } catch (error) { - commitRemoveNotification(context, loadingNotification); - commitAddNotification(context, { color: 'error', content: 'Incorrect username' }); - } - }, - async resetPassword(context: MainContext, payload: { password: string, token: string }) { - const loadingNotification = { content: 'Resetting password', showProgress: true }; - try { - commitAddNotification(context, loadingNotification); - const response = (await Promise.all([ - api.resetPassword(payload.password, payload.token), - await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)), - ]))[0]; - commitRemoveNotification(context, loadingNotification); - commitAddNotification(context, { content: 'Password successfully reset', color: 'success' }); - await dispatchLogOut(context); - } catch (error) { - commitRemoveNotification(context, loadingNotification); - commitAddNotification(context, { color: 'error', content: 'Error resetting password' }); - } - }, -}; - -const { dispatch } = getStoreAccessors(''); - -export const dispatchCheckApiError = dispatch(actions.actionCheckApiError); -export const dispatchCheckLoggedIn = dispatch(actions.actionCheckLoggedIn); -export const dispatchGetUserProfile = dispatch(actions.actionGetUserProfile); -export const dispatchLogIn = dispatch(actions.actionLogIn); -export const dispatchLogOut = dispatch(actions.actionLogOut); -export const dispatchUserLogOut = dispatch(actions.actionUserLogOut); -export const dispatchRemoveLogIn = dispatch(actions.actionRemoveLogIn); -export const dispatchRouteLoggedIn = dispatch(actions.actionRouteLoggedIn); -export const dispatchRouteLogOut = dispatch(actions.actionRouteLogOut); -export const dispatchUpdateUserProfile = dispatch(actions.actionUpdateUserProfile); -export const dispatchRemoveNotification = dispatch(actions.removeNotification); -export const dispatchPasswordRecovery = dispatch(actions.passwordRecovery); -export const dispatchResetPassword = dispatch(actions.resetPassword); diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/main/getters.ts b/{{cookiecutter.project_slug}}/frontend/src/store/main/getters.ts deleted file mode 100644 index 58f83978fa..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/main/getters.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { MainState } from './state'; -import { getStoreAccessors } from 'typesafe-vuex'; -import { State } from '../state'; - -export const getters = { - hasAdminAccess: (state: MainState) => { - return ( - state.userProfile && - state.userProfile.is_superuser && state.userProfile.is_active); - }, - loginError: (state: MainState) => state.logInError, - dashboardShowDrawer: (state: MainState) => state.dashboardShowDrawer, - dashboardMiniDrawer: (state: MainState) => state.dashboardMiniDrawer, - userProfile: (state: MainState) => state.userProfile, - token: (state: MainState) => state.token, - isLoggedIn: (state: MainState) => state.isLoggedIn, - firstNotification: (state: MainState) => state.notifications.length > 0 && state.notifications[0], -}; - -const {read} = getStoreAccessors(''); - -export const readDashboardMiniDrawer = read(getters.dashboardMiniDrawer); -export const readDashboardShowDrawer = read(getters.dashboardShowDrawer); -export const readHasAdminAccess = read(getters.hasAdminAccess); -export const readIsLoggedIn = read(getters.isLoggedIn); -export const readLoginError = read(getters.loginError); -export const readToken = read(getters.token); -export const readUserProfile = read(getters.userProfile); -export const readFirstNotification = read(getters.firstNotification); diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/main/index.ts b/{{cookiecutter.project_slug}}/frontend/src/store/main/index.ts deleted file mode 100644 index 56ba1a0c2f..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/main/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { mutations } from './mutations'; -import { getters } from './getters'; -import { actions } from './actions'; -import { MainState } from './state'; - -const defaultState: MainState = { - isLoggedIn: null, - token: '', - logInError: false, - userProfile: null, - dashboardMiniDrawer: false, - dashboardShowDrawer: true, - notifications: [], -}; - -export const mainModule = { - state: defaultState, - mutations, - actions, - getters, -}; diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/main/mutations.ts b/{{cookiecutter.project_slug}}/frontend/src/store/main/mutations.ts deleted file mode 100644 index 3e9c8ba2c0..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/main/mutations.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { IUserProfile } from '@/interfaces'; -import { MainState, AppNotification } from './state'; -import { getStoreAccessors } from 'typesafe-vuex'; -import { State } from '../state'; - - -export const mutations = { - setToken(state: MainState, payload: string) { - state.token = payload; - }, - setLoggedIn(state: MainState, payload: boolean) { - state.isLoggedIn = payload; - }, - setLogInError(state: MainState, payload: boolean) { - state.logInError = payload; - }, - setUserProfile(state: MainState, payload: IUserProfile) { - state.userProfile = payload; - }, - setDashboardMiniDrawer(state: MainState, payload: boolean) { - state.dashboardMiniDrawer = payload; - }, - setDashboardShowDrawer(state: MainState, payload: boolean) { - state.dashboardShowDrawer = payload; - }, - addNotification(state: MainState, payload: AppNotification) { - state.notifications.push(payload); - }, - removeNotification(state: MainState, payload: AppNotification) { - state.notifications = state.notifications.filter((notification) => notification !== payload); - }, -}; - -const {commit} = getStoreAccessors(''); - -export const commitSetDashboardMiniDrawer = commit(mutations.setDashboardMiniDrawer); -export const commitSetDashboardShowDrawer = commit(mutations.setDashboardShowDrawer); -export const commitSetLoggedIn = commit(mutations.setLoggedIn); -export const commitSetLogInError = commit(mutations.setLogInError); -export const commitSetToken = commit(mutations.setToken); -export const commitSetUserProfile = commit(mutations.setUserProfile); -export const commitAddNotification = commit(mutations.addNotification); -export const commitRemoveNotification = commit(mutations.removeNotification); diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/main/state.ts b/{{cookiecutter.project_slug}}/frontend/src/store/main/state.ts deleted file mode 100644 index be24b63ae9..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/main/state.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { IUserProfile } from '@/interfaces'; - -export interface AppNotification { - content: string; - color?: string; - showProgress?: boolean; -} - -export interface MainState { - token: string; - isLoggedIn: boolean | null; - logInError: boolean; - userProfile: IUserProfile | null; - dashboardMiniDrawer: boolean; - dashboardShowDrawer: boolean; - notifications: AppNotification[]; -} diff --git a/{{cookiecutter.project_slug}}/frontend/src/store/state.ts b/{{cookiecutter.project_slug}}/frontend/src/store/state.ts deleted file mode 100644 index ecec111cd8..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/store/state.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { MainState } from './main/state'; - -export interface State { - main: MainState; -} diff --git a/{{cookiecutter.project_slug}}/frontend/src/utils.ts b/{{cookiecutter.project_slug}}/frontend/src/utils.ts deleted file mode 100644 index ade11b6a2e..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const getLocalToken = () => localStorage.getItem('token'); - -export const saveLocalToken = (token: string) => localStorage.setItem('token', token); - -export const removeLocalToken = () => localStorage.removeItem('token'); diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/Login.vue b/{{cookiecutter.project_slug}}/frontend/src/views/Login.vue deleted file mode 100644 index 28bcb5965e..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/views/Login.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - - - diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/PasswordRecovery.vue b/{{cookiecutter.project_slug}}/frontend/src/views/PasswordRecovery.vue deleted file mode 100644 index bc1a7ade3f..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/views/PasswordRecovery.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - - - diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/ResetPassword.vue b/{{cookiecutter.project_slug}}/frontend/src/views/ResetPassword.vue deleted file mode 100644 index 3e680eb171..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/views/ResetPassword.vue +++ /dev/null @@ -1,84 +0,0 @@ - - - diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/Dashboard.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/Dashboard.vue deleted file mode 100644 index 421879b1b6..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/Dashboard.vue +++ /dev/null @@ -1,37 +0,0 @@ - - - diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/Main.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/Main.vue deleted file mode 100644 index 846d93bd40..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/Main.vue +++ /dev/null @@ -1,182 +0,0 @@ - - - diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/Start.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/Start.vue deleted file mode 100644 index 71eeaafeff..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/Start.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/Admin.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/Admin.vue deleted file mode 100644 index 1282176aaf..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/Admin.vue +++ /dev/null @@ -1,28 +0,0 @@ - - - diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/AdminUsers.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/AdminUsers.vue deleted file mode 100644 index 9b35d9a6c1..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/AdminUsers.vue +++ /dev/null @@ -1,83 +0,0 @@ - - - diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/CreateUser.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/CreateUser.vue deleted file mode 100644 index 892283ec6c..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/CreateUser.vue +++ /dev/null @@ -1,97 +0,0 @@ - - - diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/EditUser.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/EditUser.vue deleted file mode 100644 index 7421233140..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/EditUser.vue +++ /dev/null @@ -1,163 +0,0 @@ - - - diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfile.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfile.vue deleted file mode 100644 index 25960bd42e..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfile.vue +++ /dev/null @@ -1,46 +0,0 @@ - - - diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEdit.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEdit.vue deleted file mode 100644 index dfbea8d874..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEdit.vue +++ /dev/null @@ -1,97 +0,0 @@ - - - diff --git a/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEditPassword.vue b/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEditPassword.vue deleted file mode 100644 index 80e2cc5864..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEditPassword.vue +++ /dev/null @@ -1,86 +0,0 @@ - - - diff --git a/{{cookiecutter.project_slug}}/frontend/tests/unit/upload-button.spec.ts b/{{cookiecutter.project_slug}}/frontend/tests/unit/upload-button.spec.ts deleted file mode 100644 index b40eed7bea..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/tests/unit/upload-button.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import UploadButton from '@/components/UploadButton.vue'; -import '@/plugins/vuetify'; - -describe('UploadButton.vue', () => { - it('renders props.title when passed', () => { - const title = 'upload a file'; - const wrapper = shallowMount(UploadButton, { - slots: { - default: title, - }, - }); - expect(wrapper.text()).toMatch(title); - }); -}); diff --git a/{{cookiecutter.project_slug}}/frontend/tsconfig.json b/{{cookiecutter.project_slug}}/frontend/tsconfig.json deleted file mode 100644 index 88cfbc31d8..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/tsconfig.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "compilerOptions": { - "noImplicitAny": false, - "target": "esnext", - "module": "esnext", - "strict": true, - "jsx": "preserve", - "importHelpers": true, - "moduleResolution": "node", - "experimentalDecorators": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "sourceMap": true, - "baseUrl": ".", - "types": [ - "webpack-env", - "jest" - ], - "paths": { - "@/*": [ - "src/*" - ] - }, - "lib": [ - "esnext", - "dom", - "dom.iterable", - "scripthost" - ] - }, - "include": [ - "src/**/*.ts", - "src/**/*.tsx", - "src/**/*.vue", - "tests/**/*.ts", - "tests/**/*.tsx" - ], - "exclude": [ - "node_modules" - ] -} diff --git a/{{cookiecutter.project_slug}}/frontend/tslint.json b/{{cookiecutter.project_slug}}/frontend/tslint.json deleted file mode 100644 index 2b37e401c3..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/tslint.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "defaultSeverity": "warning", - "extends": [ - "tslint:recommended" - ], - "linterOptions": { - "exclude": [ - "node_modules/**" - ] - }, - "rules": { - "quotemark": [true, "single"], - "indent": [true, "spaces", 2], - "interface-name": false, - "ordered-imports": false, - "object-literal-sort-keys": false, - "no-consecutive-blank-lines": false - } -} diff --git a/{{cookiecutter.project_slug}}/frontend/vue.config.js b/{{cookiecutter.project_slug}}/frontend/vue.config.js deleted file mode 100644 index 140713412f..0000000000 --- a/{{cookiecutter.project_slug}}/frontend/vue.config.js +++ /dev/null @@ -1,35 +0,0 @@ -module.exports = { - // Fix Vuex-typescript in prod: https://github.com/istrib/vuex-typescript/issues/13#issuecomment-409869231 - configureWebpack: (config) => { - if (process.env.NODE_ENV === 'production') { - config.optimization.minimizer[0].options.terserOptions = Object.assign( - {}, - config.optimization.minimizer[0].options.terserOptions, - { - ecma: 5, - compress: { - keep_fnames: true, - }, - warnings: false, - mangle: { - keep_fnames: true, - }, - }, - ); - } - }, - chainWebpack: config => { - config.module - .rule('vue') - .use('vue-loader') - .loader('vue-loader') - .tap(options => Object.assign(options, { - transformAssetUrls: { - 'v-img': ['src', 'lazy-src'], - 'v-card': 'src', - 'v-card-media': 'src', - 'v-responsive': 'src', - } - })); - }, -} diff --git a/{{cookiecutter.project_slug}}/scripts/test.sh b/{{cookiecutter.project_slug}}/scripts/test.sh deleted file mode 100644 index ef63b5e4d0..0000000000 --- a/{{cookiecutter.project_slug}}/scripts/test.sh +++ /dev/null @@ -1,18 +0,0 @@ -#! /usr/bin/env sh - -# Exit in case of error -set -e - -DOMAIN=backend \ -SMTP_HOST="" \ -TRAEFIK_PUBLIC_NETWORK_IS_EXTERNAL=false \ -INSTALL_DEV=true \ -docker-compose \ --f docker-compose.yml \ -config > docker-stack.yml - -docker-compose -f docker-stack.yml build -docker-compose -f docker-stack.yml down -v --remove-orphans # Remove possibly previous broken stacks left hanging after an error -docker-compose -f docker-stack.yml up -d -docker-compose -f docker-stack.yml exec -T backend bash /app/tests-start.sh "$@" -docker-compose -f docker-stack.yml down -v --remove-orphans