diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..1c1551eb3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,73 @@ +venv +__pycache__ +.tox +.github +.vscode +.django_oauth_toolkit.egg-info +.coverage +coverage.xml + +# every time we change this we need to do the COPY . /code and +# RUN pip install -r requirements.txt again +# so don't include the Dockerfile in the context. +Dockerfile +docker-compose.yml + + +# from .gitignore +*.py[cod] + +*.swp + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 +__pycache__ + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.cache +.pytest_cache +.coverage +.tox +.pytest_cache/ +nosetests.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# PyCharm stuff +.idea + +# Sphinx build dir +_build + +# Sqlite database files +*.sqlite + +/venv/ +/coverage.xml + +db.sqlite3 +venv/ diff --git a/.env b/.env new file mode 100644 index 000000000..dc223bf0b --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +# required for vscode testing activity to discover tests +DJANGO_SETTINGS_MODULE=tests.settings \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9e41b33cf..cf8622d1a 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,3 +14,5 @@ Fixes # - [ ] documentation updated - [ ] `CHANGELOG.md` updated (only for user relevant changes) - [ ] author name in `AUTHORS` +- [ ] tests/app/idp updated to demonstrate new features +- [ ] tests/app/rp updated to demonstrate new features diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d4683cfd..0c4329265 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,7 @@ on: jobs: build: - if: github.repository == 'jazzband/django-oauth-toolkit' + if: github.repository == 'django-oauth/django-oauth-toolkit' runs-on: ubuntu-latest steps: @@ -16,25 +16,22 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install dependencies run: | - python -m pip install -U pip - python -m pip install -U setuptools twine wheel + python -m pip install -U pip build twine - name: Build package run: | - python setup.py --version - python setup.py sdist --format=gztar bdist_wheel + python -m build twine check dist/* - - name: Upload packages to Jazzband + - name: Upload packages to PyPI if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: - user: jazzband - password: ${{ secrets.JAZZBAND_RELEASE_KEY }} - repository_url: https://jazzband.co/projects/django-oauth-toolkit/upload + user: __token__ + password: ${{ secrets.PYPI_PUBLISH_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 86a21bc27..dd52da0db 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,54 +2,56 @@ name: Test on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - build: - name: build (Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }}) + test-package: + name: Test Package (Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }}) runs-on: ubuntu-latest + permissions: + id-token: write # Required for Codecov OIDC token strategy: fail-fast: false matrix: python-version: - - '3.8' - - '3.9' - '3.10' - '3.11' - '3.12' + - '3.13' django-version: - - '3.2' - - '4.0' - - '4.1' - '4.2' - '5.0' + - '5.1' + - '5.2' - 'main' - exclude: + ## include/exclude combinations, typically for the newest and oldest django/python versions. + include: # https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django - - # < Python 3.10 is not supported by Django 5.0+ - python-version: '3.8' - django-version: '5.0' + django-version: '4.2' - python-version: '3.9' + django-version: '4.2' + - python-version: '3.14' + django-version: '5.2' + - python-version: '3.14' + django-version: 'main' + exclude: + - python-version: '3.13' django-version: '5.0' - - python-version: '3.8' + - python-version: '3.13' + django-version: '4.2' + - python-version: '3.10' django-version: 'main' - - python-version: '3.9' + - python-version: '3.11' django-version: 'main' - # Python 3.12 is not supported by Django < 5.0 - - python-version: '3.12' - django-version: '3.2' - - python-version: '3.12' - django-version: '4.0' - - python-version: '3.12' - django-version: '4.1' - - python-version: '3.12' - django-version: '4.2' - steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -60,11 +62,11 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.pip-cache.outputs.dir }} key: - ${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}-${{ hashFiles('**/tox.ini') }} + ${{ matrix.python-version }}-v1-${{ hashFiles('**/pyproject.toml') }}-${{ hashFiles('**/tox.ini') }} restore-keys: | ${{ matrix.python-version }}-v1- @@ -80,12 +82,67 @@ jobs: DJANGO: ${{ matrix.django-version }} - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: name: Python ${{ matrix.python-version }} + use_oidc: true + + test-demo-rp: + name: Test Demo Relying Party + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: + - "22.x" + - "24.x" + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up NodeJS + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm install + working-directory: tests/app/rp + + - name: Run Lint + run: npm run lint + working-directory: tests/app/rp + + - name: Run build + run: npm run build + working-directory: tests/app/rp + + codecov-notify: + needs: + - test-package + - test-demo-rp + runs-on: ubuntu-latest + name: Codecov Notify + permissions: + id-token: write # Required for Codecov OIDC token + steps: + # - tell codecov to send notifications now that all jobs are complete. + # without this, codecov may notify before all coverage reports have been uploaded. + # `codecov: notify: manual_trigger: true` must be set in codecov.yml, to prevent + # processing on every upload. + # - preferred to after_n_builds so we don't need to update that number every + # time we add/remove jobs. + - name: Notify Codecov + uses: codecov/codecov-action@v5 + with: + run_command: 'send-notifications' + use_oidc: true success: - needs: build + needs: + - test-package + - test-demo-rp + - codecov-notify runs-on: ubuntu-latest name: Test successful steps: diff --git a/.gitignore b/.gitignore index c4436f57d..d84ebf1ab 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ pip-log.txt .coverage .tox .pytest_cache/ +.ruff_cache/ nosetests.xml # Translations @@ -54,3 +55,7 @@ _build db.sqlite3 venv/ + +/tests/app/idp/static + +*.orig diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eea3dd1af..f4eb471cf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,12 @@ repos: - - repo: https://github.com/psf/black - rev: 24.4.2 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.7 hooks: - - id: black - exclude: ^(oauth2_provider/migrations/|tests/migrations/) + - id: ruff + args: [ --fix ] + - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-ast - id: trailing-whitespace @@ -15,23 +16,13 @@ repos: - id: check-yaml - id: mixed-line-ending args: ['--fix=lf'] - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 - hooks: - - id: isort - exclude: ^(oauth2_provider/migrations/|tests/migrations/) - - repo: https://github.com/PyCQA/flake8 - rev: 7.0.0 - hooks: - - id: flake8 - exclude: ^(oauth2_provider/migrations/|tests/migrations/) - repo: https://github.com/sphinx-contrib/sphinx-lint - rev: v0.9.1 + rev: v1.0.0 hooks: - id: sphinx-lint - # Configuration for codespell is in pyproject.toml +# Configuration for codespell is in pyproject.toml - repo: https://github.com/codespell-project/codespell - rev: v2.2.6 + rev: v2.4.1 hooks: - id: codespell exclude: (package-lock.json|/locale/) diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..fee847fe4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "python.testing.pytestArgs": [ + "tests", + "--no-cov" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/AUTHORS b/AUTHORS index 52a3693af..2d8d5465b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -35,6 +35,8 @@ Bart Merenda Bas van Oostveen Brian Helba Carl Schwan +Cihad GUNDOGDU +Cristian Prigoana Daniel Golding Daniel 'Vector' Kerr Darrel O'Pry @@ -42,6 +44,7 @@ Dave Burkholder David Fischer David Hill David Smith +David Uzumaki Dawid Wolski Diego Garcia Dominik George @@ -51,16 +54,19 @@ Dylan Tack Eduardo Oliveira Egor Poderiagin Emanuele Palazzetti +Fazeel Ghafoor Federico Dolce Florian Demmer Frederico Vieira Gaël Utard Glauco Junior +Giovanni Giampauli Hasan Ramezani Hiroki Kiyohara Hossein Shakiba Islam Kamel Ivan Lukyanets +Jaap Roes Jadiel Teófilo Jens Timmerman Jerome Leclanche @@ -81,7 +87,9 @@ Kristian Rune Larsen Lazaros Toumanidis Ludwig Hähne Łukasz Skarżyński +Madison Swain-Bowden Marcus Sonestedt +Matej Spiller Muys Matias Seniquiel Michael Howitz Owen Gong @@ -98,12 +106,16 @@ Rodney Richardson Rustem Saiargaliev Rustem Saiargaliev Sandro Rodrigues +Sean 'Shaleh' Perry Shaheed Haque Shaun Stanworth +Sayyid Hamid Mahdavi Silvano Cerza Sora Yanai +Sören Wegener Spencer Carroll Stéphane Raimbault +Thales Barbosa Bento Tom Evans Vinay Karanam Víðir Valberg Guðmundsson @@ -113,3 +125,6 @@ pySilver Wouter Klein Heerenbrink Yaroslav Halchenko Yuri Savin +Miriam Forner +Alex Kerkum +q0w diff --git a/CHANGELOG.md b/CHANGELOG.md index c965bc21b..1821a8ae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,23 +4,88 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - +--> + +## [3.1.0] + + +**NOTE**: This is the first release under the new [django-oauth](https://github.com/django-oauth) organization. The project moved in order to be more independent and to bypass quota limits on parallel CI jobs we were encountering in Jazzband. The project will emulateDjango Commons going forward in it's operation. We're always on the look for willing maintainers and contributors. Feel free to start participating any time. PR's are always welcome. -## [unreleased] ### Added +* #1506 Support for Wildcard Origin and Redirect URIs - Adds a new setting [ALLOW_URL_WILDCARDS](https://django-oauth-toolkit.readthedocs.io/en/latest/settings.html#allow-uri-wildcards). This feature is useful for working with CI service such as cloudflare, netlify, and vercel that offer branch +deployments for development previews and user acceptance testing. +* #1586 Turkish language support added +* #1539 Add device authorization grant support + ### Changed +The project is now hosted in the django-oauth organization. + + ### Fixed +* #1517 OP prompts for logout when no OP session +* #1512 client_secret not marked sensitive +* #1521 Fix 0012 migration loading access token table into memory +* #1584 Fix IDP container in docker compose environment could not find templates and static files. +* #1562 Fix: Handle AttributeError in IntrospectTokenView +* #1583 Fix: Missing pt_BR translations + + + +## [3.0.1] - 2024-09-07 +### Fixed +* #1491 Fix migration error when there are pre-existing Access Tokens. + +## [3.0.0] - 2024-09-05 + +### WARNING - POTENTIAL BREAKING CHANGES +* Changes to the `AbstractAccessToken` model require doing a `manage.py migrate` after upgrading. +* If you use swappable models you will need to make sure your custom models are also updated (usually `manage.py makemigrations`). +* Old Django versions below 4.2 are no longer supported. +* A few deprecations warned about in 2.4.0 (#1345) have been removed. See below. + +### Added +* #1366 Add Docker containerized apps for testing IDP and RP. +* #1454 Added compatibility with `LoginRequiredMiddleware` introduced in Django 5.1. + +### Changed +* Many documentation and project internals improvements. +* #1446 Use generic models `pk` instead of `id`. This enables, for example, custom swapped models to have a different primary key field. +* #1447 Update token to TextField from CharField. Removing the 255 character limit enables supporting JWT tokens with additional claims. + This adds a SHA-256 `token_checksum` field that is used to validate tokens. +* #1450 Transactions wrapping writes of the Tokens now rely on Django's database routers to determine the correct + database to use instead of assuming that 'default' is the correct one. +* #1455 Changed minimum supported Django version to >=4.2. + +### Removed +* #1425 Remove deprecated `RedirectURIValidator`, `WildcardSet` per #1345; `validate_logout_request` per #1274 + +### Fixed +* #1444, #1476 Fix several 500 errors to instead raise appropriate errors. +* #1469 Fix `ui_locales` request parameter triggers `AttributeError` under certain circumstances + ### Security +* #1452 Add a new setting [`REFRESH_TOKEN_REUSE_PROTECTION`](https://django-oauth-toolkit.readthedocs.io/en/latest/settings.html#refresh-token-reuse-protection). + In combination with [`ROTATE_REFRESH_TOKEN`](https://django-oauth-toolkit.readthedocs.io/en/latest/settings.html#rotate-refresh-token), + this prevents refresh tokens from being used more than once. See more at + [OAuth 2.0 Security Best Current Practice](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-29#name-recommendations) +* #1481 Bump oauthlib version required to 3.2.2 and above to address [CVE-2022-36087](https://github.com/advisories/GHSA-3pgj-pg6c-r5p7). ## [2.4.0] - 2024-05-13 @@ -197,14 +262,14 @@ This is a major release with **BREAKING** changes. Please make sure to review th ## [1.6.1] 2021-12-23 ### Changed -* Note: Only Django 4.0.1+ is supported due to a regression in Django 4.0.0. [Explanation](https://github.com/jazzband/django-oauth-toolkit/pull/1046#issuecomment-998015272) +* Note: Only Django 4.0.1+ is supported due to a regression in Django 4.0.0. [Explanation](https://github.com/django-oauth/django-oauth-toolkit/pull/1046#issuecomment-998015272) ### Fixed * Miscellaneous 1.6.0 packaging issues. ## [1.6.0] 2021-12-19 ### Added -* #949 Provide django.contrib.auth.authenticate() with a `request` for compatibiity with more backends (like django-axes). +* #949 Provide django.contrib.auth.authenticate() with a `request` for compatibility with more backends (like django-axes). * #968, #1039 Add support for Django 3.2 and 4.0. * #953 Allow loopback redirect URIs using random ports as described in [RFC8252 section 7.3](https://datatracker.ietf.org/doc/html/rfc8252#section-7.3). * #972 Add Farsi/fa language support. @@ -288,7 +353,7 @@ This is a major release with **BREAKING** changes. Please make sure to review th ### Fixed * #812: Reverts #643 pass wrong request object to authenticate function. -* Fix concurrency issue with refresh token requests (#[810](https://github.com/jazzband/django-oauth-toolkit/pull/810)) +* Fix concurrency issue with refresh token requests (#[810](https://github.com/django-oauth/django-oauth-toolkit/pull/810)) * #817: Reverts #734 tutorial documentation error. @@ -327,16 +392,16 @@ This is a major release with **BREAKING** changes. Please make sure to review th ### Fixed * Fix a race condition in creation of AccessToken with external oauth2 server. -* Fix several concurrency issues. (#[638](https://github.com/jazzband/django-oauth-toolkit/issues/638)) -* Fix to pass `request` to `django.contrib.auth.authenticate()` (#[636](https://github.com/jazzband/django-oauth-toolkit/issues/636)) +* Fix several concurrency issues. (#[638](https://github.com/django-oauth/django-oauth-toolkit/issues/638)) +* Fix to pass `request` to `django.contrib.auth.authenticate()` (#[636](https://github.com/django-oauth/django-oauth-toolkit/issues/636)) * Fix missing `oauth2_error` property exception oauthlib_core.verify_request method raises exceptions in authenticate. - (#[633](https://github.com/jazzband/django-oauth-toolkit/issues/633)) + (#[633](https://github.com/django-oauth/django-oauth-toolkit/issues/633)) * Fix "django.db.utils.NotSupportedError: FOR UPDATE cannot be applied to the nullable side of an outer join" for postgresql. - (#[714](https://github.com/jazzband/django-oauth-toolkit/issues/714)) + (#[714](https://github.com/django-oauth/django-oauth-toolkit/issues/714)) * Fix to return a new refresh token during grace period rather than the recently-revoked one. - (#[702](https://github.com/jazzband/django-oauth-toolkit/issues/702)) + (#[702](https://github.com/django-oauth/django-oauth-toolkit/issues/702)) * Fix a bug in refresh token revocation. - (#[625](https://github.com/jazzband/django-oauth-toolkit/issues/625)) + (#[625](https://github.com/django-oauth/django-oauth-toolkit/issues/625)) ## 1.2.0 [2018-06-03] @@ -358,7 +423,7 @@ This is a major release with **BREAKING** changes. Please make sure to review th * **Critical**: Django OAuth Toolkit 1.1.0 contained a migration that would revoke all existing RefreshTokens (`0006_auto_20171214_2232`). This release corrects the migration. If you have already ran it in production, please see the following issue for more details: - https://github.com/jazzband/django-oauth-toolkit/issues/589 + https://github.com/django-oauth/django-oauth-toolkit/issues/589 ## 1.1.0 [2018-04-13] @@ -372,7 +437,7 @@ This is a major release with **BREAKING** changes. Please make sure to review th * **New feature**: The new setting `ERROR_RESPONSE_WITH_SCOPES` can now be set to True to include required scopes when DRF authorization fails due to improper scopes. * **New feature**: The new setting `REFRESH_TOKEN_GRACE_PERIOD_SECONDS` controls a grace period during which - refresh tokens may be re-used. + refresh tokens may be reused. * An `app_authorized` signal is fired when a token is generated. ## 1.0.0 [2017-06-07] diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index e0d5efab5..e5ee8d275 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,46 +1,108 @@ # Code of Conduct -As contributors and maintainers of the Jazzband projects, and in the interest of -fostering an open and welcoming community, we pledge to respect all people who -contribute through reporting issues, posting feature requests, updating documentation, -submitting pull requests or patches, and other activities. - -We are committed to making participation in the Jazzband a harassment-free experience -for everyone, regardless of the level of experience, gender, gender identity and -expression, sexual orientation, disability, personal appearance, body size, race, -ethnicity, age, religion, or nationality. - -Examples of unacceptable behavior by participants include: - -- The use of sexualized language or imagery -- Personal attacks -- Trolling or insulting/derogatory comments -- Public or private harassment -- Publishing other's private information, such as physical or electronic addresses, - without explicit permission -- Other unethical or unprofessional conduct - -The Jazzband roadies have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are not -aligned to this Code of Conduct, or to ban temporarily or permanently any contributor -for other behaviors that they deem inappropriate, threatening, offensive, or harmful. - -By adopting this Code of Conduct, the roadies commit themselves to fairly and -consistently applying these principles to every aspect of managing the jazzband -projects. Roadies who do not follow or enforce the Code of Conduct may be permanently -removed from the Jazzband roadies. - -This code of conduct applies both within project spaces and in public spaces when an -individual is representing the project or its community. - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by -contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and -investigated and will result in a response that is deemed necessary and appropriate to -the circumstances. Roadies are obligated to maintain confidentiality with regard to the -reporter of an incident. - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version -1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] - -[homepage]: https://contributor-covenant.org -[version]: https://contributor-covenant.org/version/1/3/0/ +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Maintainers are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[django-oauth-coc@googlegroups.com](mailto:django-oauth-coc@googlegroups.com). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Warning + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 2. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 3. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Django Commons Code of Conduct](https://github.com/django-commons/membership/blob/main/CODE_OF_CONDUCT.md) \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 49518f460..b89d471e6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,10 @@ -[![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) +# Contribute to Django OAuth Toolkit -This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines). +Thanks for your interest, we love contributions! There are many ways to participate. We are always in need of help with code review, bug fixes, feature development, documentation, and community development. -# Contribute to Django OAuth Toolkit +By contributing you agree to abide by the [Code of Conduct](./CODE_OF_CONDUCT.md) -Thanks for your interest, we love contributions! +We are striving to make a free and open source standards compliant OAuth2/OIDC Identity Provider that adheres to best practices out of the box. Let that goal be your guide as you make decisions related to the project. Please [follow these guidelines](https://django-oauth-toolkit.readthedocs.io/en/latest/contributing.html) when submitting pull requests. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..8828cead5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,70 @@ +# syntax=docker/dockerfile:1.6.0 +# this Dockerfile is located at the root so the build context +# includes oauth2_provider which is a requirement of the +# tests/app/idp. This way we build images with the source +# code from the repos for validation before publishing packages. + +FROM python:3.11.6-slim as builder + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +ENV DEBUG=False +ENV ALLOWED_HOSTS="*" +ENV TEMPLATES_DIRS="/data/templates" +ENV STATIC_ROOT="/data/static" +ENV DATABASE_URL="sqlite:////data/db.sqlite3" + +RUN apt-get update +# Build Deps +RUN apt-get install -y --no-install-recommends gcc libc-dev python3-dev git openssh-client libpq-dev file libev-dev +# bundle code in a virtual env to make copying to the final image without all the upstream stuff easier. +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" +# need to update pip and setuptools for pep517 support required by gevent. +RUN pip install --upgrade pip +RUN pip install --upgrade setuptools +COPY . /code +WORKDIR /code/tests/app/idp +RUN pip install -r requirements.txt +RUN pip install gunicorn +RUN python manage.py collectstatic --noinput + + + +FROM python:3.11.6-slim + +# allow embed sha1 at build time as release. +ARG GIT_SHA1 + +LABEL org.opencontainers.image.authors="https://github.com/django-oauth/django-oauth-toolkit/blob/master/AUTHORS" +LABEL org.opencontainers.image.source="https://github.com/django-oauth/django-oauth-toolkit" +LABEL org.opencontainers.image.revision=${GIT_SHA1} + + +ENV SENTRY_RELEASE=${GIT_SHA1} + +# disable debug mode, but allow all hosts by default when running in docker +ENV DEBUG=False +ENV ALLOWED_HOSTS="*" +ENV TEMPLATES_DIRS="/data/templates" +ENV STATIC_ROOT="/data/static" +ENV DATABASE_URL="sqlite:////data/db.sqlite3" + + + + +COPY --from=builder /opt/venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" +COPY --from=builder /code /code +RUN mkdir -p /data/static /data/templates +COPY --from=builder /code/tests/app/idp/static /data/static +COPY --from=builder /code/tests/app/idp/templates /data/templates + +WORKDIR /code/tests/app/idp +RUN apt-get update && apt-get install -y \ + libpq5 \ + && rm -rf /var/lib/apt/lists/* +EXPOSE 80 +VOLUME ["/data" ] +CMD ["gunicorn", "idp.wsgi:application", "-w 4 -b 0.0.0.0:80 --chdir=/code --worker-tmp-dir /dev/shm --timeout 120 --error-logfile '-' --log-level debug --access-logfile '-'"] diff --git a/README.rst b/README.rst index cbeedf1b4..8038cea7a 100644 --- a/README.rst +++ b/README.rst @@ -1,21 +1,17 @@ Django OAuth Toolkit ==================== -.. image:: https://jazzband.co/static/img/badge.svg - :target: https://jazzband.co/ - :alt: Jazzband - *OAuth2 goodies for the Djangonauts!* .. image:: https://badge.fury.io/py/django-oauth-toolkit.svg :target: http://badge.fury.io/py/django-oauth-toolkit -.. image:: https://github.com/jazzband/django-oauth-toolkit/workflows/Test/badge.svg - :target: https://github.com/jazzband/django-oauth-toolkit/actions +.. image:: https://github.com/django-oauth/django-oauth-toolkit/workflows/Test/badge.svg + :target: https://github.com/django-oauth/django-oauth-toolkit/actions :alt: GitHub Actions -.. image:: https://codecov.io/gh/jazzband/django-oauth-toolkit/branch/master/graph/badge.svg - :target: https://codecov.io/gh/jazzband/django-oauth-toolkit +.. image:: https://codecov.io/gh/django-oauth/django-oauth-toolkit/branch/master/graph/badge.svg + :target: https://codecov.io/gh/django-oauth/django-oauth-toolkit :alt: Coverage .. image:: https://img.shields.io/pypi/pyversions/django-oauth-toolkit.svg @@ -38,14 +34,14 @@ capabilities to your Django projects. Django OAuth Toolkit makes extensive use o Reporting security issues ------------------------- -Please report any security issues to the JazzBand security team at . Do not file an issue on the tracker. +Please report any security issues to the Django OAuth security team at . Do not file an issue on the tracker. Requirements ------------ -* Python 3.8+ -* Django 3.2, 4.0 (4.0.1+ due to a regression), 4.1, 4.2, or 5.0 -* oauthlib 3.1+ +* Python 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 or 3.14 +* Django 4.2, 5.0, 5.1 or 5.2 +* oauthlib 3.2.2+ Installation ------------ @@ -54,7 +50,7 @@ Install with pip:: pip install django-oauth-toolkit -Add `oauth2_provider` to your `INSTALLED_APPS` +Add ``oauth2_provider`` to your ``INSTALLED_APPS`` .. code-block:: python @@ -64,20 +60,21 @@ Add `oauth2_provider` to your `INSTALLED_APPS` ) -If you need an OAuth2 provider you'll want to add the following to your urls.py. -Notice that `oauth2_provider` namespace is mandatory. +If you need an OAuth2 provider you'll want to add the following to your ``urls.py``. .. code-block:: python + from oauth2_provider import urls as oauth2_urls + urlpatterns = [ ... - path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')), + path('o/', include(oauth2_urls)), ] Changelog --------- -See `CHANGELOG.md `_. +See `CHANGELOG.md `_. Documentation @@ -98,9 +95,8 @@ We need help maintaining and enhancing django-oauth-toolkit (DOT). Join the team ~~~~~~~~~~~~~ -Please consider joining `Jazzband `__ (If not -already a member) and the `DOT project -team `__. +There are no barriers to participation. Anyone can open an issue, pr, or review a pull request. Please +dive in! How you can help ~~~~~~~~~~~~~~~~ @@ -108,10 +104,16 @@ How you can help See our `contributing `__ info and the open -`issues `__ and -`PRs `__, +`issues `__ and +`PRs `__, especially those labeled -`help-wanted `__. +`help-wanted `__. + +Discussions +~~~~~~~~~~~ +Have questions or want to discuss the project? +See `the discussions `__. + Submit PRs and Perform Reviews ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -120,19 +122,12 @@ PR submissions and reviews are always appreciated! Since we require an independent review of any PR before it can be merged, having your second set of eyes looking at PRs is extremely valuable. -Please don’t merge PRs -~~~~~~~~~~~~~~~~~~~~~~ - -Please be aware that we don’t want *every* Jazzband member to merge PRs -but just a handful of project team members so that we can maintain a -modicum of control over what goes into a release of this security oriented code base. Only `project -leads `__ are able to -publish releases to Pypi and it becomes difficult when creating a new -release for the leads to deal with “unexpected” merged PRs. -Become a Project Lead +Become a Maintainer ~~~~~~~~~~~~~~~~~~~~~ -If you are interested in stepping up to be a Project Lead, please join -the -`discussion `__. +If you are interested in stepping up to be a Maintainer, please open an issue. For maintainers we're +looking for a positive attitude, attentiveness to the specifications, strong coding and +communication skills, and a willingness to work with others. Maintainers are responsible for +merging pull requests, managing issues, creating releases, and ensuring the overall health of the +project. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..6dd938982 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ + +codecov: + # since we're uploading coverage from multiple parallel jobs,Codecov doesn't + # inherently know when all reports have been uploaded for a given commit. To + # prevent premature processing and ensure a complete report, we send + # notification when all jobs are done. + notify: + manual_trigger: true \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..3a3459fde --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,40 @@ +volumes: + idp-data: + + +x-idp: &idp + image: django-oauth-toolkit/idp + volumes: + - idp-data:/data + +services: + idp-migrate: + <<: *idp + build: . + command: python manage.py migrate + + idp-loaddata: + <<: *idp + command: python manage.py loaddata fixtures/seed.json + depends_on: + idp-migrate: + condition: service_completed_successfully + + idp: + <<: *idp + command: gunicorn idp.wsgi:application -w 4 -b 0.0.0.0:80 --chdir=/code --timeout 120 --error-logfile '-' --log-level debug --access-logfile '-' + ports: + # map to dev port. + - "8000:80" + depends_on: + idp-loaddata: + condition: service_completed_successfully + + rp: + image: django-oauth-toolkit/rp + build: ./tests/app/rp + ports: + # map to dev port. + - "5173:3000" + depends_on: + - idp \ No newline at end of file diff --git a/docs/_images/application-register-device-code.png b/docs/_images/application-register-device-code.png new file mode 100644 index 000000000..4eac6d262 Binary files /dev/null and b/docs/_images/application-register-device-code.png differ diff --git a/docs/_images/device-approve-deny.png b/docs/_images/device-approve-deny.png new file mode 100644 index 000000000..dcff24b25 Binary files /dev/null and b/docs/_images/device-approve-deny.png differ diff --git a/docs/_images/device-enter-code-displayed.png b/docs/_images/device-enter-code-displayed.png new file mode 100644 index 000000000..201137ce3 Binary files /dev/null and b/docs/_images/device-enter-code-displayed.png differ diff --git a/docs/advanced_topics.rst b/docs/advanced_topics.rst index 0b2ee20b0..a99559e93 100644 --- a/docs/advanced_topics.rst +++ b/docs/advanced_topics.rst @@ -63,7 +63,18 @@ That's all, now Django OAuth Toolkit will use your model wherever an Application .. note:: ``OAUTH2_PROVIDER_APPLICATION_MODEL`` is the only setting variable that is not namespaced, this is because of the way Django currently implements swappable models. - See `issue #90 `_ for details. + See `issue #90 `_ for details. + +Configuring multiple databases +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There is no requirement that the tokens are stored in the default database or that there is a +default database provided the database routers can determine the correct Token locations. Because the +Tokens have foreign keys to the ``User`` model, you likely want to keep the tokens in the same database +as your User model. It is also important that all of the tokens are stored in the same database. +This could happen for instance if one of the Tokens is locally overridden and stored in a separate database. +The reason for this is transactions will only be made for the database where AccessToken is stored +even when writing to RefreshToken or other tokens. Multiple Grants ~~~~~~~~~~~~~~~ diff --git a/docs/contributing.rst b/docs/contributing.rst index c31e72990..a633419e2 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -2,17 +2,13 @@ Contributing ============ -.. image:: https://jazzband.co/static/img/jazzband.svg - :target: https://jazzband.co/ - :alt: Jazzband - -This is a `Jazzband `_ project. By contributing you agree to abide by the `Contributor Code of Conduct `_ and follow the `guidelines `_. +By contributing you agree to abide by the `Code of Conduct `_ and follow the `guidelines `_. Setup ===== -Fork ``django-oauth-toolkit`` repository on `GitHub `_ and follow these steps: +Fork ``django-oauth-toolkit`` repository on `GitHub `_ and follow these steps: * Create a virtualenv and activate it * Clone your repository locally @@ -21,20 +17,17 @@ Issues ====== You can find the list of bugs, enhancements and feature requests on the -`issue tracker `_. If you want to fix an issue, pick up one and +`issue tracker `_. If you want to fix an issue, pick up one and add a comment stating you're working on it. Code Style ========== -The project uses `flake8 `_ for linting, -`black `_ for formatting the code, -`isort `_ for formatting and sorting imports, -and `pre-commit `_ for checking/fixing commits for -correctness before they are made. +The project uses `ruff `_ for linting, formatting the code and sorting imports, +and `pre-commit `_ for checking/fixing commits for correctness before they are made. You will need to install ``pre-commit`` yourself, and then ``pre-commit`` will -take care of installing ``flake8``, ``black`` and ``isort``. +take care of installing ``ruff``. After cloning your repository, go into it and run:: @@ -42,14 +35,14 @@ After cloning your repository, go into it and run:: to install the hooks. On the next commit that you make, ``pre-commit`` will download and install the necessary hooks (a one off task). If anything in the -commit would fail the hooks, the commit will be abandoned. For ``black`` and -``isort``, any necessary changes will be made automatically, but not staged. +commit would fail the hooks, the commit will be abandoned. For ``ruff``, any +necessary changes will be made automatically, but not staged. Review the changes, and then re-stage and commit again. Using ``pre-commit`` ensures that code that would fail in QA does not make it into a commit in the first place, and will save you time in the long run. You can also (largely) stop worrying about code style, although you should always -check how the code looks after ``black`` has formatted it, and think if there +check how the code looks after ``ruff`` has formatted it, and think if there is a better way to structure the code so that it is more readable. Documentation @@ -164,7 +157,7 @@ When you begin your PR, you'll be asked to provide the following: * ``Fixed`` for any bug fixes. * ``Security`` in case of vulnerabilities. (Please report any security issues to the - JazzBand security team ````. Do not file an issue on the tracker + security team ````. Do not file an issue on the tracker or submit a PR until directed to do so.) * Make sure your name is in :file:`AUTHORS`. We want to give credit to all contributors! @@ -172,7 +165,7 @@ When you begin your PR, you'll be asked to provide the following: If your PR is not yet ready to be merged mark it as a Work-in-Progress By prepending ``WIP:`` to the PR title so that it doesn't get inadvertently approved and merged. -Make sure to request a review by assigning Reviewer ``jazzband/django-oauth-toolkit``. +Make sure to request a review by assigning Reviewer ``django-oauth/django-oauth-toolkit``. This will assign the review to the project team and a member will review it. In the meantime you can continue to add commits to your topic branch (and push them up to GitHub) either if you see something that needs changing, or in response to a reviewer's comments. If a reviewer asks for changes, you do not need to close the pull and reissue it @@ -187,7 +180,7 @@ outdated code and your changes diverge too far from master, the pull request has To pull in upstream changes:: - git remote add upstream https://github.com/jazzband/django-oauth-toolkit.git + git remote add upstream https://github.com/django-oauth/django-oauth-toolkit.git git fetch upstream Then merge the changes that you fetched:: @@ -255,6 +248,47 @@ Open :file:`mycoverage/index.html` in your browser and you can see a coverage su There's no need to wait for Codecov to complain after you submit your PR. +The tests are generic and written to work with both single database and multiple database configurations. tox will run +tests both ways. You can see the configurations used in tests/settings.py and tests/multi_db_settings.py. + +When there are multiple databases defined, Django tests will not work unless they are told which database(s) to work with. +For test writers this means any test must either: +- instead of Django's TestCase or TransactionTestCase use the versions of those + classes defined in tests/common_testing.py +- when using pytest's `django_db` mark, define it like this: + `@pytest.mark.django_db(databases=retrieve_current_databases())` + +In test code, anywhere the database is referenced the Django router needs to be used exactly like the package's code. + +.. code-block:: python + + token_database = router.db_for_write(AccessToken) + with self.assertNumQueries(1, using=token_database): + # call something using the database + +Without the 'using' option, this test fails in the multiple database scenario because 'default' will be used instead. + +Debugging the Tests Interactively +--------------------------------- + +Interactive Debugging allows you to set breakpoints and inspect the state of the program at runtime. We strongly +recommend using an interactive debugger to streamline your development process. + +VS Code +^^^^^^^ + +VS Code is a popular IDE that supports debugging Python code. You can debug the tests interactively in VS Code by +following these steps: + +.. code-block:: bash + + pip install .[dev] + # open the project in VS Code + # click Testing (erlenmeyer flask) on the Activity Bar + # select the test you want to run or debug + + + Code conventions matter ----------------------- @@ -265,7 +299,7 @@ add a comment. If you think a function is not trivial, add a docstrings. To see if your code formatting will pass muster use:: - tox -e flake8 + tox -e lint The contents of this page are heavily based on the docs from `django-admin2 `_ @@ -278,7 +312,7 @@ Reviewing and Merging PRs ------------------------- - Make sure the PR description includes the `pull request template - `_ + `_ - Confirm that all required checklist items from the PR template are both indicated as done in the PR description and are actually done. - Perform a careful review and ask for any needed changes. @@ -287,15 +321,37 @@ Reviewing and Merging PRs PRs that are incorrectly merged may (reluctantly) be reverted by the Project Leads. +End to End Testing +------------------ + +There is a demonstration Identity Provider (IDP) and Relying Party (RP) to allow for +end to end testing. They can be launched directly by following the instructions in +/test/apps/README.md or via docker compose. To launch via docker compose + +.. code-block:: bash + + # build the images with the current code + docker compose build + # wipe any existing services and volumes + docker compose rm -v + # start the services + docker compose up -d + +Please verify the RP behaves as expected by logging in, reloading, and logging out. + +open http://localhost:5173 in your browser and login with the following credentials: + +username: superuser +password: password Publishing a Release -------------------- -Only Project Leads can `publish a release `_ to pypi.org +Only maintainers can publish a release to pypi.org and rtfd.io. This checklist is a reminder of the required steps. - When planning a new release, create a `milestone - `_ + `_ and assign issues, PRs, etc. to that milestone. - Review all commits since the last release and confirm that they are properly documented in the CHANGELOG. Reword entries as appropriate with links to docs @@ -306,10 +362,36 @@ and rtfd.io. This checklist is a reminder of the required steps. - :file:`oauth2_provider/__init__.py` to set ``__version__ = "..."`` - Once the final PR is merged, create and push a tag for the release. You'll shortly - get a notification from Jazzband of the availability of two pypi packages (source tgz + get a notification of the availability of two pypi packages (source tgz and wheel). Download these locally before releasing them. - Do a ``tox -e build`` and extract the downloaded and built wheel zip and tgz files into temp directories and do a ``diff -r`` to make sure they have the same content. (Unfortunately the checksums do not match due to timestamps in the metadata so you need to compare all the files.) - Once happy that the above comparison checks out, approve the releases to Pypi.org. + + +Errata +====== + +Development with astral uv package and project manager. +------------------------------------------------------- + +We have experimental support for `astral uv `__. It provides an improved +developer experience over vanilla virtualenv/venv and pip by managing multiple python versions, +virtual environments and dependencies in a more efficient way. The ``uv run`` command automatically +syncs dependencies and python version before running the command, saving multiple steps when +working on multiple branches with different dependencies. + +You can use uv sync to set up your environment and install dependencies and run python:: + +... code-block:: bash + uv sync # checks deps, installs virtualenv and dependencies as necessary + uv run ... # runs command in the uv environment, syncs deps and python version first if necessary + +To run tox uv use `tox uv `__:: + +... code-block:: bash + uv tool install tox --with tox-uv # use uv to install + tox --version # validate you are using the installed tox + tox r -e py312 # will use uv diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 80ff9ed71..d2ce14ca1 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -191,10 +191,11 @@ Include ``oauth2_provider.urls`` to :file:`iam/urls.py` as follows: from django.contrib import admin from django.urls import include, path + from oauth2_provider import urls as oauth2_urls urlpatterns = [ path('admin/', admin.site.urls), - path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')), + path('o/', include(oauth2_urls)), ] This will make available endpoints to authorize, generate token and create OAuth applications. @@ -242,9 +243,9 @@ Start the development server:: python manage.py runserver -Point your browser to http://127.0.0.1:8000/o/applications/register/ lets create an application. +Point your browser to http://127.0.0.1:8000/o/applications/register/ and let's create an application. -Fill the form as show in the screenshot below and before save take note of ``Client id`` and ``Client secret``, we will use it in a minute. +Fill the form as shown in the screenshot below and before clicking on save take note of ``Client id`` and ``Client secret``, we will use it in a minute. If you want to use this application with OIDC and ``HS256`` (see :doc:`OpenID Connect `), uncheck ``Hash client secret`` to allow verifying tokens using JWT signatures. This means your client secret will be stored in cleartext but is the only way to successfully use signed JWT's with ``HS256``. diff --git a/docs/index.rst b/docs/index.rst index e0df769cd..386f85928 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,14 +16,14 @@ See our :doc:`Changelog ` for information on updates. Support ------- -If you need help please submit a `question `_. +If you need help please submit a `question `_. Requirements ------------ -* Python 3.8+ -* Django 3.2, 4.0 (4.0.1+ due to a regression), 4.1, 4.2, or 5.0 -* oauthlib 3.1+ +* Python 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 or 3.14 +* Django 4.2, 5.0, 5.1 or 5.2 +* oauthlib 3.2.2+ Index ===== diff --git a/docs/install.rst b/docs/install.rst index 7186a94c0..3d46c507d 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -20,21 +20,11 @@ If you need an OAuth2 provider you'll want to add the following to your :file:`u .. code-block:: python from django.urls import include, path + from oauth2_provider import urls as oauth2_urls urlpatterns = [ ... - path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')), - ] - -Or using ``re_path()`` - -.. code-block:: python - - from django.urls import include, re_path - - urlpatterns = [ - ... - re_path(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')), + path('o/', include(oauth2_urls)), ] Sync your database @@ -45,4 +35,3 @@ Sync your database python manage.py migrate oauth2_provider Next step is :doc:`getting started ` or :doc:`first tutorial `. - diff --git a/docs/management_commands.rst b/docs/management_commands.rst index 83770041e..0a3f1bda0 100644 --- a/docs/management_commands.rst +++ b/docs/management_commands.rst @@ -5,8 +5,6 @@ Django OAuth Toolkit exposes some useful management commands that can be run via or :doc:`Celery `. .. _cleartokens: -.. _createapplication: - cleartokens ~~~~~~~~~~~ @@ -27,7 +25,7 @@ The ``cleartokens`` management command will also delete expired access and ID to Note: Refresh tokens need to expire before AccessTokens can be removed from the database. Using ``cleartokens`` without ``REFRESH_TOKEN_EXPIRE_SECONDS`` has limited effect. - +.. _createapplication: createapplication ~~~~~~~~~~~~~~~~~ diff --git a/docs/requirements.txt b/docs/requirements.txt index b47039487..aa59757a1 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ Django -oauthlib>=3.1.0 +oauthlib>=3.2.2 m2r>=0.2.1 mistune<2 sphinx==7.2.6 diff --git a/docs/rest-framework/getting_started.rst b/docs/rest-framework/getting_started.rst index 4e6b037b0..8e019c44e 100644 --- a/docs/rest-framework/getting_started.rst +++ b/docs/rest-framework/getting_started.rst @@ -51,6 +51,7 @@ Here's our project's root :file:`urls.py` module: from rest_framework import generics, permissions, serializers + from oauth2_provider import urls as oauth2_urls from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope, TokenHasScope # first we define the serializers @@ -84,7 +85,7 @@ Here's our project's root :file:`urls.py` module: # Setup the URLs and include login URLs for the browsable API. urlpatterns = [ path('admin/', admin.site.urls), - path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')), + path('o/', include(oauth2_urls)), path('users/', UserList.as_view()), path('users//', UserDetails.as_view()), path('groups/', GroupList.as_view()), diff --git a/docs/settings.rst b/docs/settings.rst index 901fe8575..9c71bb2a8 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -1,10 +1,8 @@ Settings ======== -Our configurations are all namespaced under the ``OAUTH2_PROVIDER`` settings with the exception of -``OAUTH2_PROVIDER_APPLICATION_MODEL``, ``OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL``, ``OAUTH2_PROVIDER_GRANT_MODEL``, -``OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL``: this is because of the way Django currently implements -swappable models. See `issue #90 `_ for details. +Our configurations are all namespaced under the ``OAUTH2_PROVIDER`` settings, with the exception +of the `List of non-namespaced settings`_. For example: @@ -24,24 +22,17 @@ For example: A big *thank you* to the guys from Django REST Framework for inspiring this. -List of available settings --------------------------- +List of available settings within OAUTH2_PROVIDER +------------------------------------------------- ACCESS_TOKEN_EXPIRE_SECONDS ~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Default: ``36000`` The number of seconds an access token remains valid. Requesting a protected resource after this duration will fail. Keep this value high enough so clients can cache the token for a reasonable amount of time. -ACCESS_TOKEN_MODEL -~~~~~~~~~~~~~~~~~~ -The import string of the class (model) representing your access tokens. Overwrite -this value if you wrote your own implementation (subclass of -``oauth2_provider.models.AccessToken``). - ACCESS_TOKEN_GENERATOR ~~~~~~~~~~~~~~~~~~~~~~ Import path of a callable used to generate access tokens. @@ -49,7 +40,6 @@ Import path of a callable used to generate access tokens. ALLOWED_REDIRECT_URI_SCHEMES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Default: ``["http", "https"]`` A list of schemes that the ``redirect_uri`` field will be validated against. @@ -63,9 +53,38 @@ assigned ports. Note that you may override ``Application.get_allowed_schemes()`` to set this on a per-application basis. +ALLOW_URI_WILDCARDS +~~~~~~~~~~~~~~~~~~~ +Default: ``False`` + +SECURITY WARNING: Enabling this setting can introduce security vulnerabilities. Only enable +this setting if you understand the risks. https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2 +states "The redirection endpoint URI MUST be an absolute URI as defined by [RFC3986] Section 4.3." The +intent of the URI restrictions is to prevent open redirects and phishing attacks. If you do enable this +ensure that the wildcards restrict URIs to resources under your control. You are strongly encouragd not +to use this feature in production. + +When set to ``True``, the server will allow wildcard characters in the domains for allowed_origins and +redirect_uris. + +``*`` is the only wildcard character allowed. + +``*`` can only be used as a prefix to a domain, must be the first character in +the domain, and cannot be in the top or second level domain. Matching is done using an +endsWith check. + +For example, +``https://*.example.com`` is allowed, +``https://*-myproject.example.com`` is allowed, +``https://*.sub.example.com`` is not allowed, +``https://*.com`` is not allowed, and +``https://example.*.com`` is not allowed. + +This feature is useful for working with CI service such as cloudflare, netlify, and vercel that offer branch +deployments for development previews and user acceptance testing. + ALLOWED_SCHEMES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Default: ``["https"]`` A list of schemes that the ``allowed_origins`` field will be validated against. @@ -74,13 +93,6 @@ Adding ``"http"`` to the list is considered to be safe only for local developmen Note that `OAUTHLIB_INSECURE_TRANSPORT `_ environment variable should be also set to allow HTTP origins. - -APPLICATION_MODEL -~~~~~~~~~~~~~~~~~ -The import string of the class (model) representing your applications. Overwrite -this value if you wrote your own implementation (subclass of -``oauth2_provider.models.Application``). - AUTHORIZATION_CODE_EXPIRE_SECONDS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Default: ``60`` @@ -104,6 +116,10 @@ CLIENT_SECRET_GENERATOR_LENGTH The length of the generated secrets, in characters. If this value is too low, secrets may become subject to bruteforce guessing. +CLIENT_SECRET_HASHER +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The hasher for storing generated secrets. By default library will use the first hasher in PASSWORD_HASHERS. + EXTRA_SERVER_KWARGS ~~~~~~~~~~~~~~~~~~~ A dictionary to be passed to oauthlib's Server class. Three options @@ -179,11 +195,17 @@ period the application, the app then has only a consumed refresh token and the only recourse is to have the user re-authenticate. A suggested value, if this is enabled, is 2 minutes. -REFRESH_TOKEN_MODEL -~~~~~~~~~~~~~~~~~~~ -The import string of the class (model) representing your refresh tokens. Overwrite -this value if you wrote your own implementation (subclass of -``oauth2_provider.models.RefreshToken``). +REFRESH_TOKEN_REUSE_PROTECTION +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +When this is set to ``True`` (default ``False``), and ``ROTATE_REFRESH_TOKEN`` is used, the server will check +if a previously, already revoked refresh token is used a second time. If it detects a reuse, it will automatically +revoke all related refresh tokens. +A reused refresh token indicates a breach. Since the server can't determine which request came from the legitimate +user and which from an attacker, it will end the session for both. The user is required to perform a new login. + +Can be used in combination with ``REFRESH_TOKEN_GRACE_PERIOD_SECONDS`` + +More details at https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-29#name-recommendations ROTATE_REFRESH_TOKEN ~~~~~~~~~~~~~~~~~~~~ @@ -210,7 +232,7 @@ Defaults to ``oauth2_provider.scopes.SettingsScopes``, which reads scopes throug SCOPES ~~~~~~ -.. note:: (0.12.0+) Only used if ``ACCESS_TOKEN_GENERATOR`` is set to the SettingsScopes default. +.. note:: (0.12.0+) Only used if ``SCOPES_BACKEND_CLASS`` is set to the SettingsScopes default. A dictionary mapping each scope name to its human description. @@ -218,7 +240,7 @@ A dictionary mapping each scope name to its human description. DEFAULT_SCOPES ~~~~~~~~~~~~~~ -.. note:: (0.12.0+) Only used if ``ACCESS_TOKEN_GENERATOR`` is set to the SettingsScopes default. +.. note:: (0.12.0+) Only used if ``SCOPES_BACKEND_CLASS`` is set to the SettingsScopes default. A list of scopes that should be returned by default. This is a subset of the keys of the ``SCOPES`` setting. @@ -230,13 +252,13 @@ By default this is set to ``'__all__'`` meaning that the whole set of ``SCOPES`` READ_SCOPE ~~~~~~~~~~ -.. note:: (0.12.0+) Only used if ``ACCESS_TOKEN_GENERATOR`` is set to the SettingsScopes default. +.. note:: (0.12.0+) Only used if ``SCOPES_BACKEND_CLASS`` is set to the SettingsScopes default. The name of the *read* scope. WRITE_SCOPE ~~~~~~~~~~~ -.. note:: (0.12.0+) Only used if ``ACCESS_TOKEN_GENERATOR`` is set to the SettingsScopes default. +.. note:: (0.12.0+) Only used if ``SCOPES_BACKEND_CLASS`` is set to the SettingsScopes default. The name of the *write* scope. @@ -293,7 +315,6 @@ Default: ``False`` Whether or not :doc:`oidc` support is enabled. - OIDC_RSA_PRIVATE_KEY ~~~~~~~~~~~~~~~~~~~~ Default: ``""`` @@ -423,11 +444,47 @@ Time of sleep in seconds used by ``cleartokens`` management command between batc Set this to a non-zero value (e.g. ``0.1``) to add a pause between batch sizes to reduce system load when clearing large batches of expired tokens. +List of non-namespaced settings +------------------------------- +.. note:: + These settings must be set as top-level Django settings (outside of ``OAUTH2_PROVIDER``), + because of the way Django currently implements swappable models. + See `issue #90 `_ for details. + + +OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The import string of the class (model) representing your access tokens. +Overwrite this value if you wrote your own implementation (subclass of +``oauth2_provider.models.AccessToken``). + +OAUTH2_PROVIDER_APPLICATION_MODEL +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The import string of the class (model) representing your applications. +Overwrite this value if you wrote your own implementation (subclass of +``oauth2_provider.models.Application``). + +OAUTH2_PROVIDER_ID_TOKEN_MODEL +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The import string of the class (model) representing your OpenID Connect ID Token. +Overwrite this value if you wrote your own implementation (subclass of +``oauth2_provider.models.IDToken``). + +OAUTH2_PROVIDER_GRANT_MODEL +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The import string of the class (model) representing your OAuth2 grants. +Overwrite this value if you wrote your own implementation (subclass of +``oauth2_provider.models.Grant``). + +OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The import string of the class (model) representing your refresh tokens. +Overwrite this value if you wrote your own implementation (subclass of +``oauth2_provider.models.RefreshToken``). Settings imported from Django project ------------------------------------- USE_TZ ~~~~~~ - Used to determine whether or not to make token expire dates timezone aware. diff --git a/docs/tutorial/tutorial.rst b/docs/tutorial/tutorial.rst index 5a0662507..140313673 100644 --- a/docs/tutorial/tutorial.rst +++ b/docs/tutorial/tutorial.rst @@ -9,4 +9,4 @@ Tutorials tutorial_03 tutorial_04 tutorial_05 - + tutorial_06 diff --git a/docs/tutorial/tutorial_01.rst b/docs/tutorial/tutorial_01.rst index efd1265f7..0d0e6b45c 100644 --- a/docs/tutorial/tutorial_01.rst +++ b/docs/tutorial/tutorial_01.rst @@ -34,10 +34,11 @@ Include the Django OAuth Toolkit urls in your `urls.py`, choosing the urlspace y .. code-block:: python from django.urls import path, include + from oauth2_provider import urls as oauth2_urls urlpatterns = [ path("admin", admin.site.urls), - path("o/", include('oauth2_provider.urls', namespace='oauth2_provider')), + path("o/", include(oauth2_urls)), # ... ] diff --git a/docs/tutorial/tutorial_05.rst b/docs/tutorial/tutorial_05.rst index e75f3e23e..74feec4d2 100644 --- a/docs/tutorial/tutorial_05.rst +++ b/docs/tutorial/tutorial_05.rst @@ -65,6 +65,7 @@ Now add a new file to your app to add Celery: :file:`tutorial/celery.py`: import os from celery import Celery + from django.conf import settings # Set the default Django settings module for the 'celery' program. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tutorial.settings') diff --git a/docs/tutorial/tutorial_06.rst b/docs/tutorial/tutorial_06.rst new file mode 100644 index 000000000..386e4ef39 --- /dev/null +++ b/docs/tutorial/tutorial_06.rst @@ -0,0 +1,126 @@ +Part 6 - Device authorization grant flow +==================================================== + +Scenario +-------- +In :doc:`Part 1 ` you created your own :term:`Authorization Server` and it's running along just fine. +You have devices that your users have, and those users need to authenticate the device against your +:term:`Authorization Server` in order to make the required API calls. + +Device Authorization +-------------------- +The OAuth 2.0 device authorization grant is designed for Internet +connected devices that either lack a browser to perform a user-agent +based authorization or are input-constrained to the extent that +requiring the user to input text in order to authenticate during the +authorization flow is impractical. It enables OAuth clients on such +devices (like smart TVs, media consoles, digital picture frames, and +printers) to obtain user authorization to access protected resources +by using a user agent on a separate device. + +Point your browser to `http://127.0.0.1:8000/o/applications/register/` to create an application. + +Fill the form as shown in the screenshot below, and before saving, take note of the ``Client id``. +Make sure the client type is set to "Public." There are cases where a confidential client makes sense, +but generally, it is assumed the device is unable to safely store the client secret. + +.. image:: ../_images/application-register-device-code.png + :alt: Device Authorization application registration + +Ensure the setting ``OAUTH_DEVICE_VERIFICATION_URI`` is set to a URI you want to return in the +`verification_uri` key in the response. This is what the device will display to the user. + +1. Navigate to the tests/app/idp directory: + +.. code-block:: sh + + cd tests/app/idp + +then start the server + +.. code-block:: sh + + python manage.py runserver + +.. _RFC: https://www.rfc-editor.org/rfc/rfc8628 +.. _RFC section 3.5: https://datatracker.ietf.org/doc/html/rfc8628#section-3.5 + +2. To initiate device authorization, send this request (in the real world, the device +makes this request). In `RFC`_ Figure 1, this is step (A). + +.. code-block:: sh + + curl --location 'http://127.0.0.1:8000/o/device-authorization/' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'client_id={your application client id}' + +The OAuth2 provider will return the following response. In `RFC`_ Figure 1, this is step (B). + +.. code-block:: json + + { + "verification_uri": "http://127.0.0.1:8000/o/device", + "expires_in": 1800, + "user_code": "A32RVADM", + "device_code": "G30j94v0kNfipD4KmGLTWeL4eZnKHm", + "interval": 5 + } + +In the real world, the device will somehow make the value of the `user_code` available to the user (either on-screen display, +or Bluetooth, NFC, etc.). In `RFC`_ Figure 1, this is step (C). + +3. Go to `http://127.0.0.1:8000/o/device` in your browser. + +.. image:: ../_images/device-enter-code-displayed.png + +Enter the code, and it will redirect you to the device-confirm endpoint. In `RFC`_ Figure 1, this is step (D). + +Device-confirm endpoint +----------------------- +4. Device polling occurs concurrently while the user approves or denies the request. + +.. image:: ../_images/device-approve-deny.png + +Device polling +-------------- +Send the following request (in the real world, the device makes this request). In `RFC`_ Figure 1, this is step (E). + +.. code-block:: sh + + curl --location 'http://localhost:8000/o/token/' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'device_code={the device code from the device-authorization response}' \ + --data-urlencode 'client_id={your application client id}' \ + --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:device_code' + +In `RFC`_ Figure 1, there are multiple options for step (F), as per `RFC section 3.5`_. Until the user enters the code +in the browser and approves, the response will be 400: + +.. code-block:: json + + {"error": "authorization_pending"} + +Or if the user has denied the device, the response is 400: + +.. code-block:: json + + {"error": "access_denied"} + +Or if the token has expired, the response is 400: + +.. code-block:: json + + {"error": "expired_token"} + + +However, after the user approves, the response will be 200: + +.. code-block:: json + + { + "access_token": "SkJMgyL432P04nHDPyB63DEAM0nVxk", + "expires_in": 36000, + "token_type": "Bearer", + "scope": "openid", + "refresh_token": "Go6VumurDfFAeCeKrpCKPDtElV77id" + } diff --git a/oauth2_provider/__init__.py b/oauth2_provider/__init__.py index 3d67cd6bb..f5f41e567 100644 --- a/oauth2_provider/__init__.py +++ b/oauth2_provider/__init__.py @@ -1 +1 @@ -__version__ = "2.4.0" +__version__ = "3.1.0" diff --git a/oauth2_provider/admin.py b/oauth2_provider/admin.py index cefc75bb6..dd636184c 100644 --- a/oauth2_provider/admin.py +++ b/oauth2_provider/admin.py @@ -19,7 +19,7 @@ class ApplicationAdmin(admin.ModelAdmin): - list_display = ("id", "name", "user", "client_type", "authorization_grant_type") + list_display = ("pk", "name", "user", "client_type", "authorization_grant_type") list_filter = ("client_type", "authorization_grant_type", "skip_authorization") radio_fields = { "client_type": admin.HORIZONTAL, diff --git a/oauth2_provider/apps.py b/oauth2_provider/apps.py index 887e4e3fb..3ad08b715 100644 --- a/oauth2_provider/apps.py +++ b/oauth2_provider/apps.py @@ -4,3 +4,7 @@ class DOTConfig(AppConfig): name = "oauth2_provider" verbose_name = "Django OAuth Toolkit" + + def ready(self): + # Import checks to ensure they run. + from . import checks # noqa: F401 diff --git a/oauth2_provider/checks.py b/oauth2_provider/checks.py new file mode 100644 index 000000000..848ba1af7 --- /dev/null +++ b/oauth2_provider/checks.py @@ -0,0 +1,28 @@ +from django.apps import apps +from django.core import checks +from django.db import router + +from .settings import oauth2_settings + + +@checks.register(checks.Tags.database) +def validate_token_configuration(app_configs, **kwargs): + databases = set( + router.db_for_write(apps.get_model(model)) + for model in ( + oauth2_settings.ACCESS_TOKEN_MODEL, + oauth2_settings.ID_TOKEN_MODEL, + oauth2_settings.REFRESH_TOKEN_MODEL, + ) + ) + + # This is highly unlikely, but let's warn people just in case it does. + # If the tokens were allowed to be in different databases this would require all + # writes to have a transaction around each database. Instead, let's enforce that + # they all live together in one database. + # The tokens are not required to live in the default database provided the Django + # routers know the correct database for them. + if len(databases) > 1: + return [checks.Error("The token models are expected to be stored in the same database.")] + + return [] diff --git a/oauth2_provider/compat.py b/oauth2_provider/compat.py index 0c83cb37a..846e32d0e 100644 --- a/oauth2_provider/compat.py +++ b/oauth2_provider/compat.py @@ -2,3 +2,14 @@ The `compat` module provides support for backwards compatibility with older versions of Django and Python. """ + +try: + # Django 5.1 introduced LoginRequiredMiddleware, and login_not_required decorator + from django.contrib.auth.decorators import login_not_required +except ImportError: + + def login_not_required(view_func): + return view_func + + +__all__ = ["login_not_required"] diff --git a/oauth2_provider/contrib/rest_framework/authentication.py b/oauth2_provider/contrib/rest_framework/authentication.py index 53087f756..afa75d845 100644 --- a/oauth2_provider/contrib/rest_framework/authentication.py +++ b/oauth2_provider/contrib/rest_framework/authentication.py @@ -1,5 +1,6 @@ from collections import OrderedDict +from django.core.exceptions import SuspiciousOperation from rest_framework.authentication import BaseAuthentication from ...oauth2_backends import get_oauthlib_core @@ -23,10 +24,18 @@ def authenticate(self, request): Returns two-tuple of (user, token) if authentication succeeds, or None otherwise. """ + if request is None: + return None oauthlib_core = get_oauthlib_core() - valid, r = oauthlib_core.verify_request(request, scopes=[]) - if valid: - return r.user, r.access_token + try: + valid, r = oauthlib_core.verify_request(request, scopes=[]) + except ValueError as error: + if str(error) == "Invalid hex encoding in query string.": + raise SuspiciousOperation(error) + raise + else: + if valid: + return r.user, r.access_token request.oauth2_error = getattr(r, "oauth2_error", {}) return None diff --git a/oauth2_provider/locale/pt_BR/LC_MESSAGES/django.po b/oauth2_provider/locale/pt_BR/LC_MESSAGES/django.po index 48d673e33..e852622e3 100644 --- a/oauth2_provider/locale/pt_BR/LC_MESSAGES/django.po +++ b/oauth2_provider/locale/pt_BR/LC_MESSAGES/django.po @@ -9,10 +9,10 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-12-30 09:50-0300\n" -"PO-Revision-Date: 2021-12-30 09:50-0300\n" -"Last-Translator: Eduardo Oliveira \n" +"PO-Revision-Date: 2025-07-01 12:35-0300\n" +"Last-Translator: Thales Barbosa Bento \n" "Language-Team: LANGUAGE \n" -"Language: \n" +"Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -101,7 +101,7 @@ msgstr "Tem certeza que deseja remover a aplicação?" msgid "Cancel" msgstr "Cancelar" -#: templates/oauth2_provider/application_confirm_delete.html:13 +#: templates/oauth2_provider/application_confirm_delete.html:13F #: templates/oauth2_provider/application_detail.html:38 #: templates/oauth2_provider/authorized-token-delete.html:7 msgid "Delete" @@ -113,7 +113,7 @@ msgstr "ID do Cliente" #: templates/oauth2_provider/application_detail.html:15 msgid "Client secret" -msgstr "Palavra-Chave Secreta do Cliente" +msgstr "Chave Secreta do Cliente" #: templates/oauth2_provider/application_detail.html:20 msgid "Client type" @@ -200,3 +200,258 @@ msgstr "Revogar" #: templates/oauth2_provider/authorized-tokens.html:19 msgid "There are no authorized tokens yet." msgstr "Não existem tokens autorizados ainda." + +msgid "Allowed origins list to enable CORS, space separated" +msgstr "Lista de origens permitidas para habilitar CORS, separadas por espaços" + +msgid "Access tokens" +msgstr "Tokens de acesso" + +msgid "Applications" +msgstr "Aplicações" + +msgid "Grants" +msgstr "Concessões" + +msgid "Id tokens" +msgstr "Tokens de ID" + +msgid "Refresh tokens" +msgstr "Tokens de atualização" + +msgid "Access token" +msgstr "Token de acesso" + +msgid "Application" +msgstr "Aplicação" + +msgid "Grant" +msgstr "Concessão" + +msgid "Id token" +msgstr "Token de ID" + +msgid "Refresh token" +msgstr "Token de atualização" + +msgid "Skip authorization" +msgstr "Pular autorização" + +msgid "Algorithm" +msgstr "Algoritmo" + +msgid "Created" +msgstr "Criado" + +msgid "Updated" +msgstr "Atualizado" + +msgid "Scope" +msgstr "Escopo" + +msgid "Scopes" +msgstr "Escopos" + +msgid "Expires" +msgstr "Expira" + +msgid "Token" +msgstr "Token" + +msgid "User" +msgstr "Usuário" + +msgid "Code" +msgstr "Código" + +msgid "Redirect URI" +msgstr "URI de Redirecionamento" + +msgid "State" +msgstr "Estado" + +msgid "Nonce" +msgstr "Nonce (Number Used Once)" + +msgid "Code Challenge" +msgstr "Desafio do Código" + +msgid "Code Challenge Method" +msgstr "Método do Desafio do Código" + +msgid "PKCE required" +msgstr "PKCE obrigatório" + +msgid "Post logout redirect URI" +msgstr "URI de redirecionamento pós-logout" + +msgid "Hash client secret" +msgstr "Hash da chave secreta do cliente" + +msgid "The client secret will be hashed" +msgstr "A chave secreta do cliente será transformada em hash" + +msgid "Skip authorization completely" +msgstr "Pular autorização completamente" + +msgid "Post logout redirect URIs" +msgstr "URIs de redirecionamento pós-logout" + +msgid "Allowed Post Logout Redirect URIs list, space separated" +msgstr "Lista de URIs de redirecionamento pós-logout permitidas, separadas por espaços" + +msgid "Authorization Grant" +msgstr "Concessão de Autorização" + +msgid "Authorization Grants" +msgstr "Concessões de Autorização" + +msgid "Invalid client or client credentials" +msgstr "Cliente inválido ou credenciais do cliente inválidas" + +msgid "Invalid authorization code" +msgstr "Código de autorização inválido" + +msgid "Invalid redirect URI" +msgstr "URI de redirecionamento inválida" + +msgid "Invalid grant type" +msgstr "Tipo de concessão inválido" + +msgid "Invalid scope" +msgstr "Escopo inválido" + +msgid "Invalid request" +msgstr "Solicitação inválida" + +msgid "Unsupported grant type" +msgstr "Tipo de concessão não suportado" + +msgid "Unsupported response type" +msgstr "Tipo de resposta não suportado" + +msgid "Authorization server error" +msgstr "Erro do servidor de autorização" + +msgid "Temporarily unavailable" +msgstr "Temporariamente indisponível" + +msgid "Access denied" +msgstr "Acesso negado" + +msgid "Invalid client" +msgstr "Cliente inválido" + +msgid "The client identifier provided is invalid" +msgstr "O identificador do cliente fornecido é inválido" + +msgid "The client authentication failed" +msgstr "A autenticação do cliente falhou" + +msgid "The authorization grant is invalid, expired, or revoked" +msgstr "A concessão de autorização é inválida, expirada ou revogada" + +msgid "The authenticated client is not authorized to use this authorization grant type" +msgstr "O cliente autenticado não está autorizado a usar este tipo de concessão de autorização" + +msgid "The request is missing a required parameter" +msgstr "A solicitação está perdendo um parâmetro obrigatório" + +msgid "The authorization server encountered an unexpected condition" +msgstr "O servidor de autorização encontrou uma condição inesperada" + +msgid "The authorization server is currently unable to handle the request" +msgstr "O servidor de autorização atualmente não consegue processar a solicitação" + +msgid "The resource owner or authorization server denied the request" +msgstr "O proprietário do recurso ou servidor de autorização negou a solicitação" + +msgid "The authorization server does not support obtaining authorization codes" +msgstr "O servidor de autorização não suporta obter códigos de autorização" + +msgid "The authorization server does not support the revocation of access tokens" +msgstr "O servidor de autorização não suporta a revogação de tokens de acesso" + +msgid "The authorization server does not support the revocation of refresh tokens" +msgstr "O servidor de autorização não suporta a revogação de tokens de atualização" + +msgid "The authorization server does not support the use of the transformation parameter" +msgstr "O servidor de autorização não suporta o uso do parâmetro de transformação" + +msgid "The target resource is invalid, missing, malformed, or not supported" +msgstr "O recurso de destino é inválido, ausente, malformado ou não suportado" + +msgid "Rotate refresh token" +msgstr "Rotacionar token de atualização" + +msgid "Reuse refresh token" +msgstr "Reutilizar token de atualização" + +msgid "Application name" +msgstr "Nome da aplicação" + +msgid "Application description" +msgstr "Descrição da aplicação" + +msgid "Application logo" +msgstr "Logo da aplicação" + +msgid "Application website" +msgstr "Website da aplicação" + +msgid "Terms of service" +msgstr "Termos de serviço" + +msgid "Privacy policy" +msgstr "Política de privacidade" + +msgid "Support email" +msgstr "Email de suporte" + +msgid "Support URL" +msgstr "URL de suporte" + +msgid "Redirect URIs" +msgstr "URIs de Redirecionamento" + +msgid "Source refresh token" +msgstr "Token de atualização de origem" + +msgid "Token checksum" +msgstr "Checksum do token" + +msgid "Token family" +msgstr "Família do token" + +msgid "Code challenge" +msgstr "Desafio do código" + +msgid "Code challenge method" +msgstr "Método do desafio do código" + +msgid "Claims" +msgstr "Reivindicações" + +msgid "JWT Token ID" +msgstr "ID do Token JWT" + +msgid "Allowed origins" +msgstr "Origens permitidas" + +msgid "Client ID" +msgstr "ID do Cliente" + +msgid "Revoked" +msgstr "Revogado" + +msgid "Authorization grant type" +msgstr "Tipo de concessão de autorização" + +msgid "Hashed on Save. Copy it now if this is a new secret." +msgstr "Hashed em Salvar. Copie agora se este for um novo segredo." + +msgid "allowed origin URI Validation error" +msgstr "erro de validação de URI de origem permitido. invalid_scheme" + +msgid "Date" +msgstr "Data" diff --git a/oauth2_provider/locale/tr/LC_MESSAGES/django.po b/oauth2_provider/locale/tr/LC_MESSAGES/django.po new file mode 100644 index 000000000..cf49b1ccc --- /dev/null +++ b/oauth2_provider/locale/tr/LC_MESSAGES/django.po @@ -0,0 +1,215 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-07-11 14:11+0300\n" +"PO-Revision-Date: 2025-07-11 14:15+0300\n" +"Last-Translator: Cihad GUNDOGDU \n" +"Language-Team: LANGUAGE \n" +"Language: tr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +#: oauth2_provider/models.py:84 +msgid "Confidential" +msgstr "Gizli" + +#: oauth2_provider/models.py:85 +msgid "Public" +msgstr "Herkese açık" + +#: oauth2_provider/models.py:94 +msgid "Authorization code" +msgstr "Yetkilendirme kodu" + +#: oauth2_provider/models.py:95 +msgid "Implicit" +msgstr "Açık" + +#: oauth2_provider/models.py:96 +msgid "Resource owner password-based" +msgstr "Kaynak sahibi şifre tabanlı" + +#: oauth2_provider/models.py:97 +msgid "Client credentials" +msgstr "İstemci kimlik bilgileri" + +#: oauth2_provider/models.py:98 +msgid "OpenID connect hybrid" +msgstr "OpenID connect hibrit" + +#: oauth2_provider/models.py:105 +msgid "No OIDC support" +msgstr "OpenID Connect desteği yok" + +#: oauth2_provider/models.py:106 +msgid "RSA with SHA-2 256" +msgstr "RSA ile SHA-2 256" + +#: oauth2_provider/models.py:107 +msgid "HMAC with SHA-2 256" +msgstr "HMAC ile SHA-2 256" + +#: oauth2_provider/models.py:122 +msgid "Allowed URIs list, space separated" +msgstr "İzin verilen URI'ler listesi, boşlukla ayrılmış" + +#: oauth2_provider/models.py:126 +msgid "Allowed Post Logout URIs list, space separated" +msgstr "İzin verilen Oturum Kapatma URI'leri listesi, boşlukla ayrılmış" + +#: oauth2_provider/models.py:136 +msgid "Hashed on Save. Copy it now if this is a new secret." +msgstr "Kaydedildiğinde Hashlendi. Bu yeni bir gizli anahtar ise şimdi kopyalayın." + +#: oauth2_provider/models.py:147 +msgid "Allowed origins list to enable CORS, space separated" +msgstr "CORS'u etkinleştirmek için izin verilen kökenler listesi, boşlukla ayrılmış" + +#: oauth2_provider/models.py:227 +#, python-brace-format +msgid "redirect_uris cannot be empty with grant_type {grant_type}" +msgstr "redirect_uris {grant_type} ile boş olamaz" + +#: oauth2_provider/models.py:244 +msgid "You must set OIDC_RSA_PRIVATE_KEY to use RSA algorithm" +msgstr "RSA algoritmasını kullanmak için OIDC_RSA_PRIVATE_KEY ayarlanmalıdır" + +#: oauth2_provider/models.py:253 +msgid "You cannot use HS256 with public grants or clients" +msgstr "HS256'yı herkese açık izinler veya istemcilerle kullanamazsınız" + +#: oauth2_provider/oauth2_validators.py:225 +msgid "The access token is invalid." +msgstr "Geçersiz erişim belirteci." + +#: oauth2_provider/oauth2_validators.py:232 +msgid "The access token has expired." +msgstr "Erişim belirteci süresi dolmuş." + +#: oauth2_provider/oauth2_validators.py:239 +msgid "The access token is valid but does not have enough scope." +msgstr "Erişim belirteci geçerli ancak yeterli kapsamı yok." + +#: oauth2_provider/templates/oauth2_provider/application_confirm_delete.html:6 +msgid "Are you sure to delete the application" +msgstr "Uygulamayı silmek istediğinize emin misiniz" + +#: oauth2_provider/templates/oauth2_provider/application_confirm_delete.html:12 +#: oauth2_provider/templates/oauth2_provider/authorize.html:29 +msgid "Cancel" +msgstr "İptal" + +#: oauth2_provider/templates/oauth2_provider/application_confirm_delete.html:13 +#: oauth2_provider/templates/oauth2_provider/application_detail.html:53 +#: oauth2_provider/templates/oauth2_provider/authorized-token-delete.html:7 +msgid "Delete" +msgstr "Sil" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:10 +msgid "Client id" +msgstr "İstemci kimliği" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:15 +msgid "Client secret" +msgstr "İstemci gizli anahtarı" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:20 +msgid "Hash client secret" +msgstr "İstemci gizli anahtarını hashle" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:21 +msgid "yes,no" +msgstr "evet,hayır" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:25 +msgid "Client type" +msgstr "İstemci türü" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:30 +msgid "Authorization Grant Type" +msgstr "Yetkilendirme İzni Türü" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:35 +msgid "Redirect Uris" +msgstr "Yönlendirme URI'leri" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:40 +msgid "Post Logout Redirect Uris" +msgstr "Oturum Kapatma Yönlendirme URI'leri" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:45 +msgid "Allowed Origins" +msgstr "İzin Verilen Orijinler" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:51 +#: oauth2_provider/templates/oauth2_provider/application_form.html:35 +msgid "Go Back" +msgstr "Geri Dön" + +#: oauth2_provider/templates/oauth2_provider/application_detail.html:52 +msgid "Edit" +msgstr "Düzenle" + +#: oauth2_provider/templates/oauth2_provider/application_form.html:9 +msgid "Edit application" +msgstr "Uygulamayı düzenle" + +#: oauth2_provider/templates/oauth2_provider/application_form.html:37 +msgid "Save" +msgstr "Kaydet" + +#: oauth2_provider/templates/oauth2_provider/application_list.html:6 +msgid "Your applications" +msgstr "Uygulamalarınız" + +#: oauth2_provider/templates/oauth2_provider/application_list.html:14 +msgid "New Application" +msgstr "Yeni Uygulama" + +#: oauth2_provider/templates/oauth2_provider/application_list.html:17 +msgid "No applications defined" +msgstr "Tanımlı uygulama yok" + +#: oauth2_provider/templates/oauth2_provider/application_list.html:17 +msgid "Click here" +msgstr "Buraya tıklayın" + +#: oauth2_provider/templates/oauth2_provider/application_list.html:17 +msgid "if you want to register a new one" +msgstr "eğer yeni bir tane kaydetmek istiyorsanız" + +#: oauth2_provider/templates/oauth2_provider/application_registration_form.html:5 +msgid "Register a new application" +msgstr "Yeni bir uygulama kaydet" + +#: oauth2_provider/templates/oauth2_provider/authorize.html:8 +#: oauth2_provider/templates/oauth2_provider/authorize.html:30 +msgid "Authorize" +msgstr "Yetkilendir" + +#: oauth2_provider/templates/oauth2_provider/authorize.html:17 +msgid "Application requires the following permissions" +msgstr "Uygulama aşağıdaki izinleri gerektirir" + +#: oauth2_provider/templates/oauth2_provider/authorized-token-delete.html:6 +msgid "Are you sure you want to delete this token?" +msgstr "Bu tokeni silmek istediğinize emin misiniz?" + +#: oauth2_provider/templates/oauth2_provider/authorized-tokens.html:6 +msgid "Tokens" +msgstr "Tokenler" + +#: oauth2_provider/templates/oauth2_provider/authorized-tokens.html:11 +msgid "revoke" +msgstr "iptal et" + +#: oauth2_provider/templates/oauth2_provider/authorized-tokens.html:19 +msgid "There are no authorized tokens yet." +msgstr "Henüz yetkilendirilmiş token yok." diff --git a/oauth2_provider/middleware.py b/oauth2_provider/middleware.py index de1689894..65c9cf03c 100644 --- a/oauth2_provider/middleware.py +++ b/oauth2_provider/middleware.py @@ -1,3 +1,4 @@ +import hashlib import logging from django.contrib.auth import authenticate @@ -55,7 +56,8 @@ def __call__(self, request): tokenstring = authheader.split()[1] AccessToken = get_access_token_model() try: - token = AccessToken.objects.get(token=tokenstring) + token_checksum = hashlib.sha256(tokenstring.encode("utf-8")).hexdigest() + token = AccessToken.objects.get(token_checksum=token_checksum) request.access_token = token except AccessToken.DoesNotExist as e: log.exception(e) diff --git a/oauth2_provider/migrations/0011_refreshtoken_token_family.py b/oauth2_provider/migrations/0011_refreshtoken_token_family.py new file mode 100644 index 000000000..94fb4e171 --- /dev/null +++ b/oauth2_provider/migrations/0011_refreshtoken_token_family.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2 on 2024-08-09 16:40 + +from django.db import migrations, models +from oauth2_provider.settings import oauth2_settings + +class Migration(migrations.Migration): + + dependencies = [ + ('oauth2_provider', '0010_application_allowed_origins'), + migrations.swappable_dependency(oauth2_settings.REFRESH_TOKEN_MODEL) + ] + + operations = [ + migrations.AddField( + model_name='refreshtoken', + name='token_family', + field=models.UUIDField(blank=True, editable=False, null=True), + ), + ] diff --git a/oauth2_provider/migrations/0012_add_token_checksum.py b/oauth2_provider/migrations/0012_add_token_checksum.py new file mode 100644 index 000000000..d27c65e54 --- /dev/null +++ b/oauth2_provider/migrations/0012_add_token_checksum.py @@ -0,0 +1,40 @@ +# Generated by Django 5.0.7 on 2024-07-29 23:13 + +import oauth2_provider.models +from django.db import migrations, models +from oauth2_provider.settings import oauth2_settings + +def forwards_func(apps, schema_editor): + """ + Forward migration touches every "old" accesstoken.token which will cause the checksum to be computed. + """ + AccessToken = apps.get_model(oauth2_settings.ACCESS_TOKEN_MODEL) + accesstokens = AccessToken._default_manager.iterator() + for accesstoken in accesstokens: + accesstoken.save(update_fields=['token_checksum']) + + +class Migration(migrations.Migration): + dependencies = [ + ("oauth2_provider", "0011_refreshtoken_token_family"), + migrations.swappable_dependency(oauth2_settings.ACCESS_TOKEN_MODEL), + ] + + operations = [ + migrations.AddField( + model_name="accesstoken", + name="token_checksum", + field=oauth2_provider.models.TokenChecksumField(blank=True, null=True, max_length=64), + ), + migrations.AlterField( + model_name="accesstoken", + name="token", + field=models.TextField(), + ), + migrations.RunPython(forwards_func, migrations.RunPython.noop), + migrations.AlterField( + model_name='accesstoken', + name='token_checksum', + field=oauth2_provider.models.TokenChecksumField(blank=False, max_length=64, db_index=True, unique=True), + ), + ] diff --git a/oauth2_provider/migrations/0013_alter_application_authorization_grant_type_device.py b/oauth2_provider/migrations/0013_alter_application_authorization_grant_type_device.py new file mode 100644 index 000000000..99769c398 --- /dev/null +++ b/oauth2_provider/migrations/0013_alter_application_authorization_grant_type_device.py @@ -0,0 +1,41 @@ +# Generated by Django 5.1.5 on 2025-01-24 14:00 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('oauth2_provider', '0012_add_token_checksum'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='application', + name='authorization_grant_type', + field=models.CharField(choices=[('authorization-code', 'Authorization code'), ('urn:ietf:params:oauth:grant-type:device_code', 'Device Code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials'), ('openid-hybrid', 'OpenID connect hybrid')], max_length=44), + ), + migrations.CreateModel( + name='DeviceGrant', + fields=[ + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('device_code', models.CharField(max_length=100, unique=True)), + ('user_code', models.CharField(max_length=100)), + ('scope', models.CharField(max_length=64, null=True)), + ('interval', models.IntegerField(default=5)), + ('expires', models.DateTimeField()), + ('status', models.CharField(blank=True, choices=[('authorized', 'Authorized'), ('authorization-pending', 'Authorization pending'), ('expired', 'Expired'), ('denied', 'Denied')], default='authorization-pending', max_length=64)), + ('client_id', models.CharField(db_index=True, max_length=100)), + ('last_checked', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + 'swappable': 'OAUTH2_PROVIDER_DEVICE_GRANT_MODEL', + 'constraints': [models.UniqueConstraint(fields=('device_code',), name='oauth2_provider_devicegrant_unique_device_code')], + }, + ), + ] diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py index 661bd7dfc..523ade289 100644 --- a/oauth2_provider/models.py +++ b/oauth2_provider/models.py @@ -1,14 +1,19 @@ +import hashlib import logging import time import uuid -from datetime import timedelta +from contextlib import suppress +from dataclasses import dataclass +from datetime import datetime, timedelta +from datetime import timezone as dt_timezone +from typing import Callable, Optional, Union from urllib.parse import parse_qsl, urlparse from django.apps import apps from django.conf import settings from django.contrib.auth.hashers import identify_hasher, make_password from django.core.exceptions import ImproperlyConfigured -from django.db import models, transaction +from django.db import models, router, transaction from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -38,12 +43,20 @@ def pre_save(self, model_instance, add): logger.debug(f"{model_instance}: {self.attname} is already hashed with {hasher}.") except ValueError: logger.debug(f"{model_instance}: {self.attname} is not hashed; hashing it now.") - hashed_secret = make_password(secret) + hashed_secret = make_password(secret, hasher=oauth2_settings.CLIENT_SECRET_HASHER) setattr(model_instance, self.attname, hashed_secret) return hashed_secret return super().pre_save(model_instance, add) +class TokenChecksumField(models.CharField): + def pre_save(self, model_instance, add): + token = getattr(model_instance, "token") + checksum = hashlib.sha256(token.encode("utf-8")).hexdigest() + setattr(model_instance, self.attname, checksum) + return super().pre_save(model_instance, add) + + class AbstractApplication(models.Model): """ An Application instance represents a Client on the Authorization server. @@ -76,12 +89,14 @@ class AbstractApplication(models.Model): ) GRANT_AUTHORIZATION_CODE = "authorization-code" + GRANT_DEVICE_CODE = "urn:ietf:params:oauth:grant-type:device_code" GRANT_IMPLICIT = "implicit" GRANT_PASSWORD = "password" GRANT_CLIENT_CREDENTIALS = "client-credentials" GRANT_OPENID_HYBRID = "openid-hybrid" GRANT_TYPES = ( (GRANT_AUTHORIZATION_CODE, _("Authorization code")), + (GRANT_DEVICE_CODE, _("Device Code")), (GRANT_IMPLICIT, _("Implicit")), (GRANT_PASSWORD, _("Resource owner password-based")), (GRANT_CLIENT_CREDENTIALS, _("Client credentials")), @@ -117,7 +132,7 @@ class AbstractApplication(models.Model): default="", ) client_type = models.CharField(max_length=32, choices=CLIENT_TYPES) - authorization_grant_type = models.CharField(max_length=32, choices=GRANT_TYPES) + authorization_grant_type = models.CharField(max_length=44, choices=GRANT_TYPES) client_secret = ClientSecretField( max_length=255, blank=True, @@ -203,7 +218,11 @@ def clean(self): if redirect_uris: validator = AllowedURIValidator( - allowed_schemes, name="redirect uri", allow_path=True, allow_query=True + allowed_schemes, + name="redirect uri", + allow_path=True, + allow_query=True, + allow_hostname_wildcard=oauth2_settings.ALLOW_URI_WILDCARDS, ) for uri in redirect_uris: validator(uri) @@ -217,7 +236,11 @@ def clean(self): allowed_origins = self.allowed_origins.strip().split() if allowed_origins: # oauthlib allows only https scheme for CORS - validator = AllowedURIValidator(oauth2_settings.ALLOWED_SCHEMES, "allowed origin") + validator = AllowedURIValidator( + oauth2_settings.ALLOWED_SCHEMES, + "allowed origin", + allow_hostname_wildcard=oauth2_settings.ALLOW_URI_WILDCARDS, + ) for uri in allowed_origins: validator(uri) @@ -235,7 +258,7 @@ def clean(self): raise ValidationError(_("You cannot use HS256 with public grants or clients")) def get_absolute_url(self): - return reverse("oauth2_provider:detail", args=[str(self.id)]) + return reverse("oauth2_provider:detail", args=[str(self.pk)]) def get_allowed_schemes(self): """ @@ -379,8 +402,10 @@ class AbstractAccessToken(models.Model): null=True, related_name="refreshed_access_token", ) - token = models.CharField( - max_length=255, + token = models.TextField() + token_checksum = TokenChecksumField( + max_length=64, + blank=False, unique=True, db_index=True, ) @@ -490,6 +515,7 @@ class AbstractRefreshToken(models.Model): null=True, related_name="refresh_token", ) + token_family = models.UUIDField(null=True, blank=True, editable=False) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) @@ -500,17 +526,19 @@ def revoke(self): Mark this refresh token revoked and revoke related access token """ access_token_model = get_access_token_model() + access_token_database = router.db_for_write(access_token_model) refresh_token_model = get_refresh_token_model() - with transaction.atomic(): + + # Use the access_token_database instead of making the assumption it is in 'default'. + with transaction.atomic(using=access_token_database): token = refresh_token_model.objects.select_for_update().filter(pk=self.pk, revoked__isnull=True) if not token: return self = list(token)[0] - try: + with suppress(access_token_model.DoesNotExist): access_token_model.objects.get(id=self.access_token_id).revoke() - except access_token_model.DoesNotExist: - pass + self.access_token = None self.revoked = timezone.now() self.save() @@ -627,11 +655,109 @@ class Meta(AbstractIDToken.Meta): swappable = "OAUTH2_PROVIDER_ID_TOKEN_MODEL" +class AbstractDeviceGrant(models.Model): + class Meta: + abstract = True + constraints = [ + models.UniqueConstraint( + fields=["device_code"], + name="%(app_label)s_%(class)s_unique_device_code", + ), + ] + + AUTHORIZED = "authorized" + AUTHORIZATION_PENDING = "authorization-pending" + EXPIRED = "expired" + DENIED = "denied" + + DEVICE_FLOW_STATUS = ( + (AUTHORIZED, _("Authorized")), + (AUTHORIZATION_PENDING, _("Authorization pending")), + (EXPIRED, _("Expired")), + (DENIED, _("Denied")), + ) + + id = models.BigAutoField(primary_key=True) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + related_name="%(app_label)s_%(class)s", + null=True, + blank=True, + on_delete=models.CASCADE, + ) + device_code = models.CharField(max_length=100, unique=True) + user_code = models.CharField(max_length=100) + scope = models.CharField(max_length=64, null=True) + interval = models.IntegerField(default=5) + expires = models.DateTimeField() + status = models.CharField( + max_length=64, blank=True, choices=DEVICE_FLOW_STATUS, default=AUTHORIZATION_PENDING + ) + client_id = models.CharField(max_length=100, db_index=True) + last_checked = models.DateTimeField(auto_now=True) + + def is_expired(self): + """ + Check device flow session expiration and set the status to "expired" if current time + is past the "expires" deadline. + """ + if self.status == self.EXPIRED: + return True + + now = datetime.now(tz=dt_timezone.utc) + if now >= self.expires: + self.status = self.EXPIRED + self.save(update_fields=["status"]) + return True + + return False + + +class DeviceGrant(AbstractDeviceGrant): + class Meta(AbstractDeviceGrant.Meta): + swappable = "OAUTH2_PROVIDER_DEVICE_GRANT_MODEL" + + +@dataclass +class DeviceRequest: + # https://datatracker.ietf.org/doc/html/rfc8628#section-3.1 + # scope is optional + client_id: str + scope: Optional[str] = None + + +@dataclass +class DeviceCodeResponse: + verification_uri: str + expires_in: int + user_code: int + device_code: str + interval: int + verification_uri_complete: Optional[Union[str, Callable]] = None + + +def create_device_grant(device_request: DeviceRequest, device_response: DeviceCodeResponse) -> DeviceGrant: + now = datetime.now(tz=dt_timezone.utc) + + return DeviceGrant.objects.create( + client_id=device_request.client_id, + device_code=device_response.device_code, + user_code=device_response.user_code, + scope=device_request.scope, + expires=now + timedelta(seconds=device_response.expires_in), + ) + + def get_application_model(): """Return the Application model that is active in this project.""" return apps.get_model(oauth2_settings.APPLICATION_MODEL) +def get_device_grant_model(): + """Return the DeviceGrant model that is active in this project.""" + return apps.get_model(oauth2_settings.DEVICE_GRANT_MODEL) + + def get_grant_model(): """Return the Grant model that is active in this project.""" return apps.get_model(oauth2_settings.GRANT_MODEL) @@ -643,7 +769,7 @@ def get_access_token_model(): def get_id_token_model(): - """Return the AccessToken model that is active in this project.""" + """Return the IDToken model that is active in this project.""" return apps.get_model(oauth2_settings.ID_TOKEN_MODEL) @@ -692,7 +818,7 @@ def batch_delete(queryset, query): flat_queryset = queryset.values_list("id", flat=True)[:CLEAR_EXPIRED_TOKENS_BATCH_SIZE] batch_length = flat_queryset.count() queryset.model.objects.filter(id__in=list(flat_queryset)).delete() - logger.debug(f"{batch_length} tokens deleted, {current_no-batch_length} left") + logger.debug(f"{batch_length} tokens deleted, {current_no - batch_length} left") queryset = queryset.model.objects.filter(query) time.sleep(CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL) current_no = queryset.count() @@ -762,12 +888,28 @@ def redirect_to_uri_allowed(uri, allowed_uris): :param allowed_uris: A list of URIs that are allowed """ + if not isinstance(allowed_uris, list): + raise ValueError("allowed_uris must be a list") + parsed_uri = urlparse(uri) uqs_set = set(parse_qsl(parsed_uri.query)) for allowed_uri in allowed_uris: parsed_allowed_uri = urlparse(allowed_uri) + if parsed_allowed_uri.scheme != parsed_uri.scheme: + # match failed, continue + continue + + """ check hostname """ + if oauth2_settings.ALLOW_URI_WILDCARDS and parsed_allowed_uri.hostname.startswith("*"): + """ wildcard hostname """ + if not parsed_uri.hostname.endswith(parsed_allowed_uri.hostname[1:]): + continue + elif parsed_allowed_uri.hostname != parsed_uri.hostname: + continue + # From RFC 8252 (Section 7.3) + # https://datatracker.ietf.org/doc/html/rfc8252#section-7.3 # # Loopback redirect URIs use the "http" scheme # [...] @@ -775,26 +917,26 @@ def redirect_to_uri_allowed(uri, allowed_uris): # time of the request for loopback IP redirect URIs, to accommodate # clients that obtain an available ephemeral port from the operating # system at the time of the request. + allowed_uri_is_loopback = parsed_allowed_uri.scheme == "http" and parsed_allowed_uri.hostname in [ + "127.0.0.1", + "::1", + ] + """ check port """ + if not allowed_uri_is_loopback and parsed_allowed_uri.port != parsed_uri.port: + continue + + """ check path """ + if parsed_allowed_uri.path != parsed_uri.path: + continue + + """ check querystring """ + aqs_set = set(parse_qsl(parsed_allowed_uri.query)) + if not aqs_set.issubset(uqs_set): + continue # circuit break - allowed_uri_is_loopback = ( - parsed_allowed_uri.scheme == "http" - and parsed_allowed_uri.hostname in ["127.0.0.1", "::1"] - and parsed_allowed_uri.port is None - ) - if ( - allowed_uri_is_loopback - and parsed_allowed_uri.scheme == parsed_uri.scheme - and parsed_allowed_uri.hostname == parsed_uri.hostname - and parsed_allowed_uri.path == parsed_uri.path - ) or ( - parsed_allowed_uri.scheme == parsed_uri.scheme - and parsed_allowed_uri.netloc == parsed_uri.netloc - and parsed_allowed_uri.path == parsed_uri.path - ): - aqs_set = set(parse_qsl(parsed_allowed_uri.query)) - if aqs_set.issubset(uqs_set): - return True + return True + # if uris matched then it's not allowed return False @@ -818,4 +960,5 @@ def is_origin_allowed(origin, allowed_origins): and parsed_allowed_origin.netloc == parsed_origin.netloc ): return True + return False diff --git a/oauth2_provider/oauth2_backends.py b/oauth2_provider/oauth2_backends.py index 3ddb9c90b..accd9d3f8 100644 --- a/oauth2_provider/oauth2_backends.py +++ b/oauth2_provider/oauth2_backends.py @@ -1,6 +1,7 @@ import json from urllib.parse import urlparse, urlunparse +from django.http import HttpRequest from oauthlib import oauth2 from oauthlib.common import Request as OauthlibRequest from oauthlib.common import quote, urlencode, urlencoded @@ -75,6 +76,8 @@ def extract_headers(self, request): del headers["wsgi.errors"] if "HTTP_AUTHORIZATION" in headers: headers["Authorization"] = headers["HTTP_AUTHORIZATION"] + if "CONTENT_TYPE" in headers: + headers["Content-Type"] = headers["CONTENT_TYPE"] # Add Access-Control-Allow-Origin header to the token endpoint response for authentication code grant, # if the origin is allowed by RequestValidator.is_origin_allowed. # https://github.com/oauthlib/oauthlib/pull/791 @@ -148,6 +151,16 @@ def create_authorization_response(self, request, scopes, credentials, allow): except oauth2.OAuth2Error as error: raise OAuthToolkitError(error=error, redirect_uri=credentials["redirect_uri"]) + def create_device_authorization_response(self, request: HttpRequest): + uri, http_method, body, headers = self._extract_params(request) + try: + headers, body, status = self.server.create_device_authorization_response( + uri, http_method, body, headers + ) + return headers, body, status + except OAuth2Error as exc: + return exc.headers, exc.json, exc.status_code + def create_token_response(self, request): """ A wrapper method that calls create_token_response on `server_class` instance. diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py index 47d65e851..ec974b0c6 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -1,5 +1,6 @@ import base64 import binascii +import hashlib import http.client import inspect import json @@ -14,8 +15,7 @@ from django.contrib.auth import authenticate, get_user_model from django.contrib.auth.hashers import check_password, identify_hasher from django.core.exceptions import ObjectDoesNotExist -from django.db import transaction -from django.db.models import Q +from django.db import router, transaction from django.http import HttpRequest from django.utils import dateformat, timezone from django.utils.crypto import constant_time_compare @@ -24,7 +24,7 @@ from jwcrypto import jws, jwt from jwcrypto.common import JWException from jwcrypto.jwt import JWTExpired -from oauthlib.oauth2.rfc6749 import utils +from oauthlib.oauth2.rfc6749 import errors, utils from oauthlib.openid import RequestValidator from .exceptions import FatalClientError @@ -52,10 +52,12 @@ "client_credentials": (AbstractApplication.GRANT_CLIENT_CREDENTIALS,), "refresh_token": ( AbstractApplication.GRANT_AUTHORIZATION_CODE, + AbstractApplication.GRANT_DEVICE_CODE, AbstractApplication.GRANT_PASSWORD, AbstractApplication.GRANT_CLIENT_CREDENTIALS, AbstractApplication.GRANT_OPENID_HYBRID, ), + "urn:ietf:params:oauth:grant-type:device_code": (AbstractApplication.GRANT_DEVICE_CODE,), } Application = get_application_model() @@ -166,6 +168,11 @@ def _authenticate_basic_auth(self, request): elif request.client.client_id != client_id: log.debug("Failed basic auth: wrong client id %s" % client_id) return False + elif ( + request.client.client_type == "public" + and request.grant_type == "urn:ietf:params:oauth:grant-type:device_code" + ): + return True elif not self._check_secret(client_secret, request.client.client_secret): log.debug("Failed basic auth: wrong client secret %s" % client_secret) return False @@ -191,6 +198,11 @@ def _authenticate_request_body(self, request): if self._load_application(client_id, request) is None: log.debug("Failed body auth: Application %s does not exists" % client_id) return False + elif ( + request.client.client_type == "public" + and request.grant_type == "urn:ietf:params:oauth:grant-type:device_code" + ): + return True elif not self._check_secret(client_secret, request.client.client_secret): log.debug("Failed body auth: wrong client secret %s" % client_secret) return False @@ -318,10 +330,13 @@ def confirm_redirect_uri(self, client_id, code, redirect_uri, client, *args, **k def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs): """ - Remove the temporary grant used to swap the authorization token + Remove the temporary grant used to swap the authorization token. + + :raises: InvalidGrantError if the grant does not exist. """ - grant = Grant.objects.get(code=code, application=request.client) - grant.delete() + deleted_grant_count, _ = Grant.objects.filter(code=code, application=request.client).delete() + if not deleted_grant_count: + raise errors.InvalidGrantError(request=request) def validate_client_id(self, client_id, request, *args, **kwargs): """ @@ -462,7 +477,12 @@ def validate_bearer_token(self, token, scopes, request): return False def _load_access_token(self, token): - return AccessToken.objects.select_related("application", "user").filter(token=token).first() + token_checksum = hashlib.sha256(token.encode("utf-8")).hexdigest() + return ( + AccessToken.objects.select_related("application", "user") + .filter(token_checksum=token_checksum) + .first() + ) def validate_code(self, client_id, code, client, request, *args, **kwargs): try: @@ -557,11 +577,23 @@ def rotate_refresh_token(self, request): """ return oauth2_settings.ROTATE_REFRESH_TOKEN - @transaction.atomic def save_bearer_token(self, token, request, *args, **kwargs): """ - Save access and refresh token, If refresh token is issued, remove or - reuse old refresh token as in rfc:`6` + Save access and refresh token. + + Override _save_bearer_token and not this function when adding custom logic + for the storing of these token. This allows the transaction logic to be + separate from the token handling. + """ + # Use the AccessToken's database instead of making the assumption it is in 'default'. + with transaction.atomic(using=router.db_for_write(AccessToken)): + return self._save_bearer_token(token, request, *args, **kwargs) + + def _save_bearer_token(self, token, request, *args, **kwargs): + """ + Save access and refresh token. + + If refresh token is issued, remove or reuse old refresh token as in rfc:`6`. @see: https://rfc-editor.org/rfc/rfc6749.html#section-6 """ @@ -617,7 +649,7 @@ def save_bearer_token(self, token, request, *args, **kwargs): # from the db while acquiring a lock on it # We also put it in the "request cache" refresh_token_instance = RefreshToken.objects.select_for_update().get( - id=refresh_token_instance.id + pk=refresh_token_instance.pk ) request.refresh_token_instance = refresh_token_instance @@ -644,7 +676,9 @@ def save_bearer_token(self, token, request, *args, **kwargs): source_refresh_token=refresh_token_instance, ) - self._create_refresh_token(request, refresh_token_code, access_token) + self._create_refresh_token( + request, refresh_token_code, access_token, refresh_token_instance + ) else: # make sure that the token data we're returning matches # the existing token @@ -688,9 +722,17 @@ def _create_authorization_code(self, request, code, expires=None): claims=json.dumps(request.claims or {}), ) - def _create_refresh_token(self, request, refresh_token_code, access_token): + def _create_refresh_token(self, request, refresh_token_code, access_token, previous_refresh_token): + if previous_refresh_token: + token_family = previous_refresh_token.token_family + else: + token_family = uuid.uuid4() return RefreshToken.objects.create( - user=request.user, token=refresh_token_code, application=request.client, access_token=access_token + user=request.user, + token=refresh_token_code, + application=request.client, + access_token=access_token, + token_family=token_family, ) def revoke_token(self, token, token_type_hint, request, *args, **kwargs): @@ -741,7 +783,7 @@ def get_original_scopes(self, refresh_token, request, *args, **kwargs): rt = request.refresh_token_instance if not rt.access_token_id: try: - return AccessToken.objects.get(source_refresh_token_id=rt.id).scope + return AccessToken.objects.get(source_refresh_token_id=rt.pk).scope except AccessToken.DoesNotExist: return [] return rt.access_token.scope @@ -752,25 +794,27 @@ def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs Also attach User instance to the request object """ - null_or_recent = Q(revoked__isnull=True) | Q( - revoked__gt=timezone.now() - timedelta(seconds=oauth2_settings.REFRESH_TOKEN_GRACE_PERIOD_SECONDS) - ) - rt = ( - RefreshToken.objects.filter(null_or_recent, token=refresh_token) - .select_related("access_token") - .first() - ) + rt = RefreshToken.objects.filter(token=refresh_token).select_related("access_token").first() if not rt: return False + if rt.revoked is not None and rt.revoked <= timezone.now() - timedelta( + seconds=oauth2_settings.REFRESH_TOKEN_GRACE_PERIOD_SECONDS + ): + if oauth2_settings.REFRESH_TOKEN_REUSE_PROTECTION and rt.token_family: + rt_token_family = RefreshToken.objects.filter(token_family=rt.token_family) + for related_rt in rt_token_family.all(): + related_rt.revoke() + return False + request.user = rt.user request.refresh_token = rt.token # Temporary store RefreshToken instance to be reused by get_original_scopes and save_bearer_token. request.refresh_token_instance = rt + return rt.application == client - @transaction.atomic def _save_id_token(self, jti, request, expires, *args, **kwargs): scopes = request.scope or " ".join(request.scopes) @@ -792,9 +836,9 @@ def get_jwt_bearer_token(self, token, token_handler, request): def get_claim_dict(self, request): if self._get_additional_claims_is_request_agnostic(): - claims = {"sub": lambda r: str(r.user.id)} + claims = {"sub": lambda r: str(r.user.pk)} else: - claims = {"sub": str(request.user.id)} + claims = {"sub": str(request.user.pk)} # https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims if self._get_additional_claims_is_request_agnostic(): @@ -871,7 +915,9 @@ def finalize_id_token(self, id_token, token, token_handler, request): claims=json.dumps(id_token, default=str), ) jwt_token.make_signed_token(request.client.jwk_key) - id_token = self._save_id_token(id_token["jti"], request, expiration_time) + # Use the IDToken's database instead of making the assumption it is in 'default'. + with transaction.atomic(using=router.db_for_write(IDToken)): + id_token = self._save_id_token(id_token["jti"], request, expiration_time) # this is needed by django rest framework request.access_token = id_token request.id_token = id_token diff --git a/oauth2_provider/settings.py b/oauth2_provider/settings.py index 950ab5643..216f36ba8 100644 --- a/oauth2_provider/settings.py +++ b/oauth2_provider/settings.py @@ -24,10 +24,13 @@ from django.utils.module_loading import import_string from oauthlib.common import Request +from oauth2_provider.utils import set_oauthlib_user_to_device_request_user, user_code_generator + USER_SETTINGS = getattr(settings, "OAUTH2_PROVIDER", None) APPLICATION_MODEL = getattr(settings, "OAUTH2_PROVIDER_APPLICATION_MODEL", "oauth2_provider.Application") +DEVICE_GRANT_MODEL = getattr(settings, "OAUTH2_PROVIDER_DEVICE_GRANT_MODEL", "oauth2_provider.DeviceGrant") ACCESS_TOKEN_MODEL = getattr(settings, "OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL", "oauth2_provider.AccessToken") ID_TOKEN_MODEL = getattr(settings, "OAUTH2_PROVIDER_ID_TOKEN_MODEL", "oauth2_provider.IDToken") GRANT_MODEL = getattr(settings, "OAUTH2_PROVIDER_GRANT_MODEL", "oauth2_provider.Grant") @@ -37,7 +40,12 @@ "CLIENT_ID_GENERATOR_CLASS": "oauth2_provider.generators.ClientIdGenerator", "CLIENT_SECRET_GENERATOR_CLASS": "oauth2_provider.generators.ClientSecretGenerator", "CLIENT_SECRET_GENERATOR_LENGTH": 128, + "CLIENT_SECRET_HASHER": "default", "ACCESS_TOKEN_GENERATOR": None, + "OAUTH_DEVICE_VERIFICATION_URI": None, + "OAUTH_DEVICE_VERIFICATION_URI_COMPLETE": None, + "OAUTH_DEVICE_USER_CODE_GENERATOR": user_code_generator, + "OAUTH_PRE_TOKEN_VALIDATION": [set_oauthlib_user_to_device_request_user], "REFRESH_TOKEN_GENERATOR": None, "EXTRA_SERVER_KWARGS": {}, "OAUTH2_SERVER_CLASS": "oauthlib.oauth2.Server", @@ -54,11 +62,14 @@ "ID_TOKEN_EXPIRE_SECONDS": 36000, "REFRESH_TOKEN_EXPIRE_SECONDS": None, "REFRESH_TOKEN_GRACE_PERIOD_SECONDS": 0, + "REFRESH_TOKEN_REUSE_PROTECTION": False, "ROTATE_REFRESH_TOKEN": True, "ERROR_RESPONSE_WITH_SCOPES": False, "APPLICATION_MODEL": APPLICATION_MODEL, "ACCESS_TOKEN_MODEL": ACCESS_TOKEN_MODEL, "ID_TOKEN_MODEL": ID_TOKEN_MODEL, + "DEVICE_GRANT_MODEL": DEVICE_GRANT_MODEL, + "DEVICE_FLOW_INTERVAL": 5, "GRANT_MODEL": GRANT_MODEL, "REFRESH_TOKEN_MODEL": REFRESH_TOKEN_MODEL, "APPLICATION_ADMIN_CLASS": "oauth2_provider.admin.ApplicationAdmin", @@ -69,6 +80,7 @@ "REQUEST_APPROVAL_PROMPT": "force", "ALLOWED_REDIRECT_URI_SCHEMES": ["http", "https"], "ALLOWED_SCHEMES": ["https"], + "ALLOW_URI_WILDCARDS": False, "OIDC_ENABLED": False, "OIDC_ISS_ENDPOINT": "", "OIDC_USERINFO_ENDPOINT": "", @@ -265,6 +277,11 @@ def server_kwargs(self): ("refresh_token_expires_in", "REFRESH_TOKEN_EXPIRE_SECONDS"), ("token_generator", "ACCESS_TOKEN_GENERATOR"), ("refresh_token_generator", "REFRESH_TOKEN_GENERATOR"), + ("verification_uri", "OAUTH_DEVICE_VERIFICATION_URI"), + ("verification_uri_complete", "OAUTH_DEVICE_VERIFICATION_URI_COMPLETE"), + ("interval", "DEVICE_FLOW_INTERVAL"), + ("user_code_generator", "OAUTH_DEVICE_USER_CODE_GENERATOR"), + ("pre_token", "OAUTH_PRE_TOKEN_VALIDATION"), ] } kwargs.update(self.EXTRA_SERVER_KWARGS) diff --git a/oauth2_provider/templates/oauth2_provider/application_detail.html b/oauth2_provider/templates/oauth2_provider/application_detail.html index 440518903..74b71ee74 100644 --- a/oauth2_provider/templates/oauth2_provider/application_detail.html +++ b/oauth2_provider/templates/oauth2_provider/application_detail.html @@ -49,8 +49,8 @@

{{ application.name }}

{% endblock content %} diff --git a/oauth2_provider/templates/oauth2_provider/application_form.html b/oauth2_provider/templates/oauth2_provider/application_form.html index dd8a644e8..7d8c07989 100644 --- a/oauth2_provider/templates/oauth2_provider/application_form.html +++ b/oauth2_provider/templates/oauth2_provider/application_form.html @@ -3,7 +3,7 @@ {% load i18n %} {% block content %}
-
+

{% block app-form-title %} {% trans "Edit application" %} {{ application.name }} @@ -31,7 +31,7 @@

- + {% trans "Go Back" %} diff --git a/oauth2_provider/templates/oauth2_provider/application_list.html b/oauth2_provider/templates/oauth2_provider/application_list.html index 807c050d3..509ccfc94 100644 --- a/oauth2_provider/templates/oauth2_provider/application_list.html +++ b/oauth2_provider/templates/oauth2_provider/application_list.html @@ -7,7 +7,7 @@

{% trans "Your applications" %}

{% if applications %} diff --git a/oauth2_provider/templates/oauth2_provider/device/accept_deny.html b/oauth2_provider/templates/oauth2_provider/device/accept_deny.html new file mode 100644 index 000000000..4fd31a6fb --- /dev/null +++ b/oauth2_provider/templates/oauth2_provider/device/accept_deny.html @@ -0,0 +1,16 @@ +{% extends "oauth2_provider/base.html" %} +{% block content %} + + + Accept or Deny + + +

Please choose an action:

+ + {% csrf_token %} + + + + + +{% endblock content %} diff --git a/oauth2_provider/templates/oauth2_provider/device/device_grant_status.html b/oauth2_provider/templates/oauth2_provider/device/device_grant_status.html new file mode 100644 index 000000000..f2f0a6292 --- /dev/null +++ b/oauth2_provider/templates/oauth2_provider/device/device_grant_status.html @@ -0,0 +1,11 @@ +{% extends "oauth2_provider/base.html" %} +{% block content %} + + + Device + + +

Device {{ object.get_status_display }}

+ + +{% endblock content %} diff --git a/oauth2_provider/templates/oauth2_provider/device/user_code.html b/oauth2_provider/templates/oauth2_provider/device/user_code.html new file mode 100644 index 000000000..774b95897 --- /dev/null +++ b/oauth2_provider/templates/oauth2_provider/device/user_code.html @@ -0,0 +1,16 @@ +{% extends "oauth2_provider/base.html" %} +{% block content %} + + + Device code + + +

Enter code displayed on device

+
+ {% csrf_token %} + {{ form.as_p }} + +
+ + +{% endblock content %} diff --git a/oauth2_provider/urls.py b/oauth2_provider/urls.py index 18972612c..ea974e045 100644 --- a/oauth2_provider/urls.py +++ b/oauth2_provider/urls.py @@ -1,4 +1,4 @@ -from django.urls import re_path +from django.urls import path, re_path from . import views @@ -7,24 +7,36 @@ base_urlpatterns = [ - re_path(r"^authorize/$", views.AuthorizationView.as_view(), name="authorize"), - re_path(r"^token/$", views.TokenView.as_view(), name="token"), - re_path(r"^revoke_token/$", views.RevokeTokenView.as_view(), name="revoke-token"), - re_path(r"^introspect/$", views.IntrospectTokenView.as_view(), name="introspect"), + path("authorize/", views.AuthorizationView.as_view(), name="authorize"), + path("token/", views.TokenView.as_view(), name="token"), + path("revoke_token/", views.RevokeTokenView.as_view(), name="revoke-token"), + path("introspect/", views.IntrospectTokenView.as_view(), name="introspect"), + path("device-authorization/", views.DeviceAuthorizationView.as_view(), name="device-authorization"), + path("device/", views.DeviceUserCodeView.as_view(), name="device"), + path( + "device-confirm//", + views.DeviceConfirmView.as_view(), + name="device-confirm", + ), + path( + "device-grant-status//", + views.DeviceGrantStatusView.as_view(), + name="device-grant-status", + ), ] management_urlpatterns = [ # Application management views - re_path(r"^applications/$", views.ApplicationList.as_view(), name="list"), - re_path(r"^applications/register/$", views.ApplicationRegistration.as_view(), name="register"), - re_path(r"^applications/(?P[\w-]+)/$", views.ApplicationDetail.as_view(), name="detail"), - re_path(r"^applications/(?P[\w-]+)/delete/$", views.ApplicationDelete.as_view(), name="delete"), - re_path(r"^applications/(?P[\w-]+)/update/$", views.ApplicationUpdate.as_view(), name="update"), + path("applications/", views.ApplicationList.as_view(), name="list"), + path("applications/register/", views.ApplicationRegistration.as_view(), name="register"), + path("applications//", views.ApplicationDetail.as_view(), name="detail"), + path("applications//delete/", views.ApplicationDelete.as_view(), name="delete"), + path("applications//update/", views.ApplicationUpdate.as_view(), name="update"), # Token management views - re_path(r"^authorized_tokens/$", views.AuthorizedTokensListView.as_view(), name="authorized-token-list"), - re_path( - r"^authorized_tokens/(?P[\w-]+)/delete/$", + path("authorized_tokens/", views.AuthorizedTokensListView.as_view(), name="authorized-token-list"), + path( + "authorized_tokens//delete/", views.AuthorizedTokenDeleteView.as_view(), name="authorized-token-delete", ), @@ -40,9 +52,9 @@ views.ConnectDiscoveryInfoView.as_view(), name="oidc-connect-discovery-info", ), - re_path(r"^\.well-known/jwks.json$", views.JwksInfoView.as_view(), name="jwks-info"), - re_path(r"^userinfo/$", views.UserInfoView.as_view(), name="user-info"), - re_path(r"^logout/$", views.RPInitiatedLogoutView.as_view(), name="rp-initiated-logout"), + path(".well-known/jwks.json", views.JwksInfoView.as_view(), name="jwks-info"), + path("userinfo/", views.UserInfoView.as_view(), name="user-info"), + path("logout/", views.RPInitiatedLogoutView.as_view(), name="rp-initiated-logout"), ] diff --git a/oauth2_provider/utils.py b/oauth2_provider/utils.py index 3f48723c5..a009d8a0e 100644 --- a/oauth2_provider/utils.py +++ b/oauth2_provider/utils.py @@ -1,7 +1,9 @@ import functools +import random from django.conf import settings from jwcrypto import jwk +from oauthlib.common import Request @functools.lru_cache() @@ -32,3 +34,69 @@ def get_timezone(time_zone): return pytz.timezone(time_zone) return zoneinfo.ZoneInfo(time_zone) + + +def user_code_generator(user_code_length: int = 8) -> str: + """ + Recommended user code that retains enough entropy but doesn't + ruin the user experience of typing the code in. + + the below is based off: + https://datatracker.ietf.org/doc/html/rfc8628#section-5.1 + but with added explanation as to where 34.5 bits of entropy is coming from + + entropy (in bits) = length of user code * log2(length of set of chars) + e = 8 * log2(20) + e = 34.5 + + log2(20) is used here to say "you can make 20 yes/no decisions per user code single input character". + + _ _ _ _ - _ _ _ _ = 20^8 ~= 2^35.5 + * + + * you have 20 choices of chars to choose from (20 yes no decisions) + and so on for the other 7 spaces + + in english this means an attacker would need to try + 2^34.5 unique combinations to exhaust all possibilities. + however with a user code only being valid for 30 seconds + and rate limiting, a brute force attack is extremely unlikely + to work + + for our function we'll be using a base 32 character set + """ + if user_code_length < 1: + raise ValueError("user_code_length needs to be greater than 0") + + # base32 character space + character_space = "0123456789ABCDEFGHIJKLMNOPQRSTUV" + + # being explicit with length + user_code = [""] * user_code_length + + for i in range(user_code_length): + user_code[i] = random.choice(character_space) + + return "".join(user_code) + + +def set_oauthlib_user_to_device_request_user(request: Request) -> None: + """ + The user isn't known when the device flow is initiated by a device. + All we know is the client_id. + + However, when the user logins in order to submit the user code + from the device we now know which user is trying to authenticate + their device. We update the device user field at this point + and save it in the db. + + This function is added to the pre_token stage during the device code grant's + create_token_response where we have the oauthlib Request object which is what's used + to populate the user field in the device model + """ + # Since this function is used in the settings module, it will lead to circular imports + # since django isn't fully initialised yet when settings run + from oauth2_provider.models import DeviceGrant, get_device_grant_model + + device: DeviceGrant = get_device_grant_model().objects.get(device_code=request._params["device_code"]) + request.user = device.user diff --git a/oauth2_provider/validators.py b/oauth2_provider/validators.py index 1654dccd7..b2370cfd0 100644 --- a/oauth2_provider/validators.py +++ b/oauth2_provider/validators.py @@ -1,5 +1,4 @@ import re -import warnings from urllib.parse import urlsplit from django.core.exceptions import ValidationError @@ -19,24 +18,18 @@ class URIValidator(URLValidator): regex = re.compile(scheme_re + host_re + port_re + path_re, re.IGNORECASE) -class RedirectURIValidator(URIValidator): - def __init__(self, allowed_schemes, allow_fragments=False): - warnings.warn("This class is deprecated and will be removed in version 2.5.0.", DeprecationWarning) - super().__init__(schemes=allowed_schemes) - self.allow_fragments = allow_fragments - - def __call__(self, value): - super().__call__(value) - value = force_str(value) - scheme, netloc, path, query, fragment = urlsplit(value) - if fragment and not self.allow_fragments: - raise ValidationError("Redirect URIs must not contain fragments") - - class AllowedURIValidator(URIValidator): # TODO: find a way to get these associated with their form fields in place of passing name # TODO: submit PR to get `cause` included in the parent class ValidationError params` - def __init__(self, schemes, name, allow_path=False, allow_query=False, allow_fragments=False): + def __init__( + self, + schemes, + name, + allow_path=False, + allow_query=False, + allow_fragments=False, + allow_hostname_wildcard=False, + ): """ :param schemes: List of allowed schemes. E.g.: ["https"] :param name: Name of the validated URI. It is required for validation message. E.g.: "Origin" @@ -49,6 +42,7 @@ def __init__(self, schemes, name, allow_path=False, allow_query=False, allow_fra self.allow_path = allow_path self.allow_query = allow_query self.allow_fragments = allow_fragments + self.allow_hostname_wildcard = allow_hostname_wildcard def __call__(self, value): value = force_str(value) @@ -83,29 +77,59 @@ def __call__(self, value): params={"name": self.name, "value": value, "cause": "path not allowed"}, ) + if self.allow_hostname_wildcard and "*" in netloc: + domain_parts = netloc.split(".") + if netloc.count("*") > 1: + raise ValidationError( + "%(name)s URI validation error. %(cause)s: %(value)s", + params={ + "name": self.name, + "value": value, + "cause": "only one wildcard is allowed in the hostname", + }, + ) + if not netloc.startswith("*"): + raise ValidationError( + "%(name)s URI validation error. %(cause)s: %(value)s", + params={ + "name": self.name, + "value": value, + "cause": "wildcards must be at the beginning of the hostname", + }, + ) + if len(domain_parts) < 3: + raise ValidationError( + "%(name)s URI validation error. %(cause)s: %(value)s", + params={ + "name": self.name, + "value": value, + "cause": "wildcards cannot be in the top level or second level domain", + }, + ) + + # strip the wildcard from the netloc, we'll reassamble the value later to pass to URI Validator + if netloc.startswith("*."): + netloc = netloc[2:] + else: + netloc = netloc[1:] + + # domains cannot start with a hyphen, but can have them in the middle, so we strip hyphens + # after the wildcard so the final domain is valid and will succeed in URIVAlidator + if netloc.startswith("-"): + netloc = netloc[1:] + + # we stripped the wildcard from the netloc and path if they were allowed and present since they would + # fail validation we'll reassamble the URI to pass to the URIValidator + reassambled_uri = f"{scheme}://{netloc}{path}" + if query: + reassambled_uri += f"?{query}" + if fragment: + reassambled_uri += f"#{fragment}" + try: - super().__call__(value) + super().__call__(reassambled_uri) except ValidationError as e: raise ValidationError( "%(name)s URI validation error. %(cause)s: %(value)s", params={"name": self.name, "value": value, "cause": e}, ) - - -## -# WildcardSet is a special set that contains everything. -# This is required in order to move validation of the scheme from -# URLValidator (the base class of URIValidator), to OAuth2Application.clean(). - - -class WildcardSet(set): - """ - A set that always returns True on `in`. - """ - - def __init__(self, *args, **kwargs): - warnings.warn("This class is deprecated and will be removed in version 2.5.0.", DeprecationWarning) - super().__init__(*args, **kwargs) - - def __contains__(self, item): - return True diff --git a/oauth2_provider/views/__init__.py b/oauth2_provider/views/__init__.py index 9e32e17d8..24022f55e 100644 --- a/oauth2_provider/views/__init__.py +++ b/oauth2_provider/views/__init__.py @@ -17,3 +17,4 @@ from .introspect import IntrospectTokenView from .oidc import ConnectDiscoveryInfoView, JwksInfoView, RPInitiatedLogoutView, UserInfoView from .token import AuthorizedTokenDeleteView, AuthorizedTokensListView +from .device import DeviceAuthorizationView, DeviceUserCodeView, DeviceConfirmView, DeviceGrantStatusView diff --git a/oauth2_provider/views/base.py b/oauth2_provider/views/base.py index cad36c757..43c8e3213 100644 --- a/oauth2_provider/views/base.py +++ b/oauth2_provider/views/base.py @@ -1,7 +1,9 @@ +import hashlib import json import logging from urllib.parse import parse_qsl, urlencode, urlparse +from django import http from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.views import redirect_to_login from django.http import HttpResponse @@ -11,7 +13,11 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.debug import sensitive_post_parameters from django.views.generic import FormView, View +from oauthlib.oauth2.rfc8628 import errors as rfc8628_errors +from oauth2_provider.models import DeviceGrant + +from ..compat import login_not_required from ..exceptions import OAuthToolkitError from ..forms import AllowForm from ..http import OAuth2ResponseRedirect @@ -25,6 +31,8 @@ log = logging.getLogger("oauth2_provider") +# login_not_required decorator to bypass LoginRequiredMiddleware +@method_decorator(login_not_required, name="dispatch") class BaseAuthorizationView(LoginRequiredMixin, OAuthLibMixin, View): """ Implements a generic endpoint to handle *Authorization Requests* as in :rfc:`4.1.1`. The view @@ -182,6 +190,10 @@ def get(self, request, *args, **kwargs): # a successful response depending on "approval_prompt" url parameter require_approval = request.GET.get("approval_prompt", oauth2_settings.REQUEST_APPROVAL_PROMPT) + if "ui_locales" in credentials and isinstance(credentials["ui_locales"], list): + # Make sure ui_locales a space separated string for oauthlib to handle it correctly. + credentials["ui_locales"] = " ".join(credentials["ui_locales"]) + try: # If skip_authorization field is True, skip the authorization screen even # if this is the first use of the application and there was no previous authorization. @@ -273,6 +285,7 @@ def handle_no_permission(self): @method_decorator(csrf_exempt, name="dispatch") +@method_decorator(login_not_required, name="dispatch") class TokenView(OAuthLibMixin, View): """ Implements an endpoint to provide access tokens @@ -281,15 +294,19 @@ class TokenView(OAuthLibMixin, View): * Authorization code * Password * Client credentials + * Device code flow (specifically for the device polling stage) """ - @method_decorator(sensitive_post_parameters("password")) - def post(self, request, *args, **kwargs): + @method_decorator(sensitive_post_parameters("password", "client_secret")) + def authorization_flow_token_response( + self, request: http.HttpRequest, *args, **kwargs + ) -> http.HttpResponse: url, headers, body, status = self.create_token_response(request) if status == 200: access_token = json.loads(body).get("access_token") if access_token is not None: - token = get_access_token_model().objects.get(token=access_token) + token_checksum = hashlib.sha256(access_token.encode("utf-8")).hexdigest() + token = get_access_token_model().objects.get(token_checksum=token_checksum) app_authorized.send(sender=self, request=request, token=token) response = HttpResponse(content=body, status=status) @@ -297,8 +314,71 @@ def post(self, request, *args, **kwargs): response[k] = v return response + def device_flow_token_response( + self, request: http.HttpRequest, device_code: str, *args, **kwargs + ) -> http.HttpResponse: + try: + device = DeviceGrant.objects.get(device_code=device_code) + except DeviceGrant.DoesNotExist: + # The RFC does not mention what to return when the device is not found, + # but to keep it consistent with the other errors, we return the error + # in json format with an "error" key and the value formatted in the same + # way. + return http.HttpResponseNotFound( + content='{"error": "device_not_found"}', + content_type="application/json", + ) + + # Here we are returning the errors according to + # https://datatracker.ietf.org/doc/html/rfc8628#section-3.5 + # TODO: "slow_down" error (essentially rate-limiting). + if device.status == device.AUTHORIZATION_PENDING: + error = rfc8628_errors.AuthorizationPendingError() + elif device.status == device.DENIED: + error = rfc8628_errors.AccessDenied() + elif device.status == device.EXPIRED: + error = rfc8628_errors.ExpiredTokenError() + elif device.status != device.AUTHORIZED: + # It's technically impossible to get here because we've exhausted + # all the possible values for status. However, it does act as a + # reminder for developers when they add, in the future, new values + # (such as slow_down) that they must handle here. + return http.HttpResponseServerError( + content='{"error": "internal_error"}', + content_type="application/json", + ) + else: + # AUTHORIZED is the only accepted state, anything else is + # rejected. + error = None + + if error: + return http.HttpResponse( + content=error.json, + status=error.status_code, + content_type="application/json", + ) + + url, headers, body, status = self.create_token_response(request) + response = http.JsonResponse(data=json.loads(body), status=status) + + if status != 200: + return response + + for k, v in headers.items(): + response[k] = v + + return response + + def post(self, request: http.HttpRequest, *args, **kwargs) -> http.HttpResponse: + params = request.POST + if params.get("grant_type") == "urn:ietf:params:oauth:grant-type:device_code": + return self.device_flow_token_response(request, params["device_code"]) + return self.authorization_flow_token_response(request) + @method_decorator(csrf_exempt, name="dispatch") +@method_decorator(login_not_required, name="dispatch") class RevokeTokenView(OAuthLibMixin, View): """ Implements an endpoint to revoke access or refresh tokens diff --git a/oauth2_provider/views/device.py b/oauth2_provider/views/device.py new file mode 100644 index 000000000..f3dccf2ba --- /dev/null +++ b/oauth2_provider/views/device.py @@ -0,0 +1,196 @@ +import json + +from django import forms, http +from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.exceptions import ValidationError +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt +from django.views.generic import DetailView, FormView, View +from oauthlib.oauth2 import DeviceApplicationServer + +from oauth2_provider.compat import login_not_required +from oauth2_provider.models import ( + DeviceCodeResponse, + DeviceGrant, + DeviceRequest, + create_device_grant, + get_device_grant_model, +) +from oauth2_provider.views.mixins import OAuthLibMixin + + +@method_decorator(csrf_exempt, name="dispatch") +@method_decorator(login_not_required, name="dispatch") +class DeviceAuthorizationView(OAuthLibMixin, View): + server_class = DeviceApplicationServer + + def post(self, request, *args, **kwargs): + headers, response, status = self.create_device_authorization_response(request) + + if status != 200: + return http.JsonResponse(data=json.loads(response), status=status, headers=headers) + + device_request = DeviceRequest(client_id=request.POST["client_id"], scope=request.POST.get("scope")) + device_response = DeviceCodeResponse(**response) + create_device_grant(device_request, device_response) + + return http.JsonResponse(data=response, status=status, headers=headers) + + +class DeviceGrantForm(forms.Form): + user_code = forms.CharField(required=True) + + def clean_user_code(self): + """ + Performs validation on the user_code provided by the user and adds to the cleaned_data dict + the "device_grant" object associated with the user_code, which is useful to process the + response in the DeviceUserCodeView. + + It can raise one of the following ValidationErrors, with the associated codes: + + * incorrect_user_code: if a device grant associated with the user_code does not exist + * expired_user_code: if the device grant associated with the user_code has expired + * user_code_already_used: if the device grant associated with the user_code has been already + approved or denied. The only accepted state of the device grant is AUTHORIZATION_PENDING. + """ + cleaned_data = super().clean() + user_code: str = cleaned_data["user_code"] + try: + device_grant: DeviceGrant = get_device_grant_model().objects.get(user_code=user_code) + except DeviceGrant.DoesNotExist: + raise ValidationError("Incorrect user code", code="incorrect_user_code") + + if device_grant.is_expired(): + raise ValidationError("Expired user code", code="expired_user_code") + + # User of device has already made their decision for this device. + if device_grant.status != device_grant.AUTHORIZATION_PENDING: + raise ValidationError("User code has already been used", code="user_code_already_used") + + # Make the device_grant available to the View, saving one additional db call. + cleaned_data["device_grant"] = device_grant + + return user_code + + +class DeviceUserCodeView(LoginRequiredMixin, FormView): + """ + The view where the user is instructed (by the device) to come to in order to + enter the user code. More details in this section of the RFC: + https://datatracker.ietf.org/doc/html/rfc8628#section-3.3 + + Note: it's common to see in other implementations of this RFC that only ask the + user to sign in after they input the user code but since the user has to be signed + in regardless, to approve the device login we're making the decision here, for + simplicity, to require being logged in up front. + """ + + template_name = "oauth2_provider/device/user_code.html" + form_class = DeviceGrantForm + + def get_success_url(self): + return reverse( + "oauth2_provider:device-confirm", + kwargs={ + "client_id": self.device_grant.client_id, + "user_code": self.device_grant.user_code, + }, + ) + + def form_valid(self, form): + """ + Sets the device_grant on the instance so that it can be accessed + in get_success_url. It comes in handy when users want to overwrite + get_success_url, redirecting to the URL with the URL params pointing + to the current device. + """ + device_grant: DeviceGrant = form.cleaned_data["device_grant"] + + device_grant.user = self.request.user + device_grant.save(update_fields=["user"]) + + self.device_grant = device_grant + + return super().form_valid(form) + + +class DeviceConfirmForm(forms.Form): + """ + Simple form for the user to approve or deny the device. + """ + + action = forms.CharField(required=True) + + +class DeviceConfirmView(LoginRequiredMixin, FormView): + """ + The view where the user approves or denies a device. + """ + + template_name = "oauth2_provider/device/accept_deny.html" + form_class = DeviceConfirmForm + + def get_object(self): + """ + Returns the DeviceGrant object in the AUTHORIZATION_PENDING state identified + by the slugs client_id and user_code. Raises Http404 if not found. + """ + client_id, user_code = self.kwargs.get("client_id"), self.kwargs.get("user_code") + return get_object_or_404( + DeviceGrant, + client_id=client_id, + user_code=user_code, + status=DeviceGrant.AUTHORIZATION_PENDING, + ) + + def get_success_url(self): + return reverse( + "oauth2_provider:device-grant-status", + kwargs={ + "client_id": self.kwargs["client_id"], + "user_code": self.kwargs["user_code"], + }, + ) + + def get(self, request, *args, **kwargs): + """ + Enable GET requests for improved user experience. But validate that the URL params + are correct (i.e. there exists a device grant in the db that corresponds to the URL + params) by calling .get_object() + """ + _ = self.get_object() # raises 404 if URL parameters are incorrect + return super().get(request, args, kwargs) + + def form_valid(self, form): + """ + Uses get_object() to retrieves the DeviceGrant object and updates its state + to authorized or denied, based on the user input. + """ + device = self.get_object() + action = form.cleaned_data["action"] + + if action == "accept": + device.status = device.AUTHORIZED + device.save(update_fields=["status"]) + return super().form_valid(form) + elif action == "deny": + device.status = device.DENIED + device.save(update_fields=["status"]) + return super().form_valid(form) + else: + return http.HttpResponseBadRequest() + + +class DeviceGrantStatusView(LoginRequiredMixin, DetailView): + """ + The view to display the status of a DeviceGrant. + """ + + model = DeviceGrant + template_name = "oauth2_provider/device/device_grant_status.html" + + def get_object(self): + client_id, user_code = self.kwargs.get("client_id"), self.kwargs.get("user_code") + return get_object_or_404(DeviceGrant, client_id=client_id, user_code=user_code) diff --git a/oauth2_provider/views/introspect.py b/oauth2_provider/views/introspect.py index 04ca92a38..5b9810c82 100644 --- a/oauth2_provider/views/introspect.py +++ b/oauth2_provider/views/introspect.py @@ -1,15 +1,18 @@ import calendar +import hashlib from django.core.exceptions import ObjectDoesNotExist from django.http import JsonResponse from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt -from oauth2_provider.models import get_access_token_model -from oauth2_provider.views.generic import ClientProtectedScopedResourceView +from ..compat import login_not_required +from ..models import get_access_token_model +from ..views.generic import ClientProtectedScopedResourceView @method_decorator(csrf_exempt, name="dispatch") +@method_decorator(login_not_required, name="dispatch") class IntrospectTokenView(ClientProtectedScopedResourceView): """ Implements an endpoint for token introspection based @@ -23,9 +26,17 @@ class IntrospectTokenView(ClientProtectedScopedResourceView): @staticmethod def get_token_response(token_value=None): + if token_value is None: + return JsonResponse( + {"error": "invalid_request", "error_description": "Token parameter is missing."}, + status=400, + ) try: + token_checksum = hashlib.sha256(token_value.encode("utf-8")).hexdigest() token = ( - get_access_token_model().objects.select_related("user", "application").get(token=token_value) + get_access_token_model() + .objects.select_related("user", "application") + .get(token_checksum=token_checksum) ) except ObjectDoesNotExist: return JsonResponse({"active": False}, status=200) diff --git a/oauth2_provider/views/mixins.py b/oauth2_provider/views/mixins.py index 203d0103b..be2a77e8d 100644 --- a/oauth2_provider/views/mixins.py +++ b/oauth2_provider/views/mixins.py @@ -2,7 +2,7 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation -from django.http import HttpResponseForbidden, HttpResponseNotFound +from django.http import HttpRequest, HttpResponseForbidden, HttpResponseNotFound from ..exceptions import FatalClientError from ..scopes import get_scopes_backend @@ -114,6 +114,15 @@ def create_authorization_response(self, request, scopes, credentials, allow): core = self.get_oauthlib_core() return core.create_authorization_response(request, scopes, credentials, allow) + def create_device_authorization_response(self, request: HttpRequest): + """ + A wrapper method that calls create_device_authorization_response on `server_class` + instance. + :param request: The current django.http.HttpRequest object + """ + core = self.get_oauthlib_core() + return core.create_device_authorization_response(request) + def create_token_response(self, request): """ A wrapper method that calls create_token_response on `server_class` instance. diff --git a/oauth2_provider/views/oidc.py b/oauth2_provider/views/oidc.py index 584b0c895..a252f1be4 100644 --- a/oauth2_provider/views/oidc.py +++ b/oauth2_provider/views/oidc.py @@ -1,5 +1,4 @@ import json -import warnings from urllib.parse import urlparse from django.contrib.auth import logout @@ -15,6 +14,7 @@ from jwcrypto.jwt import JWTExpired from oauthlib.common import add_params_to_uri +from ..compat import login_not_required from ..exceptions import ( ClientIdMissmatch, InvalidIDTokenError, @@ -40,6 +40,7 @@ Application = get_application_model() +@method_decorator(login_not_required, name="dispatch") class ConnectDiscoveryInfoView(OIDCOnlyMixin, View): """ View used to show oidc provider configuration information per @@ -107,6 +108,7 @@ def get(self, request, *args, **kwargs): return response +@method_decorator(login_not_required, name="dispatch") class JwksInfoView(OIDCOnlyMixin, View): """ View used to show oidc json web key set document @@ -135,6 +137,7 @@ def get(self, request, *args, **kwargs): @method_decorator(csrf_exempt, name="dispatch") +@method_decorator(login_not_required, name="dispatch") class UserInfoView(OIDCOnlyMixin, OAuthLibMixin, View): """ View used to show Claims about the authenticated End-User @@ -212,76 +215,7 @@ def _validate_claims(request, claims): return True -def validate_logout_request(request, id_token_hint, client_id, post_logout_redirect_uri): - """ - Validate an OIDC RP-Initiated Logout Request. - `(prompt_logout, (post_logout_redirect_uri, application), token_user)` is returned. - - `prompt_logout` indicates whether the logout has to be confirmed by the user. This happens if the - specifications force a confirmation, or it is enabled by `OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT`. - `post_logout_redirect_uri` is the validated URI where the User should be redirected to after the - logout. Can be None. None will redirect to "/" of this app. If it is set `application` will also - be set to the Application that is requesting the logout. `token_user` is the id_token user, which will - used to revoke the tokens if found. - - The `id_token_hint` will be validated if given. If both `client_id` and `id_token_hint` are given they - will be validated against each other. - """ - - warnings.warn("This method is deprecated and will be removed in version 2.5.0.", DeprecationWarning) - - id_token = None - must_prompt_logout = True - token_user = None - if id_token_hint: - # Only basic validation has been done on the IDToken at this point. - id_token, claims = _load_id_token(id_token_hint) - - if not id_token or not _validate_claims(request, claims): - raise InvalidIDTokenError() - - token_user = id_token.user - - if id_token.user == request.user: - # A logout without user interaction (i.e. no prompt) is only allowed - # if an ID Token is provided that matches the current user. - must_prompt_logout = False - - # If both id_token_hint and client_id are given it must be verified that they match. - if client_id: - if id_token.application.client_id != client_id: - raise ClientIdMissmatch() - - # The standard states that a prompt should always be shown. - # This behaviour can be configured with OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT. - prompt_logout = must_prompt_logout or oauth2_settings.OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT - - application = None - # Determine the application that is requesting the logout. - if client_id: - application = get_application_model().objects.get(client_id=client_id) - elif id_token: - application = id_token.application - - # Validate `post_logout_redirect_uri` - if post_logout_redirect_uri: - if not application: - raise InvalidOIDCClientError() - scheme = urlparse(post_logout_redirect_uri)[0] - if not scheme: - raise InvalidOIDCRedirectURIError("A Scheme is required for the redirect URI.") - if oauth2_settings.OIDC_RP_INITIATED_LOGOUT_STRICT_REDIRECT_URIS and ( - scheme == "http" and application.client_type != "confidential" - ): - raise InvalidOIDCRedirectURIError("http is only allowed with confidential clients.") - if scheme not in application.get_allowed_schemes(): - raise InvalidOIDCRedirectURIError(f'Redirect to scheme "{scheme}" is not permitted.') - if not application.post_logout_redirect_uri_allowed(post_logout_redirect_uri): - raise InvalidOIDCRedirectURIError("This client does not have this redirect uri registered.") - - return prompt_logout, (post_logout_redirect_uri, application), token_user - - +@method_decorator(login_not_required, name="dispatch") class RPInitiatedLogoutView(OIDCLogoutOnlyMixin, FormView): template_name = "oauth2_provider/logout_confirm.html" form_class = ConfirmLogoutForm @@ -433,17 +367,66 @@ def validate_logout_request(self, id_token_hint, client_id, post_logout_redirect return application, id_token.user if id_token else None def must_prompt(self, token_user): - """Indicate whether the logout has to be confirmed by the user. This happens if the - specifications force a confirmation, or it is enabled by `OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT`. + """ + per: https://openid.net/specs/openid-connect-rpinitiated-1_0.html + + > At the Logout Endpoint, the OP SHOULD ask the End-User whether to log + > out of the OP as well. Furthermore, the OP MUST ask the End-User this + > question if an id_token_hint was not provided or if the supplied ID + > Token does not belong to the current OP session with the RP and/or + > currently logged in End-User. - A logout without user interaction (i.e. no prompt) is only allowed - if an ID Token is provided that matches the current user. """ - return ( - oauth2_settings.OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT - or token_user is None - or token_user != self.request.user - ) + + if not self.request.user.is_authenticated: + """ + > the OP MUST ask ask the End-User whether to log out of the OP as + + If the user does not have an active session with the OP, they cannot + end their OP session, so there is nothing to prompt for. This occurs + in cases where the user has logged out of the OP via another channel + such as the OP's own logout page, session timeout or another RP's + logout page. + """ + return False + + if oauth2_settings.OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT: + """ + > At the Logout Endpoint, the OP SHOULD ask the End-User whether to + > log out of the OP as well + + The admin has configured the OP to always prompt the userfor logout + per the SHOULD recommendation. + """ + return True + + if token_user is None: + """ + > the OP MUST ask ask the End-User whether to log out of the OP as + > well if the supplied ID Token does not belong to the current OP + > session with the RP. + + token_user will only be populated if an ID token was found for the + RP (Application) that is requesting the logout. If token_user is not + then we must prompt the user. + """ + return True + + if token_user != self.request.user: + """ + > the OP MUST ask ask the End-User whether to log out of the OP as + > well if the supplied ID Token does not belong to the logged in + > End-User. + + is_authenticated indicates that there is a logged in user and was + tested in the first condition. + token_user != self.request.user indicates that the token does not + belong to the logged in user, Therefore we need to prompt the user. + """ + return True + + """ We didn't find a reason to prompt the user """ + return False def do_logout(self, application=None, post_logout_redirect_uri=None, state=None, token_user=None): user = token_user or self.request.user diff --git a/pyproject.toml b/pyproject.toml index 900f4d3dd..a9ade3b7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,17 +1,102 @@ -[tool.black] -line-length = 110 -target-version = ['py38'] -exclude = ''' -^/( - oauth2_provider/migrations/ - | tests/migrations/ - | .tox -) -''' +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "django-oauth-toolkit" +dynamic = ["version"] +requires-python = ">=3.8,<=3.14" +authors = [ + {name = "Federico Frenguelli"}, + {name = "Massimiliano Pippi"}, + {email = "synasius@gmail.com"}, +] +description = "OAuth2 Provider for Django" +keywords = ["django", "oauth", "oauth2", "oauthlib"] +license = {file = "LICENSE"} +readme = "README.rst" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Django", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", + "Framework :: Django :: 5.1", + "Framework :: Django :: 5.2", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Internet :: WWW/HTTP", +] +dependencies = [ + "django >= 4.2", + "requests >= 2.13.0", + "oauthlib >= 3.3.0", + "jwcrypto >= 1.5.0", +] + +[project.optional-dependencies] +dev = [ + "pytest", + "pytest-cov", + "m2r", + "sphinx-rtd-theme", +] + +[project.urls] +Homepage = "https://django-oauth-toolkit.readthedocs.io/" +Repository = "https://github.com/django-oauth/django-oauth-toolkit" + +[tool.setuptools.dynamic] +version = {attr = "oauth2_provider.__version__"} # Ref: https://github.com/codespell-project/codespell#using-a-config-file [tool.codespell] -skip = '.git,package-lock.json,locale' +skip = '.git,package-lock.json,locale,AUTHORS,tox.ini' check-hidden = true ignore-regex = '.*pragma: codespell-ignore.*' -# ignore-words-list = '' +ignore-words-list = 'assertIn' + +[tool.coverage.run] +source = ["oauth2_provider"] +omit = ["*/migrations/*"] + +[tool.coverage.report] +show_missing = true + +[tool.pytest.ini_options] +django_find_project = false +addopts = [ + "--cov=oauth2_provider", + "--cov-report=", + "--cov-append", + "-s" +] +markers = [ + "oauth2_settings: Custom OAuth2 settings to use - use with oauth2_settings fixture", + "nologinrequiredmiddleware", +] + +[tool.ruff] +line-length = 110 +exclude = [".tox", "build/", "dist/", "docs/", "oauth2_provider/migrations/", "tests/migrations/", "manage.py"] + +[tool.ruff.lint] +select = ["E", "F", "I", "Q", "W"] + +[tool.ruff.lint.isort] +lines-after-imports = 2 +known-first-party = ["oauth2_provider"] + +[tool.uv.workspace] +members = [ + "tests/app/idp", +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index d015d1238..000000000 --- a/setup.cfg +++ /dev/null @@ -1,48 +0,0 @@ -[metadata] -name = django-oauth-toolkit -version = attr: oauth2_provider.__version__ -description = OAuth2 Provider for Django -long_description = file: README.rst -long_description_content_type = text/x-rst -author = Federico Frenguelli, Massimiliano Pippi -author_email = synasius@gmail.com -url = https://github.com/jazzband/django-oauth-toolkit -keywords = django, oauth, oauth2, oauthlib -classifiers = - Development Status :: 5 - Production/Stable - Environment :: Web Environment - Framework :: Django - Framework :: Django :: 3.2 - Framework :: Django :: 4.0 - Framework :: Django :: 4.1 - Framework :: Django :: 4.2 - Framework :: Django :: 5.0 - Intended Audience :: Developers - License :: OSI Approved :: BSD License - Operating System :: OS Independent - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Topic :: Internet :: WWW/HTTP - -[options] -packages = find: -include_package_data = True -zip_safe = False -python_requires = >=3.8 -# jwcrypto has a direct dependency on six, but does not list it yet in a release -# Previously, cryptography also depended on six, so this was unnoticed -install_requires = - django >= 3.2, != 4.0.0 - requests >= 2.13.0 - oauthlib >= 3.1.0 - jwcrypto >= 0.8.0 - pytz >= 2024.1 - -[options.packages.find] -exclude = - tests - tests.* diff --git a/setup.py b/setup.py deleted file mode 100755 index dd4e63e40..000000000 --- a/setup.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python - -from setuptools import setup - - -setup() diff --git a/tests/app/README.md b/tests/app/README.md index a2632b262..1d13b2414 100644 --- a/tests/app/README.md +++ b/tests/app/README.md @@ -4,6 +4,8 @@ These apps are for local end to end testing of DOT features. They were implement local test environments. You should be able to start both and instance of the IDP and RP using the directions below, then test the functionality of the IDP using the RP. +The IDP seed data includes a Device Authorization OAuth application as well. + ## /tests/app/idp This is an example IDP implementation for end to end testing. There are pre-configured fixtures which will work with the sample RP. @@ -29,9 +31,39 @@ password: password You can update data in the IDP and then dump the data to a new seed file as follows. - ``` +``` python -Xutf8 ./manage.py dumpdata -e sessions -e admin.logentry -e auth.permission -e contenttypes.contenttype -e oauth2_provider.accesstoken -e oauth2_provider.refreshtoken -e oauth2_provider.idtoken --natural-foreign --natural-primary --indent 2 > fixtures/seed.json - ``` +``` + +### Device Authorization example + +For testing out the device authorization flow, we don't really need a RP, as the device itself +is the "relying party". The seed data includes a Device Authorization Application, meaning +you could directly start the device authorization flow using `curl`. In the real world, the device +would be sending these request that we send here with `curl`. + +_Note:_ you can find these `curl` commands in the Tutorial section of the documentation as well. + +```sh +# Initiate device authorization flow on the device; here we use the client_id +# of the Device Authorization App from the seed data. +curl --location 'http://127.0.0.1:8000/o/device-authorization/' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'client_id=Qg8AaxKLs1c2W3PR70Sv5QxuSEREicKUlf83iGX3' +``` + +Follow the `verification_uri` from the response (should be similar to http://127.0.0.1:8000/o/device"), +enter the user code, approve, and then send another `curl` command to get the token. + +```sh +curl --location 'http://localhost:8000/o/token/' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'device_code={the device code from the device-authorization response}' \ + --data-urlencode 'client_id=Qg8AaxKLs1c2W3PR70Sv5QxuSEREicKUlf83iGX3' \ + --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:device_code' +``` + +The response should include the access token. ## /test/app/rp diff --git a/tests/app/idp/fixtures/seed.json b/tests/app/idp/fixtures/seed.json index b77d1f4e2..382102373 100644 --- a/tests/app/idp/fixtures/seed.json +++ b/tests/app/idp/fixtures/seed.json @@ -34,5 +34,26 @@ "algorithm": "RS256", "allowed_origins": "http://localhost:5173\r\nhttp://127.0.0.1:5173" } +}, +{ + "model": "oauth2_provider.application", + "fields": { + "client_id": "Qg8AaxKLs1c2W3PR70Sv5QxuSEREicKUlf83iGX3", + "user": [ + "superuser" + ], + "redirect_uris": "", + "post_logout_redirect_uris": "", + "client_type": "public", + "authorization_grant_type": "urn:ietf:params:oauth:grant-type:device_code", + "client_secret": "pbkdf2_sha256$870000$x1A7AKB9YMmNX7v2otXt1C$Yxucj9o/QlF16AxqN5LXo+Se0Sy3FO5x4Q35Lw1FGqM=", + "hash_client_secret": true, + "name": "Device Authorization App", + "skip_authorization": false, + "created": "2025-11-07T16:56:23.156Z", + "updated": "2025-11-07T16:56:23.156Z", + "algorithm": "", + "allowed_origins": "" + } } ] diff --git a/tests/app/idp/idp/apps.py b/tests/app/idp/idp/apps.py index f40a9f644..63a7f442f 100644 --- a/tests/app/idp/idp/apps.py +++ b/tests/app/idp/idp/apps.py @@ -3,11 +3,20 @@ def cors_allow_origin(sender, request, **kwargs): + origin = request.headers.get('Origin') + return ( request.path == "/o/userinfo/" or request.path == "/o/userinfo" or request.path == "/o/.well-known/openid-configuration" or request.path == "/o/.well-known/openid-configuration/" + # this is for testing the device authorization flow in the example rp. + # You would not normally have a browser-based client do this and shoudn't + # open the following endpoints to CORS requests in a production environment. + or (origin == 'http://localhost:5173' and request.path == "/o/device-authorization") + or (origin == 'http://localhost:5173' and request.path == "/o/device-authorization/") + or (origin == 'http://localhost:5173' and request.path == "/o/token") + or (origin == 'http://localhost:5173' and request.path == "/o/token/") ) diff --git a/tests/app/idp/idp/settings.py b/tests/app/idp/idp/settings.py index 375cdcc9b..679407604 100644 --- a/tests/app/idp/idp/settings.py +++ b/tests/app/idp/idp/settings.py @@ -13,21 +13,95 @@ import os from pathlib import Path +import environ + +from oauth2_provider.utils import set_oauthlib_user_to_device_request_user, user_code_generator + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +env = environ.FileAwareEnv( + DEBUG=(bool, True), + ALLOWED_HOSTS=(list, []), + DATABASE_URL=(str, "sqlite:///db.sqlite3"), + SECRET_KEY=(str, "django-insecure-vri27@j_q62e2it4$xiy9ca!7@qgjkhhan(*zs&lz0k@yukbb3"), + OAUTH2_PROVIDER_OIDC_ENABLED=(bool, True), + OAUTH2_PROVIDER_OIDC_RP_INITIATED_LOGOUT_ENABLED=(bool, True), + OAUTH2_PROVIDER_OIDC_RSA_PRIVATE_KEY=( + str, + """ +-----BEGIN RSA PRIVATE KEY----- +MIIJKAIBAAKCAgEAtd8X/v8pddKt+opMJZrhV4FH86gBTMPjTGXeAfKkQVf7KDUZ +Ty90n+JMe2rvCUn+Nws9yy5vmtbkomQbj8Xs1kHJOVdCnH1L2HTkvM7BjTBmJ5vc +bA94IBmSf9jJIzfIJkepshRLcGllMvHPOYQiR+lJsj58FFDLZN4/182S21C8Ri0w ++63rT64SxiQkqt6h+E1w7V+tHQJKDZq3du1QctZVXiIr6Zs5BgTjTyRURoiqUVH0 +WJ4dT2t4+Rg9mp3PBlVwTOqzw9xTcO8ke+ZdrIWP4euZuPIr/Dya5R7S2Ki8Nwag +ANGV+LghJilucuWzJlOBO8TlIVUwgUaGOqaDxMHx9P/nRLQ6vTKP81FUJ7gNv6oj +W+6No6nMhsESQ+thizvBYOgintZZoeBwpB8lebKvGJUeqRo6qhc5BeUEjAjsAgtP +sJrRNQ4t8PT8mP+2dw4sU7J5PBAtx+ZdZ9bcH/sNuohBj77+6WhyvjmeYIKgCgjO +TdZH9O+kUIMaX9mlB+WvoVsk32qensZG/CgXXa3rWyXPvOdA9aOE4V0GCv1JfWKK +OXA8aY5aUGy0VvOWXHWpft5begr8onCjNs9UR6fCdCvcrSuiHTvNpM37E6Xh4kV4 +uMzjGaj5ZLBOAY3cYzFI6LNrK4/YJvzLi9jxI1sJG1ZMz8kCywuJISEq4LcCAwEA +AQKCAgBcnbV8l7gnVhhfA9pvNAYZJ67ad+3hh8fSefWqjEP1Orad7RxsZMBBQ16r +YvNDibi5kzHurEENWu2nfM9EUgifu3SbjMJRKsVa/3wUYj3ShpkfBpIjPWVxA1TF +YkJbeuakB8507zzTi/iLDvT2V0GV2Uk8SfGp7tMFFODyJq/om56lJhJRuGmidAT/ +fhxmH2XgKp+dYiGoKihH8UgIeiWDtX5Xp5MxLWjGleqjvN5l5ObG7rM+BZbrgNFk +GGIWwNJSaWP853CQBz0+v6mWpuOBHar945quwjSACOTgVOgOiS7/3pHQmOqEdE/9 +PRAP1sV6eP/Qzh3Y8ab3zlBAwddLmZi+8sVV/sJadEMciU6AR8ZInf2zWtmxh6Ft +TNXUrSmDjKId84wyYT+pDg8Vv04X8xMNLWAIYeBawOPasEiBiFVUqDGHciPMBbhb +XxZK7Noi8akzCLWouPkrW4pjpsd5xrllakGFAFPktLvc8ZRyz2InaQKqhaaU+is5 +ykAeHpJHVxg1xFY0hX06i8pkjXQROhc7+GUuifxKvVcouCwlUiSxcHGQLqzGKnYE +fpCs9uGI8+XolEq637LyYaZ7zpWd8Ehiw4AEfE3oOVIQd4xAQ8YDJxUG1fUYQfF8 +iD5VO2+WO7a9QfScFZK+UebHEEXQGq4+JNUlP0KSnSsp3J0XkQKCAQEA3Y0sE9sE +l8VTTW3oxKChmq18UKJchyXU3BMLFnvDAPweUTdtS0QUIsDQD2pCU7wQonWOpqUj +vMwlTZjyNo+9N0l2fqleha1phzgYFCfTsgJ6gcl82y/JUvsGqMglKOUKoCFW5UtM +kUO+P5S25GqiDc0qsO6FGKSOvJ5aJLYEpEK5ez2q9uyzSYbp5aUuKwLb11rX0HW9 +JjkB7hL4OtHpJ9E9uAsOj4VIWpysmX3d8UIv1Uez8f+bilhCMShKk4U9xz8ZY2K4 +YXdfFr83b1kQybIDzeXeOQ5NQ6myS5HiqBSYx9Iy7Y54605KVM0CzLCPS5fAAcbW +5wq1H32OtxRS4wKCAQEA0iZ24W30BIYIx65YseVbBNs4cJr9ppqCAqUGqAhW8xfe +q7Atd6KG+lXWVDj2tZzuoYeb0PLjQRsmOs8CVFUZT0ntH6YAUOpPW8l8tkrWTugp +7fCx2pR4r8aFAVb7Jkc41ojSvaYMbUClKf+JVtFPsY1ug7gNxizGjVnpAq66XX+X +76BVIpMEUivZcXos6/BrVM3seFYQg1pMZkjjO3q8lETnlT3LIYpPtRjaFSvcMaMy +1Cb4dGUz+xj8BM73bLDEJtHZEsyF6nEnurlE9rSbMui9XhckcC267e1qvIbAnKB9 +JK5oJAM4L+xOylmvk71gdrul9Q9aT+QJGUXkPxwfHQKCAQBkMIQ/UmtISyb5u/to +eA+8yDmQqWvYfiY9g6se9sbfuiPnrH4TbG0Crlkor2/hOAn5vdnNyJ5ZsaQo7EKU +o/n4d5NLgkJJh3tSd+6DpuMX/AD0km6RHJIZoYWIbEJJtRJSCeGm/Z9Zjd4KGLGA +qCwyu5ZTvvmXhEs8RwwSz/FXawlAD0oyMiZ92LILdOBk+Pz77YvtLGFmWJ9jz1ZM +G0MqC3iysuVZx/dJatKu8vmcMcc51xwsEuB+9pywaD0Za0bdxM4xYKJrCTWKLtzd +0NRDseoAgbQ17x7Hu4Tyob1zLyVML+VyAlzyZEw+/xsF/849bBmbdBUZFIGGBRy1 +9E3rAoIBAQCDs3dtb+stqpJ2Ed2kH4kbUgfdCkVM1CgGYEX7qL5VOvBhyNe10jWl +TYY04j47M06aDNKp8I5bjxg2YuWi1HI4Lqxc2Tv5ed6iN3PhCqWkbftZEy9jPQkl +n9RbMpfTNW95g+YO1LGVBp5745m+vw6ix3ArPH3lZMpKa76L39UMI5qkoma4dEqQ +9MohQ+BDPTkGvMcl40oWB9E5iRRfglwMz+IStddH/dZWOGz0N7iXox+HtaSfzYz2 +IIJQwSRvCZjkez7/eQ20D5ZGfzWpJybckN+cyAQeCYrM8a2i2RB9GFdVVbgOWbYs +0nvOdMaEYHrD7nXjTuvahZ7uJ88TfhxBAoIBAG3ClX40pxUXs6kEOGZYUXHFaYDz +Upuvj8X2h6SaepTAAokkJxGOdeg5t3ohsaXDeV2WcNb8KRFmDuVtcGSo0mUWtrtT +RXgJT9SBEMl1rEPbEh0i9uXOaI8DWdBO62Ei0efeL0Wac7kxwBbObKDn8mQCmlWK +4nvzevqUB8frm9abjRGTOZX8QlNZcPs065vHubNJ8SAqr+uoe1GTb0qL7YkWT6vb +dBCCnF8FP1yPW8UgGVGSeozmIMaJwSpl2srZUMkN1KlqHwzehrOn9Tn2grA9ue/i +ipUMvb4Se0LDJnmFuv8v6gM6V4vyXkP855mNOiRHUOHOSKdQ3SeKrLlnR6I= +-----END RSA PRIVATE KEY----- +""", + ), + OAUTH2_PROVIDER_SCOPES=(dict, {"openid": "OpenID Connect scope"}), + OAUTH2_PROVIDER_ALLOWED_SCHEMES=(list, ["https", "http"]), + OAUTHLIB_INSECURE_TRANSPORT=(bool, "1"), + STATIC_ROOT=(str, BASE_DIR / "static"), + STATIC_URL=(str, "static/"), + TEMPLATES_DIRS=(list, [BASE_DIR / "templates"]), +) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure-vri27@j_q62e2it4$xiy9ca!7@qgjkhhan(*zs&lz0k@yukbb3" +SECRET_KEY = env("SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = env("DEBUG") -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = env("ALLOWED_HOSTS") # Application definition @@ -60,7 +134,7 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [BASE_DIR / "templates"], + "DIRS": env("TEMPLATES_DIRS"), "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -80,10 +154,7 @@ # https://docs.djangoproject.com/en/4.2/ref/settings/#databases DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } + "default": env.db(), } @@ -120,8 +191,8 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ - -STATIC_URL = "static/" +STATIC_ROOT = env("STATIC_ROOT") +STATIC_URL = env("STATIC_URL") # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field @@ -130,69 +201,21 @@ OAUTH2_PROVIDER = { "OAUTH2_VALIDATOR_CLASS": "idp.oauth.CustomOAuth2Validator", - "OIDC_ENABLED": True, - "OIDC_RP_INITIATED_LOGOUT_ENABLED": True, + "OAUTH_DEVICE_VERIFICATION_URI": "http://127.0.0.1:8000/o/device", + "OAUTH_PRE_TOKEN_VALIDATION": [set_oauthlib_user_to_device_request_user], + "OAUTH_DEVICE_USER_CODE_GENERATOR": user_code_generator, + "OAUTH_DEVICE_VERIFICATION_URI_COMPLETE": lambda x: f"http://127.0.0.1:8000/o/device?user_code={x}", + "OIDC_ENABLED": env("OAUTH2_PROVIDER_OIDC_ENABLED"), + "OIDC_RP_INITIATED_LOGOUT_ENABLED": env("OAUTH2_PROVIDER_OIDC_RP_INITIATED_LOGOUT_ENABLED"), # this key is just for out test app, you should never store a key like this in a production environment. - "OIDC_RSA_PRIVATE_KEY": """ ------BEGIN RSA PRIVATE KEY----- -MIIJKAIBAAKCAgEAtd8X/v8pddKt+opMJZrhV4FH86gBTMPjTGXeAfKkQVf7KDUZ -Ty90n+JMe2rvCUn+Nws9yy5vmtbkomQbj8Xs1kHJOVdCnH1L2HTkvM7BjTBmJ5vc -bA94IBmSf9jJIzfIJkepshRLcGllMvHPOYQiR+lJsj58FFDLZN4/182S21C8Ri0w -+63rT64SxiQkqt6h+E1w7V+tHQJKDZq3du1QctZVXiIr6Zs5BgTjTyRURoiqUVH0 -WJ4dT2t4+Rg9mp3PBlVwTOqzw9xTcO8ke+ZdrIWP4euZuPIr/Dya5R7S2Ki8Nwag -ANGV+LghJilucuWzJlOBO8TlIVUwgUaGOqaDxMHx9P/nRLQ6vTKP81FUJ7gNv6oj -W+6No6nMhsESQ+thizvBYOgintZZoeBwpB8lebKvGJUeqRo6qhc5BeUEjAjsAgtP -sJrRNQ4t8PT8mP+2dw4sU7J5PBAtx+ZdZ9bcH/sNuohBj77+6WhyvjmeYIKgCgjO -TdZH9O+kUIMaX9mlB+WvoVsk32qensZG/CgXXa3rWyXPvOdA9aOE4V0GCv1JfWKK -OXA8aY5aUGy0VvOWXHWpft5begr8onCjNs9UR6fCdCvcrSuiHTvNpM37E6Xh4kV4 -uMzjGaj5ZLBOAY3cYzFI6LNrK4/YJvzLi9jxI1sJG1ZMz8kCywuJISEq4LcCAwEA -AQKCAgBcnbV8l7gnVhhfA9pvNAYZJ67ad+3hh8fSefWqjEP1Orad7RxsZMBBQ16r -YvNDibi5kzHurEENWu2nfM9EUgifu3SbjMJRKsVa/3wUYj3ShpkfBpIjPWVxA1TF -YkJbeuakB8507zzTi/iLDvT2V0GV2Uk8SfGp7tMFFODyJq/om56lJhJRuGmidAT/ -fhxmH2XgKp+dYiGoKihH8UgIeiWDtX5Xp5MxLWjGleqjvN5l5ObG7rM+BZbrgNFk -GGIWwNJSaWP853CQBz0+v6mWpuOBHar945quwjSACOTgVOgOiS7/3pHQmOqEdE/9 -PRAP1sV6eP/Qzh3Y8ab3zlBAwddLmZi+8sVV/sJadEMciU6AR8ZInf2zWtmxh6Ft -TNXUrSmDjKId84wyYT+pDg8Vv04X8xMNLWAIYeBawOPasEiBiFVUqDGHciPMBbhb -XxZK7Noi8akzCLWouPkrW4pjpsd5xrllakGFAFPktLvc8ZRyz2InaQKqhaaU+is5 -ykAeHpJHVxg1xFY0hX06i8pkjXQROhc7+GUuifxKvVcouCwlUiSxcHGQLqzGKnYE -fpCs9uGI8+XolEq637LyYaZ7zpWd8Ehiw4AEfE3oOVIQd4xAQ8YDJxUG1fUYQfF8 -iD5VO2+WO7a9QfScFZK+UebHEEXQGq4+JNUlP0KSnSsp3J0XkQKCAQEA3Y0sE9sE -l8VTTW3oxKChmq18UKJchyXU3BMLFnvDAPweUTdtS0QUIsDQD2pCU7wQonWOpqUj -vMwlTZjyNo+9N0l2fqleha1phzgYFCfTsgJ6gcl82y/JUvsGqMglKOUKoCFW5UtM -kUO+P5S25GqiDc0qsO6FGKSOvJ5aJLYEpEK5ez2q9uyzSYbp5aUuKwLb11rX0HW9 -JjkB7hL4OtHpJ9E9uAsOj4VIWpysmX3d8UIv1Uez8f+bilhCMShKk4U9xz8ZY2K4 -YXdfFr83b1kQybIDzeXeOQ5NQ6myS5HiqBSYx9Iy7Y54605KVM0CzLCPS5fAAcbW -5wq1H32OtxRS4wKCAQEA0iZ24W30BIYIx65YseVbBNs4cJr9ppqCAqUGqAhW8xfe -q7Atd6KG+lXWVDj2tZzuoYeb0PLjQRsmOs8CVFUZT0ntH6YAUOpPW8l8tkrWTugp -7fCx2pR4r8aFAVb7Jkc41ojSvaYMbUClKf+JVtFPsY1ug7gNxizGjVnpAq66XX+X -76BVIpMEUivZcXos6/BrVM3seFYQg1pMZkjjO3q8lETnlT3LIYpPtRjaFSvcMaMy -1Cb4dGUz+xj8BM73bLDEJtHZEsyF6nEnurlE9rSbMui9XhckcC267e1qvIbAnKB9 -JK5oJAM4L+xOylmvk71gdrul9Q9aT+QJGUXkPxwfHQKCAQBkMIQ/UmtISyb5u/to -eA+8yDmQqWvYfiY9g6se9sbfuiPnrH4TbG0Crlkor2/hOAn5vdnNyJ5ZsaQo7EKU -o/n4d5NLgkJJh3tSd+6DpuMX/AD0km6RHJIZoYWIbEJJtRJSCeGm/Z9Zjd4KGLGA -qCwyu5ZTvvmXhEs8RwwSz/FXawlAD0oyMiZ92LILdOBk+Pz77YvtLGFmWJ9jz1ZM -G0MqC3iysuVZx/dJatKu8vmcMcc51xwsEuB+9pywaD0Za0bdxM4xYKJrCTWKLtzd -0NRDseoAgbQ17x7Hu4Tyob1zLyVML+VyAlzyZEw+/xsF/849bBmbdBUZFIGGBRy1 -9E3rAoIBAQCDs3dtb+stqpJ2Ed2kH4kbUgfdCkVM1CgGYEX7qL5VOvBhyNe10jWl -TYY04j47M06aDNKp8I5bjxg2YuWi1HI4Lqxc2Tv5ed6iN3PhCqWkbftZEy9jPQkl -n9RbMpfTNW95g+YO1LGVBp5745m+vw6ix3ArPH3lZMpKa76L39UMI5qkoma4dEqQ -9MohQ+BDPTkGvMcl40oWB9E5iRRfglwMz+IStddH/dZWOGz0N7iXox+HtaSfzYz2 -IIJQwSRvCZjkez7/eQ20D5ZGfzWpJybckN+cyAQeCYrM8a2i2RB9GFdVVbgOWbYs -0nvOdMaEYHrD7nXjTuvahZ7uJ88TfhxBAoIBAG3ClX40pxUXs6kEOGZYUXHFaYDz -Upuvj8X2h6SaepTAAokkJxGOdeg5t3ohsaXDeV2WcNb8KRFmDuVtcGSo0mUWtrtT -RXgJT9SBEMl1rEPbEh0i9uXOaI8DWdBO62Ei0efeL0Wac7kxwBbObKDn8mQCmlWK -4nvzevqUB8frm9abjRGTOZX8QlNZcPs065vHubNJ8SAqr+uoe1GTb0qL7YkWT6vb -dBCCnF8FP1yPW8UgGVGSeozmIMaJwSpl2srZUMkN1KlqHwzehrOn9Tn2grA9ue/i -ipUMvb4Se0LDJnmFuv8v6gM6V4vyXkP855mNOiRHUOHOSKdQ3SeKrLlnR6I= ------END RSA PRIVATE KEY----- -""", + "OIDC_RSA_PRIVATE_KEY": env("OAUTH2_PROVIDER_OIDC_RSA_PRIVATE_KEY"), "SCOPES": { "openid": "OpenID Connect scope", }, - "ALLOWED_SCHEMES": ["https", "http"], + "ALLOWED_SCHEMES": env("OAUTH2_PROVIDER_ALLOWED_SCHEMES"), } # needs to be set to allow cors requests from the test app, along with ALLOWED_SCHEMES=["http"] -os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" +os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = env("OAUTHLIB_INSECURE_TRANSPORT") LOGGING = { "version": 1, diff --git a/tests/app/idp/idp/urls.py b/tests/app/idp/idp/urls.py index 90e8abd48..8be65c35b 100644 --- a/tests/app/idp/idp/urls.py +++ b/tests/app/idp/idp/urls.py @@ -17,9 +17,11 @@ from django.contrib import admin from django.urls import include, path +from django.views.generic import TemplateView urlpatterns = [ + path('', TemplateView.as_view(template_name='home/index.html'), name='home'), # Maps the root URL to your home_view path("admin/", admin.site.urls), path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")), path("accounts/", include("django.contrib.auth.urls")), diff --git a/tests/app/idp/pyproject.toml b/tests/app/idp/pyproject.toml new file mode 100644 index 000000000..ee3c073a8 --- /dev/null +++ b/tests/app/idp/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "idp" +version = "0.1.0" +description = "Django OAuth Toolkit Identity Provider Test App" +readme = "README.md" +requires-python = ">=3.8,<=3.14" +dependencies = [ + "Django>=4.2,<=5.2", + "django-cors-headers==3.14.0", + "django-environ==0.11.2", + "django-oauth-toolkit", +] + + +[tool.setuptools] +# we're not packaging this as a module, so tell setup tools not to do anything. +py-modules = [] + +[tool.uv.sources] +# connect to the django-oauth-toolkit package in the parent workspace +# so changes to it are reflected here without reinstalling. +django-oauth-toolkit = { editable = true, workspace = true } diff --git a/tests/app/idp/requirements.txt b/tests/app/idp/requirements.txt index d17f9bd45..f8d653aba 100644 --- a/tests/app/idp/requirements.txt +++ b/tests/app/idp/requirements.txt @@ -1,4 +1,5 @@ -Django>=3.2,<4.2 -django-cors-headers==3.14.0 +Django>=4.2,<=5.2 +django-cors-headers==4.6.0 +django-environ==0.12.0 --e ../../../ \ No newline at end of file +-e ../../../ diff --git a/tests/app/idp/templates/device/accept_deny.html b/tests/app/idp/templates/device/accept_deny.html new file mode 100644 index 000000000..4fd31a6fb --- /dev/null +++ b/tests/app/idp/templates/device/accept_deny.html @@ -0,0 +1,16 @@ +{% extends "oauth2_provider/base.html" %} +{% block content %} + + + Accept or Deny + + +

Please choose an action:

+
+ {% csrf_token %} + + +
+ + +{% endblock content %} diff --git a/tests/app/idp/templates/device/user_code.html b/tests/app/idp/templates/device/user_code.html new file mode 100644 index 000000000..774b95897 --- /dev/null +++ b/tests/app/idp/templates/device/user_code.html @@ -0,0 +1,16 @@ +{% extends "oauth2_provider/base.html" %} +{% block content %} + + + Device code + + +

Enter code displayed on device

+
+ {% csrf_token %} + {{ form.as_p }} + +
+ + +{% endblock content %} diff --git a/tests/app/idp/templates/home/index.html b/tests/app/idp/templates/home/index.html new file mode 100644 index 000000000..5176db721 --- /dev/null +++ b/tests/app/idp/templates/home/index.html @@ -0,0 +1,19 @@ + + + + + + + Identity Provider Home + + + +

Welcome to the Identity Provider (IdP)

+

This is the home page of the Identity Provider used for testing OAuth2 flows.

+

Please ensure that the test relying party is running to proceed with authentication tests.

+ + + \ No newline at end of file diff --git a/tests/app/rp/Dockerfile b/tests/app/rp/Dockerfile new file mode 100644 index 000000000..a719a1eb4 --- /dev/null +++ b/tests/app/rp/Dockerfile @@ -0,0 +1,16 @@ +FROM node:18-alpine AS builder +WORKDIR /app +COPY package*.json . +RUN npm ci +COPY . . +RUN npm run build +RUN npm prune --production + +FROM node:18-alpine +WORKDIR /app +COPY --from=builder /app/build build/ +COPY --from=builder /app/node_modules node_modules/ +COPY package.json . +EXPOSE 3000 +ENV NODE_ENV=production +CMD [ "node", "build" ] \ No newline at end of file diff --git a/tests/app/rp/package-lock.json b/tests/app/rp/package-lock.json index 80b168437..2e63efd62 100644 --- a/tests/app/rp/package-lock.json +++ b/tests/app/rp/package-lock.json @@ -8,32 +8,51 @@ "name": "rp", "version": "0.0.1", "dependencies": { - "@dopry/svelte-oidc": "^1.1.0" + "@dopry/svelte-oidc": "^1.2.0", + "jose": "^6.1.0" }, "devDependencies": { - "@sveltejs/adapter-auto": "^3.1.1", - "@sveltejs/kit": "^2.5.0", - "prettier": "^2.8.0", - "prettier-plugin-svelte": "^2.8.1", - "svelte": "^3.54.0", - "svelte-check": "^3.0.1", - "tslib": "^2.4.1", - "typescript": "^5.0.0", - "vite": "^4.5.3" + "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/adapter-node": "^5.4.0", + "@sveltejs/kit": "^2.48.4", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "prettier": "^3.6.2", + "prettier-plugin-svelte": "^3.4.0", + "svelte": "^5.43.2", + "svelte-check": "^4.3.3", + "tslib": "^2.8.1", + "typescript": "^5.9.3", + "vite": "^7.1.12" } }, "node_modules/@dopry/svelte-oidc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@dopry/svelte-oidc/-/svelte-oidc-1.1.0.tgz", - "integrity": "sha512-FfXm/f2vRNxFsYxKs8hal1Huf94dqKrRIppDzjDIH9cNy683b9sN9NUY0mZtrHc1yJL+jyfNNsB+bY9/9fCErA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@dopry/svelte-oidc/-/svelte-oidc-1.2.0.tgz", + "integrity": "sha512-iQKkgxjua264dgkm9u2vxMRwN4CKQywOaAQlszFVxcricAjWb5hCmcxD6qeMvprhmE5gB+Bk7JXpuUAq21O72A==", "dependencies": { - "oidc-client": "1.11.5" + "oidc-client-ts": "^3.3.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -43,13 +62,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -59,13 +78,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -75,13 +94,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -91,13 +110,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -107,13 +126,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -123,13 +142,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -139,13 +158,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -155,13 +174,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -171,13 +190,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -187,13 +206,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -203,13 +222,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -219,13 +238,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -235,13 +254,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -251,13 +270,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -267,13 +286,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -283,13 +302,29 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -299,13 +334,29 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -315,13 +366,29 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -331,13 +398,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -347,13 +414,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -363,13 +430,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -379,7 +446,27 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "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==", + "dev": true, + "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, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, "node_modules/@jridgewell/resolve-uri": { @@ -392,686 +479,833 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", "dev": true }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.9", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.9.tgz", + "integrity": "sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==", "dev": true, "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" }, "engines": { - "node": ">= 8" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.24", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz", - "integrity": "sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==", - "dev": true - }, - "node_modules/@sveltejs/adapter-auto": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.1.1.tgz", - "integrity": "sha512-6LeZft2Fo/4HfmLBi5CucMYmgRxgcETweQl/yQoZo/895K3S9YWYN4Sfm/IhwlIpbJp3QNvhKmwCHbsqQNYQpw==", - "dev": true, - "dependencies": { - "import-meta-resolve": "^4.0.0" + "node": ">=16.0.0 || 14 >= 14.17" }, "peerDependencies": { - "@sveltejs/kit": "^2.0.0" + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "node_modules/@sveltejs/kit": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.0.tgz", - "integrity": "sha512-1uyXvzC2Lu1FZa30T4y5jUAC21R309ZMRG0TPt+PPPbNUoDpy8zSmSNVWYaBWxYDqLGQ5oPNWvjvvF2IjJ1jmA==", + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", "dev": true, - "hasInstallScript": true, "dependencies": { - "@types/cookie": "^0.6.0", - "cookie": "^0.6.0", - "devalue": "^4.3.2", - "esm-env": "^1.0.0", - "import-meta-resolve": "^4.0.0", - "kleur": "^4.1.5", - "magic-string": "^0.30.5", - "mrmime": "^2.0.0", - "sade": "^1.8.1", - "set-cookie-parser": "^2.6.0", - "sirv": "^2.0.4", - "tiny-glob": "^0.2.9" - }, - "bin": { - "svelte-kit": "svelte-kit.js" + "@rollup/pluginutils": "^5.1.0" }, "engines": { - "node": ">=18.13" + "node": ">=14.0.0" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.3" + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.0.2.tgz", - "integrity": "sha512-MpmF/cju2HqUls50WyTHQBZUV3ovV/Uk8k66AN2gwHogNAG8wnW8xtZDhzNBsFJJuvmq1qnzA5kE7YfMJNFv2Q==", + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", "dev": true, - "peer": true, "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^2.0.0", - "debug": "^4.3.4", - "deepmerge": "^4.3.1", - "kleur": "^4.1.5", - "magic-string": "^0.30.5", - "svelte-hmr": "^0.15.3", - "vitefu": "^0.2.5" + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" }, "engines": { - "node": "^18.0.0 || >=20" + "node": ">=14.0.0" }, "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.0" + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "node_modules/@sveltejs/vite-plugin-svelte/node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.0.0.tgz", - "integrity": "sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==", + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", "dev": true, - "peer": true, "dependencies": { - "debug": "^4.3.4" + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" }, "engines": { - "node": "^18.0.0 || >=20" + "node": ">=14.0.0" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.0" - } - }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true - }, - "node_modules/@types/pug": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.6.tgz", - "integrity": "sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==", - "dev": true - }, - "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "bin": { - "acorn": "bin/acorn" + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, - "engines": { - "node": ">=0.4.0" + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "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==", + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, "engines": { - "node": ">= 8" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" ] }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "cpu": [ + "arm64" + ], "dev": true, - "engines": { - "node": ">=8" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "cpu": [ + "x64" + ], "dev": true, - "engines": { - "node": "*" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "cpu": [ + "arm" + ], "dev": true, - "engines": { - "node": ">=6" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "cpu": [ + "arm" + ], "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "cpu": [ + "arm64" ], - "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" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } + "dev": true, + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "cpu": [ + "loong64" + ], "dev": true, - "engines": { - "node": ">= 0.6" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/core-js": { - "version": "3.30.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz", - "integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/crypto-js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "cpu": [ + "riscv64" + ], "dev": true, - "peer": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "cpu": [ + "s390x" + ], "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "cpu": [ + "x64" + ], "dev": true, - "engines": { - "node": ">=8" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/devalue": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.2.tgz", - "integrity": "sha512-KqFl6pOgOW+Y6wJgu80rHpo2/3H07vr8ntR9rkkFIRETewbf5GaYYcakYfiKz89K+sLsuPkQIZaXDMjUObZwWg==", - "dev": true + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", - "dev": true + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] }, - "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "cpu": [ + "arm64" + ], "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/esm-env": { + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", - "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", "dev": true }, - "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz", + "integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==", "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" + "peerDependencies": { + "acorn": "^8.9.0" } }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "node_modules/@sveltejs/adapter-auto": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-7.0.0.tgz", + "integrity": "sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw==", "dev": true, - "dependencies": { - "reusify": "^1.0.4" + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" } }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "node_modules/@sveltejs/adapter-node": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.4.0.tgz", + "integrity": "sha512-NMsrwGVPEn+J73zH83Uhss/hYYZN6zT3u31R3IHAn3MiKC3h8fjmIAhLfTSOeNHr5wPYfjjMg8E+1gyFgyrEcQ==", "dev": true, "dependencies": { - "to-regex-range": "^5.0.1" + "@rollup/plugin-commonjs": "^28.0.1", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "rollup": "^4.9.5" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "@sveltejs/kit": "^2.4.0" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "node_modules/@sveltejs/kit": { + "version": "2.48.4", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.48.4.tgz", + "integrity": "sha512-TGFX1pZUt9qqY20Cv5NyYvy0iLWHf2jXi8s+eCGsig7jQMdwZWKUFMR6TbvFNhfDSUpc1sH/Y5EHv20g3HHA3g==", "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", + "cookie": "^0.6.0", + "devalue": "^5.3.2", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.1.tgz", + "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "magic-string": "^0.30.17", + "vitefu": "^1.1.1" }, "engines": { - "node": "*" + "node": "^20.19 || ^22.12 || >=24" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" } }, - "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==", + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.1.tgz", + "integrity": "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==", "dev": true, "dependencies": { - "is-glob": "^4.0.1" + "debug": "^4.4.1" }, "engines": { - "node": ">= 6" + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" } }, - "node_modules/globalyzer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", - "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", "dev": true }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "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/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, - "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==", + "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, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.4.0" } }, - "node_modules/import-meta-resolve": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz", - "integrity": "sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==", + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "engines": { + "node": ">= 0.4" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "engines": { + "node": ">= 0.4" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "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==", + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "dependencies": { - "binary-extensions": "^2.0.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">=8" + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/is-extglob": { + "node_modules/clsx": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "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==", + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { - "is-extglob": "^2.1.1" + "ms": "^2.1.3" }, "engines": { - "node": ">=0.10.0" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "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==", + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, "engines": { - "node": ">=0.12.0" + "node": ">=0.10.0" } }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "node_modules/devalue": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.4.2.tgz", + "integrity": "sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { - "node": ">=6" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, - "node_modules/magic-string": { - "version": "0.30.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", - "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.2.tgz", + "integrity": "sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" - }, + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "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" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 8" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" + "function-bind": "^1.1.2" }, "engines": { - "node": ">=8.6" + "node": ">= 0.4" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" - }, + "@types/estree": "*" + } + }, + "node_modules/jose": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.0.tgz", + "integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", "engines": { - "node": "*" + "node": ">=18" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=6" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/mri": { @@ -1093,16 +1327,15 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "peer": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -1117,79 +1350,45 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "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, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/oidc-client": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/oidc-client/-/oidc-client-1.11.5.tgz", - "integrity": "sha512-LcKrKC8Av0m/KD/4EFmo9Sg8fSQ+WFJWBrmtWd+tZkNn3WT/sQG3REmPANE9tzzhbjW6VkTNy4xhAXCfPApAOg==", - "dependencies": { - "acorn": "^7.4.1", - "base64-js": "^1.5.1", - "core-js": "^3.8.3", - "crypto-js": "^4.0.0", - "serialize-javascript": "^4.0.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, + "node_modules/oidc-client-ts": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.3.0.tgz", + "integrity": "sha512-t13S540ZwFOEZKLYHJwSfITugupW4uYLwuQSSXyKH/wHwZ+7FvgHE7gnNJh1YQIZ1Yd1hKSRjqeXGSUtS0r9JA==", "dependencies": { - "callsites": "^3.0.0" + "jwt-decode": "^4.0.0" }, "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -1206,149 +1405,113 @@ } ], "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "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.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" } }, "node_modules/prettier-plugin-svelte": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-2.10.0.tgz", - "integrity": "sha512-GXMY6t86thctyCvQq+jqElO+MKdB09BkL3hexyGP3Oi8XLKRFaJP1ud/xlWCZ9ZIa2BxHka32zhHfcuU+XsRQg==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.4.0.tgz", + "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", "dev": true, "peerDependencies": { - "prettier": "^1.16.4 || ^2.0.0", - "svelte": "^3.2.0" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, "dependencies": { - "glob": "^7.1.3" + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { - "rimraf": "bin.js" + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", + "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=14.18.0", + "node": ">=18.0.0", "npm": ">=8.0.0" }, "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.5", + "@rollup/rollup-android-arm64": "4.52.5", + "@rollup/rollup-darwin-arm64": "4.52.5", + "@rollup/rollup-darwin-x64": "4.52.5", + "@rollup/rollup-freebsd-arm64": "4.52.5", + "@rollup/rollup-freebsd-x64": "4.52.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", + "@rollup/rollup-linux-arm-musleabihf": "4.52.5", + "@rollup/rollup-linux-arm64-gnu": "4.52.5", + "@rollup/rollup-linux-arm64-musl": "4.52.5", + "@rollup/rollup-linux-loong64-gnu": "4.52.5", + "@rollup/rollup-linux-ppc64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-musl": "4.52.5", + "@rollup/rollup-linux-s390x-gnu": "4.52.5", + "@rollup/rollup-linux-x64-gnu": "4.52.5", + "@rollup/rollup-linux-x64-musl": "4.52.5", + "@rollup/rollup-openharmony-arm64": "4.52.5", + "@rollup/rollup-win32-arm64-msvc": "4.52.5", + "@rollup/rollup-win32-ia32-msvc": "4.52.5", + "@rollup/rollup-win32-x64-gnu": "4.52.5", + "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -1361,45 +1524,6 @@ "node": ">=6" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/sander": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", - "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", - "dev": true, - "dependencies": { - "es6-promise": "^3.1.2", - "graceful-fs": "^4.1.3", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.2" - } - }, - "node_modules/serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", @@ -1407,9 +1531,9 @@ "dev": true }, "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", + "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", "dev": true, "dependencies": { "@polka/url": "^1.0.0-next.24", @@ -1417,183 +1541,101 @@ "totalist": "^3.0.0" }, "engines": { - "node": ">= 10" - } - }, - "node_modules/sorcery": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz", - "integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.14", - "buffer-crc32": "^0.2.5", - "minimist": "^1.2.0", - "sander": "^0.5.0" - }, - "bin": { - "sorcery": "bin/sorcery" + "node": ">=18" } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "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-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, - "dependencies": { - "min-indent": "^1.0.0" - }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/svelte": { - "version": "3.58.0", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.58.0.tgz", - "integrity": "sha512-brIBNNB76mXFmU/Kerm4wFnkskBbluBDCjx/8TcpYRb298Yh2dztS2kQ6bhtjMcvUhd5ynClfwpz5h2gnzdQ1A==", + "version": "5.43.2", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.2.tgz", + "integrity": "sha512-ro1umEzX8rT5JpCmlf0PPv7ncD8MdVob9e18bhwqTKNoLjS8kDvhVpaoYVPc+qMwDAOfcwJtyY7ZFSDbOaNPgA==", "dev": true, + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^2.1.0", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, "engines": { - "node": ">= 8" + "node": ">=18" } }, "node_modules/svelte-check": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.2.0.tgz", - "integrity": "sha512-6ZnscN8dHEN5Eq5LgIzjj07W9nc9myyBH+diXsUAuiY/3rt0l65/LCIQYlIuoFEjp2F1NhXqZiJwV9omPj9tMw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.3.tgz", + "integrity": "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", - "chokidar": "^3.4.1", - "fast-glob": "^3.2.7", - "import-fresh": "^3.2.1", + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", "picocolors": "^1.0.0", - "sade": "^1.7.4", - "svelte-preprocess": "^5.0.3", - "typescript": "^5.0.3" + "sade": "^1.7.4" }, "bin": { "svelte-check": "bin/svelte-check" }, - "peerDependencies": { - "svelte": "^3.55.0" - } - }, - "node_modules/svelte-hmr": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", - "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", - "dev": true, - "peer": true, "engines": { - "node": "^12.20 || ^14.13.1 || >= 16" + "node": ">= 18.0.0" }, "peerDependencies": { - "svelte": "^3.19.0 || ^4.0.0" + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" } }, - "node_modules/svelte-preprocess": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.3.tgz", - "integrity": "sha512-GrHF1rusdJVbOZOwgPWtpqmaexkydznKzy5qIC2FabgpFyKN57bjMUUUqPRfbBXK5igiEWn1uO/DXsa2vJ5VHA==", + "node_modules/svelte/node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "dev": true, - "hasInstallScript": true, "dependencies": { - "@types/pug": "^2.0.6", - "detect-indent": "^6.1.0", - "magic-string": "^0.27.0", - "sorcery": "^0.11.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">= 14.10.0" - }, - "peerDependencies": { - "@babel/core": "^7.10.2", - "coffeescript": "^2.5.1", - "less": "^3.11.3 || ^4.0.0", - "postcss": "^7 || ^8", - "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0", - "pug": "^3.0.0", - "sass": "^1.26.8", - "stylus": "^0.55.0", - "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", - "svelte": "^3.23.0", - "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "coffeescript": { - "optional": true - }, - "less": { - "optional": true - }, - "postcss": { - "optional": true - }, - "postcss-load-config": { - "optional": true - }, - "pug": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "typescript": { - "optional": true - } + "@types/estree": "^1.0.6" } }, - "node_modules/svelte-preprocess/node_modules/magic-string": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", - "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { - "node": ">=12" - } - }, - "node_modules/tiny-glob": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", - "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", - "dev": true, - "dependencies": { - "globalyzer": "0.1.0", - "globrex": "^0.1.2" - } - }, - "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, - "dependencies": { - "is-number": "^7.0.0" + "node": ">=12.0.0" }, - "engines": { - "node": ">=8.0" + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/totalist": { @@ -1606,59 +1648,69 @@ } }, "node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true }, "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=14.17" } }, "node_modules/vite": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", - "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", + "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" + "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": "^14.18.0 || >=16.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "fsevents": "~2.3.2" + "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": ">= 14", - "less": "*", + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.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 }, @@ -1668,6 +1720,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -1676,17 +1731,22 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, "node_modules/vitefu": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", - "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", "dev": true, - "peer": true, "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "peerDependenciesMeta": { "vite": { @@ -1694,10 +1754,10 @@ } } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", "dev": true } } diff --git a/tests/app/rp/package.json b/tests/app/rp/package.json index 4a3851d97..8662ba38a 100644 --- a/tests/app/rp/package.json +++ b/tests/app/rp/package.json @@ -12,18 +12,21 @@ "format": "prettier --plugin-search-dir . --write ." }, "devDependencies": { - "@sveltejs/adapter-auto": "^3.1.1", - "@sveltejs/kit": "^2.5.0", - "prettier": "^2.8.0", - "prettier-plugin-svelte": "^2.8.1", - "svelte": "^3.54.0", - "svelte-check": "^3.0.1", - "tslib": "^2.4.1", - "typescript": "^5.0.0", - "vite": "^4.5.3" + "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/adapter-node": "^5.4.0", + "@sveltejs/kit": "^2.48.4", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "prettier": "^3.6.2", + "prettier-plugin-svelte": "^3.4.0", + "svelte": "^5.43.2", + "svelte-check": "^4.3.3", + "tslib": "^2.8.1", + "typescript": "^5.9.3", + "vite": "^7.1.12" }, "type": "module", "dependencies": { - "@dopry/svelte-oidc": "^1.1.0" + "@dopry/svelte-oidc": "^1.2.0", + "jose": "^6.1.0" } } diff --git a/tests/app/rp/src/app.html b/tests/app/rp/src/app.html index effe0d0d2..889299587 100644 --- a/tests/app/rp/src/app.html +++ b/tests/app/rp/src/app.html @@ -1,12 +1,46 @@ - + - + + + Django OAuth Toolkit RP Demo + %sveltekit.head% -
%sveltekit.body%
+
%sveltekit.body%
diff --git a/tests/app/rp/src/routes/+layout.svelte b/tests/app/rp/src/routes/+layout.svelte new file mode 100644 index 000000000..b631c5162 --- /dev/null +++ b/tests/app/rp/src/routes/+layout.svelte @@ -0,0 +1,105 @@ + + + + + + +
+ +
+ + diff --git a/tests/app/rp/src/routes/+page.svelte b/tests/app/rp/src/routes/+page.svelte index 5853d61f1..9641425b7 100644 --- a/tests/app/rp/src/routes/+page.svelte +++ b/tests/app/rp/src/routes/+page.svelte @@ -1,44 +1,84 @@ {#if browser} - - - Login - Logout - RefreshToken
-
isLoading: {$isLoading}
-
isAuthenticated: {$isAuthenticated}
-
authToken: {$accessToken}
-
idToken: {$idToken}
-
userInfo: {JSON.stringify($userInfo, null, 2)}
-
authError: {$authError}
-
+ +
+
+ Login + Logout + refreshToken +
+
+
+
+ + + + + + + + + + + +
isLoadingisAuthenticatedauthError
{$isLoading}{$isAuthenticated}{$authError || 'None'}
+
+
+
+
+ + + + + + + + + +
storevalue
userInfo
{JSON.stringify($userInfo, null, 2) || ''}
accessToken{$accessToken}
idToken{$idToken}
+
+
+
+
+ +
+
+
{/if} diff --git a/tests/app/rp/src/routes/device/+page.svelte b/tests/app/rp/src/routes/device/+page.svelte new file mode 100644 index 000000000..cfa9555e7 --- /dev/null +++ b/tests/app/rp/src/routes/device/+page.svelte @@ -0,0 +1,490 @@ + + + + Device Authorization Flow Test + + +
+

Test the OAuth 2.0 Device Authorization Grant

+

+ This page demonstrates the Device Authorization Flow (RFC 8628), which is used by devices + with limited input capabilities (like smart TVs, IoT devices, etc.) to obtain OAuth tokens. + Do not use device-authorization in a browser, this is just an illustrative example to + streamline manual testing for maintainers. It shows how you'd need to implement the flow on + your device. Have a look at this full user journey test for an implementation in Python. +

+
+ +{#if status === 'idle'} +
+

Step 1: Initiate Authorization

+

Click the button below to start the device authorization flow.

+ +
+{/if} + +{#if status === 'authorizing'} +
+

Initiating...

+

Contacting the authorization server...

+
+
+{/if} + +{#if status === 'polling'} +
+

Step 2: Authorize the Device

+

+ Open the verification URL below in a new tab, enter the user code, and approve the + authorization. +

+ +
+
+ User Code: + {userCode} +
+
+ Verification URL: + + {verificationUri} + +
+
+ Expires in: + {expiresIn} seconds +
+
+ + + +
+
+

Polling for authorization... (checking every {interval} seconds)

+
+ + +
+{/if} + +{#if status === 'complete'} +
+

✓ Authorization Complete!

+

Successfully obtained an access token.

+ +
+
+ Token Type: + {tokenType} +
+
+ Expires In: + {expiresInToken} seconds +
+ {#if scope} +
+ Scope: + {scope} +
+ {/if} +
+ Access Token: + +
+ {#if refreshToken} +
+ Refresh Token: + +
+ {/if} +
+ + +
+{/if} + +{#if status === 'error'} +
+

Error

+

{errorMessage}

+ +
+{/if} + +
+

How it works

+
    +
  1. + Device requests authorization: The device sends a request to the authorization + server with its client ID. +
  2. +
  3. + Server returns codes: The server responds with a device code, user code, + and verification URI. +
  4. +
  5. + User authorizes: The user visits the verification URI on another device + (like a phone or computer), enters the user code, and approves the authorization. +
  6. +
  7. + Device polls for token: Meanwhile, the device polls the token endpoint using + the device code until the user completes authorization. +
  8. +
  9. + Token granted: Once the user approves, the polling request returns the access + token. +
  10. +
+
+ + diff --git a/tests/app/rp/static/materialize.min.css b/tests/app/rp/static/materialize.min.css new file mode 100644 index 000000000..aebda543f --- /dev/null +++ b/tests/app/rp/static/materialize.min.css @@ -0,0 +1,7732 @@ +/*! + * Materialize v1.0.0 (http://materializecss.com) + * Copyright 2014-2017 Materialize + * MIT License (https://raw.githubusercontent.com/Dogfalo/materialize/master/LICENSE) + */ +.materialize-red { + background-color: #e51c23 !important; +} +.materialize-red-text { + color: #e51c23 !important; +} +.materialize-red.lighten-5 { + background-color: #fdeaeb !important; +} +.materialize-red-text.text-lighten-5 { + color: #fdeaeb !important; +} +.materialize-red.lighten-4 { + background-color: #f8c1c3 !important; +} +.materialize-red-text.text-lighten-4 { + color: #f8c1c3 !important; +} +.materialize-red.lighten-3 { + background-color: #f3989b !important; +} +.materialize-red-text.text-lighten-3 { + color: #f3989b !important; +} +.materialize-red.lighten-2 { + background-color: #ee6e73 !important; +} +.materialize-red-text.text-lighten-2 { + color: #ee6e73 !important; +} +.materialize-red.lighten-1 { + background-color: #ea454b !important; +} +.materialize-red-text.text-lighten-1 { + color: #ea454b !important; +} +.materialize-red.darken-1 { + background-color: #d0181e !important; +} +.materialize-red-text.text-darken-1 { + color: #d0181e !important; +} +.materialize-red.darken-2 { + background-color: #b9151b !important; +} +.materialize-red-text.text-darken-2 { + color: #b9151b !important; +} +.materialize-red.darken-3 { + background-color: #a21318 !important; +} +.materialize-red-text.text-darken-3 { + color: #a21318 !important; +} +.materialize-red.darken-4 { + background-color: #8b1014 !important; +} +.materialize-red-text.text-darken-4 { + color: #8b1014 !important; +} +.red { + background-color: #f44336 !important; +} +.red-text { + color: #f44336 !important; +} +.red.lighten-5 { + background-color: #ffebee !important; +} +.red-text.text-lighten-5 { + color: #ffebee !important; +} +.red.lighten-4 { + background-color: #ffcdd2 !important; +} +.red-text.text-lighten-4 { + color: #ffcdd2 !important; +} +.red.lighten-3 { + background-color: #ef9a9a !important; +} +.red-text.text-lighten-3 { + color: #ef9a9a !important; +} +.red.lighten-2 { + background-color: #e57373 !important; +} +.red-text.text-lighten-2 { + color: #e57373 !important; +} +.red.lighten-1 { + background-color: #ef5350 !important; +} +.red-text.text-lighten-1 { + color: #ef5350 !important; +} +.red.darken-1 { + background-color: #e53935 !important; +} +.red-text.text-darken-1 { + color: #e53935 !important; +} +.red.darken-2 { + background-color: #d32f2f !important; +} +.red-text.text-darken-2 { + color: #d32f2f !important; +} +.red.darken-3 { + background-color: #c62828 !important; +} +.red-text.text-darken-3 { + color: #c62828 !important; +} +.red.darken-4 { + background-color: #b71c1c !important; +} +.red-text.text-darken-4 { + color: #b71c1c !important; +} +.red.accent-1 { + background-color: #ff8a80 !important; +} +.red-text.text-accent-1 { + color: #ff8a80 !important; +} +.red.accent-2 { + background-color: #ff5252 !important; +} +.red-text.text-accent-2 { + color: #ff5252 !important; +} +.red.accent-3 { + background-color: #ff1744 !important; +} +.red-text.text-accent-3 { + color: #ff1744 !important; +} +.red.accent-4 { + background-color: #d50000 !important; +} +.red-text.text-accent-4 { + color: #d50000 !important; +} +.pink { + background-color: #e91e63 !important; +} +.pink-text { + color: #e91e63 !important; +} +.pink.lighten-5 { + background-color: #fce4ec !important; +} +.pink-text.text-lighten-5 { + color: #fce4ec !important; +} +.pink.lighten-4 { + background-color: #f8bbd0 !important; +} +.pink-text.text-lighten-4 { + color: #f8bbd0 !important; +} +.pink.lighten-3 { + background-color: #f48fb1 !important; +} +.pink-text.text-lighten-3 { + color: #f48fb1 !important; +} +.pink.lighten-2 { + background-color: #f06292 !important; +} +.pink-text.text-lighten-2 { + color: #f06292 !important; +} +.pink.lighten-1 { + background-color: #ec407a !important; +} +.pink-text.text-lighten-1 { + color: #ec407a !important; +} +.pink.darken-1 { + background-color: #d81b60 !important; +} +.pink-text.text-darken-1 { + color: #d81b60 !important; +} +.pink.darken-2 { + background-color: #c2185b !important; +} +.pink-text.text-darken-2 { + color: #c2185b !important; +} +.pink.darken-3 { + background-color: #ad1457 !important; +} +.pink-text.text-darken-3 { + color: #ad1457 !important; +} +.pink.darken-4 { + background-color: #880e4f !important; +} +.pink-text.text-darken-4 { + color: #880e4f !important; +} +.pink.accent-1 { + background-color: #ff80ab !important; +} +.pink-text.text-accent-1 { + color: #ff80ab !important; +} +.pink.accent-2 { + background-color: #ff4081 !important; +} +.pink-text.text-accent-2 { + color: #ff4081 !important; +} +.pink.accent-3 { + background-color: #f50057 !important; +} +.pink-text.text-accent-3 { + color: #f50057 !important; +} +.pink.accent-4 { + background-color: #c51162 !important; +} +.pink-text.text-accent-4 { + color: #c51162 !important; +} +.purple { + background-color: #9c27b0 !important; +} +.purple-text { + color: #9c27b0 !important; +} +.purple.lighten-5 { + background-color: #f3e5f5 !important; +} +.purple-text.text-lighten-5 { + color: #f3e5f5 !important; +} +.purple.lighten-4 { + background-color: #e1bee7 !important; +} +.purple-text.text-lighten-4 { + color: #e1bee7 !important; +} +.purple.lighten-3 { + background-color: #ce93d8 !important; +} +.purple-text.text-lighten-3 { + color: #ce93d8 !important; +} +.purple.lighten-2 { + background-color: #ba68c8 !important; +} +.purple-text.text-lighten-2 { + color: #ba68c8 !important; +} +.purple.lighten-1 { + background-color: #ab47bc !important; +} +.purple-text.text-lighten-1 { + color: #ab47bc !important; +} +.purple.darken-1 { + background-color: #8e24aa !important; +} +.purple-text.text-darken-1 { + color: #8e24aa !important; +} +.purple.darken-2 { + background-color: #7b1fa2 !important; +} +.purple-text.text-darken-2 { + color: #7b1fa2 !important; +} +.purple.darken-3 { + background-color: #6a1b9a !important; +} +.purple-text.text-darken-3 { + color: #6a1b9a !important; +} +.purple.darken-4 { + background-color: #4a148c !important; +} +.purple-text.text-darken-4 { + color: #4a148c !important; +} +.purple.accent-1 { + background-color: #ea80fc !important; +} +.purple-text.text-accent-1 { + color: #ea80fc !important; +} +.purple.accent-2 { + background-color: #e040fb !important; +} +.purple-text.text-accent-2 { + color: #e040fb !important; +} +.purple.accent-3 { + background-color: #d500f9 !important; +} +.purple-text.text-accent-3 { + color: #d500f9 !important; +} +.purple.accent-4 { + background-color: #a0f !important; +} +.purple-text.text-accent-4 { + color: #a0f !important; +} +.deep-purple { + background-color: #673ab7 !important; +} +.deep-purple-text { + color: #673ab7 !important; +} +.deep-purple.lighten-5 { + background-color: #ede7f6 !important; +} +.deep-purple-text.text-lighten-5 { + color: #ede7f6 !important; +} +.deep-purple.lighten-4 { + background-color: #d1c4e9 !important; +} +.deep-purple-text.text-lighten-4 { + color: #d1c4e9 !important; +} +.deep-purple.lighten-3 { + background-color: #b39ddb !important; +} +.deep-purple-text.text-lighten-3 { + color: #b39ddb !important; +} +.deep-purple.lighten-2 { + background-color: #9575cd !important; +} +.deep-purple-text.text-lighten-2 { + color: #9575cd !important; +} +.deep-purple.lighten-1 { + background-color: #7e57c2 !important; +} +.deep-purple-text.text-lighten-1 { + color: #7e57c2 !important; +} +.deep-purple.darken-1 { + background-color: #5e35b1 !important; +} +.deep-purple-text.text-darken-1 { + color: #5e35b1 !important; +} +.deep-purple.darken-2 { + background-color: #512da8 !important; +} +.deep-purple-text.text-darken-2 { + color: #512da8 !important; +} +.deep-purple.darken-3 { + background-color: #4527a0 !important; +} +.deep-purple-text.text-darken-3 { + color: #4527a0 !important; +} +.deep-purple.darken-4 { + background-color: #311b92 !important; +} +.deep-purple-text.text-darken-4 { + color: #311b92 !important; +} +.deep-purple.accent-1 { + background-color: #b388ff !important; +} +.deep-purple-text.text-accent-1 { + color: #b388ff !important; +} +.deep-purple.accent-2 { + background-color: #7c4dff !important; +} +.deep-purple-text.text-accent-2 { + color: #7c4dff !important; +} +.deep-purple.accent-3 { + background-color: #651fff !important; +} +.deep-purple-text.text-accent-3 { + color: #651fff !important; +} +.deep-purple.accent-4 { + background-color: #6200ea !important; +} +.deep-purple-text.text-accent-4 { + color: #6200ea !important; +} +.indigo { + background-color: #3f51b5 !important; +} +.indigo-text { + color: #3f51b5 !important; +} +.indigo.lighten-5 { + background-color: #e8eaf6 !important; +} +.indigo-text.text-lighten-5 { + color: #e8eaf6 !important; +} +.indigo.lighten-4 { + background-color: #c5cae9 !important; +} +.indigo-text.text-lighten-4 { + color: #c5cae9 !important; +} +.indigo.lighten-3 { + background-color: #9fa8da !important; +} +.indigo-text.text-lighten-3 { + color: #9fa8da !important; +} +.indigo.lighten-2 { + background-color: #7986cb !important; +} +.indigo-text.text-lighten-2 { + color: #7986cb !important; +} +.indigo.lighten-1 { + background-color: #5c6bc0 !important; +} +.indigo-text.text-lighten-1 { + color: #5c6bc0 !important; +} +.indigo.darken-1 { + background-color: #3949ab !important; +} +.indigo-text.text-darken-1 { + color: #3949ab !important; +} +.indigo.darken-2 { + background-color: #303f9f !important; +} +.indigo-text.text-darken-2 { + color: #303f9f !important; +} +.indigo.darken-3 { + background-color: #283593 !important; +} +.indigo-text.text-darken-3 { + color: #283593 !important; +} +.indigo.darken-4 { + background-color: #1a237e !important; +} +.indigo-text.text-darken-4 { + color: #1a237e !important; +} +.indigo.accent-1 { + background-color: #8c9eff !important; +} +.indigo-text.text-accent-1 { + color: #8c9eff !important; +} +.indigo.accent-2 { + background-color: #536dfe !important; +} +.indigo-text.text-accent-2 { + color: #536dfe !important; +} +.indigo.accent-3 { + background-color: #3d5afe !important; +} +.indigo-text.text-accent-3 { + color: #3d5afe !important; +} +.indigo.accent-4 { + background-color: #304ffe !important; +} +.indigo-text.text-accent-4 { + color: #304ffe !important; +} +.blue { + background-color: #2196f3 !important; +} +.blue-text { + color: #2196f3 !important; +} +.blue.lighten-5 { + background-color: #e3f2fd !important; +} +.blue-text.text-lighten-5 { + color: #e3f2fd !important; +} +.blue.lighten-4 { + background-color: #bbdefb !important; +} +.blue-text.text-lighten-4 { + color: #bbdefb !important; +} +.blue.lighten-3 { + background-color: #90caf9 !important; +} +.blue-text.text-lighten-3 { + color: #90caf9 !important; +} +.blue.lighten-2 { + background-color: #64b5f6 !important; +} +.blue-text.text-lighten-2 { + color: #64b5f6 !important; +} +.blue.lighten-1 { + background-color: #42a5f5 !important; +} +.blue-text.text-lighten-1 { + color: #42a5f5 !important; +} +.blue.darken-1 { + background-color: #1e88e5 !important; +} +.blue-text.text-darken-1 { + color: #1e88e5 !important; +} +.blue.darken-2 { + background-color: #1976d2 !important; +} +.blue-text.text-darken-2 { + color: #1976d2 !important; +} +.blue.darken-3 { + background-color: #1565c0 !important; +} +.blue-text.text-darken-3 { + color: #1565c0 !important; +} +.blue.darken-4 { + background-color: #0d47a1 !important; +} +.blue-text.text-darken-4 { + color: #0d47a1 !important; +} +.blue.accent-1 { + background-color: #82b1ff !important; +} +.blue-text.text-accent-1 { + color: #82b1ff !important; +} +.blue.accent-2 { + background-color: #448aff !important; +} +.blue-text.text-accent-2 { + color: #448aff !important; +} +.blue.accent-3 { + background-color: #2979ff !important; +} +.blue-text.text-accent-3 { + color: #2979ff !important; +} +.blue.accent-4 { + background-color: #2962ff !important; +} +.blue-text.text-accent-4 { + color: #2962ff !important; +} +.light-blue { + background-color: #03a9f4 !important; +} +.light-blue-text { + color: #03a9f4 !important; +} +.light-blue.lighten-5 { + background-color: #e1f5fe !important; +} +.light-blue-text.text-lighten-5 { + color: #e1f5fe !important; +} +.light-blue.lighten-4 { + background-color: #b3e5fc !important; +} +.light-blue-text.text-lighten-4 { + color: #b3e5fc !important; +} +.light-blue.lighten-3 { + background-color: #81d4fa !important; +} +.light-blue-text.text-lighten-3 { + color: #81d4fa !important; +} +.light-blue.lighten-2 { + background-color: #4fc3f7 !important; +} +.light-blue-text.text-lighten-2 { + color: #4fc3f7 !important; +} +.light-blue.lighten-1 { + background-color: #29b6f6 !important; +} +.light-blue-text.text-lighten-1 { + color: #29b6f6 !important; +} +.light-blue.darken-1 { + background-color: #039be5 !important; +} +.light-blue-text.text-darken-1 { + color: #039be5 !important; +} +.light-blue.darken-2 { + background-color: #0288d1 !important; +} +.light-blue-text.text-darken-2 { + color: #0288d1 !important; +} +.light-blue.darken-3 { + background-color: #0277bd !important; +} +.light-blue-text.text-darken-3 { + color: #0277bd !important; +} +.light-blue.darken-4 { + background-color: #01579b !important; +} +.light-blue-text.text-darken-4 { + color: #01579b !important; +} +.light-blue.accent-1 { + background-color: #80d8ff !important; +} +.light-blue-text.text-accent-1 { + color: #80d8ff !important; +} +.light-blue.accent-2 { + background-color: #40c4ff !important; +} +.light-blue-text.text-accent-2 { + color: #40c4ff !important; +} +.light-blue.accent-3 { + background-color: #00b0ff !important; +} +.light-blue-text.text-accent-3 { + color: #00b0ff !important; +} +.light-blue.accent-4 { + background-color: #0091ea !important; +} +.light-blue-text.text-accent-4 { + color: #0091ea !important; +} +.cyan { + background-color: #00bcd4 !important; +} +.cyan-text { + color: #00bcd4 !important; +} +.cyan.lighten-5 { + background-color: #e0f7fa !important; +} +.cyan-text.text-lighten-5 { + color: #e0f7fa !important; +} +.cyan.lighten-4 { + background-color: #b2ebf2 !important; +} +.cyan-text.text-lighten-4 { + color: #b2ebf2 !important; +} +.cyan.lighten-3 { + background-color: #80deea !important; +} +.cyan-text.text-lighten-3 { + color: #80deea !important; +} +.cyan.lighten-2 { + background-color: #4dd0e1 !important; +} +.cyan-text.text-lighten-2 { + color: #4dd0e1 !important; +} +.cyan.lighten-1 { + background-color: #26c6da !important; +} +.cyan-text.text-lighten-1 { + color: #26c6da !important; +} +.cyan.darken-1 { + background-color: #00acc1 !important; +} +.cyan-text.text-darken-1 { + color: #00acc1 !important; +} +.cyan.darken-2 { + background-color: #0097a7 !important; +} +.cyan-text.text-darken-2 { + color: #0097a7 !important; +} +.cyan.darken-3 { + background-color: #00838f !important; +} +.cyan-text.text-darken-3 { + color: #00838f !important; +} +.cyan.darken-4 { + background-color: #006064 !important; +} +.cyan-text.text-darken-4 { + color: #006064 !important; +} +.cyan.accent-1 { + background-color: #84ffff !important; +} +.cyan-text.text-accent-1 { + color: #84ffff !important; +} +.cyan.accent-2 { + background-color: #18ffff !important; +} +.cyan-text.text-accent-2 { + color: #18ffff !important; +} +.cyan.accent-3 { + background-color: #00e5ff !important; +} +.cyan-text.text-accent-3 { + color: #00e5ff !important; +} +.cyan.accent-4 { + background-color: #00b8d4 !important; +} +.cyan-text.text-accent-4 { + color: #00b8d4 !important; +} +.teal { + background-color: #009688 !important; +} +.teal-text { + color: #009688 !important; +} +.teal.lighten-5 { + background-color: #e0f2f1 !important; +} +.teal-text.text-lighten-5 { + color: #e0f2f1 !important; +} +.teal.lighten-4 { + background-color: #b2dfdb !important; +} +.teal-text.text-lighten-4 { + color: #b2dfdb !important; +} +.teal.lighten-3 { + background-color: #80cbc4 !important; +} +.teal-text.text-lighten-3 { + color: #80cbc4 !important; +} +.teal.lighten-2 { + background-color: #4db6ac !important; +} +.teal-text.text-lighten-2 { + color: #4db6ac !important; +} +.teal.lighten-1 { + background-color: #26a69a !important; +} +.teal-text.text-lighten-1 { + color: #26a69a !important; +} +.teal.darken-1 { + background-color: #00897b !important; +} +.teal-text.text-darken-1 { + color: #00897b !important; +} +.teal.darken-2 { + background-color: #00796b !important; +} +.teal-text.text-darken-2 { + color: #00796b !important; +} +.teal.darken-3 { + background-color: #00695c !important; +} +.teal-text.text-darken-3 { + color: #00695c !important; +} +.teal.darken-4 { + background-color: #004d40 !important; +} +.teal-text.text-darken-4 { + color: #004d40 !important; +} +.teal.accent-1 { + background-color: #a7ffeb !important; +} +.teal-text.text-accent-1 { + color: #a7ffeb !important; +} +.teal.accent-2 { + background-color: #64ffda !important; +} +.teal-text.text-accent-2 { + color: #64ffda !important; +} +.teal.accent-3 { + background-color: #1de9b6 !important; +} +.teal-text.text-accent-3 { + color: #1de9b6 !important; +} +.teal.accent-4 { + background-color: #00bfa5 !important; +} +.teal-text.text-accent-4 { + color: #00bfa5 !important; +} +.green { + background-color: #4caf50 !important; +} +.green-text { + color: #4caf50 !important; +} +.green.lighten-5 { + background-color: #e8f5e9 !important; +} +.green-text.text-lighten-5 { + color: #e8f5e9 !important; +} +.green.lighten-4 { + background-color: #c8e6c9 !important; +} +.green-text.text-lighten-4 { + color: #c8e6c9 !important; +} +.green.lighten-3 { + background-color: #a5d6a7 !important; +} +.green-text.text-lighten-3 { + color: #a5d6a7 !important; +} +.green.lighten-2 { + background-color: #81c784 !important; +} +.green-text.text-lighten-2 { + color: #81c784 !important; +} +.green.lighten-1 { + background-color: #66bb6a !important; +} +.green-text.text-lighten-1 { + color: #66bb6a !important; +} +.green.darken-1 { + background-color: #43a047 !important; +} +.green-text.text-darken-1 { + color: #43a047 !important; +} +.green.darken-2 { + background-color: #388e3c !important; +} +.green-text.text-darken-2 { + color: #388e3c !important; +} +.green.darken-3 { + background-color: #2e7d32 !important; +} +.green-text.text-darken-3 { + color: #2e7d32 !important; +} +.green.darken-4 { + background-color: #1b5e20 !important; +} +.green-text.text-darken-4 { + color: #1b5e20 !important; +} +.green.accent-1 { + background-color: #b9f6ca !important; +} +.green-text.text-accent-1 { + color: #b9f6ca !important; +} +.green.accent-2 { + background-color: #69f0ae !important; +} +.green-text.text-accent-2 { + color: #69f0ae !important; +} +.green.accent-3 { + background-color: #00e676 !important; +} +.green-text.text-accent-3 { + color: #00e676 !important; +} +.green.accent-4 { + background-color: #00c853 !important; +} +.green-text.text-accent-4 { + color: #00c853 !important; +} +.light-green { + background-color: #8bc34a !important; +} +.light-green-text { + color: #8bc34a !important; +} +.light-green.lighten-5 { + background-color: #f1f8e9 !important; +} +.light-green-text.text-lighten-5 { + color: #f1f8e9 !important; +} +.light-green.lighten-4 { + background-color: #dcedc8 !important; +} +.light-green-text.text-lighten-4 { + color: #dcedc8 !important; +} +.light-green.lighten-3 { + background-color: #c5e1a5 !important; +} +.light-green-text.text-lighten-3 { + color: #c5e1a5 !important; +} +.light-green.lighten-2 { + background-color: #aed581 !important; +} +.light-green-text.text-lighten-2 { + color: #aed581 !important; +} +.light-green.lighten-1 { + background-color: #9ccc65 !important; +} +.light-green-text.text-lighten-1 { + color: #9ccc65 !important; +} +.light-green.darken-1 { + background-color: #7cb342 !important; +} +.light-green-text.text-darken-1 { + color: #7cb342 !important; +} +.light-green.darken-2 { + background-color: #689f38 !important; +} +.light-green-text.text-darken-2 { + color: #689f38 !important; +} +.light-green.darken-3 { + background-color: #558b2f !important; +} +.light-green-text.text-darken-3 { + color: #558b2f !important; +} +.light-green.darken-4 { + background-color: #33691e !important; +} +.light-green-text.text-darken-4 { + color: #33691e !important; +} +.light-green.accent-1 { + background-color: #ccff90 !important; +} +.light-green-text.text-accent-1 { + color: #ccff90 !important; +} +.light-green.accent-2 { + background-color: #b2ff59 !important; +} +.light-green-text.text-accent-2 { + color: #b2ff59 !important; +} +.light-green.accent-3 { + background-color: #76ff03 !important; +} +.light-green-text.text-accent-3 { + color: #76ff03 !important; +} +.light-green.accent-4 { + background-color: #64dd17 !important; +} +.light-green-text.text-accent-4 { + color: #64dd17 !important; +} +.lime { + background-color: #cddc39 !important; +} +.lime-text { + color: #cddc39 !important; +} +.lime.lighten-5 { + background-color: #f9fbe7 !important; +} +.lime-text.text-lighten-5 { + color: #f9fbe7 !important; +} +.lime.lighten-4 { + background-color: #f0f4c3 !important; +} +.lime-text.text-lighten-4 { + color: #f0f4c3 !important; +} +.lime.lighten-3 { + background-color: #e6ee9c !important; +} +.lime-text.text-lighten-3 { + color: #e6ee9c !important; +} +.lime.lighten-2 { + background-color: #dce775 !important; +} +.lime-text.text-lighten-2 { + color: #dce775 !important; +} +.lime.lighten-1 { + background-color: #d4e157 !important; +} +.lime-text.text-lighten-1 { + color: #d4e157 !important; +} +.lime.darken-1 { + background-color: #c0ca33 !important; +} +.lime-text.text-darken-1 { + color: #c0ca33 !important; +} +.lime.darken-2 { + background-color: #afb42b !important; +} +.lime-text.text-darken-2 { + color: #afb42b !important; +} +.lime.darken-3 { + background-color: #9e9d24 !important; +} +.lime-text.text-darken-3 { + color: #9e9d24 !important; +} +.lime.darken-4 { + background-color: #827717 !important; +} +.lime-text.text-darken-4 { + color: #827717 !important; +} +.lime.accent-1 { + background-color: #f4ff81 !important; +} +.lime-text.text-accent-1 { + color: #f4ff81 !important; +} +.lime.accent-2 { + background-color: #eeff41 !important; +} +.lime-text.text-accent-2 { + color: #eeff41 !important; +} +.lime.accent-3 { + background-color: #c6ff00 !important; +} +.lime-text.text-accent-3 { + color: #c6ff00 !important; +} +.lime.accent-4 { + background-color: #aeea00 !important; +} +.lime-text.text-accent-4 { + color: #aeea00 !important; +} +.yellow { + background-color: #ffeb3b !important; +} +.yellow-text { + color: #ffeb3b !important; +} +.yellow.lighten-5 { + background-color: #fffde7 !important; +} +.yellow-text.text-lighten-5 { + color: #fffde7 !important; +} +.yellow.lighten-4 { + background-color: #fff9c4 !important; +} +.yellow-text.text-lighten-4 { + color: #fff9c4 !important; +} +.yellow.lighten-3 { + background-color: #fff59d !important; +} +.yellow-text.text-lighten-3 { + color: #fff59d !important; +} +.yellow.lighten-2 { + background-color: #fff176 !important; +} +.yellow-text.text-lighten-2 { + color: #fff176 !important; +} +.yellow.lighten-1 { + background-color: #ffee58 !important; +} +.yellow-text.text-lighten-1 { + color: #ffee58 !important; +} +.yellow.darken-1 { + background-color: #fdd835 !important; +} +.yellow-text.text-darken-1 { + color: #fdd835 !important; +} +.yellow.darken-2 { + background-color: #fbc02d !important; +} +.yellow-text.text-darken-2 { + color: #fbc02d !important; +} +.yellow.darken-3 { + background-color: #f9a825 !important; +} +.yellow-text.text-darken-3 { + color: #f9a825 !important; +} +.yellow.darken-4 { + background-color: #f57f17 !important; +} +.yellow-text.text-darken-4 { + color: #f57f17 !important; +} +.yellow.accent-1 { + background-color: #ffff8d !important; +} +.yellow-text.text-accent-1 { + color: #ffff8d !important; +} +.yellow.accent-2 { + background-color: #ff0 !important; +} +.yellow-text.text-accent-2 { + color: #ff0 !important; +} +.yellow.accent-3 { + background-color: #ffea00 !important; +} +.yellow-text.text-accent-3 { + color: #ffea00 !important; +} +.yellow.accent-4 { + background-color: #ffd600 !important; +} +.yellow-text.text-accent-4 { + color: #ffd600 !important; +} +.amber { + background-color: #ffc107 !important; +} +.amber-text { + color: #ffc107 !important; +} +.amber.lighten-5 { + background-color: #fff8e1 !important; +} +.amber-text.text-lighten-5 { + color: #fff8e1 !important; +} +.amber.lighten-4 { + background-color: #ffecb3 !important; +} +.amber-text.text-lighten-4 { + color: #ffecb3 !important; +} +.amber.lighten-3 { + background-color: #ffe082 !important; +} +.amber-text.text-lighten-3 { + color: #ffe082 !important; +} +.amber.lighten-2 { + background-color: #ffd54f !important; +} +.amber-text.text-lighten-2 { + color: #ffd54f !important; +} +.amber.lighten-1 { + background-color: #ffca28 !important; +} +.amber-text.text-lighten-1 { + color: #ffca28 !important; +} +.amber.darken-1 { + background-color: #ffb300 !important; +} +.amber-text.text-darken-1 { + color: #ffb300 !important; +} +.amber.darken-2 { + background-color: #ffa000 !important; +} +.amber-text.text-darken-2 { + color: #ffa000 !important; +} +.amber.darken-3 { + background-color: #ff8f00 !important; +} +.amber-text.text-darken-3 { + color: #ff8f00 !important; +} +.amber.darken-4 { + background-color: #ff6f00 !important; +} +.amber-text.text-darken-4 { + color: #ff6f00 !important; +} +.amber.accent-1 { + background-color: #ffe57f !important; +} +.amber-text.text-accent-1 { + color: #ffe57f !important; +} +.amber.accent-2 { + background-color: #ffd740 !important; +} +.amber-text.text-accent-2 { + color: #ffd740 !important; +} +.amber.accent-3 { + background-color: #ffc400 !important; +} +.amber-text.text-accent-3 { + color: #ffc400 !important; +} +.amber.accent-4 { + background-color: #ffab00 !important; +} +.amber-text.text-accent-4 { + color: #ffab00 !important; +} +.orange { + background-color: #ff9800 !important; +} +.orange-text { + color: #ff9800 !important; +} +.orange.lighten-5 { + background-color: #fff3e0 !important; +} +.orange-text.text-lighten-5 { + color: #fff3e0 !important; +} +.orange.lighten-4 { + background-color: #ffe0b2 !important; +} +.orange-text.text-lighten-4 { + color: #ffe0b2 !important; +} +.orange.lighten-3 { + background-color: #ffcc80 !important; +} +.orange-text.text-lighten-3 { + color: #ffcc80 !important; +} +.orange.lighten-2 { + background-color: #ffb74d !important; +} +.orange-text.text-lighten-2 { + color: #ffb74d !important; +} +.orange.lighten-1 { + background-color: #ffa726 !important; +} +.orange-text.text-lighten-1 { + color: #ffa726 !important; +} +.orange.darken-1 { + background-color: #fb8c00 !important; +} +.orange-text.text-darken-1 { + color: #fb8c00 !important; +} +.orange.darken-2 { + background-color: #f57c00 !important; +} +.orange-text.text-darken-2 { + color: #f57c00 !important; +} +.orange.darken-3 { + background-color: #ef6c00 !important; +} +.orange-text.text-darken-3 { + color: #ef6c00 !important; +} +.orange.darken-4 { + background-color: #e65100 !important; +} +.orange-text.text-darken-4 { + color: #e65100 !important; +} +.orange.accent-1 { + background-color: #ffd180 !important; +} +.orange-text.text-accent-1 { + color: #ffd180 !important; +} +.orange.accent-2 { + background-color: #ffab40 !important; +} +.orange-text.text-accent-2 { + color: #ffab40 !important; +} +.orange.accent-3 { + background-color: #ff9100 !important; +} +.orange-text.text-accent-3 { + color: #ff9100 !important; +} +.orange.accent-4 { + background-color: #ff6d00 !important; +} +.orange-text.text-accent-4 { + color: #ff6d00 !important; +} +.deep-orange { + background-color: #ff5722 !important; +} +.deep-orange-text { + color: #ff5722 !important; +} +.deep-orange.lighten-5 { + background-color: #fbe9e7 !important; +} +.deep-orange-text.text-lighten-5 { + color: #fbe9e7 !important; +} +.deep-orange.lighten-4 { + background-color: #ffccbc !important; +} +.deep-orange-text.text-lighten-4 { + color: #ffccbc !important; +} +.deep-orange.lighten-3 { + background-color: #ffab91 !important; +} +.deep-orange-text.text-lighten-3 { + color: #ffab91 !important; +} +.deep-orange.lighten-2 { + background-color: #ff8a65 !important; +} +.deep-orange-text.text-lighten-2 { + color: #ff8a65 !important; +} +.deep-orange.lighten-1 { + background-color: #ff7043 !important; +} +.deep-orange-text.text-lighten-1 { + color: #ff7043 !important; +} +.deep-orange.darken-1 { + background-color: #f4511e !important; +} +.deep-orange-text.text-darken-1 { + color: #f4511e !important; +} +.deep-orange.darken-2 { + background-color: #e64a19 !important; +} +.deep-orange-text.text-darken-2 { + color: #e64a19 !important; +} +.deep-orange.darken-3 { + background-color: #d84315 !important; +} +.deep-orange-text.text-darken-3 { + color: #d84315 !important; +} +.deep-orange.darken-4 { + background-color: #bf360c !important; +} +.deep-orange-text.text-darken-4 { + color: #bf360c !important; +} +.deep-orange.accent-1 { + background-color: #ff9e80 !important; +} +.deep-orange-text.text-accent-1 { + color: #ff9e80 !important; +} +.deep-orange.accent-2 { + background-color: #ff6e40 !important; +} +.deep-orange-text.text-accent-2 { + color: #ff6e40 !important; +} +.deep-orange.accent-3 { + background-color: #ff3d00 !important; +} +.deep-orange-text.text-accent-3 { + color: #ff3d00 !important; +} +.deep-orange.accent-4 { + background-color: #dd2c00 !important; +} +.deep-orange-text.text-accent-4 { + color: #dd2c00 !important; +} +.brown { + background-color: #795548 !important; +} +.brown-text { + color: #795548 !important; +} +.brown.lighten-5 { + background-color: #efebe9 !important; +} +.brown-text.text-lighten-5 { + color: #efebe9 !important; +} +.brown.lighten-4 { + background-color: #d7ccc8 !important; +} +.brown-text.text-lighten-4 { + color: #d7ccc8 !important; +} +.brown.lighten-3 { + background-color: #bcaaa4 !important; +} +.brown-text.text-lighten-3 { + color: #bcaaa4 !important; +} +.brown.lighten-2 { + background-color: #a1887f !important; +} +.brown-text.text-lighten-2 { + color: #a1887f !important; +} +.brown.lighten-1 { + background-color: #8d6e63 !important; +} +.brown-text.text-lighten-1 { + color: #8d6e63 !important; +} +.brown.darken-1 { + background-color: #6d4c41 !important; +} +.brown-text.text-darken-1 { + color: #6d4c41 !important; +} +.brown.darken-2 { + background-color: #5d4037 !important; +} +.brown-text.text-darken-2 { + color: #5d4037 !important; +} +.brown.darken-3 { + background-color: #4e342e !important; +} +.brown-text.text-darken-3 { + color: #4e342e !important; +} +.brown.darken-4 { + background-color: #3e2723 !important; +} +.brown-text.text-darken-4 { + color: #3e2723 !important; +} +.blue-grey { + background-color: #607d8b !important; +} +.blue-grey-text { + color: #607d8b !important; +} +.blue-grey.lighten-5 { + background-color: #eceff1 !important; +} +.blue-grey-text.text-lighten-5 { + color: #eceff1 !important; +} +.blue-grey.lighten-4 { + background-color: #cfd8dc !important; +} +.blue-grey-text.text-lighten-4 { + color: #cfd8dc !important; +} +.blue-grey.lighten-3 { + background-color: #b0bec5 !important; +} +.blue-grey-text.text-lighten-3 { + color: #b0bec5 !important; +} +.blue-grey.lighten-2 { + background-color: #90a4ae !important; +} +.blue-grey-text.text-lighten-2 { + color: #90a4ae !important; +} +.blue-grey.lighten-1 { + background-color: #78909c !important; +} +.blue-grey-text.text-lighten-1 { + color: #78909c !important; +} +.blue-grey.darken-1 { + background-color: #546e7a !important; +} +.blue-grey-text.text-darken-1 { + color: #546e7a !important; +} +.blue-grey.darken-2 { + background-color: #455a64 !important; +} +.blue-grey-text.text-darken-2 { + color: #455a64 !important; +} +.blue-grey.darken-3 { + background-color: #37474f !important; +} +.blue-grey-text.text-darken-3 { + color: #37474f !important; +} +.blue-grey.darken-4 { + background-color: #263238 !important; +} +.blue-grey-text.text-darken-4 { + color: #263238 !important; +} +.grey { + background-color: #9e9e9e !important; +} +.grey-text { + color: #9e9e9e !important; +} +.grey.lighten-5 { + background-color: #fafafa !important; +} +.grey-text.text-lighten-5 { + color: #fafafa !important; +} +.grey.lighten-4 { + background-color: #f5f5f5 !important; +} +.grey-text.text-lighten-4 { + color: #f5f5f5 !important; +} +.grey.lighten-3 { + background-color: #eee !important; +} +.grey-text.text-lighten-3 { + color: #eee !important; +} +.grey.lighten-2 { + background-color: #e0e0e0 !important; +} +.grey-text.text-lighten-2 { + color: #e0e0e0 !important; +} +.grey.lighten-1 { + background-color: #bdbdbd !important; +} +.grey-text.text-lighten-1 { + color: #bdbdbd !important; +} +.grey.darken-1 { + background-color: #757575 !important; +} +.grey-text.text-darken-1 { + color: #757575 !important; +} +.grey.darken-2 { + background-color: #616161 !important; +} +.grey-text.text-darken-2 { + color: #616161 !important; +} +.grey.darken-3 { + background-color: #424242 !important; +} +.grey-text.text-darken-3 { + color: #424242 !important; +} +.grey.darken-4 { + background-color: #212121 !important; +} +.grey-text.text-darken-4 { + color: #212121 !important; +} +.black { + background-color: #000 !important; +} +.black-text { + color: #000 !important; +} +.white { + background-color: #fff !important; +} +.white-text { + color: #fff !important; +} +.transparent { + background-color: rgba(0, 0, 0, 0) !important; +} +.transparent-text { + color: rgba(0, 0, 0, 0) !important; +} /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */ +html { + line-height: 1.15; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; +} +body { + margin: 0; +} +article, +aside, +footer, +header, +nav, +section { + display: block; +} +h1 { + font-size: 2em; + margin: 0.67em 0; +} +figcaption, +figure, +main { + display: block; +} +figure { + margin: 1em 40px; +} +hr { + -webkit-box-sizing: content-box; + box-sizing: content-box; + height: 0; + overflow: visible; +} +pre { + font-family: monospace, monospace; + font-size: 1em; +} +a { + background-color: transparent; + -webkit-text-decoration-skip: objects; +} +abbr[title] { + border-bottom: none; + text-decoration: underline; + -webkit-text-decoration: underline dotted; + -moz-text-decoration: underline dotted; + text-decoration: underline dotted; +} +b, +strong { + font-weight: inherit; +} +b, +strong { + font-weight: bolder; +} +code, +kbd, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +dfn { + font-style: italic; +} +mark { + background-color: #ff0; + color: #000; +} +small { + font-size: 80%; +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +sub { + bottom: -0.25em; +} +sup { + top: -0.5em; +} +audio, +video { + display: inline-block; +} +audio:not([controls]) { + display: none; + height: 0; +} +img { + border-style: none; +} +svg:not(:root) { + overflow: hidden; +} +button, +input, +optgroup, +select, +textarea { + font-family: sans-serif; + font-size: 100%; + line-height: 1.15; + margin: 0; +} +button, +input { + overflow: visible; +} +button, +select { + text-transform: none; +} +button, +html [type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; +} +button::-moz-focus-inner, +[type='button']::-moz-focus-inner, +[type='reset']::-moz-focus-inner, +[type='submit']::-moz-focus-inner { + border-style: none; + padding: 0; +} +button:-moz-focusring, +[type='button']:-moz-focusring, +[type='reset']:-moz-focusring, +[type='submit']:-moz-focusring { + outline: 1px dotted ButtonText; +} +fieldset { + padding: 0.35em 0.75em 0.625em; +} +legend { + -webkit-box-sizing: border-box; + box-sizing: border-box; + color: inherit; + display: table; + max-width: 100%; + padding: 0; + white-space: normal; +} +progress { + display: inline-block; + vertical-align: baseline; +} +textarea { + overflow: auto; +} +[type='checkbox'], +[type='radio'] { + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding: 0; +} +[type='number']::-webkit-inner-spin-button, +[type='number']::-webkit-outer-spin-button { + height: auto; +} +[type='search'] { + -webkit-appearance: textfield; + outline-offset: -2px; +} +[type='search']::-webkit-search-cancel-button, +[type='search']::-webkit-search-decoration { + -webkit-appearance: none; +} +::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; +} +details, +menu { + display: block; +} +summary { + display: list-item; +} +canvas { + display: inline-block; +} +template { + display: none; +} +[hidden] { + display: none; +} +html { + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +*, +*:before, +*:after { + -webkit-box-sizing: inherit; + box-sizing: inherit; +} +button, +input, +optgroup, +select, +textarea { + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, + 'Helvetica Neue', sans-serif; +} +ul:not(.browser-default) { + padding-left: 0; + list-style-type: none; +} +ul:not(.browser-default) > li { + list-style-type: none; +} +a { + color: #039be5; + text-decoration: none; + -webkit-tap-highlight-color: transparent; +} +.valign-wrapper { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; +} +.clearfix { + clear: both; +} +.z-depth-0 { + -webkit-box-shadow: none !important; + box-shadow: none !important; +} +.z-depth-1, +nav, +.card-panel, +.card, +.toast, +.btn, +.btn-large, +.btn-small, +.btn-floating, +.dropdown-content, +.collapsible, +.sidenav { + -webkit-box-shadow: + 0 2px 2px 0 rgba(0, 0, 0, 0.14), + 0 3px 1px -2px rgba(0, 0, 0, 0.12), + 0 1px 5px 0 rgba(0, 0, 0, 0.2); + box-shadow: + 0 2px 2px 0 rgba(0, 0, 0, 0.14), + 0 3px 1px -2px rgba(0, 0, 0, 0.12), + 0 1px 5px 0 rgba(0, 0, 0, 0.2); +} +.z-depth-1-half, +.btn:hover, +.btn-large:hover, +.btn-small:hover, +.btn-floating:hover { + -webkit-box-shadow: + 0 3px 3px 0 rgba(0, 0, 0, 0.14), + 0 1px 7px 0 rgba(0, 0, 0, 0.12), + 0 3px 1px -1px rgba(0, 0, 0, 0.2); + box-shadow: + 0 3px 3px 0 rgba(0, 0, 0, 0.14), + 0 1px 7px 0 rgba(0, 0, 0, 0.12), + 0 3px 1px -1px rgba(0, 0, 0, 0.2); +} +.z-depth-2 { + -webkit-box-shadow: + 0 4px 5px 0 rgba(0, 0, 0, 0.14), + 0 1px 10px 0 rgba(0, 0, 0, 0.12), + 0 2px 4px -1px rgba(0, 0, 0, 0.3); + box-shadow: + 0 4px 5px 0 rgba(0, 0, 0, 0.14), + 0 1px 10px 0 rgba(0, 0, 0, 0.12), + 0 2px 4px -1px rgba(0, 0, 0, 0.3); +} +.z-depth-3 { + -webkit-box-shadow: + 0 8px 17px 2px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12), + 0 5px 5px -3px rgba(0, 0, 0, 0.2); + box-shadow: + 0 8px 17px 2px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12), + 0 5px 5px -3px rgba(0, 0, 0, 0.2); +} +.z-depth-4 { + -webkit-box-shadow: + 0 16px 24px 2px rgba(0, 0, 0, 0.14), + 0 6px 30px 5px rgba(0, 0, 0, 0.12), + 0 8px 10px -7px rgba(0, 0, 0, 0.2); + box-shadow: + 0 16px 24px 2px rgba(0, 0, 0, 0.14), + 0 6px 30px 5px rgba(0, 0, 0, 0.12), + 0 8px 10px -7px rgba(0, 0, 0, 0.2); +} +.z-depth-5, +.modal { + -webkit-box-shadow: + 0 24px 38px 3px rgba(0, 0, 0, 0.14), + 0 9px 46px 8px rgba(0, 0, 0, 0.12), + 0 11px 15px -7px rgba(0, 0, 0, 0.2); + box-shadow: + 0 24px 38px 3px rgba(0, 0, 0, 0.14), + 0 9px 46px 8px rgba(0, 0, 0, 0.12), + 0 11px 15px -7px rgba(0, 0, 0, 0.2); +} +.hoverable { + -webkit-transition: -webkit-box-shadow 0.25s; + transition: -webkit-box-shadow 0.25s; + transition: box-shadow 0.25s; + transition: + box-shadow 0.25s, + -webkit-box-shadow 0.25s; +} +.hoverable:hover { + -webkit-box-shadow: + 0 8px 17px 0 rgba(0, 0, 0, 0.2), + 0 6px 20px 0 rgba(0, 0, 0, 0.19); + box-shadow: + 0 8px 17px 0 rgba(0, 0, 0, 0.2), + 0 6px 20px 0 rgba(0, 0, 0, 0.19); +} +.divider { + height: 1px; + overflow: hidden; + background-color: #e0e0e0; +} +blockquote { + margin: 20px 0; + padding-left: 1.5rem; + border-left: 5px solid #ee6e73; +} +i { + line-height: inherit; +} +i.left { + float: left; + margin-right: 15px; +} +i.right { + float: right; + margin-left: 15px; +} +i.tiny { + font-size: 1rem; +} +i.small { + font-size: 2rem; +} +i.medium { + font-size: 4rem; +} +i.large { + font-size: 6rem; +} +img.responsive-img, +video.responsive-video { + max-width: 100%; + height: auto; +} +.pagination li { + display: inline-block; + border-radius: 2px; + text-align: center; + vertical-align: top; + height: 30px; +} +.pagination li a { + color: #444; + display: inline-block; + font-size: 1.2rem; + padding: 0 10px; + line-height: 30px; +} +.pagination li.active a { + color: #fff; +} +.pagination li.active { + background-color: #ee6e73; +} +.pagination li.disabled a { + cursor: default; + color: #999; +} +.pagination li i { + font-size: 2rem; +} +.pagination li.pages ul li { + display: inline-block; + float: none; +} +@media only screen and (max-width: 992px) { + .pagination { + width: 100%; + } + .pagination li.prev, + .pagination li.next { + width: 10%; + } + .pagination li.pages { + width: 80%; + overflow: hidden; + white-space: nowrap; + } +} +.breadcrumb { + font-size: 18px; + color: rgba(255, 255, 255, 0.7); +} +.breadcrumb i, +.breadcrumb [class^='mdi-'], +.breadcrumb [class*='mdi-'], +.breadcrumb i.material-icons { + display: inline-block; + float: left; + font-size: 24px; +} +.breadcrumb:before { + content: '\E5CC'; + color: rgba(255, 255, 255, 0.7); + vertical-align: top; + display: inline-block; + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 25px; + margin: 0 10px 0 8px; + -webkit-font-smoothing: antialiased; +} +.breadcrumb:first-child:before { + display: none; +} +.breadcrumb:last-child { + color: #fff; +} +.parallax-container { + position: relative; + overflow: hidden; + height: 500px; +} +.parallax-container .parallax { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -1; +} +.parallax-container .parallax img { + opacity: 0; + position: absolute; + left: 50%; + bottom: 0; + min-width: 100%; + min-height: 100%; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + -webkit-transform: translateX(-50%); + transform: translateX(-50%); +} +.pin-top, +.pin-bottom { + position: relative; +} +.pinned { + position: fixed !important; +} +ul.staggered-list li { + opacity: 0; +} +.fade-in { + opacity: 0; + -webkit-transform-origin: 0 50%; + transform-origin: 0 50%; +} +@media only screen and (max-width: 600px) { + .hide-on-small-only, + .hide-on-small-and-down { + display: none !important; + } +} +@media only screen and (max-width: 992px) { + .hide-on-med-and-down { + display: none !important; + } +} +@media only screen and (min-width: 601px) { + .hide-on-med-and-up { + display: none !important; + } +} +@media only screen and (min-width: 600px) and (max-width: 992px) { + .hide-on-med-only { + display: none !important; + } +} +@media only screen and (min-width: 993px) { + .hide-on-large-only { + display: none !important; + } +} +@media only screen and (min-width: 1201px) { + .hide-on-extra-large-only { + display: none !important; + } +} +@media only screen and (min-width: 1201px) { + .show-on-extra-large { + display: block !important; + } +} +@media only screen and (min-width: 993px) { + .show-on-large { + display: block !important; + } +} +@media only screen and (min-width: 600px) and (max-width: 992px) { + .show-on-medium { + display: block !important; + } +} +@media only screen and (max-width: 600px) { + .show-on-small { + display: block !important; + } +} +@media only screen and (min-width: 601px) { + .show-on-medium-and-up { + display: block !important; + } +} +@media only screen and (max-width: 992px) { + .show-on-medium-and-down { + display: block !important; + } +} +@media only screen and (max-width: 600px) { + .center-on-small-only { + text-align: center; + } +} +.page-footer { + padding-top: 20px; + color: #fff; + background-color: #ee6e73; +} +.page-footer .footer-copyright { + overflow: hidden; + min-height: 50px; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + padding: 10px 0px; + color: rgba(255, 255, 255, 0.8); + background-color: rgba(51, 51, 51, 0.08); +} +table, +th, +td { + border: none; +} +table { + width: 100%; + display: table; + border-collapse: collapse; + border-spacing: 0; +} +table.striped tr { + border-bottom: none; +} +table.striped > tbody > tr:nth-child(odd) { + background-color: rgba(242, 242, 242, 0.5); +} +table.striped > tbody > tr > td { + border-radius: 0; +} +table.highlight > tbody > tr { + -webkit-transition: background-color 0.25s ease; + transition: background-color 0.25s ease; +} +table.highlight > tbody > tr:hover { + background-color: rgba(242, 242, 242, 0.5); +} +table.centered thead tr th, +table.centered tbody tr td { + text-align: center; +} +tr { + border-bottom: 1px solid rgba(0, 0, 0, 0.12); +} +td, +th { + padding: 15px 5px; + display: table-cell; + text-align: left; + vertical-align: middle; + border-radius: 2px; +} +@media only screen and (max-width: 992px) { + table.responsive-table { + width: 100%; + border-collapse: collapse; + border-spacing: 0; + display: block; + position: relative; + } + table.responsive-table td:empty:before { + content: '\00a0'; + } + table.responsive-table th, + table.responsive-table td { + margin: 0; + vertical-align: top; + } + table.responsive-table th { + text-align: left; + } + table.responsive-table thead { + display: block; + float: left; + } + table.responsive-table thead tr { + display: block; + padding: 0 10px 0 0; + } + table.responsive-table thead tr th::before { + content: '\00a0'; + } + table.responsive-table tbody { + display: block; + width: auto; + position: relative; + overflow-x: auto; + white-space: nowrap; + } + table.responsive-table tbody tr { + display: inline-block; + vertical-align: top; + } + table.responsive-table th { + display: block; + text-align: right; + } + table.responsive-table td { + display: block; + min-height: 1.25em; + text-align: left; + } + table.responsive-table tr { + border-bottom: none; + padding: 0 10px; + } + table.responsive-table thead { + border: 0; + border-right: 1px solid rgba(0, 0, 0, 0.12); + } +} +.collection { + margin: 0.5rem 0 1rem 0; + border: 1px solid #e0e0e0; + border-radius: 2px; + overflow: hidden; + position: relative; +} +.collection .collection-item { + background-color: #fff; + line-height: 1.5rem; + padding: 10px 20px; + margin: 0; + border-bottom: 1px solid #e0e0e0; +} +.collection .collection-item.avatar { + min-height: 84px; + padding-left: 72px; + position: relative; +} +.collection .collection-item.avatar:not(.circle-clipper) > .circle, +.collection .collection-item.avatar :not(.circle-clipper) > .circle { + position: absolute; + width: 42px; + height: 42px; + overflow: hidden; + left: 15px; + display: inline-block; + vertical-align: middle; +} +.collection .collection-item.avatar i.circle { + font-size: 18px; + line-height: 42px; + color: #fff; + background-color: #999; + text-align: center; +} +.collection .collection-item.avatar .title { + font-size: 16px; +} +.collection .collection-item.avatar p { + margin: 0; +} +.collection .collection-item.avatar .secondary-content { + position: absolute; + top: 16px; + right: 16px; +} +.collection .collection-item:last-child { + border-bottom: none; +} +.collection .collection-item.active { + background-color: #26a69a; + color: #eafaf9; +} +.collection .collection-item.active .secondary-content { + color: #fff; +} +.collection a.collection-item { + display: block; + -webkit-transition: 0.25s; + transition: 0.25s; + color: #26a69a; +} +.collection a.collection-item:not(.active):hover { + background-color: #ddd; +} +.collection.with-header .collection-header { + background-color: #fff; + border-bottom: 1px solid #e0e0e0; + padding: 10px 20px; +} +.collection.with-header .collection-item { + padding-left: 30px; +} +.collection.with-header .collection-item.avatar { + padding-left: 72px; +} +.secondary-content { + float: right; + color: #26a69a; +} +.collapsible .collection { + margin: 0; + border: none; +} +.video-container { + position: relative; + padding-bottom: 56.25%; + height: 0; + overflow: hidden; +} +.video-container iframe, +.video-container object, +.video-container embed { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.progress { + position: relative; + height: 4px; + display: block; + width: 100%; + background-color: #acece6; + border-radius: 2px; + margin: 0.5rem 0 1rem 0; + overflow: hidden; +} +.progress .determinate { + position: absolute; + top: 0; + left: 0; + bottom: 0; + background-color: #26a69a; + -webkit-transition: width 0.3s linear; + transition: width 0.3s linear; +} +.progress .indeterminate { + background-color: #26a69a; +} +.progress .indeterminate:before { + content: ''; + position: absolute; + background-color: inherit; + top: 0; + left: 0; + bottom: 0; + will-change: left, right; + -webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; + animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; +} +.progress .indeterminate:after { + content: ''; + position: absolute; + background-color: inherit; + top: 0; + left: 0; + bottom: 0; + will-change: left, right; + -webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite; + animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite; + -webkit-animation-delay: 1.15s; + animation-delay: 1.15s; +} +@-webkit-keyframes indeterminate { + 0% { + left: -35%; + right: 100%; + } + 60% { + left: 100%; + right: -90%; + } + 100% { + left: 100%; + right: -90%; + } +} +@keyframes indeterminate { + 0% { + left: -35%; + right: 100%; + } + 60% { + left: 100%; + right: -90%; + } + 100% { + left: 100%; + right: -90%; + } +} +@-webkit-keyframes indeterminate-short { + 0% { + left: -200%; + right: 100%; + } + 60% { + left: 107%; + right: -8%; + } + 100% { + left: 107%; + right: -8%; + } +} +@keyframes indeterminate-short { + 0% { + left: -200%; + right: 100%; + } + 60% { + left: 107%; + right: -8%; + } + 100% { + left: 107%; + right: -8%; + } +} +.hide { + display: none !important; +} +.left-align { + text-align: left; +} +.right-align { + text-align: right; +} +.center, +.center-align { + text-align: center; +} +.left { + float: left !important; +} +.right { + float: right !important; +} +.no-select, +input[type='range'], +input[type='range'] + .thumb { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.circle { + border-radius: 50%; +} +.center-block { + display: block; + margin-left: auto; + margin-right: auto; +} +.truncate { + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.no-padding { + padding: 0 !important; +} +span.badge { + min-width: 3rem; + padding: 0 6px; + margin-left: 14px; + text-align: center; + font-size: 1rem; + line-height: 22px; + height: 22px; + color: #757575; + float: right; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +span.badge.new { + font-weight: 300; + font-size: 0.8rem; + color: #fff; + background-color: #26a69a; + border-radius: 2px; +} +span.badge.new:after { + content: ' new'; +} +span.badge[data-badge-caption]::after { + content: ' ' attr(data-badge-caption); +} +nav ul a span.badge { + display: inline-block; + float: none; + margin-left: 4px; + line-height: 22px; + height: 22px; + -webkit-font-smoothing: auto; +} +.collection-item span.badge { + margin-top: calc(0.75rem - 11px); +} +.collapsible span.badge { + margin-left: auto; +} +.sidenav span.badge { + margin-top: calc(24px - 11px); +} +table span.badge { + display: inline-block; + float: none; + margin-left: auto; +} +.material-icons { + text-rendering: optimizeLegibility; + -webkit-font-feature-settings: 'liga'; + -moz-font-feature-settings: 'liga'; + font-feature-settings: 'liga'; +} +.container { + margin: 0 auto; + max-width: 1280px; + width: 90%; +} +@media only screen and (min-width: 601px) { + .container { + width: 85%; + } +} +@media only screen and (min-width: 993px) { + .container { + width: 70%; + } +} +.col .row { + margin-left: -0.75rem; + margin-right: -0.75rem; +} +.section { + padding-top: 1rem; + padding-bottom: 1rem; +} +.section.no-pad { + padding: 0; +} +.section.no-pad-bot { + padding-bottom: 0; +} +.section.no-pad-top { + padding-top: 0; +} +.row { + margin-left: auto; + margin-right: auto; + margin-bottom: 20px; +} +.row:after { + content: ''; + display: table; + clear: both; +} +.row .col { + float: left; + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding: 0 0.75rem; + min-height: 1px; +} +.row .col[class*='push-'], +.row .col[class*='pull-'] { + position: relative; +} +.row .col.s1 { + width: 8.3333333333%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s2 { + width: 16.6666666667%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s3 { + width: 25%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s4 { + width: 33.3333333333%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s5 { + width: 41.6666666667%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s6 { + width: 50%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s7 { + width: 58.3333333333%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s8 { + width: 66.6666666667%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s9 { + width: 75%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s10 { + width: 83.3333333333%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s11 { + width: 91.6666666667%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s12 { + width: 100%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.offset-s1 { + margin-left: 8.3333333333%; +} +.row .col.pull-s1 { + right: 8.3333333333%; +} +.row .col.push-s1 { + left: 8.3333333333%; +} +.row .col.offset-s2 { + margin-left: 16.6666666667%; +} +.row .col.pull-s2 { + right: 16.6666666667%; +} +.row .col.push-s2 { + left: 16.6666666667%; +} +.row .col.offset-s3 { + margin-left: 25%; +} +.row .col.pull-s3 { + right: 25%; +} +.row .col.push-s3 { + left: 25%; +} +.row .col.offset-s4 { + margin-left: 33.3333333333%; +} +.row .col.pull-s4 { + right: 33.3333333333%; +} +.row .col.push-s4 { + left: 33.3333333333%; +} +.row .col.offset-s5 { + margin-left: 41.6666666667%; +} +.row .col.pull-s5 { + right: 41.6666666667%; +} +.row .col.push-s5 { + left: 41.6666666667%; +} +.row .col.offset-s6 { + margin-left: 50%; +} +.row .col.pull-s6 { + right: 50%; +} +.row .col.push-s6 { + left: 50%; +} +.row .col.offset-s7 { + margin-left: 58.3333333333%; +} +.row .col.pull-s7 { + right: 58.3333333333%; +} +.row .col.push-s7 { + left: 58.3333333333%; +} +.row .col.offset-s8 { + margin-left: 66.6666666667%; +} +.row .col.pull-s8 { + right: 66.6666666667%; +} +.row .col.push-s8 { + left: 66.6666666667%; +} +.row .col.offset-s9 { + margin-left: 75%; +} +.row .col.pull-s9 { + right: 75%; +} +.row .col.push-s9 { + left: 75%; +} +.row .col.offset-s10 { + margin-left: 83.3333333333%; +} +.row .col.pull-s10 { + right: 83.3333333333%; +} +.row .col.push-s10 { + left: 83.3333333333%; +} +.row .col.offset-s11 { + margin-left: 91.6666666667%; +} +.row .col.pull-s11 { + right: 91.6666666667%; +} +.row .col.push-s11 { + left: 91.6666666667%; +} +.row .col.offset-s12 { + margin-left: 100%; +} +.row .col.pull-s12 { + right: 100%; +} +.row .col.push-s12 { + left: 100%; +} +@media only screen and (min-width: 601px) { + .row .col.m1 { + width: 8.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m2 { + width: 16.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m3 { + width: 25%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m4 { + width: 33.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m5 { + width: 41.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m6 { + width: 50%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m7 { + width: 58.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m8 { + width: 66.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m9 { + width: 75%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m10 { + width: 83.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m11 { + width: 91.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m12 { + width: 100%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.offset-m1 { + margin-left: 8.3333333333%; + } + .row .col.pull-m1 { + right: 8.3333333333%; + } + .row .col.push-m1 { + left: 8.3333333333%; + } + .row .col.offset-m2 { + margin-left: 16.6666666667%; + } + .row .col.pull-m2 { + right: 16.6666666667%; + } + .row .col.push-m2 { + left: 16.6666666667%; + } + .row .col.offset-m3 { + margin-left: 25%; + } + .row .col.pull-m3 { + right: 25%; + } + .row .col.push-m3 { + left: 25%; + } + .row .col.offset-m4 { + margin-left: 33.3333333333%; + } + .row .col.pull-m4 { + right: 33.3333333333%; + } + .row .col.push-m4 { + left: 33.3333333333%; + } + .row .col.offset-m5 { + margin-left: 41.6666666667%; + } + .row .col.pull-m5 { + right: 41.6666666667%; + } + .row .col.push-m5 { + left: 41.6666666667%; + } + .row .col.offset-m6 { + margin-left: 50%; + } + .row .col.pull-m6 { + right: 50%; + } + .row .col.push-m6 { + left: 50%; + } + .row .col.offset-m7 { + margin-left: 58.3333333333%; + } + .row .col.pull-m7 { + right: 58.3333333333%; + } + .row .col.push-m7 { + left: 58.3333333333%; + } + .row .col.offset-m8 { + margin-left: 66.6666666667%; + } + .row .col.pull-m8 { + right: 66.6666666667%; + } + .row .col.push-m8 { + left: 66.6666666667%; + } + .row .col.offset-m9 { + margin-left: 75%; + } + .row .col.pull-m9 { + right: 75%; + } + .row .col.push-m9 { + left: 75%; + } + .row .col.offset-m10 { + margin-left: 83.3333333333%; + } + .row .col.pull-m10 { + right: 83.3333333333%; + } + .row .col.push-m10 { + left: 83.3333333333%; + } + .row .col.offset-m11 { + margin-left: 91.6666666667%; + } + .row .col.pull-m11 { + right: 91.6666666667%; + } + .row .col.push-m11 { + left: 91.6666666667%; + } + .row .col.offset-m12 { + margin-left: 100%; + } + .row .col.pull-m12 { + right: 100%; + } + .row .col.push-m12 { + left: 100%; + } +} +@media only screen and (min-width: 993px) { + .row .col.l1 { + width: 8.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l2 { + width: 16.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l3 { + width: 25%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l4 { + width: 33.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l5 { + width: 41.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l6 { + width: 50%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l7 { + width: 58.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l8 { + width: 66.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l9 { + width: 75%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l10 { + width: 83.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l11 { + width: 91.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l12 { + width: 100%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.offset-l1 { + margin-left: 8.3333333333%; + } + .row .col.pull-l1 { + right: 8.3333333333%; + } + .row .col.push-l1 { + left: 8.3333333333%; + } + .row .col.offset-l2 { + margin-left: 16.6666666667%; + } + .row .col.pull-l2 { + right: 16.6666666667%; + } + .row .col.push-l2 { + left: 16.6666666667%; + } + .row .col.offset-l3 { + margin-left: 25%; + } + .row .col.pull-l3 { + right: 25%; + } + .row .col.push-l3 { + left: 25%; + } + .row .col.offset-l4 { + margin-left: 33.3333333333%; + } + .row .col.pull-l4 { + right: 33.3333333333%; + } + .row .col.push-l4 { + left: 33.3333333333%; + } + .row .col.offset-l5 { + margin-left: 41.6666666667%; + } + .row .col.pull-l5 { + right: 41.6666666667%; + } + .row .col.push-l5 { + left: 41.6666666667%; + } + .row .col.offset-l6 { + margin-left: 50%; + } + .row .col.pull-l6 { + right: 50%; + } + .row .col.push-l6 { + left: 50%; + } + .row .col.offset-l7 { + margin-left: 58.3333333333%; + } + .row .col.pull-l7 { + right: 58.3333333333%; + } + .row .col.push-l7 { + left: 58.3333333333%; + } + .row .col.offset-l8 { + margin-left: 66.6666666667%; + } + .row .col.pull-l8 { + right: 66.6666666667%; + } + .row .col.push-l8 { + left: 66.6666666667%; + } + .row .col.offset-l9 { + margin-left: 75%; + } + .row .col.pull-l9 { + right: 75%; + } + .row .col.push-l9 { + left: 75%; + } + .row .col.offset-l10 { + margin-left: 83.3333333333%; + } + .row .col.pull-l10 { + right: 83.3333333333%; + } + .row .col.push-l10 { + left: 83.3333333333%; + } + .row .col.offset-l11 { + margin-left: 91.6666666667%; + } + .row .col.pull-l11 { + right: 91.6666666667%; + } + .row .col.push-l11 { + left: 91.6666666667%; + } + .row .col.offset-l12 { + margin-left: 100%; + } + .row .col.pull-l12 { + right: 100%; + } + .row .col.push-l12 { + left: 100%; + } +} +@media only screen and (min-width: 1201px) { + .row .col.xl1 { + width: 8.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl2 { + width: 16.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl3 { + width: 25%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl4 { + width: 33.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl5 { + width: 41.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl6 { + width: 50%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl7 { + width: 58.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl8 { + width: 66.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl9 { + width: 75%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl10 { + width: 83.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl11 { + width: 91.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl12 { + width: 100%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.offset-xl1 { + margin-left: 8.3333333333%; + } + .row .col.pull-xl1 { + right: 8.3333333333%; + } + .row .col.push-xl1 { + left: 8.3333333333%; + } + .row .col.offset-xl2 { + margin-left: 16.6666666667%; + } + .row .col.pull-xl2 { + right: 16.6666666667%; + } + .row .col.push-xl2 { + left: 16.6666666667%; + } + .row .col.offset-xl3 { + margin-left: 25%; + } + .row .col.pull-xl3 { + right: 25%; + } + .row .col.push-xl3 { + left: 25%; + } + .row .col.offset-xl4 { + margin-left: 33.3333333333%; + } + .row .col.pull-xl4 { + right: 33.3333333333%; + } + .row .col.push-xl4 { + left: 33.3333333333%; + } + .row .col.offset-xl5 { + margin-left: 41.6666666667%; + } + .row .col.pull-xl5 { + right: 41.6666666667%; + } + .row .col.push-xl5 { + left: 41.6666666667%; + } + .row .col.offset-xl6 { + margin-left: 50%; + } + .row .col.pull-xl6 { + right: 50%; + } + .row .col.push-xl6 { + left: 50%; + } + .row .col.offset-xl7 { + margin-left: 58.3333333333%; + } + .row .col.pull-xl7 { + right: 58.3333333333%; + } + .row .col.push-xl7 { + left: 58.3333333333%; + } + .row .col.offset-xl8 { + margin-left: 66.6666666667%; + } + .row .col.pull-xl8 { + right: 66.6666666667%; + } + .row .col.push-xl8 { + left: 66.6666666667%; + } + .row .col.offset-xl9 { + margin-left: 75%; + } + .row .col.pull-xl9 { + right: 75%; + } + .row .col.push-xl9 { + left: 75%; + } + .row .col.offset-xl10 { + margin-left: 83.3333333333%; + } + .row .col.pull-xl10 { + right: 83.3333333333%; + } + .row .col.push-xl10 { + left: 83.3333333333%; + } + .row .col.offset-xl11 { + margin-left: 91.6666666667%; + } + .row .col.pull-xl11 { + right: 91.6666666667%; + } + .row .col.push-xl11 { + left: 91.6666666667%; + } + .row .col.offset-xl12 { + margin-left: 100%; + } + .row .col.pull-xl12 { + right: 100%; + } + .row .col.push-xl12 { + left: 100%; + } +} +nav { + color: #fff; + background-color: #ee6e73; + width: 100%; + height: 56px; + line-height: 56px; +} +nav.nav-extended { + height: auto; +} +nav.nav-extended .nav-wrapper { + min-height: 56px; + height: auto; +} +nav.nav-extended .nav-content { + position: relative; + line-height: normal; +} +nav a { + color: #fff; +} +nav i, +nav [class^='mdi-'], +nav [class*='mdi-'], +nav i.material-icons { + display: block; + font-size: 24px; + height: 56px; + line-height: 56px; +} +nav .nav-wrapper { + position: relative; + height: 100%; +} +@media only screen and (min-width: 993px) { + nav a.sidenav-trigger { + display: none; + } +} +nav .sidenav-trigger { + float: left; + position: relative; + z-index: 1; + height: 56px; + margin: 0 18px; +} +nav .sidenav-trigger i { + height: 56px; + line-height: 56px; +} +nav .brand-logo { + position: absolute; + color: #fff; + display: inline-block; + font-size: 2.1rem; + padding: 0; +} +nav .brand-logo.center { + left: 50%; + -webkit-transform: translateX(-50%); + transform: translateX(-50%); +} +@media only screen and (max-width: 992px) { + nav .brand-logo { + left: 50%; + -webkit-transform: translateX(-50%); + transform: translateX(-50%); + } + nav .brand-logo.left, + nav .brand-logo.right { + padding: 0; + -webkit-transform: none; + transform: none; + } + nav .brand-logo.left { + left: 0.5rem; + } + nav .brand-logo.right { + right: 0.5rem; + left: auto; + } +} +nav .brand-logo.right { + right: 0.5rem; + padding: 0; +} +nav .brand-logo i, +nav .brand-logo [class^='mdi-'], +nav .brand-logo [class*='mdi-'], +nav .brand-logo i.material-icons { + float: left; + margin-right: 15px; +} +nav .nav-title { + display: inline-block; + font-size: 32px; + padding: 28px 0; +} +nav ul { + margin: 0; +} +nav ul li { + -webkit-transition: background-color 0.3s; + transition: background-color 0.3s; + float: left; + padding: 0; +} +nav ul li.active { + background-color: rgba(0, 0, 0, 0.1); +} +nav ul a { + -webkit-transition: background-color 0.3s; + transition: background-color 0.3s; + font-size: 1rem; + color: #fff; + display: block; + padding: 0 15px; + cursor: pointer; +} +nav ul a.btn, +nav ul a.btn-large, +nav ul a.btn-small, +nav ul a.btn-large, +nav ul a.btn-flat, +nav ul a.btn-floating { + margin-top: -2px; + margin-left: 15px; + margin-right: 15px; +} +nav ul a.btn > .material-icons, +nav ul a.btn-large > .material-icons, +nav ul a.btn-small > .material-icons, +nav ul a.btn-large > .material-icons, +nav ul a.btn-flat > .material-icons, +nav ul a.btn-floating > .material-icons { + height: inherit; + line-height: inherit; +} +nav ul a:hover { + background-color: rgba(0, 0, 0, 0.1); +} +nav ul.left { + float: left; +} +nav form { + height: 100%; +} +nav .input-field { + margin: 0; + height: 100%; +} +nav .input-field input { + height: 100%; + font-size: 1.2rem; + border: none; + padding-left: 2rem; +} +nav .input-field input:focus, +nav .input-field input[type='text']:valid, +nav .input-field input[type='password']:valid, +nav .input-field input[type='email']:valid, +nav .input-field input[type='url']:valid, +nav .input-field input[type='date']:valid { + border: none; + -webkit-box-shadow: none; + box-shadow: none; +} +nav .input-field label { + top: 0; + left: 0; +} +nav .input-field label i { + color: rgba(255, 255, 255, 0.7); + -webkit-transition: color 0.3s; + transition: color 0.3s; +} +nav .input-field label.active i { + color: #fff; +} +.navbar-fixed { + position: relative; + height: 56px; + z-index: 997; +} +.navbar-fixed nav { + position: fixed; +} +@media only screen and (min-width: 601px) { + nav.nav-extended .nav-wrapper { + min-height: 64px; + } + nav, + nav .nav-wrapper i, + nav a.sidenav-trigger, + nav a.sidenav-trigger i { + height: 64px; + line-height: 64px; + } + .navbar-fixed { + height: 64px; + } +} +a { + text-decoration: none; +} +html { + line-height: 1.5; + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, + 'Helvetica Neue', sans-serif; + font-weight: normal; + color: rgba(0, 0, 0, 0.87); +} +@media only screen and (min-width: 0) { + html { + font-size: 14px; + } +} +@media only screen and (min-width: 992px) { + html { + font-size: 14.5px; + } +} +@media only screen and (min-width: 1200px) { + html { + font-size: 15px; + } +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-weight: 400; + line-height: 1.3; +} +h1 a, +h2 a, +h3 a, +h4 a, +h5 a, +h6 a { + font-weight: inherit; +} +h1 { + font-size: 4.2rem; + line-height: 110%; + margin: 2.8rem 0 1.68rem 0; +} +h2 { + font-size: 3.56rem; + line-height: 110%; + margin: 2.3733333333rem 0 1.424rem 0; +} +h3 { + font-size: 2.92rem; + line-height: 110%; + margin: 1.9466666667rem 0 1.168rem 0; +} +h4 { + font-size: 2.28rem; + line-height: 110%; + margin: 1.52rem 0 0.912rem 0; +} +h5 { + font-size: 1.64rem; + line-height: 110%; + margin: 1.0933333333rem 0 0.656rem 0; +} +h6 { + font-size: 1.15rem; + line-height: 110%; + margin: 0.7666666667rem 0 0.46rem 0; +} +em { + font-style: italic; +} +strong { + font-weight: 500; +} +small { + font-size: 75%; +} +.light { + font-weight: 300; +} +.thin { + font-weight: 200; +} +@media only screen and (min-width: 360px) { + .flow-text { + font-size: 1.2rem; + } +} +@media only screen and (min-width: 390px) { + .flow-text { + font-size: 1.224rem; + } +} +@media only screen and (min-width: 420px) { + .flow-text { + font-size: 1.248rem; + } +} +@media only screen and (min-width: 450px) { + .flow-text { + font-size: 1.272rem; + } +} +@media only screen and (min-width: 480px) { + .flow-text { + font-size: 1.296rem; + } +} +@media only screen and (min-width: 510px) { + .flow-text { + font-size: 1.32rem; + } +} +@media only screen and (min-width: 540px) { + .flow-text { + font-size: 1.344rem; + } +} +@media only screen and (min-width: 570px) { + .flow-text { + font-size: 1.368rem; + } +} +@media only screen and (min-width: 600px) { + .flow-text { + font-size: 1.392rem; + } +} +@media only screen and (min-width: 630px) { + .flow-text { + font-size: 1.416rem; + } +} +@media only screen and (min-width: 660px) { + .flow-text { + font-size: 1.44rem; + } +} +@media only screen and (min-width: 690px) { + .flow-text { + font-size: 1.464rem; + } +} +@media only screen and (min-width: 720px) { + .flow-text { + font-size: 1.488rem; + } +} +@media only screen and (min-width: 750px) { + .flow-text { + font-size: 1.512rem; + } +} +@media only screen and (min-width: 780px) { + .flow-text { + font-size: 1.536rem; + } +} +@media only screen and (min-width: 810px) { + .flow-text { + font-size: 1.56rem; + } +} +@media only screen and (min-width: 840px) { + .flow-text { + font-size: 1.584rem; + } +} +@media only screen and (min-width: 870px) { + .flow-text { + font-size: 1.608rem; + } +} +@media only screen and (min-width: 900px) { + .flow-text { + font-size: 1.632rem; + } +} +@media only screen and (min-width: 930px) { + .flow-text { + font-size: 1.656rem; + } +} +@media only screen and (min-width: 960px) { + .flow-text { + font-size: 1.68rem; + } +} +@media only screen and (max-width: 360px) { + .flow-text { + font-size: 1.2rem; + } +} +.scale-transition { + -webkit-transition: -webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important; + transition: -webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important; + transition: transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important; + transition: + transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63), + -webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important; +} +.scale-transition.scale-out { + -webkit-transform: scale(0); + transform: scale(0); + -webkit-transition: -webkit-transform 0.2s !important; + transition: -webkit-transform 0.2s !important; + transition: transform 0.2s !important; + transition: + transform 0.2s, + -webkit-transform 0.2s !important; +} +.scale-transition.scale-in { + -webkit-transform: scale(1); + transform: scale(1); +} +.card-panel { + -webkit-transition: -webkit-box-shadow 0.25s; + transition: -webkit-box-shadow 0.25s; + transition: box-shadow 0.25s; + transition: + box-shadow 0.25s, + -webkit-box-shadow 0.25s; + padding: 24px; + margin: 0.5rem 0 1rem 0; + border-radius: 2px; + background-color: #fff; +} +.card { + position: relative; + margin: 0.5rem 0 1rem 0; + background-color: #fff; + -webkit-transition: -webkit-box-shadow 0.25s; + transition: -webkit-box-shadow 0.25s; + transition: box-shadow 0.25s; + transition: + box-shadow 0.25s, + -webkit-box-shadow 0.25s; + border-radius: 2px; +} +.card .card-title { + font-size: 24px; + font-weight: 300; +} +.card .card-title.activator { + cursor: pointer; +} +.card.small, +.card.medium, +.card.large { + position: relative; +} +.card.small .card-image, +.card.medium .card-image, +.card.large .card-image { + max-height: 60%; + overflow: hidden; +} +.card.small .card-image + .card-content, +.card.medium .card-image + .card-content, +.card.large .card-image + .card-content { + max-height: 40%; +} +.card.small .card-content, +.card.medium .card-content, +.card.large .card-content { + max-height: 100%; + overflow: hidden; +} +.card.small .card-action, +.card.medium .card-action, +.card.large .card-action { + position: absolute; + bottom: 0; + left: 0; + right: 0; +} +.card.small { + height: 300px; +} +.card.medium { + height: 400px; +} +.card.large { + height: 500px; +} +.card.horizontal { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} +.card.horizontal.small .card-image, +.card.horizontal.medium .card-image, +.card.horizontal.large .card-image { + height: 100%; + max-height: none; + overflow: visible; +} +.card.horizontal.small .card-image img, +.card.horizontal.medium .card-image img, +.card.horizontal.large .card-image img { + height: 100%; +} +.card.horizontal .card-image { + max-width: 50%; +} +.card.horizontal .card-image img { + border-radius: 2px 0 0 2px; + max-width: 100%; + width: auto; +} +.card.horizontal .card-stacked { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + position: relative; +} +.card.horizontal .card-stacked .card-content { + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} +.card.sticky-action .card-action { + z-index: 2; +} +.card.sticky-action .card-reveal { + z-index: 1; + padding-bottom: 64px; +} +.card .card-image { + position: relative; +} +.card .card-image img { + display: block; + border-radius: 2px 2px 0 0; + position: relative; + left: 0; + right: 0; + top: 0; + bottom: 0; + width: 100%; +} +.card .card-image .card-title { + color: #fff; + position: absolute; + bottom: 0; + left: 0; + max-width: 100%; + padding: 24px; +} +.card .card-content { + padding: 24px; + border-radius: 0 0 2px 2px; +} +.card .card-content p { + margin: 0; +} +.card .card-content .card-title { + display: block; + line-height: 32px; + margin-bottom: 8px; +} +.card .card-content .card-title i { + line-height: 32px; +} +.card .card-action { + background-color: inherit; + border-top: 1px solid rgba(160, 160, 160, 0.2); + position: relative; + padding: 16px 24px; +} +.card .card-action:last-child { + border-radius: 0 0 2px 2px; +} +.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating) { + color: #ffab40; + margin-right: 24px; + -webkit-transition: color 0.3s ease; + transition: color 0.3s ease; + text-transform: uppercase; +} +.card + .card-action + a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating):hover { + color: #ffd8a6; +} +.card .card-reveal { + padding: 24px; + position: absolute; + background-color: #fff; + width: 100%; + overflow-y: auto; + left: 0; + top: 100%; + height: 100%; + z-index: 3; + display: none; +} +.card .card-reveal .card-title { + cursor: pointer; + display: block; +} +#toast-container { + display: block; + position: fixed; + z-index: 10000; +} +@media only screen and (max-width: 600px) { + #toast-container { + min-width: 100%; + bottom: 0%; + } +} +@media only screen and (min-width: 601px) and (max-width: 992px) { + #toast-container { + left: 5%; + bottom: 7%; + max-width: 90%; + } +} +@media only screen and (min-width: 993px) { + #toast-container { + top: 10%; + right: 7%; + max-width: 86%; + } +} +.toast { + border-radius: 2px; + top: 35px; + width: auto; + margin-top: 10px; + position: relative; + max-width: 100%; + height: auto; + min-height: 48px; + line-height: 1.5em; + background-color: #323232; + padding: 10px 25px; + font-size: 1.1rem; + font-weight: 300; + color: #fff; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + cursor: default; +} +.toast .toast-action { + color: #eeff41; + font-weight: 500; + margin-right: -25px; + margin-left: 3rem; +} +.toast.rounded { + border-radius: 24px; +} +@media only screen and (max-width: 600px) { + .toast { + width: 100%; + border-radius: 0; + } +} +.tabs { + position: relative; + overflow-x: auto; + overflow-y: hidden; + height: 48px; + width: 100%; + background-color: #fff; + margin: 0 auto; + white-space: nowrap; +} +.tabs.tabs-transparent { + background-color: transparent; +} +.tabs.tabs-transparent .tab a, +.tabs.tabs-transparent .tab.disabled a, +.tabs.tabs-transparent .tab.disabled a:hover { + color: rgba(255, 255, 255, 0.7); +} +.tabs.tabs-transparent .tab a:hover, +.tabs.tabs-transparent .tab a.active { + color: #fff; +} +.tabs.tabs-transparent .indicator { + background-color: #fff; +} +.tabs.tabs-fixed-width { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} +.tabs.tabs-fixed-width .tab { + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} +.tabs .tab { + display: inline-block; + text-align: center; + line-height: 48px; + height: 48px; + padding: 0; + margin: 0; + text-transform: uppercase; +} +.tabs .tab a { + color: rgba(238, 110, 115, 0.7); + display: block; + width: 100%; + height: 100%; + padding: 0 24px; + font-size: 14px; + text-overflow: ellipsis; + overflow: hidden; + -webkit-transition: + color 0.28s ease, + background-color 0.28s ease; + transition: + color 0.28s ease, + background-color 0.28s ease; +} +.tabs .tab a:focus, +.tabs .tab a:focus.active { + background-color: rgba(246, 178, 181, 0.2); + outline: none; +} +.tabs .tab a:hover, +.tabs .tab a.active { + background-color: transparent; + color: #ee6e73; +} +.tabs .tab.disabled a, +.tabs .tab.disabled a:hover { + color: rgba(238, 110, 115, 0.4); + cursor: default; +} +.tabs .indicator { + position: absolute; + bottom: 0; + height: 2px; + background-color: #f6b2b5; + will-change: left, right; +} +@media only screen and (max-width: 992px) { + .tabs { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + } + .tabs .tab { + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + } + .tabs .tab a { + padding: 0 12px; + } +} +.material-tooltip { + padding: 10px 8px; + font-size: 1rem; + z-index: 2000; + background-color: transparent; + border-radius: 2px; + color: #fff; + min-height: 36px; + line-height: 120%; + opacity: 0; + position: absolute; + text-align: center; + max-width: calc(100% - 4px); + overflow: hidden; + left: 0; + top: 0; + pointer-events: none; + visibility: hidden; + background-color: #323232; +} +.backdrop { + position: absolute; + opacity: 0; + height: 7px; + width: 14px; + border-radius: 0 0 50% 50%; + background-color: #323232; + z-index: -1; + -webkit-transform-origin: 50% 0%; + transform-origin: 50% 0%; + visibility: hidden; +} +.btn, +.btn-large, +.btn-small, +.btn-flat { + border: none; + border-radius: 2px; + display: inline-block; + height: 36px; + line-height: 36px; + padding: 0 16px; + text-transform: uppercase; + vertical-align: middle; + -webkit-tap-highlight-color: transparent; +} +.btn.disabled, +.disabled.btn-large, +.disabled.btn-small, +.btn-floating.disabled, +.btn-large.disabled, +.btn-small.disabled, +.btn-flat.disabled, +.btn:disabled, +.btn-large:disabled, +.btn-small:disabled, +.btn-floating:disabled, +.btn-large:disabled, +.btn-small:disabled, +.btn-flat:disabled, +.btn[disabled], +.btn-large[disabled], +.btn-small[disabled], +.btn-floating[disabled], +.btn-large[disabled], +.btn-small[disabled], +.btn-flat[disabled] { + pointer-events: none; + background-color: #dfdfdf !important; + -webkit-box-shadow: none; + box-shadow: none; + color: #9f9f9f !important; + cursor: default; +} +.btn.disabled:hover, +.disabled.btn-large:hover, +.disabled.btn-small:hover, +.btn-floating.disabled:hover, +.btn-large.disabled:hover, +.btn-small.disabled:hover, +.btn-flat.disabled:hover, +.btn:disabled:hover, +.btn-large:disabled:hover, +.btn-small:disabled:hover, +.btn-floating:disabled:hover, +.btn-large:disabled:hover, +.btn-small:disabled:hover, +.btn-flat:disabled:hover, +.btn[disabled]:hover, +.btn-large[disabled]:hover, +.btn-small[disabled]:hover, +.btn-floating[disabled]:hover, +.btn-large[disabled]:hover, +.btn-small[disabled]:hover, +.btn-flat[disabled]:hover { + background-color: #dfdfdf !important; + color: #9f9f9f !important; +} +.btn, +.btn-large, +.btn-small, +.btn-floating, +.btn-large, +.btn-small, +.btn-flat { + font-size: 14px; + outline: 0; +} +.btn i, +.btn-large i, +.btn-small i, +.btn-floating i, +.btn-large i, +.btn-small i, +.btn-flat i { + font-size: 1.3rem; + line-height: inherit; +} +.btn:focus, +.btn-large:focus, +.btn-small:focus, +.btn-floating:focus { + background-color: #1d7d74; +} +.btn, +.btn-large, +.btn-small { + text-decoration: none; + color: #fff; + background-color: #26a69a; + text-align: center; + letter-spacing: 0.5px; + -webkit-transition: background-color 0.2s ease-out; + transition: background-color 0.2s ease-out; + cursor: pointer; +} +.btn:hover, +.btn-large:hover, +.btn-small:hover { + background-color: #2bbbad; +} +.btn-floating { + display: inline-block; + color: #fff; + position: relative; + overflow: hidden; + z-index: 1; + width: 40px; + height: 40px; + line-height: 40px; + padding: 0; + background-color: #26a69a; + border-radius: 50%; + -webkit-transition: background-color 0.3s; + transition: background-color 0.3s; + cursor: pointer; + vertical-align: middle; +} +.btn-floating:hover { + background-color: #26a69a; +} +.btn-floating:before { + border-radius: 0; +} +.btn-floating.btn-large { + width: 56px; + height: 56px; + padding: 0; +} +.btn-floating.btn-large.halfway-fab { + bottom: -28px; +} +.btn-floating.btn-large i { + line-height: 56px; +} +.btn-floating.btn-small { + width: 32.4px; + height: 32.4px; +} +.btn-floating.btn-small.halfway-fab { + bottom: -16.2px; +} +.btn-floating.btn-small i { + line-height: 32.4px; +} +.btn-floating.halfway-fab { + position: absolute; + right: 24px; + bottom: -20px; +} +.btn-floating.halfway-fab.left { + right: auto; + left: 24px; +} +.btn-floating i { + width: inherit; + display: inline-block; + text-align: center; + color: #fff; + font-size: 1.6rem; + line-height: 40px; +} +button.btn-floating { + border: none; +} +.fixed-action-btn { + position: fixed; + right: 23px; + bottom: 23px; + padding-top: 15px; + margin-bottom: 0; + z-index: 997; +} +.fixed-action-btn.active ul { + visibility: visible; +} +.fixed-action-btn.direction-left, +.fixed-action-btn.direction-right { + padding: 0 0 0 15px; +} +.fixed-action-btn.direction-left ul, +.fixed-action-btn.direction-right ul { + text-align: right; + right: 64px; + top: 50%; + -webkit-transform: translateY(-50%); + transform: translateY(-50%); + height: 100%; + left: auto; + width: 500px; +} +.fixed-action-btn.direction-left ul li, +.fixed-action-btn.direction-right ul li { + display: inline-block; + margin: 7.5px 15px 0 0; +} +.fixed-action-btn.direction-right { + padding: 0 15px 0 0; +} +.fixed-action-btn.direction-right ul { + text-align: left; + direction: rtl; + left: 64px; + right: auto; +} +.fixed-action-btn.direction-right ul li { + margin: 7.5px 0 0 15px; +} +.fixed-action-btn.direction-bottom { + padding: 0 0 15px 0; +} +.fixed-action-btn.direction-bottom ul { + top: 64px; + bottom: auto; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: reverse; + -webkit-flex-direction: column-reverse; + -ms-flex-direction: column-reverse; + flex-direction: column-reverse; +} +.fixed-action-btn.direction-bottom ul li { + margin: 15px 0 0 0; +} +.fixed-action-btn.toolbar { + padding: 0; + height: 56px; +} +.fixed-action-btn.toolbar.active > a i { + opacity: 0; +} +.fixed-action-btn.toolbar ul { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + top: 0; + bottom: 0; + z-index: 1; +} +.fixed-action-btn.toolbar ul li { + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: inline-block; + margin: 0; + height: 100%; + -webkit-transition: none; + transition: none; +} +.fixed-action-btn.toolbar ul li a { + display: block; + overflow: hidden; + position: relative; + width: 100%; + height: 100%; + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; + color: #fff; + line-height: 56px; + z-index: 1; +} +.fixed-action-btn.toolbar ul li a i { + line-height: inherit; +} +.fixed-action-btn ul { + left: 0; + right: 0; + text-align: center; + position: absolute; + bottom: 64px; + margin: 0; + visibility: hidden; +} +.fixed-action-btn ul li { + margin-bottom: 15px; +} +.fixed-action-btn ul a.btn-floating { + opacity: 0; +} +.fixed-action-btn .fab-backdrop { + position: absolute; + top: 0; + left: 0; + z-index: -1; + width: 40px; + height: 40px; + background-color: #26a69a; + border-radius: 50%; + -webkit-transform: scale(0); + transform: scale(0); +} +.btn-flat { + -webkit-box-shadow: none; + box-shadow: none; + background-color: transparent; + color: #343434; + cursor: pointer; + -webkit-transition: background-color 0.2s; + transition: background-color 0.2s; +} +.btn-flat:focus, +.btn-flat:hover { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn-flat:focus { + background-color: rgba(0, 0, 0, 0.1); +} +.btn-flat.disabled, +.btn-flat.btn-flat[disabled] { + background-color: transparent !important; + color: #b3b2b2 !important; + cursor: default; +} +.btn-large { + height: 54px; + line-height: 54px; + font-size: 15px; + padding: 0 28px; +} +.btn-large i { + font-size: 1.6rem; +} +.btn-small { + height: 32.4px; + line-height: 32.4px; + font-size: 13px; +} +.btn-small i { + font-size: 1.2rem; +} +.btn-block { + display: block; +} +.dropdown-content { + background-color: #fff; + margin: 0; + display: none; + min-width: 100px; + overflow-y: auto; + opacity: 0; + position: absolute; + left: 0; + top: 0; + z-index: 9999; + -webkit-transform-origin: 0 0; + transform-origin: 0 0; +} +.dropdown-content:focus { + outline: 0; +} +.dropdown-content li { + clear: both; + color: rgba(0, 0, 0, 0.87); + cursor: pointer; + min-height: 50px; + line-height: 1.5rem; + width: 100%; + text-align: left; +} +.dropdown-content li:hover, +.dropdown-content li.active { + background-color: #eee; +} +.dropdown-content li:focus { + outline: none; +} +.dropdown-content li.divider { + min-height: 0; + height: 1px; +} +.dropdown-content li > a, +.dropdown-content li > span { + font-size: 16px; + color: #26a69a; + display: block; + line-height: 22px; + padding: 14px 16px; +} +.dropdown-content li > span > label { + top: 1px; + left: 0; + height: 18px; +} +.dropdown-content li > a > i { + height: inherit; + line-height: inherit; + float: left; + margin: 0 24px 0 0; + width: 24px; +} +body.keyboard-focused .dropdown-content li:focus { + background-color: #dadada; +} +.input-field.col .dropdown-content [type='checkbox'] + label { + top: 1px; + left: 0; + height: 18px; + -webkit-transform: none; + transform: none; +} +.dropdown-trigger { + cursor: pointer; +} /*! + * Waves v0.6.0 + * http://fian.my.id/Waves + * + * Copyright 2014 Alfiana E. Sibuea and other contributors + * Released under the MIT license + * https://github.com/fians/Waves/blob/master/LICENSE + */ +.waves-effect { + position: relative; + cursor: pointer; + display: inline-block; + overflow: hidden; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-tap-highlight-color: transparent; + vertical-align: middle; + z-index: 1; + -webkit-transition: 0.3s ease-out; + transition: 0.3s ease-out; +} +.waves-effect .waves-ripple { + position: absolute; + border-radius: 50%; + width: 20px; + height: 20px; + margin-top: -10px; + margin-left: -10px; + opacity: 0; + background: rgba(0, 0, 0, 0.2); + -webkit-transition: all 0.7s ease-out; + transition: all 0.7s ease-out; + -webkit-transition-property: + opacity, + -webkit-transform; + transition-property: + opacity, + -webkit-transform; + transition-property: transform, opacity; + transition-property: + transform, + opacity, + -webkit-transform; + -webkit-transform: scale(0); + transform: scale(0); + pointer-events: none; +} +.waves-effect.waves-light .waves-ripple { + background-color: rgba(255, 255, 255, 0.45); +} +.waves-effect.waves-red .waves-ripple { + background-color: rgba(244, 67, 54, 0.7); +} +.waves-effect.waves-yellow .waves-ripple { + background-color: rgba(255, 235, 59, 0.7); +} +.waves-effect.waves-orange .waves-ripple { + background-color: rgba(255, 152, 0, 0.7); +} +.waves-effect.waves-purple .waves-ripple { + background-color: rgba(156, 39, 176, 0.7); +} +.waves-effect.waves-green .waves-ripple { + background-color: rgba(76, 175, 80, 0.7); +} +.waves-effect.waves-teal .waves-ripple { + background-color: rgba(0, 150, 136, 0.7); +} +.waves-effect input[type='button'], +.waves-effect input[type='reset'], +.waves-effect input[type='submit'] { + border: 0; + font-style: normal; + font-size: inherit; + text-transform: inherit; + background: none; +} +.waves-effect img { + position: relative; + z-index: -1; +} +.waves-notransition { + -webkit-transition: none !important; + transition: none !important; +} +.waves-circle { + -webkit-transform: translateZ(0); + transform: translateZ(0); + -webkit-mask-image: -webkit-radial-gradient(circle, white 100%, black 100%); +} +.waves-input-wrapper { + border-radius: 0.2em; + vertical-align: bottom; +} +.waves-input-wrapper .waves-button-input { + position: relative; + top: 0; + left: 0; + z-index: 1; +} +.waves-circle { + text-align: center; + width: 2.5em; + height: 2.5em; + line-height: 2.5em; + border-radius: 50%; + -webkit-mask-image: none; +} +.waves-block { + display: block; +} +.waves-effect .waves-ripple { + z-index: -1; +} +.modal { + display: none; + position: fixed; + left: 0; + right: 0; + background-color: #fafafa; + padding: 0; + max-height: 70%; + width: 55%; + margin: auto; + overflow-y: auto; + border-radius: 2px; + will-change: top, opacity; +} +.modal:focus { + outline: none; +} +@media only screen and (max-width: 992px) { + .modal { + width: 80%; + } +} +.modal h1, +.modal h2, +.modal h3, +.modal h4 { + margin-top: 0; +} +.modal .modal-content { + padding: 24px; +} +.modal .modal-close { + cursor: pointer; +} +.modal .modal-footer { + border-radius: 0 0 2px 2px; + background-color: #fafafa; + padding: 4px 6px; + height: 56px; + width: 100%; + text-align: right; +} +.modal .modal-footer .btn, +.modal .modal-footer .btn-large, +.modal .modal-footer .btn-small, +.modal .modal-footer .btn-flat { + margin: 6px 0; +} +.modal-overlay { + position: fixed; + z-index: 999; + top: -25%; + left: 0; + bottom: 0; + right: 0; + height: 125%; + width: 100%; + background: #000; + display: none; + will-change: opacity; +} +.modal.modal-fixed-footer { + padding: 0; + height: 70%; +} +.modal.modal-fixed-footer .modal-content { + position: absolute; + height: calc(100% - 56px); + max-height: 100%; + width: 100%; + overflow-y: auto; +} +.modal.modal-fixed-footer .modal-footer { + border-top: 1px solid rgba(0, 0, 0, 0.1); + position: absolute; + bottom: 0; +} +.modal.bottom-sheet { + top: auto; + bottom: -100%; + margin: 0; + width: 100%; + max-height: 45%; + border-radius: 0; + will-change: bottom, opacity; +} +.collapsible { + border-top: 1px solid #ddd; + border-right: 1px solid #ddd; + border-left: 1px solid #ddd; + margin: 0.5rem 0 1rem 0; +} +.collapsible-header { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + line-height: 1.5; + padding: 1rem; + background-color: #fff; + border-bottom: 1px solid #ddd; +} +.collapsible-header:focus { + outline: 0; +} +.collapsible-header i { + width: 2rem; + font-size: 1.6rem; + display: inline-block; + text-align: center; + margin-right: 1rem; +} +.keyboard-focused .collapsible-header:focus { + background-color: #eee; +} +.collapsible-body { + display: none; + border-bottom: 1px solid #ddd; + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding: 2rem; +} +.sidenav .collapsible, +.sidenav.fixed .collapsible { + border: none; + -webkit-box-shadow: none; + box-shadow: none; +} +.sidenav .collapsible li, +.sidenav.fixed .collapsible li { + padding: 0; +} +.sidenav .collapsible-header, +.sidenav.fixed .collapsible-header { + background-color: transparent; + border: none; + line-height: inherit; + height: inherit; + padding: 0 16px; +} +.sidenav .collapsible-header:hover, +.sidenav.fixed .collapsible-header:hover { + background-color: rgba(0, 0, 0, 0.05); +} +.sidenav .collapsible-header i, +.sidenav.fixed .collapsible-header i { + line-height: inherit; +} +.sidenav .collapsible-body, +.sidenav.fixed .collapsible-body { + border: 0; + background-color: #fff; +} +.sidenav .collapsible-body li a, +.sidenav.fixed .collapsible-body li a { + padding: 0 23.5px 0 31px; +} +.collapsible.popout { + border: none; + -webkit-box-shadow: none; + box-shadow: none; +} +.collapsible.popout > li { + -webkit-box-shadow: + 0 2px 5px 0 rgba(0, 0, 0, 0.16), + 0 2px 10px 0 rgba(0, 0, 0, 0.12); + box-shadow: + 0 2px 5px 0 rgba(0, 0, 0, 0.16), + 0 2px 10px 0 rgba(0, 0, 0, 0.12); + margin: 0 24px; + -webkit-transition: margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94); + transition: margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94); +} +.collapsible.popout > li.active { + -webkit-box-shadow: + 0 5px 11px 0 rgba(0, 0, 0, 0.18), + 0 4px 15px 0 rgba(0, 0, 0, 0.15); + box-shadow: + 0 5px 11px 0 rgba(0, 0, 0, 0.18), + 0 4px 15px 0 rgba(0, 0, 0, 0.15); + margin: 16px 0; +} +.chip { + display: inline-block; + height: 32px; + font-size: 13px; + font-weight: 500; + color: rgba(0, 0, 0, 0.6); + line-height: 32px; + padding: 0 12px; + border-radius: 16px; + background-color: #e4e4e4; + margin-bottom: 5px; + margin-right: 5px; +} +.chip:focus { + outline: none; + background-color: #26a69a; + color: #fff; +} +.chip > img { + float: left; + margin: 0 8px 0 -12px; + height: 32px; + width: 32px; + border-radius: 50%; +} +.chip .close { + cursor: pointer; + float: right; + font-size: 16px; + line-height: 32px; + padding-left: 8px; +} +.chips { + border: none; + border-bottom: 1px solid #9e9e9e; + -webkit-box-shadow: none; + box-shadow: none; + margin: 0 0 8px 0; + min-height: 45px; + outline: none; + -webkit-transition: all 0.3s; + transition: all 0.3s; +} +.chips.focus { + border-bottom: 1px solid #26a69a; + -webkit-box-shadow: 0 1px 0 0 #26a69a; + box-shadow: 0 1px 0 0 #26a69a; +} +.chips:hover { + cursor: text; +} +.chips .input { + background: none; + border: 0; + color: rgba(0, 0, 0, 0.6); + display: inline-block; + font-size: 16px; + height: 3rem; + line-height: 32px; + outline: 0; + margin: 0; + padding: 0 !important; + width: 120px !important; +} +.chips .input:focus { + border: 0 !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; +} +.chips .autocomplete-content { + margin-top: 0; + margin-bottom: 0; +} +.prefix ~ .chips { + margin-left: 3rem; + width: 92%; + width: calc(100% - 3rem); +} +.chips:empty ~ label { + font-size: 0.8rem; + -webkit-transform: translateY(-140%); + transform: translateY(-140%); +} +.materialboxed { + display: block; + cursor: -webkit-zoom-in; + cursor: zoom-in; + position: relative; + -webkit-transition: opacity 0.4s; + transition: opacity 0.4s; + -webkit-backface-visibility: hidden; +} +.materialboxed:hover:not(.active) { + opacity: 0.8; +} +.materialboxed.active { + cursor: -webkit-zoom-out; + cursor: zoom-out; +} +#materialbox-overlay { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: #292929; + z-index: 1000; + will-change: opacity; +} +.materialbox-caption { + position: fixed; + display: none; + color: #fff; + line-height: 50px; + bottom: 0; + left: 0; + width: 100%; + text-align: center; + padding: 0% 15%; + height: 50px; + z-index: 1000; + -webkit-font-smoothing: antialiased; +} +select:focus { + outline: 1px solid #c9f3ef; +} +button:focus { + outline: none; + background-color: #2ab7a9; +} +label { + font-size: 0.8rem; + color: #9e9e9e; +} +::-webkit-input-placeholder { + color: #d1d1d1; +} +::-moz-placeholder { + color: #d1d1d1; +} +:-ms-input-placeholder { + color: #d1d1d1; +} +::-ms-input-placeholder { + color: #d1d1d1; +} +::placeholder { + color: #d1d1d1; +} +input:not([type]), +input[type='text']:not(.browser-default), +input[type='password']:not(.browser-default), +input[type='email']:not(.browser-default), +input[type='url']:not(.browser-default), +input[type='time']:not(.browser-default), +input[type='date']:not(.browser-default), +input[type='datetime']:not(.browser-default), +input[type='datetime-local']:not(.browser-default), +input[type='tel']:not(.browser-default), +input[type='number']:not(.browser-default), +input[type='search']:not(.browser-default), +textarea.materialize-textarea { + background-color: transparent; + border: none; + border-bottom: 1px solid #9e9e9e; + border-radius: 0; + outline: none; + height: 3rem; + width: 100%; + font-size: 16px; + margin: 0 0 8px 0; + padding: 0; + -webkit-box-shadow: none; + box-shadow: none; + -webkit-box-sizing: content-box; + box-sizing: content-box; + -webkit-transition: + border 0.3s, + -webkit-box-shadow 0.3s; + transition: + border 0.3s, + -webkit-box-shadow 0.3s; + transition: + box-shadow 0.3s, + border 0.3s; + transition: + box-shadow 0.3s, + border 0.3s, + -webkit-box-shadow 0.3s; +} +input:not([type]):disabled, +input:not([type])[readonly='readonly'], +input[type='text']:not(.browser-default):disabled, +input[type='text']:not(.browser-default)[readonly='readonly'], +input[type='password']:not(.browser-default):disabled, +input[type='password']:not(.browser-default)[readonly='readonly'], +input[type='email']:not(.browser-default):disabled, +input[type='email']:not(.browser-default)[readonly='readonly'], +input[type='url']:not(.browser-default):disabled, +input[type='url']:not(.browser-default)[readonly='readonly'], +input[type='time']:not(.browser-default):disabled, +input[type='time']:not(.browser-default)[readonly='readonly'], +input[type='date']:not(.browser-default):disabled, +input[type='date']:not(.browser-default)[readonly='readonly'], +input[type='datetime']:not(.browser-default):disabled, +input[type='datetime']:not(.browser-default)[readonly='readonly'], +input[type='datetime-local']:not(.browser-default):disabled, +input[type='datetime-local']:not(.browser-default)[readonly='readonly'], +input[type='tel']:not(.browser-default):disabled, +input[type='tel']:not(.browser-default)[readonly='readonly'], +input[type='number']:not(.browser-default):disabled, +input[type='number']:not(.browser-default)[readonly='readonly'], +input[type='search']:not(.browser-default):disabled, +input[type='search']:not(.browser-default)[readonly='readonly'], +textarea.materialize-textarea:disabled, +textarea.materialize-textarea[readonly='readonly'] { + color: rgba(0, 0, 0, 0.42); + border-bottom: 1px dotted rgba(0, 0, 0, 0.42); +} +input:not([type]):disabled + label, +input:not([type])[readonly='readonly'] + label, +input[type='text']:not(.browser-default):disabled + label, +input[type='text']:not(.browser-default)[readonly='readonly'] + label, +input[type='password']:not(.browser-default):disabled + label, +input[type='password']:not(.browser-default)[readonly='readonly'] + label, +input[type='email']:not(.browser-default):disabled + label, +input[type='email']:not(.browser-default)[readonly='readonly'] + label, +input[type='url']:not(.browser-default):disabled + label, +input[type='url']:not(.browser-default)[readonly='readonly'] + label, +input[type='time']:not(.browser-default):disabled + label, +input[type='time']:not(.browser-default)[readonly='readonly'] + label, +input[type='date']:not(.browser-default):disabled + label, +input[type='date']:not(.browser-default)[readonly='readonly'] + label, +input[type='datetime']:not(.browser-default):disabled + label, +input[type='datetime']:not(.browser-default)[readonly='readonly'] + label, +input[type='datetime-local']:not(.browser-default):disabled + label, +input[type='datetime-local']:not(.browser-default)[readonly='readonly'] + label, +input[type='tel']:not(.browser-default):disabled + label, +input[type='tel']:not(.browser-default)[readonly='readonly'] + label, +input[type='number']:not(.browser-default):disabled + label, +input[type='number']:not(.browser-default)[readonly='readonly'] + label, +input[type='search']:not(.browser-default):disabled + label, +input[type='search']:not(.browser-default)[readonly='readonly'] + label, +textarea.materialize-textarea:disabled + label, +textarea.materialize-textarea[readonly='readonly'] + label { + color: rgba(0, 0, 0, 0.42); +} +input:not([type]):focus:not([readonly]), +input[type='text']:not(.browser-default):focus:not([readonly]), +input[type='password']:not(.browser-default):focus:not([readonly]), +input[type='email']:not(.browser-default):focus:not([readonly]), +input[type='url']:not(.browser-default):focus:not([readonly]), +input[type='time']:not(.browser-default):focus:not([readonly]), +input[type='date']:not(.browser-default):focus:not([readonly]), +input[type='datetime']:not(.browser-default):focus:not([readonly]), +input[type='datetime-local']:not(.browser-default):focus:not([readonly]), +input[type='tel']:not(.browser-default):focus:not([readonly]), +input[type='number']:not(.browser-default):focus:not([readonly]), +input[type='search']:not(.browser-default):focus:not([readonly]), +textarea.materialize-textarea:focus:not([readonly]) { + border-bottom: 1px solid #26a69a; + -webkit-box-shadow: 0 1px 0 0 #26a69a; + box-shadow: 0 1px 0 0 #26a69a; +} +input:not([type]):focus:not([readonly]) + label, +input[type='text']:not(.browser-default):focus:not([readonly]) + label, +input[type='password']:not(.browser-default):focus:not([readonly]) + label, +input[type='email']:not(.browser-default):focus:not([readonly]) + label, +input[type='url']:not(.browser-default):focus:not([readonly]) + label, +input[type='time']:not(.browser-default):focus:not([readonly]) + label, +input[type='date']:not(.browser-default):focus:not([readonly]) + label, +input[type='datetime']:not(.browser-default):focus:not([readonly]) + label, +input[type='datetime-local']:not(.browser-default):focus:not([readonly]) + label, +input[type='tel']:not(.browser-default):focus:not([readonly]) + label, +input[type='number']:not(.browser-default):focus:not([readonly]) + label, +input[type='search']:not(.browser-default):focus:not([readonly]) + label, +textarea.materialize-textarea:focus:not([readonly]) + label { + color: #26a69a; +} +input:not([type]):focus.valid ~ label, +input[type='text']:not(.browser-default):focus.valid ~ label, +input[type='password']:not(.browser-default):focus.valid ~ label, +input[type='email']:not(.browser-default):focus.valid ~ label, +input[type='url']:not(.browser-default):focus.valid ~ label, +input[type='time']:not(.browser-default):focus.valid ~ label, +input[type='date']:not(.browser-default):focus.valid ~ label, +input[type='datetime']:not(.browser-default):focus.valid ~ label, +input[type='datetime-local']:not(.browser-default):focus.valid ~ label, +input[type='tel']:not(.browser-default):focus.valid ~ label, +input[type='number']:not(.browser-default):focus.valid ~ label, +input[type='search']:not(.browser-default):focus.valid ~ label, +textarea.materialize-textarea:focus.valid ~ label { + color: #4caf50; +} +input:not([type]):focus.invalid ~ label, +input[type='text']:not(.browser-default):focus.invalid ~ label, +input[type='password']:not(.browser-default):focus.invalid ~ label, +input[type='email']:not(.browser-default):focus.invalid ~ label, +input[type='url']:not(.browser-default):focus.invalid ~ label, +input[type='time']:not(.browser-default):focus.invalid ~ label, +input[type='date']:not(.browser-default):focus.invalid ~ label, +input[type='datetime']:not(.browser-default):focus.invalid ~ label, +input[type='datetime-local']:not(.browser-default):focus.invalid ~ label, +input[type='tel']:not(.browser-default):focus.invalid ~ label, +input[type='number']:not(.browser-default):focus.invalid ~ label, +input[type='search']:not(.browser-default):focus.invalid ~ label, +textarea.materialize-textarea:focus.invalid ~ label { + color: #f44336; +} +input:not([type]).validate + label, +input[type='text']:not(.browser-default).validate + label, +input[type='password']:not(.browser-default).validate + label, +input[type='email']:not(.browser-default).validate + label, +input[type='url']:not(.browser-default).validate + label, +input[type='time']:not(.browser-default).validate + label, +input[type='date']:not(.browser-default).validate + label, +input[type='datetime']:not(.browser-default).validate + label, +input[type='datetime-local']:not(.browser-default).validate + label, +input[type='tel']:not(.browser-default).validate + label, +input[type='number']:not(.browser-default).validate + label, +input[type='search']:not(.browser-default).validate + label, +textarea.materialize-textarea.validate + label { + width: 100%; +} +input.valid:not([type]), +input.valid:not([type]):focus, +input.valid[type='text']:not(.browser-default), +input.valid[type='text']:not(.browser-default):focus, +input.valid[type='password']:not(.browser-default), +input.valid[type='password']:not(.browser-default):focus, +input.valid[type='email']:not(.browser-default), +input.valid[type='email']:not(.browser-default):focus, +input.valid[type='url']:not(.browser-default), +input.valid[type='url']:not(.browser-default):focus, +input.valid[type='time']:not(.browser-default), +input.valid[type='time']:not(.browser-default):focus, +input.valid[type='date']:not(.browser-default), +input.valid[type='date']:not(.browser-default):focus, +input.valid[type='datetime']:not(.browser-default), +input.valid[type='datetime']:not(.browser-default):focus, +input.valid[type='datetime-local']:not(.browser-default), +input.valid[type='datetime-local']:not(.browser-default):focus, +input.valid[type='tel']:not(.browser-default), +input.valid[type='tel']:not(.browser-default):focus, +input.valid[type='number']:not(.browser-default), +input.valid[type='number']:not(.browser-default):focus, +input.valid[type='search']:not(.browser-default), +input.valid[type='search']:not(.browser-default):focus, +textarea.materialize-textarea.valid, +textarea.materialize-textarea.valid:focus, +.select-wrapper.valid > input.select-dropdown { + border-bottom: 1px solid #4caf50; + -webkit-box-shadow: 0 1px 0 0 #4caf50; + box-shadow: 0 1px 0 0 #4caf50; +} +input.invalid:not([type]), +input.invalid:not([type]):focus, +input.invalid[type='text']:not(.browser-default), +input.invalid[type='text']:not(.browser-default):focus, +input.invalid[type='password']:not(.browser-default), +input.invalid[type='password']:not(.browser-default):focus, +input.invalid[type='email']:not(.browser-default), +input.invalid[type='email']:not(.browser-default):focus, +input.invalid[type='url']:not(.browser-default), +input.invalid[type='url']:not(.browser-default):focus, +input.invalid[type='time']:not(.browser-default), +input.invalid[type='time']:not(.browser-default):focus, +input.invalid[type='date']:not(.browser-default), +input.invalid[type='date']:not(.browser-default):focus, +input.invalid[type='datetime']:not(.browser-default), +input.invalid[type='datetime']:not(.browser-default):focus, +input.invalid[type='datetime-local']:not(.browser-default), +input.invalid[type='datetime-local']:not(.browser-default):focus, +input.invalid[type='tel']:not(.browser-default), +input.invalid[type='tel']:not(.browser-default):focus, +input.invalid[type='number']:not(.browser-default), +input.invalid[type='number']:not(.browser-default):focus, +input.invalid[type='search']:not(.browser-default), +input.invalid[type='search']:not(.browser-default):focus, +textarea.materialize-textarea.invalid, +textarea.materialize-textarea.invalid:focus, +.select-wrapper.invalid > input.select-dropdown, +.select-wrapper.invalid > input.select-dropdown:focus { + border-bottom: 1px solid #f44336; + -webkit-box-shadow: 0 1px 0 0 #f44336; + box-shadow: 0 1px 0 0 #f44336; +} +input:not([type]).valid ~ .helper-text[data-success], +input:not([type]):focus.valid ~ .helper-text[data-success], +input:not([type]).invalid ~ .helper-text[data-error], +input:not([type]):focus.invalid ~ .helper-text[data-error], +input[type='text']:not(.browser-default).valid ~ .helper-text[data-success], +input[type='text']:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type='text']:not(.browser-default).invalid ~ .helper-text[data-error], +input[type='text']:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type='password']:not(.browser-default).valid ~ .helper-text[data-success], +input[type='password']:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type='password']:not(.browser-default).invalid ~ .helper-text[data-error], +input[type='password']:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type='email']:not(.browser-default).valid ~ .helper-text[data-success], +input[type='email']:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type='email']:not(.browser-default).invalid ~ .helper-text[data-error], +input[type='email']:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type='url']:not(.browser-default).valid ~ .helper-text[data-success], +input[type='url']:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type='url']:not(.browser-default).invalid ~ .helper-text[data-error], +input[type='url']:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type='time']:not(.browser-default).valid ~ .helper-text[data-success], +input[type='time']:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type='time']:not(.browser-default).invalid ~ .helper-text[data-error], +input[type='time']:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type='date']:not(.browser-default).valid ~ .helper-text[data-success], +input[type='date']:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type='date']:not(.browser-default).invalid ~ .helper-text[data-error], +input[type='date']:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type='datetime']:not(.browser-default).valid ~ .helper-text[data-success], +input[type='datetime']:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type='datetime']:not(.browser-default).invalid ~ .helper-text[data-error], +input[type='datetime']:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type='datetime-local']:not(.browser-default).valid ~ .helper-text[data-success], +input[type='datetime-local']:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type='datetime-local']:not(.browser-default).invalid ~ .helper-text[data-error], +input[type='datetime-local']:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type='tel']:not(.browser-default).valid ~ .helper-text[data-success], +input[type='tel']:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type='tel']:not(.browser-default).invalid ~ .helper-text[data-error], +input[type='tel']:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type='number']:not(.browser-default).valid ~ .helper-text[data-success], +input[type='number']:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type='number']:not(.browser-default).invalid ~ .helper-text[data-error], +input[type='number']:not(.browser-default):focus.invalid ~ .helper-text[data-error], +input[type='search']:not(.browser-default).valid ~ .helper-text[data-success], +input[type='search']:not(.browser-default):focus.valid ~ .helper-text[data-success], +input[type='search']:not(.browser-default).invalid ~ .helper-text[data-error], +input[type='search']:not(.browser-default):focus.invalid ~ .helper-text[data-error], +textarea.materialize-textarea.valid ~ .helper-text[data-success], +textarea.materialize-textarea:focus.valid ~ .helper-text[data-success], +textarea.materialize-textarea.invalid ~ .helper-text[data-error], +textarea.materialize-textarea:focus.invalid ~ .helper-text[data-error], +.select-wrapper.valid .helper-text[data-success], +.select-wrapper.invalid ~ .helper-text[data-error] { + color: transparent; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; +} +input:not([type]).valid ~ .helper-text:after, +input:not([type]):focus.valid ~ .helper-text:after, +input[type='text']:not(.browser-default).valid ~ .helper-text:after, +input[type='text']:not(.browser-default):focus.valid ~ .helper-text:after, +input[type='password']:not(.browser-default).valid ~ .helper-text:after, +input[type='password']:not(.browser-default):focus.valid ~ .helper-text:after, +input[type='email']:not(.browser-default).valid ~ .helper-text:after, +input[type='email']:not(.browser-default):focus.valid ~ .helper-text:after, +input[type='url']:not(.browser-default).valid ~ .helper-text:after, +input[type='url']:not(.browser-default):focus.valid ~ .helper-text:after, +input[type='time']:not(.browser-default).valid ~ .helper-text:after, +input[type='time']:not(.browser-default):focus.valid ~ .helper-text:after, +input[type='date']:not(.browser-default).valid ~ .helper-text:after, +input[type='date']:not(.browser-default):focus.valid ~ .helper-text:after, +input[type='datetime']:not(.browser-default).valid ~ .helper-text:after, +input[type='datetime']:not(.browser-default):focus.valid ~ .helper-text:after, +input[type='datetime-local']:not(.browser-default).valid ~ .helper-text:after, +input[type='datetime-local']:not(.browser-default):focus.valid ~ .helper-text:after, +input[type='tel']:not(.browser-default).valid ~ .helper-text:after, +input[type='tel']:not(.browser-default):focus.valid ~ .helper-text:after, +input[type='number']:not(.browser-default).valid ~ .helper-text:after, +input[type='number']:not(.browser-default):focus.valid ~ .helper-text:after, +input[type='search']:not(.browser-default).valid ~ .helper-text:after, +input[type='search']:not(.browser-default):focus.valid ~ .helper-text:after, +textarea.materialize-textarea.valid ~ .helper-text:after, +textarea.materialize-textarea:focus.valid ~ .helper-text:after, +.select-wrapper.valid ~ .helper-text:after { + content: attr(data-success); + color: #4caf50; +} +input:not([type]).invalid ~ .helper-text:after, +input:not([type]):focus.invalid ~ .helper-text:after, +input[type='text']:not(.browser-default).invalid ~ .helper-text:after, +input[type='text']:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type='password']:not(.browser-default).invalid ~ .helper-text:after, +input[type='password']:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type='email']:not(.browser-default).invalid ~ .helper-text:after, +input[type='email']:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type='url']:not(.browser-default).invalid ~ .helper-text:after, +input[type='url']:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type='time']:not(.browser-default).invalid ~ .helper-text:after, +input[type='time']:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type='date']:not(.browser-default).invalid ~ .helper-text:after, +input[type='date']:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type='datetime']:not(.browser-default).invalid ~ .helper-text:after, +input[type='datetime']:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type='datetime-local']:not(.browser-default).invalid ~ .helper-text:after, +input[type='datetime-local']:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type='tel']:not(.browser-default).invalid ~ .helper-text:after, +input[type='tel']:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type='number']:not(.browser-default).invalid ~ .helper-text:after, +input[type='number']:not(.browser-default):focus.invalid ~ .helper-text:after, +input[type='search']:not(.browser-default).invalid ~ .helper-text:after, +input[type='search']:not(.browser-default):focus.invalid ~ .helper-text:after, +textarea.materialize-textarea.invalid ~ .helper-text:after, +textarea.materialize-textarea:focus.invalid ~ .helper-text:after, +.select-wrapper.invalid ~ .helper-text:after { + content: attr(data-error); + color: #f44336; +} +input:not([type]) + label:after, +input[type='text']:not(.browser-default) + label:after, +input[type='password']:not(.browser-default) + label:after, +input[type='email']:not(.browser-default) + label:after, +input[type='url']:not(.browser-default) + label:after, +input[type='time']:not(.browser-default) + label:after, +input[type='date']:not(.browser-default) + label:after, +input[type='datetime']:not(.browser-default) + label:after, +input[type='datetime-local']:not(.browser-default) + label:after, +input[type='tel']:not(.browser-default) + label:after, +input[type='number']:not(.browser-default) + label:after, +input[type='search']:not(.browser-default) + label:after, +textarea.materialize-textarea + label:after, +.select-wrapper + label:after { + display: block; + content: ''; + position: absolute; + top: 100%; + left: 0; + opacity: 0; + -webkit-transition: + 0.2s opacity ease-out, + 0.2s color ease-out; + transition: + 0.2s opacity ease-out, + 0.2s color ease-out; +} +.input-field { + position: relative; + margin-top: 1rem; + margin-bottom: 1rem; +} +.input-field.inline { + display: inline-block; + vertical-align: middle; + margin-left: 5px; +} +.input-field.inline input, +.input-field.inline .select-dropdown { + margin-bottom: 1rem; +} +.input-field.col label { + left: 0.75rem; +} +.input-field.col .prefix ~ label, +.input-field.col .prefix ~ .validate ~ label { + width: calc(100% - 3rem - 1.5rem); +} +.input-field > label { + color: #9e9e9e; + position: absolute; + top: 0; + left: 0; + font-size: 1rem; + cursor: text; + -webkit-transition: + color 0.2s ease-out, + -webkit-transform 0.2s ease-out; + transition: + color 0.2s ease-out, + -webkit-transform 0.2s ease-out; + transition: + transform 0.2s ease-out, + color 0.2s ease-out; + transition: + transform 0.2s ease-out, + color 0.2s ease-out, + -webkit-transform 0.2s ease-out; + -webkit-transform-origin: 0% 100%; + transform-origin: 0% 100%; + text-align: initial; + -webkit-transform: translateY(12px); + transform: translateY(12px); +} +.input-field > label:not(.label-icon).active { + -webkit-transform: translateY(-14px) scale(0.8); + transform: translateY(-14px) scale(0.8); + -webkit-transform-origin: 0 0; + transform-origin: 0 0; +} +.input-field > input[type]:-webkit-autofill:not(.browser-default):not([type='search']) + label, +.input-field > input[type='date']:not(.browser-default) + label, +.input-field > input[type='time']:not(.browser-default) + label { + -webkit-transform: translateY(-14px) scale(0.8); + transform: translateY(-14px) scale(0.8); + -webkit-transform-origin: 0 0; + transform-origin: 0 0; +} +.input-field .helper-text { + position: relative; + min-height: 18px; + display: block; + font-size: 12px; + color: rgba(0, 0, 0, 0.54); +} +.input-field .helper-text::after { + opacity: 1; + position: absolute; + top: 0; + left: 0; +} +.input-field .prefix { + position: absolute; + width: 3rem; + font-size: 2rem; + -webkit-transition: color 0.2s; + transition: color 0.2s; + top: 0.5rem; +} +.input-field .prefix.active { + color: #26a69a; +} +.input-field .prefix ~ input, +.input-field .prefix ~ textarea, +.input-field .prefix ~ label, +.input-field .prefix ~ .validate ~ label, +.input-field .prefix ~ .helper-text, +.input-field .prefix ~ .autocomplete-content { + margin-left: 3rem; + width: 92%; + width: calc(100% - 3rem); +} +.input-field .prefix ~ label { + margin-left: 3rem; +} +@media only screen and (max-width: 992px) { + .input-field .prefix ~ input { + width: 86%; + width: calc(100% - 3rem); + } +} +@media only screen and (max-width: 600px) { + .input-field .prefix ~ input { + width: 80%; + width: calc(100% - 3rem); + } +} +.input-field input[type='search'] { + display: block; + line-height: inherit; + -webkit-transition: 0.3s background-color; + transition: 0.3s background-color; +} +.nav-wrapper .input-field input[type='search'] { + height: inherit; + padding-left: 4rem; + width: calc(100% - 4rem); + border: 0; + -webkit-box-shadow: none; + box-shadow: none; +} +.input-field input[type='search']:focus:not(.browser-default) { + background-color: #fff; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + color: #444; +} +.input-field input[type='search']:focus:not(.browser-default) + label i, +.input-field input[type='search']:focus:not(.browser-default) ~ .mdi-navigation-close, +.input-field input[type='search']:focus:not(.browser-default) ~ .material-icons { + color: #444; +} +.input-field input[type='search'] + .label-icon { + -webkit-transform: none; + transform: none; + left: 1rem; +} +.input-field input[type='search'] ~ .mdi-navigation-close, +.input-field input[type='search'] ~ .material-icons { + position: absolute; + top: 0; + right: 1rem; + color: transparent; + cursor: pointer; + font-size: 2rem; + -webkit-transition: 0.3s color; + transition: 0.3s color; +} +textarea { + width: 100%; + height: 3rem; + background-color: transparent; +} +textarea.materialize-textarea { + line-height: normal; + overflow-y: hidden; + padding: 0.8rem 0 0.8rem 0; + resize: none; + min-height: 3rem; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +.hiddendiv { + visibility: hidden; + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: break-word; + padding-top: 1.2rem; + position: absolute; + top: 0; + z-index: -1; +} +.autocomplete-content li .highlight { + color: #444; +} +.autocomplete-content li img { + height: 40px; + width: 40px; + margin: 5px 15px; +} +.character-counter { + min-height: 18px; +} +[type='radio']:not(:checked), +[type='radio']:checked { + position: absolute; + opacity: 0; + pointer-events: none; +} +[type='radio']:not(:checked) + span, +[type='radio']:checked + span { + position: relative; + padding-left: 35px; + cursor: pointer; + display: inline-block; + height: 25px; + line-height: 25px; + font-size: 1rem; + -webkit-transition: 0.28s ease; + transition: 0.28s ease; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +[type='radio'] + span:before, +[type='radio'] + span:after { + content: ''; + position: absolute; + left: 0; + top: 0; + margin: 4px; + width: 16px; + height: 16px; + z-index: 0; + -webkit-transition: 0.28s ease; + transition: 0.28s ease; +} +[type='radio']:not(:checked) + span:before, +[type='radio']:not(:checked) + span:after, +[type='radio']:checked + span:before, +[type='radio']:checked + span:after, +[type='radio'].with-gap:checked + span:before, +[type='radio'].with-gap:checked + span:after { + border-radius: 50%; +} +[type='radio']:not(:checked) + span:before, +[type='radio']:not(:checked) + span:after { + border: 2px solid #5a5a5a; +} +[type='radio']:not(:checked) + span:after { + -webkit-transform: scale(0); + transform: scale(0); +} +[type='radio']:checked + span:before { + border: 2px solid transparent; +} +[type='radio']:checked + span:after, +[type='radio'].with-gap:checked + span:before, +[type='radio'].with-gap:checked + span:after { + border: 2px solid #26a69a; +} +[type='radio']:checked + span:after, +[type='radio'].with-gap:checked + span:after { + background-color: #26a69a; +} +[type='radio']:checked + span:after { + -webkit-transform: scale(1.02); + transform: scale(1.02); +} +[type='radio'].with-gap:checked + span:after { + -webkit-transform: scale(0.5); + transform: scale(0.5); +} +[type='radio'].tabbed:focus + span:before { + -webkit-box-shadow: 0 0 0 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 0 0 10px rgba(0, 0, 0, 0.1); +} +[type='radio'].with-gap:disabled:checked + span:before { + border: 2px solid rgba(0, 0, 0, 0.42); +} +[type='radio'].with-gap:disabled:checked + span:after { + border: none; + background-color: rgba(0, 0, 0, 0.42); +} +[type='radio']:disabled:not(:checked) + span:before, +[type='radio']:disabled:checked + span:before { + background-color: transparent; + border-color: rgba(0, 0, 0, 0.42); +} +[type='radio']:disabled + span { + color: rgba(0, 0, 0, 0.42); +} +[type='radio']:disabled:not(:checked) + span:before { + border-color: rgba(0, 0, 0, 0.42); +} +[type='radio']:disabled:checked + span:after { + background-color: rgba(0, 0, 0, 0.42); + border-color: #949494; +} +[type='checkbox']:not(:checked), +[type='checkbox']:checked { + position: absolute; + opacity: 0; + pointer-events: none; +} +[type='checkbox'] + span:not(.lever) { + position: relative; + padding-left: 35px; + cursor: pointer; + display: inline-block; + height: 25px; + line-height: 25px; + font-size: 1rem; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +[type='checkbox'] + span:not(.lever):before, +[type='checkbox']:not(.filled-in) + span:not(.lever):after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 18px; + height: 18px; + z-index: 0; + border: 2px solid #5a5a5a; + border-radius: 1px; + margin-top: 3px; + -webkit-transition: 0.2s; + transition: 0.2s; +} +[type='checkbox']:not(.filled-in) + span:not(.lever):after { + border: 0; + -webkit-transform: scale(0); + transform: scale(0); +} +[type='checkbox']:not(:checked):disabled + span:not(.lever):before { + border: none; + background-color: rgba(0, 0, 0, 0.42); +} +[type='checkbox'].tabbed:focus + span:not(.lever):after { + -webkit-transform: scale(1); + transform: scale(1); + border: 0; + border-radius: 50%; + -webkit-box-shadow: 0 0 0 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 0 0 10px rgba(0, 0, 0, 0.1); + background-color: rgba(0, 0, 0, 0.1); +} +[type='checkbox']:checked + span:not(.lever):before { + top: -4px; + left: -5px; + width: 12px; + height: 22px; + border-top: 2px solid transparent; + border-left: 2px solid transparent; + border-right: 2px solid #26a69a; + border-bottom: 2px solid #26a69a; + -webkit-transform: rotate(40deg); + transform: rotate(40deg); + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-transform-origin: 100% 100%; + transform-origin: 100% 100%; +} +[type='checkbox']:checked:disabled + span:before { + border-right: 2px solid rgba(0, 0, 0, 0.42); + border-bottom: 2px solid rgba(0, 0, 0, 0.42); +} +[type='checkbox']:indeterminate + span:not(.lever):before { + top: -11px; + left: -12px; + width: 10px; + height: 22px; + border-top: none; + border-left: none; + border-right: 2px solid #26a69a; + border-bottom: none; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-transform-origin: 100% 100%; + transform-origin: 100% 100%; +} +[type='checkbox']:indeterminate:disabled + span:not(.lever):before { + border-right: 2px solid rgba(0, 0, 0, 0.42); + background-color: transparent; +} +[type='checkbox'].filled-in + span:not(.lever):after { + border-radius: 2px; +} +[type='checkbox'].filled-in + span:not(.lever):before, +[type='checkbox'].filled-in + span:not(.lever):after { + content: ''; + left: 0; + position: absolute; + -webkit-transition: + border 0.25s, + background-color 0.25s, + width 0.2s 0.1s, + height 0.2s 0.1s, + top 0.2s 0.1s, + left 0.2s 0.1s; + transition: + border 0.25s, + background-color 0.25s, + width 0.2s 0.1s, + height 0.2s 0.1s, + top 0.2s 0.1s, + left 0.2s 0.1s; + z-index: 1; +} +[type='checkbox'].filled-in:not(:checked) + span:not(.lever):before { + width: 0; + height: 0; + border: 3px solid transparent; + left: 6px; + top: 10px; + -webkit-transform: rotateZ(37deg); + transform: rotateZ(37deg); + -webkit-transform-origin: 100% 100%; + transform-origin: 100% 100%; +} +[type='checkbox'].filled-in:not(:checked) + span:not(.lever):after { + height: 20px; + width: 20px; + background-color: transparent; + border: 2px solid #5a5a5a; + top: 0px; + z-index: 0; +} +[type='checkbox'].filled-in:checked + span:not(.lever):before { + top: 0; + left: 1px; + width: 8px; + height: 13px; + border-top: 2px solid transparent; + border-left: 2px solid transparent; + border-right: 2px solid #fff; + border-bottom: 2px solid #fff; + -webkit-transform: rotateZ(37deg); + transform: rotateZ(37deg); + -webkit-transform-origin: 100% 100%; + transform-origin: 100% 100%; +} +[type='checkbox'].filled-in:checked + span:not(.lever):after { + top: 0; + width: 20px; + height: 20px; + border: 2px solid #26a69a; + background-color: #26a69a; + z-index: 0; +} +[type='checkbox'].filled-in.tabbed:focus + span:not(.lever):after { + border-radius: 2px; + border-color: #5a5a5a; + background-color: rgba(0, 0, 0, 0.1); +} +[type='checkbox'].filled-in.tabbed:checked:focus + span:not(.lever):after { + border-radius: 2px; + background-color: #26a69a; + border-color: #26a69a; +} +[type='checkbox'].filled-in:disabled:not(:checked) + span:not(.lever):before { + background-color: transparent; + border: 2px solid transparent; +} +[type='checkbox'].filled-in:disabled:not(:checked) + span:not(.lever):after { + border-color: transparent; + background-color: #949494; +} +[type='checkbox'].filled-in:disabled:checked + span:not(.lever):before { + background-color: transparent; +} +[type='checkbox'].filled-in:disabled:checked + span:not(.lever):after { + background-color: #949494; + border-color: #949494; +} +.switch, +.switch * { + -webkit-tap-highlight-color: transparent; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.switch label { + cursor: pointer; +} +.switch label input[type='checkbox'] { + opacity: 0; + width: 0; + height: 0; +} +.switch label input[type='checkbox']:checked + .lever { + background-color: #84c7c1; +} +.switch label input[type='checkbox']:checked + .lever:before, +.switch label input[type='checkbox']:checked + .lever:after { + left: 18px; +} +.switch label input[type='checkbox']:checked + .lever:after { + background-color: #26a69a; +} +.switch label .lever { + content: ''; + display: inline-block; + position: relative; + width: 36px; + height: 14px; + background-color: rgba(0, 0, 0, 0.38); + border-radius: 15px; + margin-right: 10px; + -webkit-transition: background 0.3s ease; + transition: background 0.3s ease; + vertical-align: middle; + margin: 0 16px; +} +.switch label .lever:before, +.switch label .lever:after { + content: ''; + position: absolute; + display: inline-block; + width: 20px; + height: 20px; + border-radius: 50%; + left: 0; + top: -3px; + -webkit-transition: + left 0.3s ease, + background 0.3s ease, + -webkit-box-shadow 0.1s ease, + -webkit-transform 0.1s ease; + transition: + left 0.3s ease, + background 0.3s ease, + -webkit-box-shadow 0.1s ease, + -webkit-transform 0.1s ease; + transition: + left 0.3s ease, + background 0.3s ease, + box-shadow 0.1s ease, + transform 0.1s ease; + transition: + left 0.3s ease, + background 0.3s ease, + box-shadow 0.1s ease, + transform 0.1s ease, + -webkit-box-shadow 0.1s ease, + -webkit-transform 0.1s ease; +} +.switch label .lever:before { + background-color: rgba(38, 166, 154, 0.15); +} +.switch label .lever:after { + background-color: #f1f1f1; + -webkit-box-shadow: + 0px 3px 1px -2px rgba(0, 0, 0, 0.2), + 0px 2px 2px 0px rgba(0, 0, 0, 0.14), + 0px 1px 5px 0px rgba(0, 0, 0, 0.12); + box-shadow: + 0px 3px 1px -2px rgba(0, 0, 0, 0.2), + 0px 2px 2px 0px rgba(0, 0, 0, 0.14), + 0px 1px 5px 0px rgba(0, 0, 0, 0.12); +} +input[type='checkbox']:checked:not(:disabled) ~ .lever:active::before, +input[type='checkbox']:checked:not(:disabled).tabbed:focus ~ .lever::before { + -webkit-transform: scale(2.4); + transform: scale(2.4); + background-color: rgba(38, 166, 154, 0.15); +} +input[type='checkbox']:not(:disabled) ~ .lever:active:before, +input[type='checkbox']:not(:disabled).tabbed:focus ~ .lever::before { + -webkit-transform: scale(2.4); + transform: scale(2.4); + background-color: rgba(0, 0, 0, 0.08); +} +.switch input[type='checkbox'][disabled] + .lever { + cursor: default; + background-color: rgba(0, 0, 0, 0.12); +} +.switch label input[type='checkbox'][disabled] + .lever:after, +.switch label input[type='checkbox'][disabled]:checked + .lever:after { + background-color: #949494; +} +select { + display: none; +} +select.browser-default { + display: block; +} +select { + background-color: rgba(255, 255, 255, 0.9); + width: 100%; + padding: 5px; + border: 1px solid #f2f2f2; + border-radius: 2px; + height: 3rem; +} +.select-label { + position: absolute; +} +.select-wrapper { + position: relative; +} +.select-wrapper.valid + label, +.select-wrapper.invalid + label { + width: 100%; + pointer-events: none; +} +.select-wrapper input.select-dropdown { + position: relative; + cursor: pointer; + background-color: transparent; + border: none; + border-bottom: 1px solid #9e9e9e; + outline: none; + height: 3rem; + line-height: 3rem; + width: 100%; + font-size: 16px; + margin: 0 0 8px 0; + padding: 0; + display: block; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + z-index: 1; +} +.select-wrapper input.select-dropdown:focus { + border-bottom: 1px solid #26a69a; +} +.select-wrapper .caret { + position: absolute; + right: 0; + top: 0; + bottom: 0; + margin: auto 0; + z-index: 0; + fill: rgba(0, 0, 0, 0.87); +} +.select-wrapper + label { + position: absolute; + top: -26px; + font-size: 0.8rem; +} +select:disabled { + color: rgba(0, 0, 0, 0.42); +} +.select-wrapper.disabled + label { + color: rgba(0, 0, 0, 0.42); +} +.select-wrapper.disabled .caret { + fill: rgba(0, 0, 0, 0.42); +} +.select-wrapper input.select-dropdown:disabled { + color: rgba(0, 0, 0, 0.42); + cursor: default; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.select-wrapper i { + color: rgba(0, 0, 0, 0.3); +} +.select-dropdown li.disabled, +.select-dropdown li.disabled > span, +.select-dropdown li.optgroup { + color: rgba(0, 0, 0, 0.3); + background-color: transparent; +} +body.keyboard-focused .select-dropdown.dropdown-content li:focus { + background-color: rgba(0, 0, 0, 0.08); +} +.select-dropdown.dropdown-content li:hover { + background-color: rgba(0, 0, 0, 0.08); +} +.select-dropdown.dropdown-content li.selected { + background-color: rgba(0, 0, 0, 0.03); +} +.prefix ~ .select-wrapper { + margin-left: 3rem; + width: 92%; + width: calc(100% - 3rem); +} +.prefix ~ label { + margin-left: 3rem; +} +.select-dropdown li img { + height: 40px; + width: 40px; + margin: 5px 15px; + float: right; +} +.select-dropdown li.optgroup { + border-top: 1px solid #eee; +} +.select-dropdown li.optgroup.selected > span { + color: rgba(0, 0, 0, 0.7); +} +.select-dropdown li.optgroup > span { + color: rgba(0, 0, 0, 0.4); +} +.select-dropdown li.optgroup ~ li.optgroup-option { + padding-left: 1rem; +} +.file-field { + position: relative; +} +.file-field .file-path-wrapper { + overflow: hidden; + padding-left: 10px; +} +.file-field input.file-path { + width: 100%; +} +.file-field .btn, +.file-field .btn-large, +.file-field .btn-small { + float: left; + height: 3rem; + line-height: 3rem; +} +.file-field span { + cursor: pointer; +} +.file-field input[type='file'] { + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; + width: 100%; + margin: 0; + padding: 0; + font-size: 20px; + cursor: pointer; + opacity: 0; + filter: alpha(opacity=0); +} +.file-field input[type='file']::-webkit-file-upload-button { + display: none; +} +.range-field { + position: relative; +} +input[type='range'], +input[type='range'] + .thumb { + cursor: pointer; +} +input[type='range'] { + position: relative; + background-color: transparent; + border: none; + outline: none; + width: 100%; + margin: 15px 0; + padding: 0; +} +input[type='range']:focus { + outline: none; +} +input[type='range'] + .thumb { + position: absolute; + top: 10px; + left: 0; + border: none; + height: 0; + width: 0; + border-radius: 50%; + background-color: #26a69a; + margin-left: 7px; + -webkit-transform-origin: 50% 50%; + transform-origin: 50% 50%; + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); +} +input[type='range'] + .thumb .value { + display: block; + width: 30px; + text-align: center; + color: #26a69a; + font-size: 0; + -webkit-transform: rotate(45deg); + transform: rotate(45deg); +} +input[type='range'] + .thumb.active { + border-radius: 50% 50% 50% 0; +} +input[type='range'] + .thumb.active .value { + color: #fff; + margin-left: -1px; + margin-top: 8px; + font-size: 10px; +} +input[type='range'] { + -webkit-appearance: none; +} +input[type='range']::-webkit-slider-runnable-track { + height: 3px; + background: #c2c0c2; + border: none; +} +input[type='range']::-webkit-slider-thumb { + border: none; + height: 14px; + width: 14px; + border-radius: 50%; + background: #26a69a; + -webkit-transition: -webkit-box-shadow 0.3s; + transition: -webkit-box-shadow 0.3s; + transition: box-shadow 0.3s; + transition: + box-shadow 0.3s, + -webkit-box-shadow 0.3s; + -webkit-appearance: none; + background-color: #26a69a; + -webkit-transform-origin: 50% 50%; + transform-origin: 50% 50%; + margin: -5px 0 0 0; +} +.keyboard-focused input[type='range']:focus:not(.active)::-webkit-slider-thumb { + -webkit-box-shadow: 0 0 0 10px rgba(38, 166, 154, 0.26); + box-shadow: 0 0 0 10px rgba(38, 166, 154, 0.26); +} +input[type='range'] { + border: 1px solid white; +} +input[type='range']::-moz-range-track { + height: 3px; + background: #c2c0c2; + border: none; +} +input[type='range']::-moz-focus-inner { + border: 0; +} +input[type='range']::-moz-range-thumb { + border: none; + height: 14px; + width: 14px; + border-radius: 50%; + background: #26a69a; + -webkit-transition: -webkit-box-shadow 0.3s; + transition: -webkit-box-shadow 0.3s; + transition: box-shadow 0.3s; + transition: + box-shadow 0.3s, + -webkit-box-shadow 0.3s; + margin-top: -5px; +} +input[type='range']:-moz-focusring { + outline: 1px solid #fff; + outline-offset: -1px; +} +.keyboard-focused input[type='range']:focus:not(.active)::-moz-range-thumb { + box-shadow: 0 0 0 10px rgba(38, 166, 154, 0.26); +} +input[type='range']::-ms-track { + height: 3px; + background: transparent; + border-color: transparent; + border-width: 6px 0; + color: transparent; +} +input[type='range']::-ms-fill-lower { + background: #777; +} +input[type='range']::-ms-fill-upper { + background: #ddd; +} +input[type='range']::-ms-thumb { + border: none; + height: 14px; + width: 14px; + border-radius: 50%; + background: #26a69a; + -webkit-transition: -webkit-box-shadow 0.3s; + transition: -webkit-box-shadow 0.3s; + transition: box-shadow 0.3s; + transition: + box-shadow 0.3s, + -webkit-box-shadow 0.3s; +} +.keyboard-focused input[type='range']:focus:not(.active)::-ms-thumb { + box-shadow: 0 0 0 10px rgba(38, 166, 154, 0.26); +} +.table-of-contents.fixed { + position: fixed; +} +.table-of-contents li { + padding: 2px 0; +} +.table-of-contents a { + display: inline-block; + font-weight: 300; + color: #757575; + padding-left: 16px; + height: 1.5rem; + line-height: 1.5rem; + letter-spacing: 0.4; + display: inline-block; +} +.table-of-contents a:hover { + color: #a8a8a8; + padding-left: 15px; + border-left: 1px solid #ee6e73; +} +.table-of-contents a.active { + font-weight: 500; + padding-left: 14px; + border-left: 2px solid #ee6e73; +} +.sidenav { + position: fixed; + width: 300px; + left: 0; + top: 0; + margin: 0; + -webkit-transform: translateX(-100%); + transform: translateX(-100%); + height: 100%; + height: calc(100% + 60px); + height: -moz-calc(100%); + padding-bottom: 60px; + background-color: #fff; + z-index: 999; + overflow-y: auto; + will-change: transform; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-transform: translateX(-105%); + transform: translateX(-105%); +} +.sidenav.right-aligned { + right: 0; + -webkit-transform: translateX(105%); + transform: translateX(105%); + left: auto; + -webkit-transform: translateX(100%); + transform: translateX(100%); +} +.sidenav .collapsible { + margin: 0; +} +.sidenav li { + float: none; + line-height: 48px; +} +.sidenav li.active { + background-color: rgba(0, 0, 0, 0.05); +} +.sidenav li > a { + color: rgba(0, 0, 0, 0.87); + display: block; + font-size: 14px; + font-weight: 500; + height: 48px; + line-height: 48px; + padding: 0 32px; +} +.sidenav li > a:hover { + background-color: rgba(0, 0, 0, 0.05); +} +.sidenav li > a.btn, +.sidenav li > a.btn-large, +.sidenav li > a.btn-small, +.sidenav li > a.btn-large, +.sidenav li > a.btn-flat, +.sidenav li > a.btn-floating { + margin: 10px 15px; +} +.sidenav li > a.btn, +.sidenav li > a.btn-large, +.sidenav li > a.btn-small, +.sidenav li > a.btn-large, +.sidenav li > a.btn-floating { + color: #fff; +} +.sidenav li > a.btn-flat { + color: #343434; +} +.sidenav li > a.btn:hover, +.sidenav li > a.btn-large:hover, +.sidenav li > a.btn-small:hover, +.sidenav li > a.btn-large:hover { + background-color: #2bbbad; +} +.sidenav li > a.btn-floating:hover { + background-color: #26a69a; +} +.sidenav li > a > i, +.sidenav li > a > [class^='mdi-'], +.sidenav li > a li > a > [class*='mdi-'], +.sidenav li > a > i.material-icons { + float: left; + height: 48px; + line-height: 48px; + margin: 0 32px 0 0; + width: 24px; + color: rgba(0, 0, 0, 0.54); +} +.sidenav .divider { + margin: 8px 0 0 0; +} +.sidenav .subheader { + cursor: initial; + pointer-events: none; + color: rgba(0, 0, 0, 0.54); + font-size: 14px; + font-weight: 500; + line-height: 48px; +} +.sidenav .subheader:hover { + background-color: transparent; +} +.sidenav .user-view { + position: relative; + padding: 32px 32px 0; + margin-bottom: 8px; +} +.sidenav .user-view > a { + height: auto; + padding: 0; +} +.sidenav .user-view > a:hover { + background-color: transparent; +} +.sidenav .user-view .background { + overflow: hidden; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: -1; +} +.sidenav .user-view .circle, +.sidenav .user-view .name, +.sidenav .user-view .email { + display: block; +} +.sidenav .user-view .circle { + height: 64px; + width: 64px; +} +.sidenav .user-view .name, +.sidenav .user-view .email { + font-size: 14px; + line-height: 24px; +} +.sidenav .user-view .name { + margin-top: 16px; + font-weight: 500; +} +.sidenav .user-view .email { + padding-bottom: 16px; + font-weight: 400; +} +.drag-target { + height: 100%; + width: 10px; + position: fixed; + top: 0; + z-index: 998; +} +.drag-target.right-aligned { + right: 0; +} +.sidenav.sidenav-fixed { + left: 0; + -webkit-transform: translateX(0); + transform: translateX(0); + position: fixed; +} +.sidenav.sidenav-fixed.right-aligned { + right: 0; + left: auto; +} +@media only screen and (max-width: 992px) { + .sidenav.sidenav-fixed { + -webkit-transform: translateX(-105%); + transform: translateX(-105%); + } + .sidenav.sidenav-fixed.right-aligned { + -webkit-transform: translateX(105%); + transform: translateX(105%); + } + .sidenav > a { + padding: 0 16px; + } + .sidenav .user-view { + padding: 16px 16px 0; + } +} +.sidenav .collapsible-body > ul:not(.collapsible) > li.active, +.sidenav.sidenav-fixed .collapsible-body > ul:not(.collapsible) > li.active { + background-color: #ee6e73; +} +.sidenav .collapsible-body > ul:not(.collapsible) > li.active a, +.sidenav.sidenav-fixed .collapsible-body > ul:not(.collapsible) > li.active a { + color: #fff; +} +.sidenav .collapsible-body { + padding: 0; +} +.sidenav-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + opacity: 0; + height: 120vh; + background-color: rgba(0, 0, 0, 0.5); + z-index: 997; + display: none; +} +.preloader-wrapper { + display: inline-block; + position: relative; + width: 50px; + height: 50px; +} +.preloader-wrapper.small { + width: 36px; + height: 36px; +} +.preloader-wrapper.big { + width: 64px; + height: 64px; +} +.preloader-wrapper.active { + -webkit-animation: container-rotate 1568ms linear infinite; + animation: container-rotate 1568ms linear infinite; +} +@-webkit-keyframes container-rotate { + to { + -webkit-transform: rotate(360deg); + } +} +@keyframes container-rotate { + to { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +.spinner-layer { + position: absolute; + width: 100%; + height: 100%; + opacity: 0; + border-color: #26a69a; +} +.spinner-blue, +.spinner-blue-only { + border-color: #4285f4; +} +.spinner-red, +.spinner-red-only { + border-color: #db4437; +} +.spinner-yellow, +.spinner-yellow-only { + border-color: #f4b400; +} +.spinner-green, +.spinner-green-only { + border-color: #0f9d58; +} +.active .spinner-layer.spinner-blue { + -webkit-animation: + fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, + blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: + fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, + blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; +} +.active .spinner-layer.spinner-red { + -webkit-animation: + fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, + red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: + fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, + red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; +} +.active .spinner-layer.spinner-yellow { + -webkit-animation: + fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, + yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: + fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, + yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; +} +.active .spinner-layer.spinner-green { + -webkit-animation: + fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, + green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: + fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, + green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; +} +.active .spinner-layer, +.active .spinner-layer.spinner-blue-only, +.active .spinner-layer.spinner-red-only, +.active .spinner-layer.spinner-yellow-only, +.active .spinner-layer.spinner-green-only { + opacity: 1; + -webkit-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; +} +@-webkit-keyframes fill-unfill-rotate { + 12.5% { + -webkit-transform: rotate(135deg); + } + 25% { + -webkit-transform: rotate(270deg); + } + 37.5% { + -webkit-transform: rotate(405deg); + } + 50% { + -webkit-transform: rotate(540deg); + } + 62.5% { + -webkit-transform: rotate(675deg); + } + 75% { + -webkit-transform: rotate(810deg); + } + 87.5% { + -webkit-transform: rotate(945deg); + } + to { + -webkit-transform: rotate(1080deg); + } +} +@keyframes fill-unfill-rotate { + 12.5% { + -webkit-transform: rotate(135deg); + transform: rotate(135deg); + } + 25% { + -webkit-transform: rotate(270deg); + transform: rotate(270deg); + } + 37.5% { + -webkit-transform: rotate(405deg); + transform: rotate(405deg); + } + 50% { + -webkit-transform: rotate(540deg); + transform: rotate(540deg); + } + 62.5% { + -webkit-transform: rotate(675deg); + transform: rotate(675deg); + } + 75% { + -webkit-transform: rotate(810deg); + transform: rotate(810deg); + } + 87.5% { + -webkit-transform: rotate(945deg); + transform: rotate(945deg); + } + to { + -webkit-transform: rotate(1080deg); + transform: rotate(1080deg); + } +} +@-webkit-keyframes blue-fade-in-out { + from { + opacity: 1; + } + 25% { + opacity: 1; + } + 26% { + opacity: 0; + } + 89% { + opacity: 0; + } + 90% { + opacity: 1; + } + 100% { + opacity: 1; + } +} +@keyframes blue-fade-in-out { + from { + opacity: 1; + } + 25% { + opacity: 1; + } + 26% { + opacity: 0; + } + 89% { + opacity: 0; + } + 90% { + opacity: 1; + } + 100% { + opacity: 1; + } +} +@-webkit-keyframes red-fade-in-out { + from { + opacity: 0; + } + 15% { + opacity: 0; + } + 25% { + opacity: 1; + } + 50% { + opacity: 1; + } + 51% { + opacity: 0; + } +} +@keyframes red-fade-in-out { + from { + opacity: 0; + } + 15% { + opacity: 0; + } + 25% { + opacity: 1; + } + 50% { + opacity: 1; + } + 51% { + opacity: 0; + } +} +@-webkit-keyframes yellow-fade-in-out { + from { + opacity: 0; + } + 40% { + opacity: 0; + } + 50% { + opacity: 1; + } + 75% { + opacity: 1; + } + 76% { + opacity: 0; + } +} +@keyframes yellow-fade-in-out { + from { + opacity: 0; + } + 40% { + opacity: 0; + } + 50% { + opacity: 1; + } + 75% { + opacity: 1; + } + 76% { + opacity: 0; + } +} +@-webkit-keyframes green-fade-in-out { + from { + opacity: 0; + } + 65% { + opacity: 0; + } + 75% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + opacity: 0; + } +} +@keyframes green-fade-in-out { + from { + opacity: 0; + } + 65% { + opacity: 0; + } + 75% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + opacity: 0; + } +} +.gap-patch { + position: absolute; + top: 0; + left: 45%; + width: 10%; + height: 100%; + overflow: hidden; + border-color: inherit; +} +.gap-patch .circle { + width: 1000%; + left: -450%; +} +.circle-clipper { + display: inline-block; + position: relative; + width: 50%; + height: 100%; + overflow: hidden; + border-color: inherit; +} +.circle-clipper .circle { + width: 200%; + height: 100%; + border-width: 3px; + border-style: solid; + border-color: inherit; + border-bottom-color: transparent !important; + border-radius: 50%; + -webkit-animation: none; + animation: none; + position: absolute; + top: 0; + right: 0; + bottom: 0; +} +.circle-clipper.left .circle { + left: 0; + border-right-color: transparent !important; + -webkit-transform: rotate(129deg); + transform: rotate(129deg); +} +.circle-clipper.right .circle { + left: -100%; + border-left-color: transparent !important; + -webkit-transform: rotate(-129deg); + transform: rotate(-129deg); +} +.active .circle-clipper.left .circle { + -webkit-animation: left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; +} +.active .circle-clipper.right .circle { + -webkit-animation: right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; +} +@-webkit-keyframes left-spin { + from { + -webkit-transform: rotate(130deg); + } + 50% { + -webkit-transform: rotate(-5deg); + } + to { + -webkit-transform: rotate(130deg); + } +} +@keyframes left-spin { + from { + -webkit-transform: rotate(130deg); + transform: rotate(130deg); + } + 50% { + -webkit-transform: rotate(-5deg); + transform: rotate(-5deg); + } + to { + -webkit-transform: rotate(130deg); + transform: rotate(130deg); + } +} +@-webkit-keyframes right-spin { + from { + -webkit-transform: rotate(-130deg); + } + 50% { + -webkit-transform: rotate(5deg); + } + to { + -webkit-transform: rotate(-130deg); + } +} +@keyframes right-spin { + from { + -webkit-transform: rotate(-130deg); + transform: rotate(-130deg); + } + 50% { + -webkit-transform: rotate(5deg); + transform: rotate(5deg); + } + to { + -webkit-transform: rotate(-130deg); + transform: rotate(-130deg); + } +} +#spinnerContainer.cooldown { + -webkit-animation: + container-rotate 1568ms linear infinite, + fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1); + animation: + container-rotate 1568ms linear infinite, + fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1); +} +@-webkit-keyframes fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } +} +@keyframes fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } +} +.slider { + position: relative; + height: 400px; + width: 100%; +} +.slider.fullscreen { + height: 100%; + width: 100%; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} +.slider.fullscreen ul.slides { + height: 100%; +} +.slider.fullscreen ul.indicators { + z-index: 2; + bottom: 30px; +} +.slider .slides { + background-color: #9e9e9e; + margin: 0; + height: 400px; +} +.slider .slides li { + opacity: 0; + position: absolute; + top: 0; + left: 0; + z-index: 1; + width: 100%; + height: inherit; + overflow: hidden; +} +.slider .slides li img { + height: 100%; + width: 100%; + background-size: cover; + background-position: center; +} +.slider .slides li .caption { + color: #fff; + position: absolute; + top: 15%; + left: 15%; + width: 70%; + opacity: 0; +} +.slider .slides li .caption p { + color: #e0e0e0; +} +.slider .slides li.active { + z-index: 2; +} +.slider .indicators { + position: absolute; + text-align: center; + left: 0; + right: 0; + bottom: 0; + margin: 0; +} +.slider .indicators .indicator-item { + display: inline-block; + position: relative; + cursor: pointer; + height: 16px; + width: 16px; + margin: 0 12px; + background-color: #e0e0e0; + -webkit-transition: background-color 0.3s; + transition: background-color 0.3s; + border-radius: 50%; +} +.slider .indicators .indicator-item.active { + background-color: #4caf50; +} +.carousel { + overflow: hidden; + position: relative; + width: 100%; + height: 400px; + -webkit-perspective: 500px; + perspective: 500px; + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; + -webkit-transform-origin: 0% 50%; + transform-origin: 0% 50%; +} +.carousel.carousel-slider { + top: 0; + left: 0; +} +.carousel.carousel-slider .carousel-fixed-item { + position: absolute; + left: 0; + right: 0; + bottom: 20px; + z-index: 1; +} +.carousel.carousel-slider .carousel-fixed-item.with-indicators { + bottom: 68px; +} +.carousel.carousel-slider .carousel-item { + width: 100%; + height: 100%; + min-height: 400px; + position: absolute; + top: 0; + left: 0; +} +.carousel.carousel-slider .carousel-item h2 { + font-size: 24px; + font-weight: 500; + line-height: 32px; +} +.carousel.carousel-slider .carousel-item p { + font-size: 15px; +} +.carousel .carousel-item { + visibility: hidden; + width: 200px; + height: 200px; + position: absolute; + top: 0; + left: 0; +} +.carousel .carousel-item > img { + width: 100%; +} +.carousel .indicators { + position: absolute; + text-align: center; + left: 0; + right: 0; + bottom: 0; + margin: 0; +} +.carousel .indicators .indicator-item { + display: inline-block; + position: relative; + cursor: pointer; + height: 8px; + width: 8px; + margin: 24px 4px; + background-color: rgba(255, 255, 255, 0.5); + -webkit-transition: background-color 0.3s; + transition: background-color 0.3s; + border-radius: 50%; +} +.carousel .indicators .indicator-item.active { + background-color: #fff; +} +.carousel.scrolling .carousel-item .materialboxed, +.carousel .carousel-item:not(.active) .materialboxed { + pointer-events: none; +} +.tap-target-wrapper { + width: 800px; + height: 800px; + position: fixed; + z-index: 1000; + visibility: hidden; + -webkit-transition: visibility 0s 0.3s; + transition: visibility 0s 0.3s; +} +.tap-target-wrapper.open { + visibility: visible; + -webkit-transition: visibility 0s; + transition: visibility 0s; +} +.tap-target-wrapper.open .tap-target { + -webkit-transform: scale(1); + transform: scale(1); + opacity: 0.95; + -webkit-transition: + opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1), + -webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1); + transition: + opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1), + -webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1); + transition: + transform 0.3s cubic-bezier(0.42, 0, 0.58, 1), + opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1); + transition: + transform 0.3s cubic-bezier(0.42, 0, 0.58, 1), + opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1), + -webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1); +} +.tap-target-wrapper.open .tap-target-wave::before { + -webkit-transform: scale(1); + transform: scale(1); +} +.tap-target-wrapper.open .tap-target-wave::after { + visibility: visible; + -webkit-animation: pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite; + animation: pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite; + -webkit-transition: + opacity 0.3s, + visibility 0s 1s, + -webkit-transform 0.3s; + transition: + opacity 0.3s, + visibility 0s 1s, + -webkit-transform 0.3s; + transition: + opacity 0.3s, + transform 0.3s, + visibility 0s 1s; + transition: + opacity 0.3s, + transform 0.3s, + visibility 0s 1s, + -webkit-transform 0.3s; +} +.tap-target { + position: absolute; + font-size: 1rem; + border-radius: 50%; + background-color: #ee6e73; + -webkit-box-shadow: + 0 20px 20px 0 rgba(0, 0, 0, 0.14), + 0 10px 50px 0 rgba(0, 0, 0, 0.12), + 0 30px 10px -20px rgba(0, 0, 0, 0.2); + box-shadow: + 0 20px 20px 0 rgba(0, 0, 0, 0.14), + 0 10px 50px 0 rgba(0, 0, 0, 0.12), + 0 30px 10px -20px rgba(0, 0, 0, 0.2); + width: 100%; + height: 100%; + opacity: 0; + -webkit-transform: scale(0); + transform: scale(0); + -webkit-transition: + opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1), + -webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1); + transition: + opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1), + -webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1); + transition: + transform 0.3s cubic-bezier(0.42, 0, 0.58, 1), + opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1); + transition: + transform 0.3s cubic-bezier(0.42, 0, 0.58, 1), + opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1), + -webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1); +} +.tap-target-content { + position: relative; + display: table-cell; +} +.tap-target-wave { + position: absolute; + border-radius: 50%; + z-index: 10001; +} +.tap-target-wave::before, +.tap-target-wave::after { + content: ''; + display: block; + position: absolute; + width: 100%; + height: 100%; + border-radius: 50%; + background-color: #ffffff; +} +.tap-target-wave::before { + -webkit-transform: scale(0); + transform: scale(0); + -webkit-transition: -webkit-transform 0.3s; + transition: -webkit-transform 0.3s; + transition: transform 0.3s; + transition: + transform 0.3s, + -webkit-transform 0.3s; +} +.tap-target-wave::after { + visibility: hidden; + -webkit-transition: + opacity 0.3s, + visibility 0s, + -webkit-transform 0.3s; + transition: + opacity 0.3s, + visibility 0s, + -webkit-transform 0.3s; + transition: + opacity 0.3s, + transform 0.3s, + visibility 0s; + transition: + opacity 0.3s, + transform 0.3s, + visibility 0s, + -webkit-transform 0.3s; + z-index: -1; +} +.tap-target-origin { + top: 50%; + left: 50%; + -webkit-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + z-index: 10002; + position: absolute !important; +} +.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small), +.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small):hover { + background: none; +} +@media only screen and (max-width: 600px) { + .tap-target, + .tap-target-wrapper { + width: 600px; + height: 600px; + } +} +.pulse { + overflow: visible; + position: relative; +} +.pulse::before { + content: ''; + display: block; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: inherit; + border-radius: inherit; + -webkit-transition: + opacity 0.3s, + -webkit-transform 0.3s; + transition: + opacity 0.3s, + -webkit-transform 0.3s; + transition: + opacity 0.3s, + transform 0.3s; + transition: + opacity 0.3s, + transform 0.3s, + -webkit-transform 0.3s; + -webkit-animation: pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite; + animation: pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite; + z-index: -1; +} +@-webkit-keyframes pulse-animation { + 0% { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + } + 50% { + opacity: 0; + -webkit-transform: scale(1.5); + transform: scale(1.5); + } + 100% { + opacity: 0; + -webkit-transform: scale(1.5); + transform: scale(1.5); + } +} +@keyframes pulse-animation { + 0% { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + } + 50% { + opacity: 0; + -webkit-transform: scale(1.5); + transform: scale(1.5); + } + 100% { + opacity: 0; + -webkit-transform: scale(1.5); + transform: scale(1.5); + } +} +.datepicker-modal { + max-width: 325px; + min-width: 300px; + max-height: none; +} +.datepicker-container.modal-content { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + padding: 0; +} +.datepicker-controls { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + width: 280px; + margin: 0 auto; +} +.datepicker-controls .selects-container { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} +.datepicker-controls .select-wrapper input { + border-bottom: none; + text-align: center; + margin: 0; +} +.datepicker-controls .select-wrapper input:focus { + border-bottom: none; +} +.datepicker-controls .select-wrapper .caret { + display: none; +} +.datepicker-controls .select-year input { + width: 50px; +} +.datepicker-controls .select-month input { + width: 70px; +} +.month-prev, +.month-next { + margin-top: 4px; + cursor: pointer; + background-color: transparent; + border: none; +} +.datepicker-date-display { + -webkit-box-flex: 1; + -webkit-flex: 1 auto; + -ms-flex: 1 auto; + flex: 1 auto; + background-color: #26a69a; + color: #fff; + padding: 20px 22px; + font-weight: 500; +} +.datepicker-date-display .year-text { + display: block; + font-size: 1.5rem; + line-height: 25px; + color: rgba(255, 255, 255, 0.7); +} +.datepicker-date-display .date-text { + display: block; + font-size: 2.8rem; + line-height: 47px; + font-weight: 500; +} +.datepicker-calendar-container { + -webkit-box-flex: 2.5; + -webkit-flex: 2.5 auto; + -ms-flex: 2.5 auto; + flex: 2.5 auto; +} +.datepicker-table { + width: 280px; + font-size: 1rem; + margin: 0 auto; +} +.datepicker-table thead { + border-bottom: none; +} +.datepicker-table th { + padding: 10px 5px; + text-align: center; +} +.datepicker-table tr { + border: none; +} +.datepicker-table abbr { + text-decoration: none; + color: #999; +} +.datepicker-table td { + border-radius: 50%; + padding: 0; +} +.datepicker-table td.is-today { + color: #26a69a; +} +.datepicker-table td.is-selected { + background-color: #26a69a; + color: #fff; +} +.datepicker-table td.is-outside-current-month, +.datepicker-table td.is-disabled { + color: rgba(0, 0, 0, 0.3); + pointer-events: none; +} +.datepicker-day-button { + background-color: transparent; + border: none; + line-height: 38px; + display: block; + width: 100%; + border-radius: 50%; + padding: 0 5px; + cursor: pointer; + color: inherit; +} +.datepicker-day-button:focus { + background-color: rgba(43, 161, 150, 0.25); +} +.datepicker-footer { + width: 280px; + margin: 0 auto; + padding-bottom: 5px; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} +.datepicker-cancel, +.datepicker-clear, +.datepicker-today, +.datepicker-done { + color: #26a69a; + padding: 0 1rem; +} +.datepicker-clear { + color: #f44336; +} +@media only screen and (min-width: 601px) { + .datepicker-modal { + max-width: 625px; + } + .datepicker-container.modal-content { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + } + .datepicker-date-display { + -webkit-box-flex: 0; + -webkit-flex: 0 1 270px; + -ms-flex: 0 1 270px; + flex: 0 1 270px; + } + .datepicker-controls, + .datepicker-table, + .datepicker-footer { + width: 320px; + } + .datepicker-day-button { + line-height: 44px; + } +} +.timepicker-modal { + max-width: 325px; + max-height: none; +} +.timepicker-container.modal-content { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + padding: 0; +} +.text-primary { + color: #fff; +} +.timepicker-digital-display { + -webkit-box-flex: 1; + -webkit-flex: 1 auto; + -ms-flex: 1 auto; + flex: 1 auto; + background-color: #26a69a; + padding: 10px; + font-weight: 300; +} +.timepicker-text-container { + font-size: 4rem; + font-weight: bold; + text-align: center; + color: rgba(255, 255, 255, 0.6); + font-weight: 400; + position: relative; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.timepicker-span-hours, +.timepicker-span-minutes, +.timepicker-span-am-pm div { + cursor: pointer; +} +.timepicker-span-hours { + margin-right: 3px; +} +.timepicker-span-minutes { + margin-left: 3px; +} +.timepicker-display-am-pm { + font-size: 1.3rem; + position: absolute; + right: 1rem; + bottom: 1rem; + font-weight: 400; +} +.timepicker-analog-display { + -webkit-box-flex: 2.5; + -webkit-flex: 2.5 auto; + -ms-flex: 2.5 auto; + flex: 2.5 auto; +} +.timepicker-plate { + background-color: #eee; + border-radius: 50%; + width: 270px; + height: 270px; + overflow: visible; + position: relative; + margin: auto; + margin-top: 25px; + margin-bottom: 5px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.timepicker-canvas, +.timepicker-dial { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; +} +.timepicker-minutes { + visibility: hidden; +} +.timepicker-tick { + border-radius: 50%; + color: rgba(0, 0, 0, 0.87); + line-height: 40px; + text-align: center; + width: 40px; + height: 40px; + position: absolute; + cursor: pointer; + font-size: 15px; +} +.timepicker-tick.active, +.timepicker-tick:hover { + background-color: rgba(38, 166, 154, 0.25); +} +.timepicker-dial { + -webkit-transition: + opacity 350ms, + -webkit-transform 350ms; + transition: + opacity 350ms, + -webkit-transform 350ms; + transition: + transform 350ms, + opacity 350ms; + transition: + transform 350ms, + opacity 350ms, + -webkit-transform 350ms; +} +.timepicker-dial-out { + opacity: 0; +} +.timepicker-dial-out.timepicker-hours { + -webkit-transform: scale(1.1, 1.1); + transform: scale(1.1, 1.1); +} +.timepicker-dial-out.timepicker-minutes { + -webkit-transform: scale(0.8, 0.8); + transform: scale(0.8, 0.8); +} +.timepicker-canvas { + -webkit-transition: opacity 175ms; + transition: opacity 175ms; +} +.timepicker-canvas line { + stroke: #26a69a; + stroke-width: 4; + stroke-linecap: round; +} +.timepicker-canvas-out { + opacity: 0.25; +} +.timepicker-canvas-bearing { + stroke: none; + fill: #26a69a; +} +.timepicker-canvas-bg { + stroke: none; + fill: #26a69a; +} +.timepicker-footer { + margin: 0 auto; + padding: 5px 1rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} +.timepicker-clear { + color: #f44336; +} +.timepicker-close { + color: #26a69a; +} +.timepicker-clear, +.timepicker-close { + padding: 0 20px; +} +@media only screen and (min-width: 601px) { + .timepicker-modal { + max-width: 600px; + } + .timepicker-container.modal-content { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + } + .timepicker-text-container { + top: 32%; + } + .timepicker-display-am-pm { + position: relative; + right: auto; + bottom: auto; + text-align: center; + margin-top: 1.2rem; + } +} diff --git a/tests/app/rp/svelte.config.js b/tests/app/rp/svelte.config.js index 1cf26a00d..1023568ae 100644 --- a/tests/app/rp/svelte.config.js +++ b/tests/app/rp/svelte.config.js @@ -1,5 +1,5 @@ -import adapter from '@sveltejs/adapter-auto'; -import { vitePreprocess } from '@sveltejs/kit/vite'; +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ const config = { @@ -8,9 +8,7 @@ const config = { preprocess: vitePreprocess(), kit: { - // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. - // If your environment is not supported or you settled on a specific environment, switch out the adapter. - // See https://kit.svelte.dev/docs/adapters for more information about adapters. + // build to run in containerized node.js environment adapter: adapter() } }; diff --git a/tests/common_testing.py b/tests/common_testing.py new file mode 100644 index 000000000..6f6a5b745 --- /dev/null +++ b/tests/common_testing.py @@ -0,0 +1,33 @@ +from django.conf import settings +from django.test import TestCase as DjangoTestCase +from django.test import TransactionTestCase as DjangoTransactionTestCase + + +# The multiple database scenario setup for these tests purposefully defines 'default' as +# an empty database in order to catch any assumptions in this package about database names +# and in particular to ensure there is no assumption that 'default' is a valid database. +# +# When there are multiple databases defined, Django tests will not work unless they are +# told which database(s) to work with. + + +def retrieve_current_databases(): + if len(settings.DATABASES) > 1: + return [name for name in settings.DATABASES if name != "default"] + else: + return ["default"] + + +class OAuth2ProviderBase: + @classmethod + def setUpClass(cls): + cls.databases = retrieve_current_databases() + super().setUpClass() + + +class OAuth2ProviderTestCase(OAuth2ProviderBase, DjangoTestCase): + """Place holder to allow overriding behaviors.""" + + +class OAuth2ProviderTransactionTestCase(OAuth2ProviderBase, DjangoTransactionTestCase): + """Place holder to allow overriding behaviors.""" diff --git a/tests/conftest.py b/tests/conftest.py index eff48f7fb..2510025ce 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ from urllib.parse import parse_qs, urlparse import pytest +from django import VERSION from django.conf import settings as test_settings from django.contrib.auth import get_user_model from django.urls import reverse @@ -294,3 +295,13 @@ def oidc_non_confidential_tokens(oauth2_settings, public_application, test_user, "openid", "http://other.org", ) + + +@pytest.fixture(autouse=True) +def django_login_required_middleware(settings, request): + if "nologinrequiredmiddleware" in request.keywords: + return + + # Django 5.1 introduced LoginRequiredMiddleware + if VERSION[0] >= 5 and VERSION[1] >= 1: + settings.MIDDLEWARE = [*settings.MIDDLEWARE, "django.contrib.auth.middleware.LoginRequiredMiddleware"] diff --git a/tests/custom_hasher.py b/tests/custom_hasher.py new file mode 100644 index 000000000..5f7ceb89c --- /dev/null +++ b/tests/custom_hasher.py @@ -0,0 +1,10 @@ +from django.contrib.auth.hashers import PBKDF2PasswordHasher + + +class MyPBKDF2PasswordHasher(PBKDF2PasswordHasher): + """ + A subclass of PBKDF2PasswordHasher that uses less iterations. + """ + + algorithm = "fast_pbkdf2" + iterations = 10000 diff --git a/tests/db_router.py b/tests/db_router.py new file mode 100644 index 000000000..7aa354ed8 --- /dev/null +++ b/tests/db_router.py @@ -0,0 +1,76 @@ +apps_in_beta = {"some_other_app", "this_one_too"} + +# These are bare minimum routers to fake the scenario where there is actually a +# decision around where an application's models might live. + + +class AlphaRouter: + # alpha is where the core Django models are stored including user. To keep things + # simple this is where the oauth2 provider models are stored as well because they + # have a foreign key to User. + + def db_for_read(self, model, **hints): + if model._meta.app_label not in apps_in_beta: + return "alpha" + return None + + def db_for_write(self, model, **hints): + if model._meta.app_label not in apps_in_beta: + return "alpha" + return None + + def allow_relation(self, obj1, obj2, **hints): + if obj1._state.db == "alpha" and obj2._state.db == "alpha": + return True + return None + + def allow_migrate(self, db, app_label, model_name=None, **hints): + if app_label not in apps_in_beta: + return db == "alpha" + return None + + +class BetaRouter: + def db_for_read(self, model, **hints): + if model._meta.app_label in apps_in_beta: + return "beta" + return None + + def db_for_write(self, model, **hints): + if model._meta.app_label in apps_in_beta: + return "beta" + return None + + def allow_relation(self, obj1, obj2, **hints): + if obj1._state.db == "beta" and obj2._state.db == "beta": + return True + return None + + def allow_migrate(self, db, app_label, model_name=None, **hints): + if app_label in apps_in_beta: + return db == "beta" + + +class CrossDatabaseRouter: + # alpha is where the core Django models are stored including user. To keep things + # simple this is where the oauth2 provider models are stored as well because they + # have a foreign key to User. + def db_for_read(self, model, **hints): + if model._meta.model_name == "accesstoken": + return "beta" + return None + + def db_for_write(self, model, **hints): + if model._meta.model_name == "accesstoken": + return "beta" + return None + + def allow_relation(self, obj1, obj2, **hints): + if obj1._state.db == "beta" and obj2._state.db == "beta": + return True + return None + + def allow_migrate(self, db, app_label, model_name=None, **hints): + if model_name == "accesstoken": + return db == "beta" + return None diff --git a/tests/migrations/0002_swapped_models.py b/tests/migrations/0002_swapped_models.py index 412f19927..e168a053d 100644 --- a/tests/migrations/0002_swapped_models.py +++ b/tests/migrations/0002_swapped_models.py @@ -118,10 +118,14 @@ class Migration(migrations.Migration): field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='s_refreshed_access_token', to=settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL), ), migrations.AddField( - model_name='sampleaccesstoken', - name='token', - field=models.CharField(max_length=255, unique=True), - preserve_default=False, + model_name="sampleaccesstoken", + name="token", + field=models.TextField(), + ), + migrations.AddField( + model_name="sampleaccesstoken", + name="token_checksum", + field=models.CharField(max_length=64, unique=True, db_index=True), ), migrations.AddField( model_name='sampleaccesstoken', diff --git a/tests/migrations/0006_basetestapplication_token_family.py b/tests/migrations/0006_basetestapplication_token_family.py new file mode 100644 index 000000000..6b065a242 --- /dev/null +++ b/tests/migrations/0006_basetestapplication_token_family.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2 on 2024-08-09 16:40 + +from django.db import migrations, models +from oauth2_provider.settings import oauth2_settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('tests', '0005_basetestapplication_allowed_origins_and_more'), + migrations.swappable_dependency(oauth2_settings.REFRESH_TOKEN_MODEL) + ] + + operations = [ + migrations.AddField( + model_name='samplerefreshtoken', + name='token_family', + field=models.UUIDField(blank=True, editable=False, null=True), + ), + ] diff --git a/tests/migrations/0007_add_localidtoken.py b/tests/migrations/0007_add_localidtoken.py new file mode 100644 index 000000000..f74cce5b6 --- /dev/null +++ b/tests/migrations/0007_add_localidtoken.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.25 on 2024-08-08 22:47 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('tests', '0006_basetestapplication_token_family'), + ] + + operations = [ + migrations.CreateModel( + name='LocalIDToken', + fields=[ + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('jti', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='JWT Token ID')), + ('expires', models.DateTimeField()), + ('scope', models.TextField(blank=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tests_localidtoken', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/tests/models.py b/tests/models.py index 355bc1b57..9f3643db8 100644 --- a/tests/models.py +++ b/tests/models.py @@ -4,6 +4,7 @@ AbstractAccessToken, AbstractApplication, AbstractGrant, + AbstractIDToken, AbstractRefreshToken, ) from oauth2_provider.settings import oauth2_settings @@ -54,3 +55,9 @@ class SampleRefreshToken(AbstractRefreshToken): class SampleGrant(AbstractGrant): custom_field = models.CharField(max_length=255) + + +class LocalIDToken(AbstractIDToken): + """Exists to be improperly configured for multiple databases.""" + + # The other token types will be in 'alpha' database. diff --git a/tests/multi_db_settings.py b/tests/multi_db_settings.py new file mode 100644 index 000000000..a6daf04a3 --- /dev/null +++ b/tests/multi_db_settings.py @@ -0,0 +1,19 @@ +# Import the test settings and then override DATABASES. + +from .settings import * # noqa: F401, F403 + + +DATABASES = { + "alpha": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", + }, + "beta": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", + }, + # As https://docs.djangoproject.com/en/4.2/topics/db/multi-db/#defining-your-databases + # indicates, it is ok to have no default database. + "default": {}, +} +DATABASE_ROUTERS = ["tests.db_router.AlphaRouter", "tests.db_router.BetaRouter"] diff --git a/tests/multi_db_settings_invalid_token_configuration.py b/tests/multi_db_settings_invalid_token_configuration.py new file mode 100644 index 000000000..ed2804f79 --- /dev/null +++ b/tests/multi_db_settings_invalid_token_configuration.py @@ -0,0 +1,8 @@ +from .multi_db_settings import * # noqa: F401, F403 + + +OAUTH2_PROVIDER = { + # The other two tokens will be in alpha. This will cause a failure when the + # app's ready method is called. + "ID_TOKEN_MODEL": "tests.LocalIDToken", +} diff --git a/tests/settings.py b/tests/settings.py index db807947c..c4d9f59ad 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -89,6 +89,8 @@ "tests", ) +PASSWORD_HASHERS = django.conf.settings.PASSWORD_HASHERS + ["tests.custom_hasher.MyPBKDF2PasswordHasher"] + LOGGING = { "version": 1, "disable_existing_loggers": False, diff --git a/tests/test_application_views.py b/tests/test_application_views.py index c8c145d9b..d4c7e28a9 100644 --- a/tests/test_application_views.py +++ b/tests/test_application_views.py @@ -1,11 +1,11 @@ import pytest from django.contrib.auth import get_user_model -from django.test import TestCase from django.urls import reverse from oauth2_provider.models import get_application_model from oauth2_provider.views.application import ApplicationRegistration +from .common_testing import OAuth2ProviderTestCase as TestCase from .models import SampleApplication @@ -63,6 +63,156 @@ def test_application_registration_user(self): self.assertEqual(app.algorithm, form_data["algorithm"]) +@pytest.mark.usefixtures("oauth2_settings") +@pytest.mark.oauth2_settings({"ALLOW_URI_WILDCARDS": True}) +class TestApplicationRegistrationViewRedirectURIWithWildcard(BaseTest): + def _test_valid(self, redirect_uri): + self.client.login(username="foo_user", password="123456") + + form_data = { + "name": "Foo app", + "client_id": "client_id", + "client_secret": "client_secret", + "client_type": Application.CLIENT_CONFIDENTIAL, + "redirect_uris": redirect_uri, + "post_logout_redirect_uris": "http://example.com", + "authorization_grant_type": Application.GRANT_AUTHORIZATION_CODE, + "algorithm": "", + } + + response = self.client.post(reverse("oauth2_provider:register"), form_data) + self.assertEqual(response.status_code, 302) + + app = get_application_model().objects.get(name="Foo app") + self.assertEqual(app.user.username, "foo_user") + app = Application.objects.get() + self.assertEqual(app.name, form_data["name"]) + self.assertEqual(app.client_id, form_data["client_id"]) + self.assertEqual(app.redirect_uris, form_data["redirect_uris"]) + self.assertEqual(app.post_logout_redirect_uris, form_data["post_logout_redirect_uris"]) + self.assertEqual(app.client_type, form_data["client_type"]) + self.assertEqual(app.authorization_grant_type, form_data["authorization_grant_type"]) + self.assertEqual(app.algorithm, form_data["algorithm"]) + + def _test_invalid(self, uri, error_message): + self.client.login(username="foo_user", password="123456") + + form_data = { + "name": "Foo app", + "client_id": "client_id", + "client_secret": "client_secret", + "client_type": Application.CLIENT_CONFIDENTIAL, + "redirect_uris": uri, + "post_logout_redirect_uris": "http://example.com", + "authorization_grant_type": Application.GRANT_AUTHORIZATION_CODE, + "algorithm": "", + } + + response = self.client.post(reverse("oauth2_provider:register"), form_data) + self.assertEqual(response.status_code, 200) + self.assertContains(response, error_message) + + def test_application_registration_valid_3ld_wildcard(self): + self._test_valid("https://*.example.com") + + def test_application_registration_valid_3ld_partial_wildcard(self): + self._test_valid("https://*-partial.example.com") + + def test_application_registration_invalid_star(self): + self._test_invalid("*", "invalid_scheme: *") + + def test_application_registration_invalid_tld_wildcard(self): + self._test_invalid("https://*", "wildcards cannot be in the top level or second level domain") + + def test_application_registration_invalid_tld_partial_wildcard(self): + self._test_invalid("https://*-partial", "wildcards cannot be in the top level or second level domain") + + def test_application_registration_invalid_tld_not_startswith_wildcard_tld(self): + self._test_invalid("https://example.*", "wildcards must be at the beginning of the hostname") + + def test_application_registration_invalid_2ld_wildcard(self): + self._test_invalid("https://*.com", "wildcards cannot be in the top level or second level domain") + + def test_application_registration_invalid_2ld_partial_wildcard(self): + self._test_invalid( + "https://*-partial.com", "wildcards cannot be in the top level or second level domain" + ) + + def test_application_registration_invalid_2ld_not_startswith_wildcard_tld(self): + self._test_invalid("https://example.*.com", "wildcards must be at the beginning of the hostname") + + def test_application_registration_invalid_3ld_partial_not_startswith_wildcard_2ld(self): + self._test_invalid( + "https://invalid-*.example.com", "wildcards must be at the beginning of the hostname" + ) + + def test_application_registration_invalid_4ld_not_startswith_wildcard_3ld(self): + self._test_invalid( + "https://invalid.*.invalid.example.com", + "wildcards must be at the beginning of the hostname", + ) + + def test_application_registration_invalid_4ld_partial_not_startswith_wildcard_2ld(self): + self._test_invalid( + "https://invalid-*.invalid.example.com", + "wildcards must be at the beginning of the hostname", + ) + + +@pytest.mark.usefixtures("oauth2_settings") +@pytest.mark.oauth2_settings({"ALLOW_URI_WILDCARDS": True}) +class TestApplicationRegistrationViewAllowedOriginWithWildcard( + TestApplicationRegistrationViewRedirectURIWithWildcard +): + def _test_valid(self, uris): + self.client.login(username="foo_user", password="123456") + + form_data = { + "name": "Foo app", + "client_id": "client_id", + "client_secret": "client_secret", + "client_type": Application.CLIENT_CONFIDENTIAL, + "allowed_origins": uris, + "redirect_uris": "https://example.com", + "post_logout_redirect_uris": "http://example.com", + "authorization_grant_type": Application.GRANT_AUTHORIZATION_CODE, + "algorithm": "", + } + + response = self.client.post(reverse("oauth2_provider:register"), form_data) + self.assertEqual(response.status_code, 302) + + app = get_application_model().objects.get(name="Foo app") + self.assertEqual(app.user.username, "foo_user") + app = Application.objects.get() + self.assertEqual(app.name, form_data["name"]) + self.assertEqual(app.client_id, form_data["client_id"]) + self.assertEqual(app.redirect_uris, form_data["redirect_uris"]) + self.assertEqual(app.post_logout_redirect_uris, form_data["post_logout_redirect_uris"]) + self.assertEqual(app.client_type, form_data["client_type"]) + self.assertEqual(app.authorization_grant_type, form_data["authorization_grant_type"]) + self.assertEqual(app.algorithm, form_data["algorithm"]) + + def _test_invalid(self, uri, error_message): + self.client.login(username="foo_user", password="123456") + + form_data = { + "name": "Foo app", + "client_id": "client_id", + "client_secret": "client_secret", + "client_type": Application.CLIENT_CONFIDENTIAL, + "allowed_origins": uri, + "redirect_uris": "http://example.com", + "post_logout_redirect_uris": "http://example.com", + "authorization_grant_type": Application.GRANT_AUTHORIZATION_CODE, + "algorithm": "", + } + + response = self.client.post(reverse("oauth2_provider:register"), form_data) + self.assertEqual(response.status_code, 200) + self.assertContains(response, error_message) + + class TestApplicationViews(BaseTest): @classmethod def _create_application(cls, name, user): diff --git a/tests/test_auth_backends.py b/tests/test_auth_backends.py index b0ff145ab..49729b1c4 100644 --- a/tests/test_auth_backends.py +++ b/tests/test_auth_backends.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import AnonymousUser from django.core.exceptions import SuspiciousOperation from django.http import HttpResponse -from django.test import RequestFactory, TestCase +from django.test import RequestFactory from django.test.utils import modify_settings, override_settings from django.utils.timezone import now, timedelta @@ -13,6 +13,8 @@ from oauth2_provider.middleware import OAuth2ExtraTokenMiddleware, OAuth2TokenMiddleware from oauth2_provider.models import get_access_token_model, get_application_model +from .common_testing import OAuth2ProviderTestCase as TestCase + UserModel = get_user_model() ApplicationModel = get_application_model() diff --git a/tests/test_authorization_code.py b/tests/test_authorization_code.py index b77f4f9ba..360fac957 100644 --- a/tests/test_authorization_code.py +++ b/tests/test_authorization_code.py @@ -7,7 +7,7 @@ import pytest from django.conf import settings from django.contrib.auth import get_user_model -from django.test import RequestFactory, TestCase +from django.test import RequestFactory from django.urls import reverse from django.utils import timezone from django.utils.crypto import get_random_string @@ -23,6 +23,7 @@ from oauth2_provider.views import ProtectedResourceView from . import presets +from .common_testing import OAuth2ProviderTestCase as TestCase from .utils import get_basic_auth_header @@ -504,7 +505,7 @@ def test_code_post_auth_failing_redirection_uri_with_querystring(self): """ Test that in case of error the querystring of the redirection uri is preserved - See https://github.com/jazzband/django-oauth-toolkit/issues/238 + See https://github.com/django-oauth/django-oauth-toolkit/issues/238 """ self.client.login(username="test_user", password="123456") @@ -985,6 +986,54 @@ def test_refresh_fail_repeating_requests(self): response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 400) + def test_refresh_repeating_requests_revokes_old_token(self): + """ + If a refresh token is reused, the server should invalidate *all* access tokens that have a relation + to the reused token. This forces a malicious actor to be logged out. + The server can't determine whether the first or the second client was legitimate, so it needs to + revoke both. + See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-29#name-recommendations + """ + self.oauth2_settings.REFRESH_TOKEN_REUSE_PROTECTION = True + self.client.login(username="test_user", password="123456") + authorization_code = self.get_auth() + + token_request_data = { + "grant_type": "authorization_code", + "code": authorization_code, + "redirect_uri": "http://example.org", + } + auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET) + + response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) + content = json.loads(response.content.decode("utf-8")) + self.assertTrue("refresh_token" in content) + + token_request_data = { + "grant_type": "refresh_token", + "refresh_token": content["refresh_token"], + "scope": content["scope"], + } + # First response works as usual + response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) + self.assertEqual(response.status_code, 200) + new_tokens = json.loads(response.content.decode("utf-8")) + + # Second request fails + response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) + self.assertEqual(response.status_code, 400) + + # Previously returned tokens are now invalid as well + new_token_request_data = { + "grant_type": "refresh_token", + "refresh_token": new_tokens["refresh_token"], + "scope": new_tokens["scope"], + } + response = self.client.post( + reverse("oauth2_provider:token"), data=new_token_request_data, **auth_headers + ) + self.assertEqual(response.status_code, 400) + def test_refresh_repeating_requests(self): """ Trying to refresh an access token with the same refresh token more than @@ -1024,6 +1073,63 @@ def test_refresh_repeating_requests(self): response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) self.assertEqual(response.status_code, 400) + def test_refresh_repeating_requests_grace_period_with_reuse_protection(self): + """ + Trying to refresh an access token with the same refresh token more than + once succeeds. Should work within the grace period, but should revoke previous tokens + """ + self.oauth2_settings.REFRESH_TOKEN_GRACE_PERIOD_SECONDS = 120 + self.oauth2_settings.REFRESH_TOKEN_REUSE_PROTECTION = True + self.client.login(username="test_user", password="123456") + authorization_code = self.get_auth() + + token_request_data = { + "grant_type": "authorization_code", + "code": authorization_code, + "redirect_uri": "http://example.org", + } + auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET) + + response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) + content = json.loads(response.content.decode("utf-8")) + self.assertTrue("refresh_token" in content) + + refresh_token_1 = content["refresh_token"] + token_request_data = { + "grant_type": "refresh_token", + "refresh_token": refresh_token_1, + "scope": content["scope"], + } + response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) + self.assertEqual(response.status_code, 200) + refresh_token_2 = json.loads(response.content.decode("utf-8"))["refresh_token"] + + response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) + self.assertEqual(response.status_code, 200) + refresh_token_3 = json.loads(response.content.decode("utf-8"))["refresh_token"] + + self.assertEqual(refresh_token_2, refresh_token_3) + + # Let the first refresh token expire + rt = RefreshToken.objects.get(token=refresh_token_1) + rt.revoked = timezone.now() - datetime.timedelta(minutes=10) + rt.save() + + # Using the expired token fails + response = self.client.post(reverse("oauth2_provider:token"), data=token_request_data, **auth_headers) + self.assertEqual(response.status_code, 400) + + # Because we used the expired token, the recently issued token is also revoked + new_token_request_data = { + "grant_type": "refresh_token", + "refresh_token": refresh_token_2, + "scope": content["scope"], + } + response = self.client.post( + reverse("oauth2_provider:token"), data=new_token_request_data, **auth_headers + ) + self.assertEqual(response.status_code, 400) + def test_refresh_repeating_requests_non_rotating_tokens(self): """ Try refreshing an access token with the same refresh token more than once when not rotating tokens. @@ -1761,7 +1867,7 @@ def test_id_token(self): # Check decoding JWT using HS256 key = self.application.jwk_key - assert key.key_type == "oct" + assert key.kty == "oct" jwt_token = jwt.JWT(key=key, jwt=content["id_token"]) claims = json.loads(jwt_token.claims) assert claims["sub"] == "1" diff --git a/tests/test_client_credential.py b/tests/test_client_credential.py index 4c6e384d0..3572f432d 100644 --- a/tests/test_client_credential.py +++ b/tests/test_client_credential.py @@ -4,7 +4,7 @@ import pytest from django.contrib.auth import get_user_model from django.core.exceptions import SuspiciousOperation -from django.test import RequestFactory, TestCase +from django.test import RequestFactory from django.urls import reverse from django.views.generic import View from oauthlib.oauth2 import BackendApplicationServer @@ -16,6 +16,7 @@ from oauth2_provider.views.mixins import OAuthLibMixin from . import presets +from .common_testing import OAuth2ProviderTestCase as TestCase from .utils import get_basic_auth_header diff --git a/tests/test_commands.py b/tests/test_commands.py index 8861f5698..c4d359ce5 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -5,11 +5,11 @@ from django.contrib.auth.hashers import check_password from django.core.management import call_command from django.core.management.base import CommandError -from django.test import TestCase from oauth2_provider.models import get_application_model from . import presets +from .common_testing import OAuth2ProviderTestCase as TestCase Application = get_application_model() @@ -130,6 +130,8 @@ def test_application_created_with_algorithm(self): self.assertEqual(app.algorithm, "RS256") def test_validation_failed_message(self): + import django + output = StringIO() call_command( "createapplication", @@ -140,6 +142,10 @@ def test_validation_failed_message(self): stdout=output, ) - self.assertIn("user", output.getvalue()) - self.assertIn("783", output.getvalue()) - self.assertIn("does not exist", output.getvalue()) + output_str = output.getvalue() + self.assertIn("user", output_str) + self.assertIn("783", output_str) + if django.VERSION < (5, 2): + self.assertIn("does not exist", output_str) + else: + self.assertIn("is not a valid choice", output_str) diff --git a/tests/test_decorators.py b/tests/test_decorators.py index a8ee788b5..f91ada2ac 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -1,12 +1,14 @@ from datetime import timedelta from django.contrib.auth import get_user_model -from django.test import RequestFactory, TestCase +from django.test import RequestFactory from django.utils import timezone from oauth2_provider.decorators import protected_resource, rw_protected_resource from oauth2_provider.models import get_access_token_model, get_application_model +from .common_testing import OAuth2ProviderTestCase as TestCase + Application = get_application_model() AccessToken = get_access_token_model() diff --git a/tests/test_device.py b/tests/test_device.py new file mode 100644 index 000000000..727c81002 --- /dev/null +++ b/tests/test_device.py @@ -0,0 +1,769 @@ +from datetime import datetime, timedelta, timezone +from unittest import mock +from urllib.parse import urlencode + +import django.http.response +import pytest +from django import http +from django.conf import settings +from django.contrib.auth import get_user_model +from django.test import RequestFactory +from django.urls import reverse + +import oauth2_provider.models +from oauth2_provider.models import ( + get_access_token_model, + get_application_model, + get_device_grant_model, + get_refresh_token_model, +) +from oauth2_provider.utils import set_oauthlib_user_to_device_request_user + +from . import presets +from .common_testing import OAuth2ProviderTestCase as TestCase + + +Application = get_application_model() +AccessToken = get_access_token_model() +RefreshToken = get_refresh_token_model() +UserModel = get_user_model() +DeviceModel: oauth2_provider.models.DeviceGrant = get_device_grant_model() + + +@pytest.mark.usefixtures("oauth2_settings") +@pytest.mark.oauth2_settings(presets.DEFAULT_SCOPES_RW) +class DeviceFlowBaseTestCase(TestCase): + factory = RequestFactory() + + @classmethod + def setUpTestData(cls): + cls.test_user = UserModel.objects.create_user("test_user", "test@example.com", "123456") + cls.dev_user = UserModel.objects.create_user("dev_user", "dev@example.com", "123456") + + cls.application = Application.objects.create( + name="test_client_credentials_app", + user=cls.dev_user, + client_type=Application.CLIENT_PUBLIC, + authorization_grant_type=Application.GRANT_DEVICE_CODE, + client_secret="abcdefghijklmnopqrstuvwxyz1234567890", + ) + + def tearDown(self): + DeviceModel.objects.all().delete() + return super().tearDown() + + +class TestDeviceFlow(DeviceFlowBaseTestCase): + """ + The first 2 tests test the device flow in order + how the device flow works + """ + + @mock.patch( + "oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token", + lambda: "abc", + ) + def test_device_flow_authorization_initiation(self): + """ + Tests the initial stage of the flow when the device sends its device authorization + request to the authorization server. + + Device Authorization Request(https://datatracker.ietf.org/doc/html/rfc8628#section-3.1) + + This request shape: + POST /device_authorization HTTP/1.1 + Host: server.example.com + Content-Type: application/x-www-form-urlencoded + + client_id=1406020730&scope=example_scope + + Should respond with this response shape: + Device Authorization Response (https://datatracker.ietf.org/doc/html/rfc8628#section-3.2) + { + "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS", + "user_code": "WDJB-MJHT", + "verification_uri": "https://example.com/device", + "expires_in": 1800, + "interval": 5 + } + """ + + self.oauth2_settings.OAUTH_DEVICE_VERIFICATION_URI = "example.com/device" + self.oauth2_settings.OAUTH_DEVICE_USER_CODE_GENERATOR = lambda: "xyz" + + request_data: dict[str, str] = { + "client_id": self.application.client_id, + } + request_as_x_www_form_urlencoded: str = urlencode(request_data) + + response: django.http.response.JsonResponse = self.client.post( + reverse("oauth2_provider:device-authorization"), + data=request_as_x_www_form_urlencoded, + content_type="application/x-www-form-urlencoded", + ) + + assert response.status_code == 200 + + # let's make sure the device was created in the db + assert DeviceModel.objects.get(device_code="abc").status == DeviceModel.AUTHORIZATION_PENDING + + assert response.json() == { + "verification_uri": "example.com/device", + "expires_in": 1800, + "user_code": "xyz", + "device_code": "abc", + "interval": 5, + } + + @mock.patch( + "oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token", + lambda: "abc", + ) + def test_device_flow_authorization_user_code_confirm_and_access_token(self): + """ + This is a full user journey test. + + The device initiates the flow by calling the /device-authorization endpoint and starts + polling the /authorize endpoint getting back error until the user approves in the + browser. + + In the meantime, the user visits the /device endpoint in their browsers to submit the + user code and approve, after which the /authorize returns the tokens to the device. + """ + + # ----------------------- + # 0: Setup device flow, where the device sends an authorization request and + # starts polling. The polling will fail because the user has not approved yet + # ----------------------- + self.oauth2_settings.OAUTH_DEVICE_VERIFICATION_URI = "example.com/device" + self.oauth2_settings.OAUTH_DEVICE_USER_CODE_GENERATOR = lambda: "xyz" + self.oauth2_settings.OAUTH_PRE_TOKEN_VALIDATION = [set_oauthlib_user_to_device_request_user] + + request_data: dict[str, str] = { + "client_id": self.application.client_id, + } + request_as_x_www_form_urlencoded: str = urlencode(request_data) + + device_authorization_response: http.response.JsonResponse = self.client.post( + reverse("oauth2_provider:device-authorization"), + data=request_as_x_www_form_urlencoded, + content_type="application/x-www-form-urlencoded", + ) + + assert device_authorization_response.__getitem__("content-type") == "application/json" + device = DeviceModel.objects.get(device_code="abc") + self.assertJSONEqual( + raw=device_authorization_response.content, + expected_data={ + "verification_uri": "example.com/device", + "expires_in": 1800, + "user_code": device.user_code, + "device_code": device.device_code, + "interval": 5, + }, + ) + + # Device polls /token and gets back error because the user hasn't approved yet + token_payload = { + "device_code": device.device_code, + "client_id": self.application.client_id, + "grant_type": "urn:ietf:params:oauth:grant-type:device_code", + } + + token_response: http.response.JsonResponse = self.client.post( + "/o/token/", + data=urlencode(token_payload), + content_type="application/x-www-form-urlencoded", + ) + # TokenView should always respond with application/json as it's meant to be + # consumed by devices. + assert token_response.__getitem__("content-type") == "application/json" + assert token_response.status_code == 400 + self.assertJSONEqual(raw=token_response.content, expected_data={"error": "authorization_pending"}) + + # /device and /device_confirm require a user to be logged in + # to access it + UserModel.objects.create_user( + username="test_user_device_flow", + email="test_device@example.com", + password="password123", + ) + self.client.login(username="test_user_device_flow", password="password123") + + # -------------------------------------------------------------------------------- + # 1. User visits the /device endpoint in their browsers and submits the user code + # submits wrong code then right code + # -------------------------------------------------------------------------------- + + # 1. User visits the /device endpoint in their browsers and submits the user code + # (GET Request to load it) + get_response = self.client.get(reverse("oauth2_provider:device")) + assert get_response.status_code == 200 + assert "form" in get_response.context # Ensure the form is rendered in the context + + # 1.1.0 User visits the /device endpoint in their browsers and submits wrong user code + self.assertContains( + self.client.post(reverse("oauth2_provider:device"), data={"user_code": "invalid_code"}), + status_code=200, + text="Incorrect user code", + count=1, + ) + + # Note: the device not being in the expected test covered in the other tests + + # 1.1.1: user submits valid user code + device_confirm_url = reverse( + "oauth2_provider:device-confirm", + kwargs={"user_code": "xyz", "client_id": self.application.client_id}, + ) + + self.assertRedirects( + response=self.client.post( + reverse("oauth2_provider:device"), + data={"user_code": "xyz"}, + ), + expected_url=device_confirm_url, + ) + + # -------------------------------------------------------------------------------- + # 2: We redirect to the accept/deny form (the user is still in their browser) + # and approves + # -------------------------------------------------------------------------------- + device_grant_status_url = reverse( + "oauth2_provider:device-grant-status", + kwargs={"user_code": "xyz", "client_id": self.application.client_id}, + ) + + self.assertRedirects( + response=self.client.post(device_confirm_url, data={"action": "accept"}), + expected_url=device_grant_status_url, + ) + + # -------------------------------------------------------------------------------- + # 3: We redirect to the device grant status page (the user is still in their browser) + # -------------------------------------------------------------------------------- + self.assertContains( + response=self.client.get(device_grant_status_url), + text="Device Authorized", + count=1, + ) + + device = DeviceModel.objects.get(device_code="abc") + assert device.status == device.AUTHORIZED + + # ------------------------- + # 4: Device polls /token successfully + # ------------------------- + token_payload = { + "device_code": device.device_code, + "client_id": self.application.client_id, + "grant_type": "urn:ietf:params:oauth:grant-type:device_code", + } + + token_response = self.client.post( + "/o/token/", + data=urlencode(token_payload), + content_type="application/x-www-form-urlencoded", + ) + # TokenView should always respond with application/json as it's meant to be + # consumed by devices. + assert token_response.__getitem__("content-type") == "application/json" + assert token_response.status_code == 200 + + token_data = token_response.json() + assert token_data == { + "access_token": mock.ANY, + "expires_in": 36000, + "token_type": "Bearer", + "scope": "read write", + "refresh_token": mock.ANY, + } + + # ensure the access token and refresh token have the same user as the device that just authenticated + access_token: oauth2_provider.models.AccessToken = AccessToken.objects.get( + token=token_data["access_token"] + ) + assert access_token.user == device.user + + refresh_token: oauth2_provider.models.RefreshToken = RefreshToken.objects.get( + token=token_data["refresh_token"] + ) + assert refresh_token.user == device.user + + def test_user_denies_access(self): + """ + This test asserts the when the user denies access, the state of the grant is saved + and the user is redirected to the page where they can see the "denied" state. + + The /token View returning the appropriate message for the "denied" state is covered + in test_token_view_returns_error_if_device_in_invalid_state. + """ + UserModel.objects.create_user( + username="test_user_device_flow", + email="test_device@example.com", + password="password123", + ) + self.client.login(username="test_user_device_flow", password="password123") + + device = DeviceModel( + client_id="client_id", + device_code="device_code", + user_code="user_code", + scope="scope", + expires=datetime.now() + timedelta(days=1), + status=DeviceModel.AUTHORIZATION_PENDING, + ) + device.save() + + device_confirm_url = reverse( + "oauth2_provider:device-confirm", + kwargs={"user_code": "user_code", "client_id": "client_id"}, + ) + + device_grant_status_url = reverse( + "oauth2_provider:device-grant-status", + kwargs={"user_code": "user_code", "client_id": "client_id"}, + ) + + self.assertRedirects( + response=self.client.post(device_confirm_url, data={"action": "deny"}), + expected_url=device_grant_status_url, + ) + + device.refresh_from_db() + assert device.status == device.DENIED + + def test_device_confirm_view_returns_400_on_incorrect_action(self): + """ + This test asserts that the confirm view returns 400 if action is not + "accept" or "deny". + """ + UserModel.objects.create_user( + username="test_user_device_flow", + email="test_device@example.com", + password="password123", + ) + self.client.login(username="test_user_device_flow", password="password123") + + device = DeviceModel( + client_id="client_id", + device_code="device_code", + user_code="user_code", + scope="scope", + expires=datetime.now() + timedelta(days=1), + status=DeviceModel.AUTHORIZATION_PENDING, + ) + device.save() + + device_confirm_url = reverse( + "oauth2_provider:device-confirm", + kwargs={"user_code": "user_code", "client_id": "client_id"}, + ) + response = self.client.post(device_confirm_url, data={"action": "inccorect_action"}) + + assert response.status_code == 400 + + def test_device_flow_authorization_device_invalid_state_returns_form_error(self): + """ + This test asserts that only devices in the expected state (authorization-pending) + can be approved/denied by the user. + """ + + UserModel.objects.create_user( + username="test_user_device_flow", + email="test_device@example.com", + password="password123", + ) + self.client.login(username="test_user_device_flow", password="password123") + + device = DeviceModel( + client_id="client_id", + device_code="device_code", + user_code="user_code", + scope="scope", + expires=datetime.now() + timedelta(days=1), + ) + device.save() + + # This simulates pytest.mark.parameterize, which unfortunately does not work with unittest + # and consequently with Django TestCase. + for invalid_state in ["authorized", "denied", "LOL_status"]: + # Set the device into an incorrect state. + device.status = invalid_state + device.save(update_fields=["status"]) + + self.assertContains( + response=self.client.post( + reverse("oauth2_provider:device"), + data={"user_code": "user_code"}, + ), + status_code=200, + text="User code has already been used", + count=1, + ) + + def test_device_flow_authorization_device_expired_returns_form_error(self): + """ + This test asserts that only devices in the expected state (authorization-pending) + can be approved/denied by the user. + """ + + UserModel.objects.create_user( + username="test_user_device_flow", + email="test_device@example.com", + password="password123", + ) + self.client.login(username="test_user_device_flow", password="password123") + + device = DeviceModel( + client_id="client_id", + device_code="device_code", + user_code="user_code", + scope="scope", + expires=datetime.now() + timedelta(seconds=-1), # <- essentially expired + ) + device.save() + + self.assertContains( + response=self.client.post( + reverse("oauth2_provider:device"), + data={"user_code": "user_code"}, + ), + status_code=200, + text="Expired user code", + count=1, + ) + + def test_token_view_returns_error_if_device_in_invalid_state(self): + """ + This test asserts that the token view returns the appropriate errors as specified + in https://datatracker.ietf.org/doc/html/rfc8628#section-3.5, in case the device + has not yet been approved by the user. + """ + + device = DeviceModel( + client_id="client_id", + device_code="device_code", + user_code="user_code", + scope="scope", + expires=datetime.now() + timedelta(seconds=60), + ) + device.save() + + token_payload = { + "device_code": "device_code", + "client_id": "client_id", + "grant_type": "urn:ietf:params:oauth:grant-type:device_code", + } + + testcases = [ + ("authorization-pending", '{"error": "authorization_pending"}', 400), + ("expired", '{"error": "expired_token"}', 400), + ("denied", '{"error": "access_denied"}', 400), + ("LOL_status", '{"error": "internal_error"}', 500), + ] + for invalid_state, expected_error_message, expected_error_code in testcases: + device.status = invalid_state + device.save(update_fields=["status"]) + + response = self.client.post( + "/o/token/", + data=urlencode(token_payload), + content_type="application/x-www-form-urlencoded", + ) + self.assertContains( + response=response, + status_code=expected_error_code, + text=expected_error_message, + count=1, + ) + # TokenView should always respond with application/json as it's meant to be + # consumed by devices. + self.assertEqual(response.__getitem__("content-type"), "application/json") + + def test_token_view_returns_404_error_if_device_not_found(self): + device = DeviceModel( + client_id="client_id", + device_code="device_code", + user_code="user_code", + scope="scope", + expires=datetime.now() + timedelta(seconds=60), + ) + device.save() + + token_payload = { + "device_code": "another_device_code", + "client_id": "client_id", + "grant_type": "urn:ietf:params:oauth:grant-type:device_code", + } + + response = self.client.post( + "/o/token/", + data=urlencode(token_payload), + content_type="application/x-www-form-urlencoded", + ) + self.assertContains( + response=response, + status_code=404, + text="device_not_found", + count=1, + ) + # TokenView should always respond with application/json as it's meant to be + # consumed by devices. + self.assertEqual(response.__getitem__("content-type"), "application/json") + + def test_token_view_status_equals_what_oauthlib_token_response_method_returns(self): + """ + Tests the use case where oauthlib create_token_response returns a status different + than 200. + """ + + class MockOauthlibCoreClass: + def create_token_response(self, _): + return "url", {"headers_are_ignored": True}, '{"Key": "Value"}', 299 + + device = DeviceModel( + client_id="client_id", + device_code="device_code", + user_code="user_code", + scope="scope", + expires=datetime.now() + timedelta(seconds=60), + status="authorized", + ) + device.save() + + token_payload = { + "device_code": "device_code", + "client_id": "client_id", + "grant_type": "urn:ietf:params:oauth:grant-type:device_code", + } + + with mock.patch( + "oauth2_provider.views.mixins.OAuthLibMixin.get_oauthlib_core", MockOauthlibCoreClass + ): + response = self.client.post( + "/o/token/", + data=urlencode(token_payload), + content_type="application/x-www-form-urlencoded", + ) + + self.assertEqual(response["content-type"], "application/json") + self.assertContains( + response=response, + status_code=299, + text='{"Key": "Value"}', + count=1, + ) + assert not response.has_header("headers_are_ignored") + + @mock.patch( + "oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token", + lambda: "abc", + ) + def test_device_polling_interval_can_be_changed(self): + """ + Tests the device polling rate(interval) can be changed to something other than the default + of 5 seconds. + """ + + self.oauth2_settings.OAUTH_DEVICE_VERIFICATION_URI = "example.com/device" + self.oauth2_settings.OAUTH_DEVICE_USER_CODE_GENERATOR = lambda: "xyz" + + self.oauth2_settings.DEVICE_FLOW_INTERVAL = 10 + + request_data: dict[str, str] = { + "client_id": self.application.client_id, + } + request_as_x_www_form_urlencoded: str = urlencode(request_data) + + response: django.http.response.JsonResponse = self.client.post( + reverse("oauth2_provider:device-authorization"), + data=request_as_x_www_form_urlencoded, + content_type="application/x-www-form-urlencoded", + ) + + assert response.status_code == 200 + + assert response.json() == { + "verification_uri": "example.com/device", + "expires_in": 1800, + "user_code": "xyz", + "device_code": "abc", + "interval": 10, + } + + def test_incorrect_client_id_sent(self): + """ + Ensure the correct error is returned when an invalid client is sent + """ + request_data: dict[str, str] = { + "client_id": "client_id_that_does_not_exist", + } + request_as_x_www_form_urlencoded: str = urlencode(request_data) + + response: django.http.response.JsonResponse = self.client.post( + reverse("oauth2_provider:device-authorization"), + data=request_as_x_www_form_urlencoded, + content_type="application/x-www-form-urlencoded", + ) + + assert response.status_code == 400 + + assert response.json() == { + "error": "invalid_request", + "error_description": "Invalid client_id parameter value.", + } + + def test_missing_client_id(self): + """ + Ensure the correct error is returned when the client id is missing. + """ + request_data: dict[str, str] = { + "not_client_id": "client_id_that_does_not_exist", + } + request_as_x_www_form_urlencoded: str = urlencode(request_data) + + response: django.http.response.JsonResponse = self.client.post( + reverse("oauth2_provider:device-authorization"), + data=request_as_x_www_form_urlencoded, + content_type="application/x-www-form-urlencoded", + ) + + assert response.status_code == 400 + + assert response.json() == { + "error": "invalid_request", + "error_description": "Missing client_id parameter.", + } + + def test_device_confirm_and_user_code_views_require_login(self): + URLs = [ + reverse("oauth2_provider:device-confirm", kwargs={"user_code": None, "client_id": "abc"}), + reverse("oauth2_provider:device-confirm", kwargs={"user_code": "abc", "client_id": "abc"}), + reverse("oauth2_provider:device"), + ] + + for url in URLs: + r = self.client.get(url) + assert r.status_code == 302 + assert r["Location"] == f"{settings.LOGIN_URL}?next={url}" + + r = self.client.post(url) + assert r.status_code == 302 + assert r["Location"] == f"{settings.LOGIN_URL}?next={url}" + + def test_device_confirm_view_GET_returns_404_when_device_does_not_exist(self): + UserModel.objects.create_user( + username="test_user_device_flow", + email="test_device@example.com", + password="password123", + ) + self.client.login(username="test_user_device_flow", password="password123") + + device = DeviceModel( + client_id="client_id", + device_code="device_code", + user_code="user_code", + scope="scope", + expires=datetime.now(), + ) + device.save() + + self.assertContains( + response=self.client.get( + reverse( + "oauth2_provider:device-confirm", + kwargs={"user_code": "not_user_code", "client_id": "not_client_id"}, + ) + ), + status_code=404, + text="The requested resource was not found on this server.", + ) + + # Asserts for valid user_code and client_id but invalid states + for invalid_state in ["authorized", "denied", "expired"]: + device.status = invalid_state + device.save(update_fields=["status"]) + + self.assertContains( + response=self.client.get( + reverse( + "oauth2_provider:device-confirm", + kwargs={"user_code": "not_user_code", "client_id": "client_id"}, + ) + ), + status_code=404, + text="The requested resource was not found on this server.", + ) + + def test_device_confirm_view_POST_returns_404_when_device_does_not_exist(self): + UserModel.objects.create_user( + username="test_user_device_flow", + email="test_device@example.com", + password="password123", + ) + self.client.login(username="test_user_device_flow", password="password123") + + device = DeviceModel( + client_id="client_id", + device_code="device_code", + user_code="user_code", + scope="scope", + expires=datetime.now(), + ) + device.save() + + self.assertContains( + response=self.client.post( + reverse( + "oauth2_provider:device-confirm", + kwargs={"user_code": "not_user_code", "client_id": "client_id"}, + ), + data={"action": "accept"}, + ), + status_code=404, + text="The requested resource was not found on this server.", + count=1, + ) + + # Asserts for valid user_code and client_id but invalid states + for invalid_state in ["authorized", "denied", "expired"]: + device.status = invalid_state + device.save(update_fields=["status"]) + + self.assertContains( + response=self.client.post( + reverse( + "oauth2_provider:device-confirm", + kwargs={"user_code": "user_code", "client_id": "client_id"}, + ), + data={"action": "accept"}, + ), + status_code=404, + text="The requested resource was not found on this server.", + count=1, + ) + + def test_device_is_expired_method_sets_status_to_expired_if_deadline_passed(self): + device = DeviceModel( + client_id="client_id", + device_code="device_code", + user_code="user_code", + scope="scope", + expires=datetime.now(tz=timezone.utc) + timedelta(seconds=-1), # <- essentially expired + ) + device.save() + + assert device.status == device.AUTHORIZATION_PENDING # default value + + # call is_expired() which should update the state + is_expired = device.is_expired() + + assert is_expired + assert device.status == device.EXPIRED + + # calling again is_expired() should return true and not change the state + is_expired = device.is_expired() + + assert is_expired + assert device.status == device.EXPIRED diff --git a/tests/test_django_checks.py b/tests/test_django_checks.py new file mode 100644 index 000000000..77025b115 --- /dev/null +++ b/tests/test_django_checks.py @@ -0,0 +1,20 @@ +from django.core.management import call_command +from django.core.management.base import SystemCheckError +from django.test import override_settings + +from .common_testing import OAuth2ProviderTestCase as TestCase + + +class DjangoChecksTestCase(TestCase): + def test_checks_pass(self): + call_command("check") + + # CrossDatabaseRouter claims AccessToken is in beta while everything else is in alpha. + # This will cause the database checks to fail. + @override_settings( + DATABASE_ROUTERS=["tests.db_router.CrossDatabaseRouter", "tests.db_router.AlphaRouter"] + ) + def test_checks_fail_when_router_crosses_databases(self): + message = "The token models are expected to be stored in the same database." + with self.assertRaisesMessage(SystemCheckError, message): + call_command("check") diff --git a/tests/test_generator.py b/tests/test_generator.py index cc7928017..201200b00 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -1,8 +1,9 @@ import pytest -from django.test import TestCase from oauth2_provider.generators import BaseHashGenerator, generate_client_id, generate_client_secret +from .common_testing import OAuth2ProviderTestCase as TestCase + class MockHashGenerator(BaseHashGenerator): def hash(self): diff --git a/tests/test_hybrid.py b/tests/test_hybrid.py index 40cd8c56f..67c29a54e 100644 --- a/tests/test_hybrid.py +++ b/tests/test_hybrid.py @@ -5,7 +5,7 @@ import pytest from django.contrib.auth import get_user_model -from django.test import RequestFactory, TestCase +from django.test import RequestFactory from django.urls import reverse from django.utils import timezone from jwcrypto import jwt @@ -21,6 +21,8 @@ from oauth2_provider.views import ProtectedResourceView, ScopedProtectedResourceView from . import presets +from .common_testing import OAuth2ProviderTestCase as TestCase +from .common_testing import retrieve_current_databases from .utils import get_basic_auth_header, spy_on @@ -1318,7 +1320,7 @@ def test_pre_auth_default_scopes(self): self.assertEqual(form["client_id"].value(), self.application.client_id) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) def test_id_token_nonce_in_token_response(oauth2_settings, test_user, hybrid_application, client, oidc_key): client.force_login(test_user) @@ -1367,7 +1369,7 @@ def test_id_token_nonce_in_token_response(oauth2_settings, test_user, hybrid_app assert claims["nonce"] == "random_nonce_string" -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) def test_claims_passed_to_code_generation( oauth2_settings, test_user, hybrid_application, client, mocker, oidc_key diff --git a/tests/test_implicit.py b/tests/test_implicit.py index 3f16cf71f..85e773d22 100644 --- a/tests/test_implicit.py +++ b/tests/test_implicit.py @@ -3,7 +3,7 @@ import pytest from django.contrib.auth import get_user_model -from django.test import RequestFactory, TestCase +from django.test import RequestFactory from django.urls import reverse from jwcrypto import jwt @@ -11,6 +11,7 @@ from oauth2_provider.views import ProtectedResourceView from . import presets +from .common_testing import OAuth2ProviderTestCase as TestCase Application = get_application_model() diff --git a/tests/test_introspection_auth.py b/tests/test_introspection_auth.py index 100ef064e..e1a096428 100644 --- a/tests/test_introspection_auth.py +++ b/tests/test_introspection_auth.py @@ -6,17 +6,19 @@ from django.conf.urls import include from django.contrib.auth import get_user_model from django.http import HttpResponse -from django.test import TestCase, override_settings +from django.test import override_settings from django.urls import path from django.utils import timezone from oauthlib.common import Request +from oauth2_provider.compat import login_not_required from oauth2_provider.models import get_access_token_model, get_application_model from oauth2_provider.oauth2_validators import OAuth2Validator from oauth2_provider.settings import oauth2_settings from oauth2_provider.views import ScopedProtectedResourceView from . import presets +from .common_testing import OAuth2ProviderTestCase as TestCase try: @@ -93,7 +95,7 @@ def mocked_introspect_request_short_living_token(url, data, *args, **kwargs): urlpatterns = [ path("oauth2/", include("oauth2_provider.urls")), - path("oauth2-test-resource/", ScopeResourceView.as_view()), + path("oauth2-test-resource/", login_not_required(ScopeResourceView.as_view())), ] diff --git a/tests/test_introspection_view.py b/tests/test_introspection_view.py index b82e922be..ad7d8983d 100644 --- a/tests/test_introspection_view.py +++ b/tests/test_introspection_view.py @@ -3,13 +3,14 @@ import pytest from django.contrib.auth import get_user_model -from django.test import TestCase +from django.db import router from django.urls import reverse from django.utils import timezone from oauth2_provider.models import get_access_token_model, get_application_model from . import presets +from .common_testing import OAuth2ProviderTestCase as TestCase from .utils import get_basic_auth_header @@ -278,6 +279,20 @@ def test_view_post_notexisting_token(self): }, ) + def test_view_post_no_token(self): + """ + Test that when you pass no token HTTP 400 is returned + """ + auth_headers = { + "HTTP_AUTHORIZATION": "Bearer " + self.resource_server_token.token, + } + response = self.client.post(reverse("oauth2_provider:introspect"), **auth_headers) + + self.assertEqual(response.status_code, 400) + content = response.json() + self.assertIsInstance(content, dict) + self.assertEqual(content["error"], "invalid_request") + def test_view_post_valid_client_creds_basic_auth(self): """Test HTTP basic auth working""" auth_headers = get_basic_auth_header(self.application.client_id, CLEARTEXT_SECRET) @@ -343,5 +358,6 @@ def test_view_post_invalid_client_creds_plaintext(self): self.assertEqual(response.status_code, 403) def test_select_related_in_view_for_less_db_queries(self): - with self.assertNumQueries(1): + token_database = router.db_for_write(AccessToken) + with self.assertNumQueries(1, using=token_database): self.client.post(reverse("oauth2_provider:introspect")) diff --git a/tests/test_mixins.py b/tests/test_mixins.py index 327a99194..1cefa1334 100644 --- a/tests/test_mixins.py +++ b/tests/test_mixins.py @@ -3,7 +3,7 @@ import pytest from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponse -from django.test import RequestFactory, TestCase +from django.test import RequestFactory from django.views.generic import View from oauthlib.oauth2 import Server @@ -18,6 +18,7 @@ ) from . import presets +from .common_testing import OAuth2ProviderTestCase as TestCase @pytest.mark.usefixtures("oauth2_settings") diff --git a/tests/test_models.py b/tests/test_models.py index 586bef124..8c0048066 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,10 +1,11 @@ +import hashlib +import secrets from datetime import timedelta import pytest from django.contrib.auth import get_user_model from django.contrib.auth.hashers import check_password from django.core.exceptions import ImproperlyConfigured, ValidationError -from django.test import TestCase from django.test.utils import override_settings from django.utils import timezone @@ -15,9 +16,12 @@ get_grant_model, get_id_token_model, get_refresh_token_model, + redirect_to_uri_allowed, ) from . import presets +from .common_testing import OAuth2ProviderTestCase as TestCase +from .common_testing import retrieve_current_databases CLEARTEXT_SECRET = "1234567890abcdefghijklmnopqrstuvwxyz" @@ -69,6 +73,22 @@ def test_hashed_secret(self): self.assertNotEqual(app.client_secret, CLEARTEXT_SECRET) self.assertTrue(check_password(CLEARTEXT_SECRET, app.client_secret)) + @override_settings(OAUTH2_PROVIDER={"CLIENT_SECRET_HASHER": "fast_pbkdf2"}) + def test_hashed_from_settings(self): + app = Application.objects.create( + name="test_app", + redirect_uris="http://localhost http://example.com http://example.org", + user=self.user, + client_type=Application.CLIENT_CONFIDENTIAL, + authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, + client_secret=CLEARTEXT_SECRET, + hash_client_secret=True, + ) + + self.assertNotEqual(app.client_secret, CLEARTEXT_SECRET) + self.assertIn("fast_pbkdf2", app.client_secret) + self.assertTrue(check_password(CLEARTEXT_SECRET, app.client_secret)) + def test_unhashed_secret(self): app = Application.objects.create( name="test_app", @@ -148,7 +168,7 @@ def test_custom_application_model(self): If a custom application model is installed, it should be present in the related objects and not the swapped out one. - See issue #90 (https://github.com/jazzband/django-oauth-toolkit/issues/90) + See issue #90 (https://github.com/django-oauth/django-oauth-toolkit/issues/90) """ related_object_names = [ f.name @@ -310,6 +330,17 @@ def test_expires_can_be_none(self): self.assertIsNone(access_token.expires) self.assertTrue(access_token.is_expired()) + def test_token_checksum_field(self): + token = secrets.token_urlsafe(32) + access_token = AccessToken.objects.create( + user=self.user, + token=token, + expires=timezone.now() + timedelta(hours=1), + ) + expected_checksum = hashlib.sha256(token.encode()).hexdigest() + + self.assertEqual(access_token.token_checksum, expected_checksum) + class TestRefreshTokenModel(BaseTestModels): def test_str(self): @@ -402,25 +433,25 @@ def test_clear_expired_tokens_with_tokens(self): initial_at_count = AccessToken.objects.count() assert initial_at_count == 2 * self.num_tokens, f"{2 * self.num_tokens} access tokens should exist." initial_expired_at_count = AccessToken.objects.filter(expires__lte=self.now).count() - assert ( - initial_expired_at_count == self.num_tokens - ), f"{self.num_tokens} expired access tokens should exist." + assert initial_expired_at_count == self.num_tokens, ( + f"{self.num_tokens} expired access tokens should exist." + ) initial_current_at_count = AccessToken.objects.filter(expires__gt=self.now).count() - assert ( - initial_current_at_count == self.num_tokens - ), f"{self.num_tokens} current access tokens should exist." + assert initial_current_at_count == self.num_tokens, ( + f"{self.num_tokens} current access tokens should exist." + ) initial_rt_count = RefreshToken.objects.count() - assert ( - initial_rt_count == self.num_tokens // 2 - ), f"{self.num_tokens // 2} refresh tokens should exist." + assert initial_rt_count == self.num_tokens // 2, ( + f"{self.num_tokens // 2} refresh tokens should exist." + ) initial_rt_expired_at_count = RefreshToken.objects.filter(access_token__expires__lte=self.now).count() - assert ( - initial_rt_expired_at_count == initial_rt_count / 2 - ), "half the refresh tokens should be for expired access tokens." + assert initial_rt_expired_at_count == initial_rt_count / 2, ( + "half the refresh tokens should be for expired access tokens." + ) initial_rt_current_at_count = RefreshToken.objects.filter(access_token__expires__gt=self.now).count() - assert ( - initial_rt_current_at_count == initial_rt_count / 2 - ), "half the refresh tokens should be for current access tokens." + assert initial_rt_current_at_count == initial_rt_count / 2, ( + "half the refresh tokens should be for current access tokens." + ) initial_gt_count = Grant.objects.count() assert initial_gt_count == self.num_tokens * 2, f"{self.num_tokens * 2} grants should exist." @@ -428,15 +459,15 @@ def test_clear_expired_tokens_with_tokens(self): # after clear_expired(): remaining_at_count = AccessToken.objects.count() - assert ( - remaining_at_count == initial_at_count // 2 - ), "half the initial access tokens should still exist." + assert remaining_at_count == initial_at_count // 2, ( + "half the initial access tokens should still exist." + ) remaining_expired_at_count = AccessToken.objects.filter(expires__lte=self.now).count() assert remaining_expired_at_count == 0, "no remaining expired access tokens should still exist." remaining_current_at_count = AccessToken.objects.filter(expires__gt=self.now).count() - assert ( - remaining_current_at_count == initial_current_at_count - ), "all current access tokens should still exist." + assert remaining_current_at_count == initial_current_at_count, ( + "all current access tokens should still exist." + ) remaining_rt_count = RefreshToken.objects.count() assert remaining_rt_count == initial_rt_count // 2, "half the refresh tokens should still exist." remaining_rt_expired_at_count = RefreshToken.objects.filter( @@ -446,14 +477,14 @@ def test_clear_expired_tokens_with_tokens(self): remaining_rt_current_at_count = RefreshToken.objects.filter( access_token__expires__gt=self.now ).count() - assert ( - remaining_rt_current_at_count == initial_rt_current_at_count - ), "all the refresh tokens for current access tokens should still exist." + assert remaining_rt_current_at_count == initial_rt_current_at_count, ( + "all the refresh tokens for current access tokens should still exist." + ) remaining_gt_count = Grant.objects.count() assert remaining_gt_count == initial_gt_count // 2, "half the remaining grants should still exist." -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) def test_id_token_methods(oidc_tokens, rf): id_token = IDToken.objects.get() @@ -488,7 +519,7 @@ def test_id_token_methods(oidc_tokens, rf): assert IDToken.objects.filter(jti=id_token.jti).count() == 0 -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) def test_clear_expired_id_tokens(oauth2_settings, oidc_tokens, rf): id_token = IDToken.objects.get() @@ -527,12 +558,12 @@ def test_clear_expired_id_tokens(oauth2_settings, oidc_tokens, rf): assert not IDToken.objects.filter(jti=id_token.jti).exists() -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) def test_application_key(oauth2_settings, application): # RS256 key key = application.jwk_key - assert key.key_type == "RSA" + assert key.kty == "RSA" # RS256 key, but not configured oauth2_settings.OIDC_RSA_PRIVATE_KEY = None @@ -543,7 +574,7 @@ def test_application_key(oauth2_settings, application): # HS256 key application.algorithm = Application.HS256_ALGORITHM key = application.jwk_key - assert key.key_type == "oct" + assert key.kty == "oct" # No algorithm application.algorithm = Application.NO_ALGORITHM @@ -552,7 +583,7 @@ def test_application_key(oauth2_settings, application): assert "This application does not support signed tokens" == str(exc.value) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) def test_application_clean(oauth2_settings, application): # RS256, RSA key is configured @@ -592,7 +623,80 @@ def test_application_clean(oauth2_settings, application): application.clean() -@pytest.mark.django_db +def _test_wildcard_redirect_uris_valid(oauth2_settings, application, uris): + oauth2_settings.ALLOW_URI_WILDCARDS = True + application.redirect_uris = uris + application.clean() + + +def _test_wildcard_redirect_uris_invalid(oauth2_settings, application, uris): + oauth2_settings.ALLOW_URI_WILDCARDS = True + application.redirect_uris = uris + with pytest.raises(ValidationError): + application.clean() + + +@pytest.mark.django_db(databases=retrieve_current_databases()) +@pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) +def test_application_clean_wildcard_redirect_uris_valid_3ld(oauth2_settings, application): + _test_wildcard_redirect_uris_valid(oauth2_settings, application, "https://*.example.com/path") + + +@pytest.mark.django_db(databases=retrieve_current_databases()) +@pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) +def test_application_clean_wildcard_redirect_uris_valid_partial_3ld(oauth2_settings, application): + _test_wildcard_redirect_uris_valid(oauth2_settings, application, "https://*-partial.example.com/path") + + +@pytest.mark.django_db(databases=retrieve_current_databases()) +@pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) +def test_application_clean_wildcard_redirect_uris_invalid_3ld_not_starting_with_wildcard( + oauth2_settings, application +): + _test_wildcard_redirect_uris_invalid(oauth2_settings, application, "https://invalid-*.example.com/path") + + +@pytest.mark.django_db(databases=retrieve_current_databases()) +@pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) +def test_application_clean_wildcard_redirect_uris_invalid_2ld(oauth2_settings, application): + _test_wildcard_redirect_uris_invalid(oauth2_settings, application, "https://*.com/path") + + +@pytest.mark.django_db(databases=retrieve_current_databases()) +@pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) +def test_application_clean_wildcard_redirect_uris_invalid_partial_2ld(oauth2_settings, application): + _test_wildcard_redirect_uris_invalid(oauth2_settings, application, "https://*-partial.com/path") + + +@pytest.mark.django_db(databases=retrieve_current_databases()) +@pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) +def test_application_clean_wildcard_redirect_uris_invalid_2ld_not_starting_with_wildcard( + oauth2_settings, application +): + _test_wildcard_redirect_uris_invalid(oauth2_settings, application, "https://invalid-*.com/path") + + +@pytest.mark.django_db(databases=retrieve_current_databases()) +@pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) +def test_application_clean_wildcard_redirect_uris_invalid_tld(oauth2_settings, application): + _test_wildcard_redirect_uris_invalid(oauth2_settings, application, "https://*/path") + + +@pytest.mark.django_db(databases=retrieve_current_databases()) +@pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) +def test_application_clean_wildcard_redirect_uris_invalid_tld_partial(oauth2_settings, application): + _test_wildcard_redirect_uris_invalid(oauth2_settings, application, "https://*-partial/path") + + +@pytest.mark.django_db(databases=retrieve_current_databases()) +@pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) +def test_application_clean_wildcard_redirect_uris_invalid_tld_not_starting_with_wildcard( + oauth2_settings, application +): + _test_wildcard_redirect_uris_invalid(oauth2_settings, application, "https://invalid-*/path") + + +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.ALLOWED_SCHEMES_DEFAULT) def test_application_origin_allowed_default_https(oauth2_settings, cors_application): """Test that http schemes are not allowed because ALLOWED_SCHEMES allows only https""" @@ -600,9 +704,41 @@ def test_application_origin_allowed_default_https(oauth2_settings, cors_applicat assert not cors_application.origin_allowed("http://example.com") -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.ALLOWED_SCHEMES_HTTP) def test_application_origin_allowed_http(oauth2_settings, cors_application): """Test that http schemes are allowed because http was added to ALLOWED_SCHEMES""" assert cors_application.origin_allowed("https://example.com") assert cors_application.origin_allowed("http://example.com") + + +def test_redirect_to_uri_allowed_expects_allowed_uri_list(): + with pytest.raises(ValueError): + redirect_to_uri_allowed("https://example.com", "https://example.com") + assert redirect_to_uri_allowed("https://example.com", ["https://example.com"]) + + +valid_wildcard_redirect_to_params = [ + ("https://valid.example.com", ["https://*.example.com"]), + ("https://valid.valid.example.com", ["https://*.example.com"]), + ("https://valid-partial.example.com", ["https://*-partial.example.com"]), + ("https://valid.valid-partial.example.com", ["https://*-partial.example.com"]), +] + + +@pytest.mark.parametrize("uri, allowed_uri", valid_wildcard_redirect_to_params) +def test_wildcard_redirect_to_uri_allowed_valid(uri, allowed_uri, oauth2_settings): + oauth2_settings.ALLOW_URI_WILDCARDS = True + assert redirect_to_uri_allowed(uri, allowed_uri) + + +invalid_wildcard_redirect_to_params = [ + ("https://invalid.com", ["https://*.example.com"]), + ("https://invalid.example.com", ["https://*-partial.example.com"]), +] + + +@pytest.mark.parametrize("uri, allowed_uri", invalid_wildcard_redirect_to_params) +def test_wildcard_redirect_to_uri_allowed_invalid(uri, allowed_uri, oauth2_settings): + oauth2_settings.ALLOW_URI_WILDCARDS = True + assert not redirect_to_uri_allowed(uri, allowed_uri) diff --git a/tests/test_oauth2_backends.py b/tests/test_oauth2_backends.py index 21dd7a0c3..a4408f8e6 100644 --- a/tests/test_oauth2_backends.py +++ b/tests/test_oauth2_backends.py @@ -3,12 +3,13 @@ import pytest from django.contrib.auth import get_user_model -from django.test import RequestFactory, TestCase +from django.test import RequestFactory from django.utils.timezone import now, timedelta from oauth2_provider.backends import get_oauthlib_core from oauth2_provider.models import get_access_token_model, get_application_model, redirect_to_uri_allowed from oauth2_provider.oauth2_backends import JSONOAuthLibCore, OAuthLibCore +from tests.common_testing import OAuth2ProviderTestCase as TestCase try: diff --git a/tests/test_oauth2_validators.py b/tests/test_oauth2_validators.py index ca80aedb0..7e7e46de7 100644 --- a/tests/test_oauth2_validators.py +++ b/tests/test_oauth2_validators.py @@ -3,19 +3,28 @@ import json import pytest +import requests from django.contrib.auth import get_user_model from django.contrib.auth.hashers import make_password -from django.test import TestCase, TransactionTestCase from django.utils import timezone from jwcrypto import jwt from oauthlib.common import Request +from oauthlib.oauth2.rfc6749 import errors as rfc6749_errors from oauth2_provider.exceptions import FatalClientError -from oauth2_provider.models import get_access_token_model, get_application_model, get_refresh_token_model +from oauth2_provider.models import ( + get_access_token_model, + get_application_model, + get_grant_model, + get_refresh_token_model, +) from oauth2_provider.oauth2_backends import get_oauthlib_core from oauth2_provider.oauth2_validators import OAuth2Validator from . import presets +from .common_testing import OAuth2ProviderTestCase as TestCase +from .common_testing import OAuth2ProviderTransactionTestCase as TransactionTestCase +from .common_testing import retrieve_current_databases from .utils import get_basic_auth_header @@ -28,6 +37,7 @@ UserModel = get_user_model() Application = get_application_model() AccessToken = get_access_token_model() +Grant = get_grant_model() RefreshToken = get_refresh_token_model() CLEARTEXT_SECRET = "1234567890abcdefghijklmnopqrstuvwxyz" @@ -170,6 +180,12 @@ def test_authenticate_basic_auth_not_utf8(self): self.request.headers = {"HTTP_AUTHORIZATION": "Basic test"} self.assertFalse(self.validator._authenticate_basic_auth(self.request)) + def test_authenticate_basic_auth_public_app_with_device_code(self): + self.request.grant_type = "urn:ietf:params:oauth:grant-type:device_code" + self.request.headers = get_basic_auth_header("client_id", CLEARTEXT_SECRET) + self.application.client_type = Application.CLIENT_PUBLIC + self.assertTrue(self.validator._authenticate_basic_auth(self.request)) + def test_authenticate_check_secret(self): hashed = make_password(CLEARTEXT_SECRET) self.assertTrue(self.validator._check_secret(CLEARTEXT_SECRET, CLEARTEXT_SECRET)) @@ -492,18 +508,26 @@ def setUpTestData(cls): cls.introspection_token = "test_introspection_token" cls.validator = OAuth2Validator() - def test_response_when_auth_server_response_return_404(self): - with self.assertLogs(logger="oauth2_provider") as mock_log: - self.validator._get_token_from_authentication_server( - self.token, self.introspection_url, self.introspection_token, None - ) - self.assertIn( - "ERROR:oauth2_provider:Introspection: Failed to " - "get a valid response from authentication server. " - "Status code: 404, Reason: " - "Not Found.\nNoneType: None", - mock_log.output, - ) + def test_response_when_auth_server_response_not_200(self): + """ + Ensure we log the error when the authentication server returns a non-200 response. + """ + mock_response = requests.Response() + mock_response.status_code = 404 + mock_response.reason = "Not Found" + with mock.patch("requests.post") as mock_post: + mock_post.return_value = mock_response + with self.assertLogs(logger="oauth2_provider") as mock_log: + self.validator._get_token_from_authentication_server( + self.token, self.introspection_url, self.introspection_token, None + ) + self.assertIn( + "ERROR:oauth2_provider:Introspection: Failed to " + "get a valid response from authentication server. " + "Status code: 404, Reason: " + "Not Found.\nNoneType: None", + mock_log.output, + ) @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) @@ -545,7 +569,7 @@ def test_get_jwt_bearer_token(oauth2_settings, mocker): assert mock_get_id_token.call_args[1] == {} -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) def test_validate_id_token_expired_jwt(oauth2_settings, mocker, oidc_tokens): mocker.patch("oauth2_provider.oauth2_validators.jwt.JWT", side_effect=jwt.JWTExpired) @@ -561,7 +585,7 @@ def test_validate_id_token_no_token(oauth2_settings, mocker): assert status is False -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) def test_validate_id_token_app_removed(oauth2_settings, mocker, oidc_tokens): oidc_tokens.application.delete() @@ -570,7 +594,7 @@ def test_validate_id_token_app_removed(oauth2_settings, mocker, oidc_tokens): assert status is False -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RW) def test_validate_id_token_bad_token_no_aud(oauth2_settings, mocker, oidc_key): token = jwt.JWT(header=json.dumps({"alg": "RS256"}), claims=json.dumps({"bad": "token"})) @@ -578,3 +602,14 @@ def test_validate_id_token_bad_token_no_aud(oauth2_settings, mocker, oidc_key): validator = OAuth2Validator() status = validator.validate_id_token(token.serialize(), ["openid"], mocker.sentinel.request) assert status is False + + +@pytest.mark.django_db +def test_invalidate_authorization_token_returns_invalid_grant_error_when_grant_does_not_exist(): + client_id = "123" + code = "12345" + request = Request("/") + assert Grant.objects.all().count() == 0 + with pytest.raises(rfc6749_errors.InvalidGrantError): + validator = OAuth2Validator() + validator.invalidate_authorization_code(client_id=client_id, code=code, request=request) diff --git a/tests/test_oidc_views.py b/tests/test_oidc_views.py index 4bcf839ef..65197cbd1 100644 --- a/tests/test_oidc_views.py +++ b/tests/test_oidc_views.py @@ -1,7 +1,7 @@ import pytest from django.contrib.auth import get_user from django.contrib.auth.models import AnonymousUser -from django.test import RequestFactory, TestCase +from django.test import RequestFactory from django.urls import reverse from django.utils import timezone from pytest_django.asserts import assertRedirects @@ -15,14 +15,11 @@ from oauth2_provider.models import get_access_token_model, get_id_token_model, get_refresh_token_model from oauth2_provider.oauth2_validators import OAuth2Validator from oauth2_provider.settings import oauth2_settings -from oauth2_provider.views.oidc import ( - RPInitiatedLogoutView, - _load_id_token, - _validate_claims, - validate_logout_request, -) +from oauth2_provider.views.oidc import RPInitiatedLogoutView, _load_id_token, _validate_claims from . import presets +from .common_testing import OAuth2ProviderTestCase as TestCase +from .common_testing import retrieve_current_databases @pytest.mark.usefixtures("oauth2_settings") @@ -225,105 +222,7 @@ def mock_request_for(user): return request -@pytest.mark.django_db -@pytest.mark.parametrize("ALWAYS_PROMPT", [True, False]) -def test_deprecated_validate_logout_request( - oidc_tokens, public_application, other_user, rp_settings, ALWAYS_PROMPT -): - rp_settings.OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT = ALWAYS_PROMPT - oidc_tokens = oidc_tokens - application = oidc_tokens.application - client_id = application.client_id - id_token = oidc_tokens.id_token - assert validate_logout_request( - request=mock_request_for(oidc_tokens.user), - id_token_hint=None, - client_id=None, - post_logout_redirect_uri=None, - ) == (True, (None, None), None) - assert validate_logout_request( - request=mock_request_for(oidc_tokens.user), - id_token_hint=None, - client_id=client_id, - post_logout_redirect_uri=None, - ) == (True, (None, application), None) - assert validate_logout_request( - request=mock_request_for(oidc_tokens.user), - id_token_hint=None, - client_id=client_id, - post_logout_redirect_uri="http://example.org", - ) == (True, ("http://example.org", application), None) - assert validate_logout_request( - request=mock_request_for(oidc_tokens.user), - id_token_hint=id_token, - client_id=None, - post_logout_redirect_uri="http://example.org", - ) == (ALWAYS_PROMPT, ("http://example.org", application), oidc_tokens.user) - assert validate_logout_request( - request=mock_request_for(other_user), - id_token_hint=id_token, - client_id=None, - post_logout_redirect_uri="http://example.org", - ) == (True, ("http://example.org", application), oidc_tokens.user) - assert validate_logout_request( - request=mock_request_for(oidc_tokens.user), - id_token_hint=id_token, - client_id=client_id, - post_logout_redirect_uri="http://example.org", - ) == (ALWAYS_PROMPT, ("http://example.org", application), oidc_tokens.user) - with pytest.raises(InvalidIDTokenError): - validate_logout_request( - request=mock_request_for(oidc_tokens.user), - id_token_hint="111", - client_id=public_application.client_id, - post_logout_redirect_uri="http://other.org", - ) - with pytest.raises(ClientIdMissmatch): - validate_logout_request( - request=mock_request_for(oidc_tokens.user), - id_token_hint=id_token, - client_id=public_application.client_id, - post_logout_redirect_uri="http://other.org", - ) - with pytest.raises(InvalidOIDCClientError): - validate_logout_request( - request=mock_request_for(oidc_tokens.user), - id_token_hint=None, - client_id=None, - post_logout_redirect_uri="http://example.org", - ) - with pytest.raises(InvalidOIDCRedirectURIError): - validate_logout_request( - request=mock_request_for(oidc_tokens.user), - id_token_hint=None, - client_id=client_id, - post_logout_redirect_uri="example.org", - ) - with pytest.raises(InvalidOIDCRedirectURIError): - validate_logout_request( - request=mock_request_for(oidc_tokens.user), - id_token_hint=None, - client_id=client_id, - post_logout_redirect_uri="imap://example.org", - ) - with pytest.raises(InvalidOIDCRedirectURIError): - validate_logout_request( - request=mock_request_for(oidc_tokens.user), - id_token_hint=None, - client_id=client_id, - post_logout_redirect_uri="http://other.org", - ) - with pytest.raises(InvalidOIDCRedirectURIError): - rp_settings.OIDC_RP_INITIATED_LOGOUT_STRICT_REDIRECT_URIS = True - validate_logout_request( - request=mock_request_for(oidc_tokens.user), - id_token_hint=None, - client_id=public_application.client_id, - post_logout_redirect_uri="http://other.org", - ) - - -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_validate_logout_request(oidc_tokens, public_application, rp_settings): oidc_tokens = oidc_tokens application = oidc_tokens.application @@ -401,7 +300,7 @@ def test_validate_logout_request(oidc_tokens, public_application, rp_settings): ) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.parametrize("ALWAYS_PROMPT", [True, False]) def test_must_prompt(oidc_tokens, other_user, rp_settings, ALWAYS_PROMPT): rp_settings.OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT = ALWAYS_PROMPT @@ -412,6 +311,10 @@ def test_must_prompt(oidc_tokens, other_user, rp_settings, ALWAYS_PROMPT): == ALWAYS_PROMPT ) assert RPInitiatedLogoutView(request=mock_request_for(other_user)).must_prompt(oidc_tokens.user) is True + assert ( + RPInitiatedLogoutView(request=mock_request_for(AnonymousUser())).must_prompt(oidc_tokens.user) + is False + ) def test__load_id_token(): @@ -422,14 +325,14 @@ def is_logged_in(client): return get_user(client).is_authenticated -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_rp_initiated_logout_get(logged_in_client, rp_settings): rsp = logged_in_client.get(reverse("oauth2_provider:rp-initiated-logout"), data={}) assert rsp.status_code == 200 assert is_logged_in(logged_in_client) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_rp_initiated_logout_get_id_token(logged_in_client, oidc_tokens, rp_settings): rsp = logged_in_client.get( reverse("oauth2_provider:rp-initiated-logout"), data={"id_token_hint": oidc_tokens.id_token} @@ -439,7 +342,7 @@ def test_rp_initiated_logout_get_id_token(logged_in_client, oidc_tokens, rp_sett assert not is_logged_in(logged_in_client) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_rp_initiated_logout_get_revoked_id_token(logged_in_client, oidc_tokens, rp_settings): validator = oauth2_settings.OAUTH2_VALIDATOR_CLASS() validator._load_id_token(oidc_tokens.id_token).revoke() @@ -450,7 +353,7 @@ def test_rp_initiated_logout_get_revoked_id_token(logged_in_client, oidc_tokens, assert is_logged_in(logged_in_client) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_rp_initiated_logout_get_id_token_redirect(logged_in_client, oidc_tokens, rp_settings): rsp = logged_in_client.get( reverse("oauth2_provider:rp-initiated-logout"), @@ -461,7 +364,7 @@ def test_rp_initiated_logout_get_id_token_redirect(logged_in_client, oidc_tokens assert not is_logged_in(logged_in_client) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_rp_initiated_logout_get_id_token_redirect_with_state(logged_in_client, oidc_tokens, rp_settings): rsp = logged_in_client.get( reverse("oauth2_provider:rp-initiated-logout"), @@ -476,7 +379,7 @@ def test_rp_initiated_logout_get_id_token_redirect_with_state(logged_in_client, assert not is_logged_in(logged_in_client) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_rp_initiated_logout_get_id_token_missmatch_client_id( logged_in_client, oidc_tokens, public_application, rp_settings ): @@ -488,7 +391,7 @@ def test_rp_initiated_logout_get_id_token_missmatch_client_id( assert is_logged_in(logged_in_client) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_rp_initiated_logout_public_client_redirect_client_id( logged_in_client, oidc_non_confidential_tokens, public_application, rp_settings ): @@ -504,7 +407,7 @@ def test_rp_initiated_logout_public_client_redirect_client_id( assert not is_logged_in(logged_in_client) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_rp_initiated_logout_public_client_strict_redirect_client_id( logged_in_client, oidc_non_confidential_tokens, public_application, oauth2_settings ): @@ -521,7 +424,7 @@ def test_rp_initiated_logout_public_client_strict_redirect_client_id( assert is_logged_in(logged_in_client) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_rp_initiated_logout_get_client_id(logged_in_client, oidc_tokens, rp_settings): rsp = logged_in_client.get( reverse("oauth2_provider:rp-initiated-logout"), data={"client_id": oidc_tokens.application.client_id} @@ -530,7 +433,7 @@ def test_rp_initiated_logout_get_client_id(logged_in_client, oidc_tokens, rp_set assert is_logged_in(logged_in_client) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_rp_initiated_logout_post(logged_in_client, oidc_tokens, rp_settings): form_data = { "client_id": oidc_tokens.application.client_id, @@ -540,7 +443,7 @@ def test_rp_initiated_logout_post(logged_in_client, oidc_tokens, rp_settings): assert is_logged_in(logged_in_client) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_rp_initiated_logout_post_allowed(logged_in_client, oidc_tokens, rp_settings): form_data = {"client_id": oidc_tokens.application.client_id, "allow": True} rsp = logged_in_client.post(reverse("oauth2_provider:rp-initiated-logout"), form_data) @@ -549,7 +452,7 @@ def test_rp_initiated_logout_post_allowed(logged_in_client, oidc_tokens, rp_sett assert not is_logged_in(logged_in_client) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_rp_initiated_logout_post_no_session(client, oidc_tokens, rp_settings): form_data = {"client_id": oidc_tokens.application.client_id, "allow": True} rsp = client.post(reverse("oauth2_provider:rp-initiated-logout"), form_data) @@ -558,7 +461,7 @@ def test_rp_initiated_logout_post_no_session(client, oidc_tokens, rp_settings): assert not is_logged_in(client) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RP_LOGOUT) def test_rp_initiated_logout_expired_tokens_accept(logged_in_client, application, expired_id_token): # Accepting expired (but otherwise valid and signed by us) tokens is enabled. Logout should go through. @@ -573,7 +476,7 @@ def test_rp_initiated_logout_expired_tokens_accept(logged_in_client, application assert not is_logged_in(logged_in_client) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RP_LOGOUT_DENY_EXPIRED) def test_rp_initiated_logout_expired_tokens_deny(logged_in_client, application, expired_id_token): # Expired tokens should not be accepted by default. @@ -588,14 +491,14 @@ def test_rp_initiated_logout_expired_tokens_deny(logged_in_client, application, assert is_logged_in(logged_in_client) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RP_LOGOUT) def test_load_id_token_accept_expired(expired_id_token): id_token, _ = _load_id_token(expired_id_token) assert isinstance(id_token, get_id_token_model()) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RP_LOGOUT) def test_load_id_token_wrong_aud(id_token_wrong_aud): id_token, claims = _load_id_token(id_token_wrong_aud) @@ -603,7 +506,7 @@ def test_load_id_token_wrong_aud(id_token_wrong_aud): assert claims is None -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RP_LOGOUT_DENY_EXPIRED) def test_load_id_token_deny_expired(expired_id_token): id_token, claims = _load_id_token(expired_id_token) @@ -611,7 +514,7 @@ def test_load_id_token_deny_expired(expired_id_token): assert claims is None -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RP_LOGOUT) def test_validate_claims_wrong_iss(id_token_wrong_iss): id_token, claims = _load_id_token(id_token_wrong_iss) @@ -620,7 +523,7 @@ def test_validate_claims_wrong_iss(id_token_wrong_iss): assert not _validate_claims(mock_request(), claims) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RP_LOGOUT) def test_validate_claims(oidc_tokens): id_token, claims = _load_id_token(oidc_tokens.id_token) @@ -628,7 +531,7 @@ def test_validate_claims(oidc_tokens): assert _validate_claims(mock_request_for(oidc_tokens.user), claims) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.parametrize("method", ["get", "post"]) def test_userinfo_endpoint(oidc_tokens, client, method): auth_header = "Bearer %s" % oidc_tokens.access_token @@ -641,7 +544,7 @@ def test_userinfo_endpoint(oidc_tokens, client, method): assert data["sub"] == str(oidc_tokens.user.pk) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_userinfo_endpoint_bad_token(oidc_tokens, client): # No access token rsp = client.get(reverse("oauth2_provider:user-info")) @@ -654,7 +557,7 @@ def test_userinfo_endpoint_bad_token(oidc_tokens, client): assert rsp.status_code == 401 -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_token_deletion_on_logout(oidc_tokens, logged_in_client, rp_settings): AccessToken = get_access_token_model() IDToken = get_id_token_model() @@ -677,14 +580,15 @@ def test_token_deletion_on_logout(oidc_tokens, logged_in_client, rp_settings): assert all([token.revoked <= timezone.now() for token in RefreshToken.objects.all()]) -@pytest.mark.django_db -def test_token_deletion_on_logout_expired_session(oidc_tokens, client, rp_settings): +@pytest.mark.django_db(databases=retrieve_current_databases()) +def test_token_deletion_on_logout_without_op_session_get(oidc_tokens, client, rp_settings): AccessToken = get_access_token_model() IDToken = get_id_token_model() RefreshToken = get_refresh_token_model() assert AccessToken.objects.count() == 1 assert IDToken.objects.count() == 1 assert RefreshToken.objects.count() == 1 + rsp = client.get( reverse("oauth2_provider:rp-initiated-logout"), data={ @@ -692,15 +596,31 @@ def test_token_deletion_on_logout_expired_session(oidc_tokens, client, rp_settin "client_id": oidc_tokens.application.client_id, }, ) - assert rsp.status_code == 200 + assert rsp.status_code == 302 assert not is_logged_in(client) # Check that all tokens are active. - access_token = AccessToken.objects.get() - assert not access_token.is_expired() - id_token = IDToken.objects.get() - assert not id_token.is_expired() + assert AccessToken.objects.count() == 0 + assert IDToken.objects.count() == 0 + assert RefreshToken.objects.count() == 1 + + with pytest.raises(AccessToken.DoesNotExist): + AccessToken.objects.get() + + with pytest.raises(IDToken.DoesNotExist): + IDToken.objects.get() + refresh_token = RefreshToken.objects.get() - assert refresh_token.revoked is None + assert refresh_token.revoked is not None + + +@pytest.mark.django_db(databases=retrieve_current_databases()) +def test_token_deletion_on_logout_without_op_session_post(oidc_tokens, client, rp_settings): + AccessToken = get_access_token_model() + IDToken = get_id_token_model() + RefreshToken = get_refresh_token_model() + assert AccessToken.objects.count() == 1 + assert IDToken.objects.count() == 1 + assert RefreshToken.objects.count() == 1 rsp = client.post( reverse("oauth2_provider:rp-initiated-logout"), @@ -718,7 +638,7 @@ def test_token_deletion_on_logout_expired_session(oidc_tokens, client, rp_settin assert all(token.revoked <= timezone.now() for token in RefreshToken.objects.all()) -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) @pytest.mark.oauth2_settings(presets.OIDC_SETTINGS_RP_LOGOUT_KEEP_TOKENS) def test_token_deletion_on_logout_disabled(oidc_tokens, logged_in_client, rp_settings): rp_settings.OIDC_RP_INITIATED_LOGOUT_DELETE_TOKENS = False @@ -754,7 +674,7 @@ def claim_user_email(request): return EXAMPLE_EMAIL -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_userinfo_endpoint_custom_claims_callable(oidc_tokens, client, oauth2_settings): class CustomValidator(OAuth2Validator): oidc_claim_scope = None @@ -782,7 +702,7 @@ def get_additional_claims(self): assert data["email"] == EXAMPLE_EMAIL -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_userinfo_endpoint_custom_claims_email_scope_callable( oidc_email_scope_tokens, client, oauth2_settings ): @@ -809,7 +729,7 @@ def get_additional_claims(self): assert data["email"] == EXAMPLE_EMAIL -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_userinfo_endpoint_custom_claims_plain(oidc_tokens, client, oauth2_settings): class CustomValidator(OAuth2Validator): oidc_claim_scope = None @@ -837,7 +757,7 @@ def get_additional_claims(self, request): assert data["email"] == EXAMPLE_EMAIL -@pytest.mark.django_db +@pytest.mark.django_db(databases=retrieve_current_databases()) def test_userinfo_endpoint_custom_claims_email_scopeplain(oidc_email_scope_tokens, client, oauth2_settings): class CustomValidator(OAuth2Validator): def get_additional_claims(self, request): diff --git a/tests/test_password.py b/tests/test_password.py index ec9f17f54..65cf5a8b5 100644 --- a/tests/test_password.py +++ b/tests/test_password.py @@ -2,12 +2,13 @@ import pytest from django.contrib.auth import get_user_model -from django.test import RequestFactory, TestCase +from django.test import RequestFactory from django.urls import reverse from oauth2_provider.models import get_application_model from oauth2_provider.views import ProtectedResourceView +from .common_testing import OAuth2ProviderTestCase as TestCase from .utils import get_basic_auth_header diff --git a/tests/test_rest_framework.py b/tests/test_rest_framework.py index 0061f8d3a..f8ff86f23 100644 --- a/tests/test_rest_framework.py +++ b/tests/test_rest_framework.py @@ -5,7 +5,6 @@ from django.contrib.auth import get_user_model from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponse -from django.test import TestCase from django.test.utils import override_settings from django.urls import path, re_path from django.utils import timezone @@ -25,6 +24,7 @@ from oauth2_provider.models import get_access_token_model, get_application_model from . import presets +from .common_testing import OAuth2ProviderTestCase as TestCase Application = get_application_model() @@ -127,6 +127,7 @@ class AuthenticationNoneOAuth2View(MockView): @override_settings(ROOT_URLCONF=__name__) +@pytest.mark.nologinrequiredmiddleware @pytest.mark.usefixtures("oauth2_settings") @pytest.mark.oauth2_settings(presets.REST_FRAMEWORK_SCOPES) class TestOAuth2Authentication(TestCase): @@ -415,3 +416,9 @@ def test_authentication_none(self): auth = self._create_authorization_header(self.access_token.token) response = self.client.get("/oauth2-authentication-none/", HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 401) + + def test_invalid_hex_string_in_query(self): + auth = self._create_authorization_header(self.access_token.token) + response = self.client.get("/oauth2-test/?q=73%%20of%20Arkansans", HTTP_AUTHORIZATION=auth) + # Should respond with a 400 rather than raise a ValueError + self.assertEqual(response.status_code, 400) diff --git a/tests/test_scopes.py b/tests/test_scopes.py index ec36da418..4dae0d3c4 100644 --- a/tests/test_scopes.py +++ b/tests/test_scopes.py @@ -4,12 +4,13 @@ import pytest from django.contrib.auth import get_user_model from django.core.exceptions import ImproperlyConfigured -from django.test import RequestFactory, TestCase +from django.test import RequestFactory from django.urls import reverse from oauth2_provider.models import get_access_token_model, get_application_model, get_grant_model from oauth2_provider.views import ReadWriteScopedResourceView, ScopedProtectedResourceView +from .common_testing import OAuth2ProviderTestCase as TestCase from .utils import get_basic_auth_header diff --git a/tests/test_settings.py b/tests/test_settings.py index f9f540339..b64fc31db 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,6 +1,5 @@ import pytest from django.core.exceptions import ImproperlyConfigured -from django.test import TestCase from django.test.utils import override_settings from oauthlib.common import Request @@ -19,6 +18,7 @@ CustomIDTokenAdmin, CustomRefreshTokenAdmin, ) +from tests.common_testing import OAuth2ProviderTestCase as TestCase from . import presets diff --git a/tests/test_token_endpoint_cors.py b/tests/test_token_endpoint_cors.py index 791237b4a..6eaea6560 100644 --- a/tests/test_token_endpoint_cors.py +++ b/tests/test_token_endpoint_cors.py @@ -3,12 +3,13 @@ import pytest from django.contrib.auth import get_user_model -from django.test import RequestFactory, TestCase +from django.test import RequestFactory from django.urls import reverse from oauth2_provider.models import get_application_model from . import presets +from .common_testing import OAuth2ProviderTestCase as TestCase from .utils import get_basic_auth_header diff --git a/tests/test_token_revocation.py b/tests/test_token_revocation.py index 8655a5b3e..fa836b6a2 100644 --- a/tests/test_token_revocation.py +++ b/tests/test_token_revocation.py @@ -1,12 +1,14 @@ import datetime from django.contrib.auth import get_user_model -from django.test import RequestFactory, TestCase +from django.test import RequestFactory from django.urls import reverse from django.utils import timezone from oauth2_provider.models import get_access_token_model, get_application_model, get_refresh_token_model +from .common_testing import OAuth2ProviderTestCase as TestCase + Application = get_application_model() AccessToken = get_access_token_model() @@ -53,7 +55,7 @@ def test_revoke_access_token(self): response = self.client.post(url, data=data) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"") - self.assertFalse(AccessToken.objects.filter(id=tok.id).exists()) + self.assertFalse(AccessToken.objects.filter(pk=tok.pk).exists()) def test_revoke_access_token_public(self): public_app = Application( @@ -101,7 +103,7 @@ def test_revoke_access_token_with_hint(self): url = reverse("oauth2_provider:revoke-token") response = self.client.post(url, data=data) self.assertEqual(response.status_code, 200) - self.assertFalse(AccessToken.objects.filter(id=tok.id).exists()) + self.assertFalse(AccessToken.objects.filter(pk=tok.pk).exists()) def test_revoke_access_token_with_invalid_hint(self): tok = AccessToken.objects.create( @@ -123,7 +125,7 @@ def test_revoke_access_token_with_invalid_hint(self): url = reverse("oauth2_provider:revoke-token") response = self.client.post(url, data=data) self.assertEqual(response.status_code, 200) - self.assertFalse(AccessToken.objects.filter(id=tok.id).exists()) + self.assertFalse(AccessToken.objects.filter(pk=tok.pk).exists()) def test_revoke_refresh_token(self): tok = AccessToken.objects.create( @@ -146,9 +148,9 @@ def test_revoke_refresh_token(self): url = reverse("oauth2_provider:revoke-token") response = self.client.post(url, data=data) self.assertEqual(response.status_code, 200) - refresh_token = RefreshToken.objects.filter(id=rtok.id).first() + refresh_token = RefreshToken.objects.filter(pk=rtok.pk).first() self.assertIsNotNone(refresh_token.revoked) - self.assertFalse(AccessToken.objects.filter(id=rtok.access_token.id).exists()) + self.assertFalse(AccessToken.objects.filter(pk=rtok.access_token.pk).exists()) def test_revoke_refresh_token_with_revoked_access_token(self): tok = AccessToken.objects.create( @@ -172,8 +174,8 @@ def test_revoke_refresh_token_with_revoked_access_token(self): response = self.client.post(url, data=data) self.assertEqual(response.status_code, 200) - self.assertFalse(AccessToken.objects.filter(id=tok.id).exists()) - refresh_token = RefreshToken.objects.filter(id=rtok.id).first() + self.assertFalse(AccessToken.objects.filter(pk=tok.pk).exists()) + refresh_token = RefreshToken.objects.filter(pk=rtok.pk).first() self.assertIsNotNone(refresh_token.revoked) def test_revoke_token_with_wrong_hint(self): @@ -202,4 +204,4 @@ def test_revoke_token_with_wrong_hint(self): url = reverse("oauth2_provider:revoke-token") response = self.client.post(url, data=data) self.assertEqual(response.status_code, 200) - self.assertFalse(AccessToken.objects.filter(id=tok.id).exists()) + self.assertFalse(AccessToken.objects.filter(pk=tok.pk).exists()) diff --git a/tests/test_token_view.py b/tests/test_token_view.py index fc73c2a66..63e76ed2f 100644 --- a/tests/test_token_view.py +++ b/tests/test_token_view.py @@ -1,12 +1,13 @@ import datetime from django.contrib.auth import get_user_model -from django.test import TestCase from django.urls import reverse from django.utils import timezone from oauth2_provider.models import get_access_token_model, get_application_model +from .common_testing import OAuth2ProviderTestCase as TestCase + Application = get_application_model() AccessToken = get_access_token_model() diff --git a/tests/test_ui_locales.py b/tests/test_ui_locales.py new file mode 100644 index 000000000..d375dc55c --- /dev/null +++ b/tests/test_ui_locales.py @@ -0,0 +1,57 @@ +from django.contrib.auth import get_user_model +from django.test import TestCase, override_settings +from django.urls import reverse + +from oauth2_provider.models import get_application_model + + +UserModel = get_user_model() +Application = get_application_model() + + +@override_settings( + OAUTH2_PROVIDER={ + "OIDC_ENABLED": True, + "PKCE_REQUIRED": False, + "SCOPES": { + "openid": "OpenID connect", + }, + } +) +class TestUILocalesParam(TestCase): + @classmethod + def setUpTestData(cls): + cls.application = Application.objects.create( + name="Test Application", + client_id="test", + redirect_uris="https://www.example.com/", + client_type=Application.CLIENT_PUBLIC, + authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, + ) + cls.trusted_application = Application.objects.create( + name="Trusted Application", + client_id="trusted", + redirect_uris="https://www.example.com/", + client_type=Application.CLIENT_PUBLIC, + authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, + skip_authorization=True, + ) + cls.user = UserModel.objects.create_user("test_user") + cls.url = reverse("oauth2_provider:authorize") + + def setUp(self): + self.client.force_login(self.user) + + def test_application_ui_locales_param(self): + response = self.client.get( + f"{self.url}?response_type=code&client_id=test&scope=openid&ui_locales=de", + ) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "oauth2_provider/authorize.html") + + def test_trusted_application_ui_locales_param(self): + response = self.client.get( + f"{self.url}?response_type=code&client_id=trusted&scope=openid&ui_locales=de", + ) + self.assertEqual(response.status_code, 302) + self.assertRegex(response.url, r"https://www\.example\.com/\?code=[a-zA-Z0-9]+") diff --git a/tests/test_utils.py b/tests/test_utils.py index 2c319b6ea..eef4b985c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,3 +1,5 @@ +import pytest + from oauth2_provider import utils @@ -25,3 +27,24 @@ def test_jwk_from_pem_caches_jwk(): jwk3 = utils.jwk_from_pem(a_different_tiny_rsa_key) assert jwk3 is not jwk1 + + +def test_user_code_generator(): + # Default argument, 8 characters + user_code = utils.user_code_generator() + assert isinstance(user_code, str) + assert len(user_code) == 8 + + for character in user_code: + assert character >= "0" + assert character <= "V" + + another_user_code = utils.user_code_generator() + assert another_user_code != user_code + + shorter_user_code = utils.user_code_generator(user_code_length=1) + assert len(shorter_user_code) == 1 + + with pytest.raises(ValueError): + utils.user_code_generator(user_code_length=0) + utils.user_code_generator(user_code_length=-1) diff --git a/tests/test_validators.py b/tests/test_validators.py index b2bbb2970..a77a1e16a 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -1,102 +1,9 @@ import pytest from django.core.validators import ValidationError -from django.test import TestCase -from oauth2_provider.validators import AllowedURIValidator, RedirectURIValidator, WildcardSet +from oauth2_provider.validators import AllowedURIValidator - -@pytest.mark.usefixtures("oauth2_settings") -class TestValidators(TestCase): - def test_validate_good_uris(self): - validator = RedirectURIValidator(allowed_schemes=["https"]) - good_uris = [ - "https://example.com/", - "https://example.org/?key=val", - "https://example", - "https://localhost", - "https://1.1.1.1", - "https://127.0.0.1", - "https://255.255.255.255", - ] - for uri in good_uris: - # Check ValidationError not thrown - validator(uri) - - def test_validate_custom_uri_scheme(self): - validator = RedirectURIValidator(allowed_schemes=["my-scheme", "https", "git+ssh"]) - good_uris = [ - "my-scheme://example.com", - "my-scheme://example", - "my-scheme://localhost", - "https://example.com", - "HTTPS://example.com", - "git+ssh://example.com", - ] - for uri in good_uris: - # Check ValidationError not thrown - validator(uri) - - def test_validate_bad_uris(self): - validator = RedirectURIValidator(allowed_schemes=["https"]) - self.oauth2_settings.ALLOWED_REDIRECT_URI_SCHEMES = ["https", "good"] - bad_uris = [ - "http:/example.com", - "HTTP://localhost", - "HTTP://example.com", - "HTTP://example.com.", - "http://example.com/#fragment", - "123://example.com", - "http://fe80::1", - "git+ssh://example.com", - "my-scheme://example.com", - "uri-without-a-scheme", - "https://example.com/#fragment", - "good://example.com/#fragment", - " ", - "", - # Bad IPv6 URL, urlparse behaves differently for these - 'https://[">', - ] - - for uri in bad_uris: - with self.assertRaises(ValidationError): - validator(uri) - - def test_validate_wildcard_scheme__bad_uris(self): - validator = RedirectURIValidator(allowed_schemes=WildcardSet()) - bad_uris = [ - "http:/example.com#fragment", - "HTTP://localhost#fragment", - "http://example.com/#fragment", - "good://example.com/#fragment", - " ", - "", - # Bad IPv6 URL, urlparse behaves differently for these - 'https://[">', - ] - - for uri in bad_uris: - with self.assertRaises(ValidationError, msg=uri): - validator(uri) - - def test_validate_wildcard_scheme_good_uris(self): - validator = RedirectURIValidator(allowed_schemes=WildcardSet()) - good_uris = [ - "my-scheme://example.com", - "my-scheme://example", - "my-scheme://localhost", - "https://example.com", - "HTTPS://example.com", - "HTTPS://example.com.", - "git+ssh://example.com", - "ANY://localhost", - "scheme://example.com", - "at://example.com", - "all://example.com", - ] - for uri in good_uris: - # Check ValidationError not thrown - validator(uri) +from .common_testing import OAuth2ProviderTestCase as TestCase @pytest.mark.usefixtures("oauth2_settings") @@ -264,3 +171,27 @@ def test_allow_fragment_invalid_urls(self): for uri in bad_uris: with self.assertRaises(ValidationError): validator(uri) + + def test_allow_hostname_wildcard(self): + validator = AllowedURIValidator(["https"], "test", allow_hostname_wildcard=True) + good_uris = [ + "https://*.example.com", + "https://*-partial.example.com", + "https://*.partial.example.com", + "https://*-partial.valid.example.com", + ] + for uri in good_uris: + # Check ValidationError not thrown + validator(uri) + + bad_uris = [ + "https://*/", + "https://*-partial", + "https://*.com", + "https://*-partial.com", + "https://*.*.example.com", + "https://invalid.*.example.com", + ] + for uri in bad_uris: + with self.assertRaises(ValidationError): + validator(uri) diff --git a/tests/urls.py b/tests/urls.py index 0661a9336..6f8f56832 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,11 +1,13 @@ from django.contrib import admin from django.urls import include, path +from oauth2_provider import urls as oauth2_urls + admin.autodiscover() urlpatterns = [ - path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")), + path("o/", include(oauth2_urls)), path("admin/", admin.site.urls), ] diff --git a/tox.ini b/tox.ini index 61b983b5b..29e93a2ae 100644 --- a/tox.ini +++ b/tox.ini @@ -1,45 +1,35 @@ [tox] envlist = - flake8, migrations, migrate_swapped, docs, + lint, sphinxlint, - py{38,39,310}-dj32, - py{38,39,310}-dj40, - py{38,39,310,311}-dj41, py{38,39,310,311,312}-dj42, - py{310,311,312}-dj50, - py{310,311,312}-djmain, + py{310,311,312,313}-dj50, + py{310,311,312,313}-dj51, + py{310,311,312,313,314}-dj52, + py{310,311,312,313,314}-djmain, + py39-multi-db-dj-42 [gh-actions] python = - 3.8: py38, docs, flake8, migrations, migrate_swapped, sphinxlint + 3.8: py38, docs, lint, migrations, migrate_swapped, sphinxlint 3.9: py39 3.10: py310 3.11: py311 3.12: py312 + 3.13: py313 + 3.14: py314 [gh-actions:env] DJANGO = - 2.2: dj22 - 3.2: dj32 - 4.0: dj40 - 4.1: dj41 4.2: dj42 5.0: dj50 + 5.1: dj51 + 5.2: dj52 main: djmain -[pytest] -django_find_project = false -addopts = - --cov=oauth2_provider - --cov-report= - --cov-append - -s -markers = - oauth2_settings: Custom OAuth2 settings to use - use with oauth2_settings fixture - [testenv] commands = pytest {posargs} @@ -50,15 +40,13 @@ setenv = PYTHONPATH = {toxinidir} PYTHONWARNINGS = all deps = - dj22: Django>=2.2,<3 - dj32: Django>=3.2,<3.3 - dj40: Django>=4.0.0,<4.1 - dj41: Django>=4.1,<4.2 dj42: Django>=4.2,<4.3 - dj50: Django>=5.0b1,<5.1 + dj50: Django>=5.0,<5.1 + dj51: Django>=5.1,<5.2 + dj52: Django>=5.2,<6.0 djmain: https://github.com/django/django/archive/main.tar.gz djangorestframework - oauthlib>=3.1.0 + oauthlib>=3.3.0 jwcrypto coverage pytest @@ -67,10 +55,11 @@ deps = pytest-xdist pytest-mock requests + pytz; python_version < '3.9' passenv = PYTEST_ADDOPTS -[testenv:py{310,311,312}-djmain] +[testenv:py{310,311,312,313,314}-djmain] ignore_errors = true ignore_outcome = true @@ -90,7 +79,7 @@ commands = deps = Jinja2<3.1 sphinx<3 - oauthlib>=3.1.0 + oauthlib>=3.3.0 m2r>=0.2.1 mistune<2 sphinx-rtd-theme @@ -98,15 +87,13 @@ deps = jwcrypto django -[testenv:flake8] +[testenv:lint] basepython = python3.8 +deps = ruff>=0.6 skip_install = True -commands = flake8 {toxinidir} -deps = - flake8 - flake8-isort - flake8-quotes - flake8-black +commands = + ruff format --check + ruff check [testenv:migrations] setenv = @@ -115,6 +102,12 @@ setenv = PYTHONWARNINGS = all commands = django-admin makemigrations --dry-run --check +[testenv:py39-multi-db-dj42] +setenv = + DJANGO_SETTINGS_MODULE = tests.multi_db_settings + PYTHONPATH = {toxinidir} + PYTHONWARNINGS = all + [testenv:migrate_swapped] setenv = DJANGO_SETTINGS_MODULE = tests.settings_swapped @@ -125,35 +118,10 @@ commands = [testenv:build] deps = - setuptools>=39.0 - wheel -whitelist_externals = rm + build + twine +allowlist_externals = rm commands = rm -rf dist - python setup.py sdist bdist_wheel - -[coverage:run] -source = oauth2_provider -omit = */migrations/* - -[coverage:report] -show_missing = True - -[flake8] -max-line-length = 110 -exclude = docs/, oauth2_provider/migrations/, tests/migrations/, .tox/, build/, dist/ -application-import-names = oauth2_provider -inline-quotes = double -extend-ignore = E203, W503 - -[isort] -default_section = THIRDPARTY -known_first_party = oauth2_provider -line_length = 110 -lines_after_imports = 2 -multi_line_output = 3 -include_trailing_comma = True -force_grid_wrap = 0 -use_parentheses = True -ensure_newline_before_comments = True -skip = oauth2_provider/migrations/, .tox/, tests/migrations/ + python -m build + twine check dist/* diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..d5f28ba2c --- /dev/null +++ b/uv.lock @@ -0,0 +1,1866 @@ +version = 1 +revision = 3 +requires-python = ">=3.8, <=3.14" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] + +[manifest] +members = [ + "django-oauth-toolkit", + "idp", +] + +[[package]] +name = "alabaster" +version = "0.7.13" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/94/71/a8ee96d1fd95ca04a0d2e2d9c4081dac4c2d2b12f7ddb899c8cb9bfd1532/alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2", size = 11454, upload-time = "2023-01-13T06:42:53.797Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/88/c7083fc61120ab661c5d0b82cb77079fc1429d3f913a456c1c82cf4658f7/alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3", size = 13857, upload-time = "2023-01-13T06:42:52.336Z" }, +] + +[[package]] +name = "alabaster" +version = "0.7.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "asgiref" +version = "3.8.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186, upload-time = "2024-03-22T14:39:36.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" }, +] + +[[package]] +name = "asgiref" +version = "3.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/08/4dfec9b90758a59acc6be32ac82e98d1fbfc321cb5cfa410436dbacf821c/asgiref-3.10.0.tar.gz", hash = "sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e", size = 37483, upload-time = "2025-10-05T09:15:06.557Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/9c/fc2331f538fbf7eedba64b2052e99ccf9ba9d6888e2f41441ee28847004b/asgiref-3.10.0-py3-none-any.whl", hash = "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734", size = 24050, upload-time = "2025-10-05T09:15:05.11Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "backports-zoneinfo" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/85/475e514c3140937cf435954f78dedea1861aeab7662d11de232bdaa90655/backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2", size = 74098, upload-time = "2020-06-23T13:51:22.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/6d/eca004eeadcbf8bd64cc96feb9e355536147f0577420b44d80c7cac70767/backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987", size = 35816, upload-time = "2020-06-23T13:51:21.244Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9b1b920a6a95652463143943fa3b8c000cb0b932ab463764a6f2a2416560/backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1", size = 72147, upload-time = "2020-06-23T13:51:17.562Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ab/3e941e3fcf1b7d3ab3d0233194d99d6a0ed6b24f8f956fc81e47edc8c079/backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9", size = 74033, upload-time = "2020-06-23T13:51:14.592Z" }, + { url = "https://files.pythonhosted.org/packages/c0/34/5fdb0a3a28841d215c255be8fc60b8666257bb6632193c86fd04b63d4a31/backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328", size = 36803, upload-time = "2020-06-23T13:51:07.517Z" }, + { url = "https://files.pythonhosted.org/packages/78/cc/e27fd6493bbce8dbea7e6c1bc861fe3d3bc22c4f7c81f4c3befb8ff5bfaf/backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6", size = 38967, upload-time = "2020-06-23T13:51:13.735Z" }, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "pycparser", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, + { url = "https://files.pythonhosted.org/packages/48/08/15bf6b43ae9bd06f6b00ad8a91f5a8fe1069d4c9fab550a866755402724e/cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", size = 182457, upload-time = "2024-09-04T20:44:47.892Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5b/f1523dd545f92f7df468e5f653ffa4df30ac222f3c884e51e139878f1cb5/cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", size = 425932, upload-time = "2024-09-04T20:44:49.491Z" }, + { url = "https://files.pythonhosted.org/packages/53/93/7e547ab4105969cc8c93b38a667b82a835dd2cc78f3a7dad6130cfd41e1d/cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", size = 448585, upload-time = "2024-09-04T20:44:51.671Z" }, + { url = "https://files.pythonhosted.org/packages/56/c4/a308f2c332006206bb511de219efeff090e9d63529ba0a77aae72e82248b/cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", size = 456268, upload-time = "2024-09-04T20:44:53.51Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5b/b63681518265f2f4060d2b60755c1c77ec89e5e045fc3773b72735ddaad5/cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", size = 436592, upload-time = "2024-09-04T20:44:55.085Z" }, + { url = "https://files.pythonhosted.org/packages/bb/19/b51af9f4a4faa4a8ac5a0e5d5c2522dcd9703d07fac69da34a36c4d960d3/cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", size = 446512, upload-time = "2024-09-04T20:44:57.135Z" }, + { url = "https://files.pythonhosted.org/packages/e2/63/2bed8323890cb613bbecda807688a31ed11a7fe7afe31f8faaae0206a9a3/cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", size = 171576, upload-time = "2024-09-04T20:44:58.535Z" }, + { url = "https://files.pythonhosted.org/packages/2f/70/80c33b044ebc79527447fd4fbc5455d514c3bb840dede4455de97da39b4d/cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", size = 181229, upload-time = "2024-09-04T20:44:59.963Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220, upload-time = "2024-09-04T20:45:01.577Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605, upload-time = "2024-09-04T20:45:03.837Z" }, + { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910, upload-time = "2024-09-04T20:45:05.315Z" }, + { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200, upload-time = "2024-09-04T20:45:06.903Z" }, + { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565, upload-time = "2024-09-04T20:45:08.975Z" }, + { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635, upload-time = "2024-09-04T20:45:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218, upload-time = "2024-09-04T20:45:12.366Z" }, + { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486, upload-time = "2024-09-04T20:45:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911, upload-time = "2024-09-04T20:45:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632, upload-time = "2024-09-04T20:45:17.284Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820, upload-time = "2024-09-04T20:45:18.762Z" }, + { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290, upload-time = "2024-09-04T20:45:20.226Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "pycparser", marker = "python_full_version >= '3.9' and implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" }, + { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" }, + { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" }, + { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, + { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4e/3926a1c11f0433791985727965263f788af00db3482d89a7545ca5ecc921/charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", size = 198599, upload-time = "2025-10-14T04:41:53.213Z" }, + { url = "https://files.pythonhosted.org/packages/ec/7c/b92d1d1dcffc34592e71ea19c882b6709e43d20fa498042dea8b815638d7/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", size = 143090, upload-time = "2025-10-14T04:41:54.385Z" }, + { url = "https://files.pythonhosted.org/packages/84/ce/61a28d3bb77281eb24107b937a497f3c43089326d27832a63dcedaab0478/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", size = 139490, upload-time = "2025-10-14T04:41:55.551Z" }, + { url = "https://files.pythonhosted.org/packages/c0/bd/c9e59a91b2061c6f8bb98a150670cb16d4cd7c4ba7d11ad0cdf789155f41/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", size = 155334, upload-time = "2025-10-14T04:41:56.724Z" }, + { url = "https://files.pythonhosted.org/packages/bf/37/f17ae176a80f22ff823456af91ba3bc59df308154ff53aef0d39eb3d3419/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", size = 152823, upload-time = "2025-10-14T04:41:58.236Z" }, + { url = "https://files.pythonhosted.org/packages/bf/fa/cf5bb2409a385f78750e78c8d2e24780964976acdaaed65dbd6083ae5b40/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", size = 147618, upload-time = "2025-10-14T04:41:59.409Z" }, + { url = "https://files.pythonhosted.org/packages/9b/63/579784a65bc7de2d4518d40bb8f1870900163e86f17f21fd1384318c459d/charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", size = 145516, upload-time = "2025-10-14T04:42:00.579Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a9/94ec6266cd394e8f93a4d69cca651d61bf6ac58d2a0422163b30c698f2c7/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", size = 145266, upload-time = "2025-10-14T04:42:01.684Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/d6626eb97764b58c2779fa7928fa7d1a49adb8ce687c2dbba4db003c1939/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", size = 139559, upload-time = "2025-10-14T04:42:02.902Z" }, + { url = "https://files.pythonhosted.org/packages/09/01/ddbe6b01313ba191dbb0a43c7563bc770f2448c18127f9ea4b119c44dff0/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", size = 156653, upload-time = "2025-10-14T04:42:04.005Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/d05543378bea89296e9af4510b44c704626e191da447235c8fdedfc5b7b2/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", size = 145644, upload-time = "2025-10-14T04:42:05.211Z" }, + { url = "https://files.pythonhosted.org/packages/72/01/2866c4377998ef8a1f6802f6431e774a4c8ebe75b0a6e569ceec55c9cbfb/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", size = 153964, upload-time = "2025-10-14T04:42:06.341Z" }, + { url = "https://files.pythonhosted.org/packages/4a/66/66c72468a737b4cbd7851ba2c522fe35c600575fbeac944460b4fd4a06fe/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", size = 148777, upload-time = "2025-10-14T04:42:07.535Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/d0d56677fdddbffa8ca00ec411f67bb8c947f9876374ddc9d160d4f2c4b3/charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", size = 98687, upload-time = "2025-10-14T04:42:08.678Z" }, + { url = "https://files.pythonhosted.org/packages/00/64/c3bc303d1b586480b1c8e6e1e2191a6d6dd40255244e5cf16763dcec52e6/charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", size = 106115, upload-time = "2025-10-14T04:42:09.793Z" }, + { url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" }, + { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" }, + { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" }, + { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" }, + { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" }, + { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" }, + { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[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" } +resolution-markers = [ + "python_full_version < '3.9'", +] +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/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674, upload-time = "2024-08-04T19:44:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101, upload-time = "2024-08-04T19:44:49.32Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554, upload-time = "2024-08-04T19:44:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440, upload-time = "2024-08-04T19:44:53.464Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889, upload-time = "2024-08-04T19:44:55.165Z" }, + { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142, upload-time = "2024-08-04T19:44:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805, upload-time = "2024-08-04T19:44:59.033Z" }, + { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655, upload-time = "2024-08-04T19:45:01.398Z" }, + { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296, upload-time = "2024-08-04T19:45:03.819Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137, upload-time = "2024-08-04T19:45:06.25Z" }, + { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688, upload-time = "2024-08-04T19:45:08.358Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120, upload-time = "2024-08-04T19:45:11.526Z" }, + { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249, upload-time = "2024-08-04T19:45:13.202Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237, upload-time = "2024-08-04T19:45:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311, upload-time = "2024-08-04T19:45:16.924Z" }, + { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453, upload-time = "2024-08-04T19:45:18.672Z" }, + { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958, upload-time = "2024-08-04T19:45:20.63Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938, upload-time = "2024-08-04T19:45:23.062Z" }, + { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352, upload-time = "2024-08-04T19:45:25.042Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153, upload-time = "2024-08-04T19:45:27.079Z" }, + { 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.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version < '3.9'" }, +] + +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, + { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, + { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version == '3.9.*'" }, +] + +[[package]] +name = "coverage" +version = "7.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905, upload-time = "2025-10-15T15:15:08.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/95/c49df0aceb5507a80b9fe5172d3d39bf23f05be40c23c8d77d556df96cec/coverage-7.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb53f1e8adeeb2e78962bade0c08bfdc461853c7969706ed901821e009b35e31", size = 215800, upload-time = "2025-10-15T15:12:19.824Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c6/7bb46ce01ed634fff1d7bb53a54049f539971862cc388b304ff3c51b4f66/coverage-7.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9a03ec6cb9f40a5c360f138b88266fd8f58408d71e89f536b4f91d85721d075", size = 216198, upload-time = "2025-10-15T15:12:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/94/b2/75d9d8fbf2900268aca5de29cd0a0fe671b0f69ef88be16767cc3c828b85/coverage-7.11.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d7f0616c557cbc3d1c2090334eddcbb70e1ae3a40b07222d62b3aa47f608fab", size = 242953, upload-time = "2025-10-15T15:12:24.139Z" }, + { url = "https://files.pythonhosted.org/packages/65/ac/acaa984c18f440170525a8743eb4b6c960ace2dbad80dc22056a437fc3c6/coverage-7.11.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e44a86a47bbdf83b0a3ea4d7df5410d6b1a0de984fbd805fa5101f3624b9abe0", size = 244766, upload-time = "2025-10-15T15:12:25.974Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0d/938d0bff76dfa4a6b228c3fc4b3e1c0e2ad4aa6200c141fcda2bd1170227/coverage-7.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:596763d2f9a0ee7eec6e643e29660def2eef297e1de0d334c78c08706f1cb785", size = 246625, upload-time = "2025-10-15T15:12:27.387Z" }, + { url = "https://files.pythonhosted.org/packages/38/54/8f5f5e84bfa268df98f46b2cb396b1009734cfb1e5d6adb663d284893b32/coverage-7.11.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef55537ff511b5e0a43edb4c50a7bf7ba1c3eea20b4f49b1490f1e8e0e42c591", size = 243568, upload-time = "2025-10-15T15:12:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/68/30/8ba337c2877fe3f2e1af0ed7ff4be0c0c4aca44d6f4007040f3ca2255e99/coverage-7.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cbabd8f4d0d3dc571d77ae5bdbfa6afe5061e679a9d74b6797c48d143307088", size = 244665, upload-time = "2025-10-15T15:12:30.297Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fb/c6f1d6d9a665536b7dde2333346f0cc41dc6a60bd1ffc10cd5c33e7eb000/coverage-7.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e24045453384e0ae2a587d562df2a04d852672eb63051d16096d3f08aa4c7c2f", size = 242681, upload-time = "2025-10-15T15:12:32.326Z" }, + { url = "https://files.pythonhosted.org/packages/be/38/1b532319af5f991fa153c20373291dc65c2bf532af7dbcffdeef745c8f79/coverage-7.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:7161edd3426c8d19bdccde7d49e6f27f748f3c31cc350c5de7c633fea445d866", size = 242912, upload-time = "2025-10-15T15:12:34.079Z" }, + { url = "https://files.pythonhosted.org/packages/67/3d/f39331c60ef6050d2a861dc1b514fa78f85f792820b68e8c04196ad733d6/coverage-7.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d4ed4de17e692ba6415b0587bc7f12bc80915031fc9db46a23ce70fc88c9841", size = 243559, upload-time = "2025-10-15T15:12:35.809Z" }, + { url = "https://files.pythonhosted.org/packages/4b/55/cb7c9df9d0495036ce582a8a2958d50c23cd73f84a23284bc23bd4711a6f/coverage-7.11.0-cp310-cp310-win32.whl", hash = "sha256:765c0bc8fe46f48e341ef737c91c715bd2a53a12792592296a095f0c237e09cf", size = 218266, upload-time = "2025-10-15T15:12:37.429Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/b79cb275fa7bd0208767f89d57a1b5f6ba830813875738599741b97c2e04/coverage-7.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:24d6f3128f1b2d20d84b24f4074475457faedc3d4613a7e66b5e769939c7d969", size = 219169, upload-time = "2025-10-15T15:12:39.25Z" }, + { url = "https://files.pythonhosted.org/packages/49/3a/ee1074c15c408ddddddb1db7dd904f6b81bc524e01f5a1c5920e13dbde23/coverage-7.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847", size = 215912, upload-time = "2025-10-15T15:12:40.665Z" }, + { url = "https://files.pythonhosted.org/packages/70/c4/9f44bebe5cb15f31608597b037d78799cc5f450044465bcd1ae8cb222fe1/coverage-7.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc", size = 216310, upload-time = "2025-10-15T15:12:42.461Z" }, + { url = "https://files.pythonhosted.org/packages/42/01/5e06077cfef92d8af926bdd86b84fb28bf9bc6ad27343d68be9b501d89f2/coverage-7.11.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0", size = 246706, upload-time = "2025-10-15T15:12:44.001Z" }, + { url = "https://files.pythonhosted.org/packages/40/b8/7a3f1f33b35cc4a6c37e759137533119560d06c0cc14753d1a803be0cd4a/coverage-7.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7", size = 248634, upload-time = "2025-10-15T15:12:45.768Z" }, + { url = "https://files.pythonhosted.org/packages/7a/41/7f987eb33de386bc4c665ab0bf98d15fcf203369d6aacae74f5dd8ec489a/coverage-7.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623", size = 250741, upload-time = "2025-10-15T15:12:47.222Z" }, + { url = "https://files.pythonhosted.org/packages/23/c1/a4e0ca6a4e83069fb8216b49b30a7352061ca0cb38654bd2dc96b7b3b7da/coverage-7.11.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287", size = 246837, upload-time = "2025-10-15T15:12:48.904Z" }, + { url = "https://files.pythonhosted.org/packages/5d/03/ced062a17f7c38b4728ff76c3acb40d8465634b20b4833cdb3cc3a74e115/coverage-7.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552", size = 248429, upload-time = "2025-10-15T15:12:50.73Z" }, + { url = "https://files.pythonhosted.org/packages/97/af/a7c6f194bb8c5a2705ae019036b8fe7f49ea818d638eedb15fdb7bed227c/coverage-7.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de", size = 246490, upload-time = "2025-10-15T15:12:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c3/aab4df02b04a8fde79068c3c41ad7a622b0ef2b12e1ed154da986a727c3f/coverage-7.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601", size = 246208, upload-time = "2025-10-15T15:12:54.586Z" }, + { url = "https://files.pythonhosted.org/packages/30/d8/e282ec19cd658238d60ed404f99ef2e45eed52e81b866ab1518c0d4163cf/coverage-7.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e", size = 247126, upload-time = "2025-10-15T15:12:56.485Z" }, + { url = "https://files.pythonhosted.org/packages/d1/17/a635fa07fac23adb1a5451ec756216768c2767efaed2e4331710342a3399/coverage-7.11.0-cp311-cp311-win32.whl", hash = "sha256:fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c", size = 218314, upload-time = "2025-10-15T15:12:58.365Z" }, + { url = "https://files.pythonhosted.org/packages/2a/29/2ac1dfcdd4ab9a70026edc8d715ece9b4be9a1653075c658ee6f271f394d/coverage-7.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9", size = 219203, upload-time = "2025-10-15T15:12:59.902Z" }, + { url = "https://files.pythonhosted.org/packages/03/21/5ce8b3a0133179115af4c041abf2ee652395837cb896614beb8ce8ddcfd9/coverage-7.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745", size = 217879, upload-time = "2025-10-15T15:13:01.35Z" }, + { url = "https://files.pythonhosted.org/packages/c4/db/86f6906a7c7edc1a52b2c6682d6dd9be775d73c0dfe2b84f8923dfea5784/coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1", size = 216098, upload-time = "2025-10-15T15:13:02.916Z" }, + { url = "https://files.pythonhosted.org/packages/21/54/e7b26157048c7ba555596aad8569ff903d6cd67867d41b75287323678ede/coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007", size = 216331, upload-time = "2025-10-15T15:13:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/b9/19/1ce6bf444f858b83a733171306134a0544eaddf1ca8851ede6540a55b2ad/coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46", size = 247825, upload-time = "2025-10-15T15:13:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/d3bcbbc259fcced5fb67c5d78f6e7ee965f49760c14afd931e9e663a83b2/coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893", size = 250573, upload-time = "2025-10-15T15:13:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/58/8d/b0ff3641a320abb047258d36ed1c21d16be33beed4152628331a1baf3365/coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115", size = 251706, upload-time = "2025-10-15T15:13:09.4Z" }, + { url = "https://files.pythonhosted.org/packages/59/c8/5a586fe8c7b0458053d9c687f5cff515a74b66c85931f7fe17a1c958b4ac/coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415", size = 248221, upload-time = "2025-10-15T15:13:10.964Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ff/3a25e3132804ba44cfa9a778cdf2b73dbbe63ef4b0945e39602fc896ba52/coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186", size = 249624, upload-time = "2025-10-15T15:13:12.5Z" }, + { url = "https://files.pythonhosted.org/packages/c5/12/ff10c8ce3895e1b17a73485ea79ebc1896a9e466a9d0f4aef63e0d17b718/coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d", size = 247744, upload-time = "2025-10-15T15:13:14.554Z" }, + { url = "https://files.pythonhosted.org/packages/16/02/d500b91f5471b2975947e0629b8980e5e90786fe316b6d7299852c1d793d/coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d", size = 247325, upload-time = "2025-10-15T15:13:16.438Z" }, + { url = "https://files.pythonhosted.org/packages/77/11/dee0284fbbd9cd64cfce806b827452c6df3f100d9e66188e82dfe771d4af/coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2", size = 249180, upload-time = "2025-10-15T15:13:17.959Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/cdf1def928f0a150a057cab03286774e73e29c2395f0d30ce3d9e9f8e697/coverage-7.11.0-cp312-cp312-win32.whl", hash = "sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5", size = 218479, upload-time = "2025-10-15T15:13:19.608Z" }, + { url = "https://files.pythonhosted.org/packages/ff/55/e5884d55e031da9c15b94b90a23beccc9d6beee65e9835cd6da0a79e4f3a/coverage-7.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0", size = 219290, upload-time = "2025-10-15T15:13:21.593Z" }, + { url = "https://files.pythonhosted.org/packages/23/a8/faa930cfc71c1d16bc78f9a19bb73700464f9c331d9e547bfbc1dbd3a108/coverage-7.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad", size = 217924, upload-time = "2025-10-15T15:13:23.39Z" }, + { url = "https://files.pythonhosted.org/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1", size = 216129, upload-time = "2025-10-15T15:13:25.371Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be", size = 216380, upload-time = "2025-10-15T15:13:26.976Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d", size = 247375, upload-time = "2025-10-15T15:13:28.923Z" }, + { url = "https://files.pythonhosted.org/packages/65/6c/f7f59c342359a235559d2bc76b0c73cfc4bac7d61bb0df210965cb1ecffd/coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82", size = 249978, upload-time = "2025-10-15T15:13:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8c/042dede2e23525e863bf1ccd2b92689692a148d8b5fd37c37899ba882645/coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52", size = 251253, upload-time = "2025-10-15T15:13:32.174Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/3c58df67bfa809a7bddd786356d9c5283e45d693edb5f3f55d0986dd905a/coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b", size = 247591, upload-time = "2025-10-15T15:13:34.147Z" }, + { url = "https://files.pythonhosted.org/packages/26/5b/c7f32efd862ee0477a18c41e4761305de6ddd2d49cdeda0c1116227570fd/coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4", size = 249411, upload-time = "2025-10-15T15:13:38.425Z" }, + { url = "https://files.pythonhosted.org/packages/76/b5/78cb4f1e86c1611431c990423ec0768122905b03837e1b4c6a6f388a858b/coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd", size = 247303, upload-time = "2025-10-15T15:13:40.464Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/23c753a8641a330f45f221286e707c427e46d0ffd1719b080cedc984ec40/coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc", size = 247157, upload-time = "2025-10-15T15:13:42.087Z" }, + { url = "https://files.pythonhosted.org/packages/c5/42/6e0cc71dc8a464486e944a4fa0d85bdec031cc2969e98ed41532a98336b9/coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48", size = 248921, upload-time = "2025-10-15T15:13:43.715Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1c/743c2ef665e6858cccb0f84377dfe3a4c25add51e8c7ef19249be92465b6/coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040", size = 218526, upload-time = "2025-10-15T15:13:45.336Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d5/226daadfd1bf8ddbccefbd3aa3547d7b960fb48e1bdac124e2dd13a2b71a/coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05", size = 219317, upload-time = "2025-10-15T15:13:47.401Z" }, + { url = "https://files.pythonhosted.org/packages/97/54/47db81dcbe571a48a298f206183ba8a7ba79200a37cd0d9f4788fcd2af4a/coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a", size = 217948, upload-time = "2025-10-15T15:13:49.096Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8b/cb68425420154e7e2a82fd779a8cc01549b6fa83c2ad3679cd6c088ebd07/coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b", size = 216837, upload-time = "2025-10-15T15:13:51.09Z" }, + { url = "https://files.pythonhosted.org/packages/33/55/9d61b5765a025685e14659c8d07037247de6383c0385757544ffe4606475/coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37", size = 217061, upload-time = "2025-10-15T15:13:52.747Z" }, + { url = "https://files.pythonhosted.org/packages/52/85/292459c9186d70dcec6538f06ea251bc968046922497377bf4a1dc9a71de/coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de", size = 258398, upload-time = "2025-10-15T15:13:54.45Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e2/46edd73fb8bf51446c41148d81944c54ed224854812b6ca549be25113ee0/coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f", size = 260574, upload-time = "2025-10-15T15:13:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/07/5e/1df469a19007ff82e2ca8fe509822820a31e251f80ee7344c34f6cd2ec43/coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c", size = 262797, upload-time = "2025-10-15T15:13:58.635Z" }, + { url = "https://files.pythonhosted.org/packages/f9/50/de216b31a1434b94d9b34a964c09943c6be45069ec704bfc379d8d89a649/coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa", size = 257361, upload-time = "2025-10-15T15:14:00.409Z" }, + { url = "https://files.pythonhosted.org/packages/82/1e/3f9f8344a48111e152e0fd495b6fff13cc743e771a6050abf1627a7ba918/coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740", size = 260349, upload-time = "2025-10-15T15:14:02.188Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/3f52741f9e7d82124272f3070bbe316006a7de1bad1093f88d59bfc6c548/coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef", size = 258114, upload-time = "2025-10-15T15:14:03.907Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8b/918f0e15f0365d50d3986bbd3338ca01178717ac5678301f3f547b6619e6/coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0", size = 256723, upload-time = "2025-10-15T15:14:06.324Z" }, + { url = "https://files.pythonhosted.org/packages/44/9e/7776829f82d3cf630878a7965a7d70cc6ca94f22c7d20ec4944f7148cb46/coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca", size = 259238, upload-time = "2025-10-15T15:14:08.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b8/49cf253e1e7a3bedb85199b201862dd7ca4859f75b6cf25ffa7298aa0760/coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2", size = 219180, upload-time = "2025-10-15T15:14:09.786Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e1/1a541703826be7ae2125a0fb7f821af5729d56bb71e946e7b933cc7a89a4/coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268", size = 220241, upload-time = "2025-10-15T15:14:11.471Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/5ee0e0a08621140fd418ec4020f595b4d52d7eb429ae6a0c6542b4ba6f14/coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836", size = 218510, upload-time = "2025-10-15T15:14:13.46Z" }, + { url = "https://files.pythonhosted.org/packages/f4/06/e923830c1985ce808e40a3fa3eb46c13350b3224b7da59757d37b6ce12b8/coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497", size = 216110, upload-time = "2025-10-15T15:14:15.157Z" }, + { url = "https://files.pythonhosted.org/packages/42/82/cdeed03bfead45203fb651ed756dfb5266028f5f939e7f06efac4041dad5/coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e", size = 216395, upload-time = "2025-10-15T15:14:16.863Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ba/e1c80caffc3199aa699813f73ff097bc2df7b31642bdbc7493600a8f1de5/coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1", size = 247433, upload-time = "2025-10-15T15:14:18.589Z" }, + { url = "https://files.pythonhosted.org/packages/80/c0/5b259b029694ce0a5bbc1548834c7ba3db41d3efd3474489d7efce4ceb18/coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca", size = 249970, upload-time = "2025-10-15T15:14:20.307Z" }, + { url = "https://files.pythonhosted.org/packages/8c/86/171b2b5e1aac7e2fd9b43f7158b987dbeb95f06d1fbecad54ad8163ae3e8/coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd", size = 251324, upload-time = "2025-10-15T15:14:22.419Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/7e10414d343385b92024af3932a27a1caf75c6e27ee88ba211221ff1a145/coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43", size = 247445, upload-time = "2025-10-15T15:14:24.205Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/e4f966b21f5be8c4bf86ad75ae94efa0de4c99c7bbb8114476323102e345/coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777", size = 249324, upload-time = "2025-10-15T15:14:26.234Z" }, + { url = "https://files.pythonhosted.org/packages/00/a2/8479325576dfcd909244d0df215f077f47437ab852ab778cfa2f8bf4d954/coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2", size = 247261, upload-time = "2025-10-15T15:14:28.42Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d8/3a9e2db19d94d65771d0f2e21a9ea587d11b831332a73622f901157cc24b/coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d", size = 247092, upload-time = "2025-10-15T15:14:30.784Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b1/bbca3c472544f9e2ad2d5116b2379732957048be4b93a9c543fcd0207e5f/coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4", size = 248755, upload-time = "2025-10-15T15:14:32.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/49/638d5a45a6a0f00af53d6b637c87007eb2297042186334e9923a61aa8854/coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721", size = 218793, upload-time = "2025-10-15T15:14:34.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/cc/b675a51f2d068adb3cdf3799212c662239b0ca27f4691d1fff81b92ea850/coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad", size = 219587, upload-time = "2025-10-15T15:14:37.047Z" }, + { url = "https://files.pythonhosted.org/packages/93/98/5ac886876026de04f00820e5094fe22166b98dcb8b426bf6827aaf67048c/coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479", size = 218168, upload-time = "2025-10-15T15:14:38.861Z" }, + { url = "https://files.pythonhosted.org/packages/14/d1/b4145d35b3e3ecf4d917e97fc8895bcf027d854879ba401d9ff0f533f997/coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f", size = 216850, upload-time = "2025-10-15T15:14:40.651Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d1/7f645fc2eccd318369a8a9948acc447bb7c1ade2911e31d3c5620544c22b/coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e", size = 217071, upload-time = "2025-10-15T15:14:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/64d124649db2737ceced1dfcbdcb79898d5868d311730f622f8ecae84250/coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44", size = 258570, upload-time = "2025-10-15T15:14:44.542Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3f/6f5922f80dc6f2d8b2c6f974835c43f53eb4257a7797727e6ca5b7b2ec1f/coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3", size = 260738, upload-time = "2025-10-15T15:14:46.436Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5f/9e883523c4647c860b3812b417a2017e361eca5b635ee658387dc11b13c1/coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b", size = 262994, upload-time = "2025-10-15T15:14:48.3Z" }, + { url = "https://files.pythonhosted.org/packages/07/bb/43b5a8e94c09c8bf51743ffc65c4c841a4ca5d3ed191d0a6919c379a1b83/coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d", size = 257282, upload-time = "2025-10-15T15:14:50.236Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e5/0ead8af411411330b928733e1d201384b39251a5f043c1612970310e8283/coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2", size = 260430, upload-time = "2025-10-15T15:14:52.413Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/03dd8bb0ba5b971620dcaac145461950f6d8204953e535d2b20c6b65d729/coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e", size = 258190, upload-time = "2025-10-15T15:14:54.268Z" }, + { url = "https://files.pythonhosted.org/packages/45/ae/28a9cce40bf3174426cb2f7e71ee172d98e7f6446dff936a7ccecee34b14/coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996", size = 256658, upload-time = "2025-10-15T15:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7c/3a44234a8599513684bfc8684878fd7b126c2760f79712bb78c56f19efc4/coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11", size = 259342, upload-time = "2025-10-15T15:14:58.538Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e6/0108519cba871af0351725ebdb8660fd7a0fe2ba3850d56d32490c7d9b4b/coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73", size = 219568, upload-time = "2025-10-15T15:15:00.382Z" }, + { url = "https://files.pythonhosted.org/packages/c9/76/44ba876e0942b4e62fdde23ccb029ddb16d19ba1bef081edd00857ba0b16/coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547", size = 220687, upload-time = "2025-10-15T15:15:02.322Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0c/0df55ecb20d0d0ed5c322e10a441775e1a3a5d78c60f0c4e1abfe6fcf949/coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3", size = 218711, upload-time = "2025-10-15T15:15:04.575Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9' and platform_python_implementation != 'PyPy'" }, + { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, +] + +[[package]] +name = "django" +version = "4.2.25" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref", version = "3.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "asgiref", version = "3.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "backports-zoneinfo", marker = "python_full_version < '3.9'" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/f1/230c6c20a77f8f1812c01dfd0166416e7c000a43e05f701b0b83301ebfc1/django-4.2.25.tar.gz", hash = "sha256:2391ab3d78191caaae2c963c19fd70b99e9751008da22a0adcc667c5a4f8d311", size = 10456257, upload-time = "2025-10-01T15:05:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/5d/2210dcf9a03931be94072deab1de2d3b73fa62ce91714eaea9e69f6e35c6/django-4.2.25-py3-none-any.whl", hash = "sha256:9584cf26b174b35620e53c2558b09d7eb180a655a3470474f513ff9acb494f8c", size = 7993964, upload-time = "2025-10-01T15:05:46.545Z" }, +] + +[[package]] +name = "django-cors-headers" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/f4/8b8c3d5e9a0aeea43576fbe623599052a7699abb54378ddb44adb1ef1ed3/django_cors_headers-3.14.0.tar.gz", hash = "sha256:5fbd58a6fb4119d975754b2bc090f35ec160a8373f276612c675b00e8a138739", size = 24892, upload-time = "2023-02-25T07:20:18.41Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/f1/972b3a183192841b811e4b7496de0a31c09ae1d83a5391743c8ae0cbd86e/django_cors_headers-3.14.0-py3-none-any.whl", hash = "sha256:684180013cc7277bdd8702b80a3c5a4b3fcae4abb2bf134dceb9f5dfe300228e", size = 13187, upload-time = "2023-02-25T07:20:16.376Z" }, +] + +[[package]] +name = "django-environ" +version = "0.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/0b/f2c024529ee4bbf8b95176eebeb86c6e695192a9ce0e91059cb83a33c1d3/django-environ-0.11.2.tar.gz", hash = "sha256:f32a87aa0899894c27d4e1776fa6b477e8164ed7f6b3e410a62a6d72caaf64be", size = 54326, upload-time = "2023-09-01T21:03:02.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/f1/468b49cccba3b42dda571063a14c668bb0b53a1d5712426d18e36663bd53/django_environ-0.11.2-py2.py3-none-any.whl", hash = "sha256:0ff95ab4344bfeff693836aa978e6840abef2e2f1145adff7735892711590c05", size = 19141, upload-time = "2023-09-01T21:02:59.88Z" }, +] + +[[package]] +name = "django-oauth-toolkit" +source = { editable = "." } +dependencies = [ + { name = "django" }, + { name = "jwcrypto" }, + { name = "oauthlib" }, + { name = "requests", version = "2.32.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] + +[package.optional-dependencies] +dev = [ + { name = "m2r" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest-cov", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pytest-cov", version = "7.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "sphinx-rtd-theme" }, +] + +[package.metadata] +requires-dist = [ + { name = "django", specifier = ">=4.2" }, + { name = "jwcrypto", specifier = ">=1.5.0" }, + { name = "m2r", marker = "extra == 'dev'" }, + { name = "oauthlib", specifier = ">=3.3.0" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "pytest-cov", marker = "extra == 'dev'" }, + { name = "requests", specifier = ">=2.13.0" }, + { name = "sphinx-rtd-theme", marker = "extra == 'dev'" }, +] +provides-extras = ["dev"] + +[[package]] +name = "docutils" +version = "0.20.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/53/a5da4f2c5739cf66290fac1431ee52aff6851c7c8ffd8264f13affd7bcdd/docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b", size = 2058365, upload-time = "2023-05-16T23:39:19.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/87/f238c0670b94533ac0353a4e2a1a771a0cc73277b88bff23d3ae35a256c1/docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6", size = 572666, upload-time = "2023-05-16T23:39:15.976Z" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "idp" +version = "0.1.0" +source = { virtual = "tests/app/idp" } +dependencies = [ + { name = "django" }, + { name = "django-cors-headers" }, + { name = "django-environ" }, + { name = "django-oauth-toolkit" }, +] + +[package.metadata] +requires-dist = [ + { name = "django", specifier = ">=4.2,<=5.2" }, + { name = "django-cors-headers", specifier = "==3.14.0" }, + { name = "django-environ", specifier = "==0.11.2" }, + { name = "django-oauth-toolkit", editable = "." }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "zipp", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "zipp", version = "3.23.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +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 = "jwcrypto" +version = "1.5.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/db/870e5d5fb311b0bcf049630b5ba3abca2d339fd5e13ba175b4c13b456d08/jwcrypto-1.5.6.tar.gz", hash = "sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039", size = 87168, upload-time = "2024-03-06T19:58:31.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/58/4a1880ea64032185e9ae9f63940c9327c6952d5584ea544a8f66972f2fda/jwcrypto-1.5.6-py3-none-any.whl", hash = "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789", size = 92520, upload-time = "2024-03-06T19:58:29.765Z" }, +] + +[[package]] +name = "m2r" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils", version = "0.20.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mistune" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/65/fd40fbdc608298e760affb95869c3baed237dfe5649d62da1eaa1deca8f3/m2r-0.3.1.tar.gz", hash = "sha256:aafb67fc49cfb1d89e46a3443ac747e15f4bb42df20ed04f067ad9efbee256ab", size = 16622, upload-time = "2022-11-17T08:12:08.781Z" } + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +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" }, + { url = "https://files.pythonhosted.org/packages/f8/ff/2c942a82c35a49df5de3a630ce0a8456ac2969691b230e530ac12314364c/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", size = 18192, upload-time = "2024-02-02T16:30:57.715Z" }, + { url = "https://files.pythonhosted.org/packages/4f/14/6f294b9c4f969d0c801a4615e221c1e084722ea6114ab2114189c5b8cbe0/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", size = 14072, upload-time = "2024-02-02T16:30:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/81/d4/fd74714ed30a1dedd0b82427c02fa4deec64f173831ec716da11c51a50aa/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", size = 26928, upload-time = "2024-02-02T16:30:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/50319665ce81bb10e90d1cf76f9e1aa269ea6f7fa30ab4521f14d122a3df/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", size = 26106, upload-time = "2024-02-02T16:31:01.582Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6f/f2b0f675635b05f6afd5ea03c094557bdb8622fa8e673387444fe8d8e787/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68", size = 25781, upload-time = "2024-02-02T16:31:02.71Z" }, + { url = "https://files.pythonhosted.org/packages/51/e0/393467cf899b34a9d3678e78961c2c8cdf49fb902a959ba54ece01273fb1/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", size = 30518, upload-time = "2024-02-02T16:31:04.392Z" }, + { url = "https://files.pythonhosted.org/packages/f6/02/5437e2ad33047290dafced9df741d9efc3e716b75583bbd73a9984f1b6f7/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", size = 29669, upload-time = "2024-02-02T16:31:05.53Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7d/968284145ffd9d726183ed6237c77938c021abacde4e073020f920e060b2/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", size = 29933, upload-time = "2024-02-02T16:31:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f3/ecb00fc8ab02b7beae8699f34db9357ae49d9f21d4d3de6f305f34fa949e/MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", size = 16656, upload-time = "2024-02-02T16:31:07.767Z" }, + { url = "https://files.pythonhosted.org/packages/92/21/357205f03514a49b293e214ac39de01fadd0970a6e05e4bf1ddd0ffd0881/MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", size = 17206, upload-time = "2024-02-02T16:31:08.843Z" }, + { url = "https://files.pythonhosted.org/packages/0f/31/780bb297db036ba7b7bbede5e1d7f1e14d704ad4beb3ce53fb495d22bc62/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", size = 18193, upload-time = "2024-02-02T16:31:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d77701bbef72892affe060cdacb7a2ed7fd68dae3b477a8642f15ad3b132/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", size = 14073, upload-time = "2024-02-02T16:31:11.442Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a7/1e558b4f78454c8a3a0199292d96159eb4d091f983bc35ef258314fe7269/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", size = 26486, upload-time = "2024-02-02T16:31:12.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", size = 25685, upload-time = "2024-02-02T16:31:13.726Z" }, + { url = "https://files.pythonhosted.org/packages/6a/18/ae5a258e3401f9b8312f92b028c54d7026a97ec3ab20bfaddbdfa7d8cce8/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", size = 25338, upload-time = "2024-02-02T16:31:14.812Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cc/48206bd61c5b9d0129f4d75243b156929b04c94c09041321456fd06a876d/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", size = 30439, upload-time = "2024-02-02T16:31:15.946Z" }, + { url = "https://files.pythonhosted.org/packages/d1/06/a41c112ab9ffdeeb5f77bc3e331fdadf97fa65e52e44ba31880f4e7f983c/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", size = 29531, upload-time = "2024-02-02T16:31:17.13Z" }, + { url = "https://files.pythonhosted.org/packages/02/8c/ab9a463301a50dab04d5472e998acbd4080597abc048166ded5c7aa768c8/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", size = 29823, upload-time = "2024-02-02T16:31:18.247Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/9bc18da763496b055d8e98ce476c8e718dcfd78157e17f555ce6dd7d0895/MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", size = 16658, upload-time = "2024-02-02T16:31:19.583Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", size = 17211, upload-time = "2024-02-02T16:31:20.96Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, + { url = "https://files.pythonhosted.org/packages/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, + { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, +] + +[[package]] +name = "mistune" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/a4/509f6e7783ddd35482feda27bc7f72e65b5e7dc910eca4ab2164daf9c577/mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e", size = 58322, upload-time = "2018-10-11T06:59:27.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/ec/4b43dae793655b7d8a25f76119624350b4d65eb663459eb9603d7f1f0345/mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4", size = 16220, upload-time = "2018-10-11T06:59:26.044Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +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 = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "packaging", marker = "python_full_version < '3.9'" }, + { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.9' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.9'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pygments", marker = "python_full_version >= '3.9'" }, + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.9'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042, upload-time = "2024-03-24T20:16:34.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990, upload-time = "2024-03-24T20:16:32.444Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version == '3.9.*'" }, + { name = "coverage", version = "7.11.0", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version < '3.9'" }, + { name = "charset-normalizer", marker = "python_full_version < '3.9'" }, + { name = "idna", marker = "python_full_version < '3.9'" }, + { name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.9'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.9'" }, + { name = "idna", marker = "python_full_version >= '3.9'" }, + { name = "urllib3", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "roman-numerals-py" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017, upload-time = "2025-02-22T07:34:54.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "sphinx" +version = "7.1.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "alabaster", version = "0.7.13", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "babel", marker = "python_full_version < '3.9'" }, + { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.20.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "imagesize", marker = "python_full_version < '3.9'" }, + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "jinja2", marker = "python_full_version < '3.9'" }, + { name = "packaging", marker = "python_full_version < '3.9'" }, + { name = "pygments", marker = "python_full_version < '3.9'" }, + { name = "requests", version = "2.32.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.9'" }, + { name = "sphinxcontrib-applehelp", version = "1.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinxcontrib-devhelp", version = "1.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinxcontrib-htmlhelp", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.9'" }, + { name = "sphinxcontrib-qthelp", version = "1.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinxcontrib-serializinghtml", version = "1.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/01/688bdf9282241dca09fe6e3a1110eda399fa9b10d0672db609e37c2e7a39/sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f", size = 6828258, upload-time = "2023-08-02T02:06:09.375Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/17/325cf6a257d84751a48ae90752b3d8fe0be8f9535b6253add61c49d0d9bc/sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe", size = 3169543, upload-time = "2023-08-02T02:06:06.816Z" }, +] + +[[package]] +name = "sphinx" +version = "7.4.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "babel", marker = "python_full_version == '3.9.*'" }, + { name = "colorama", marker = "python_full_version == '3.9.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "imagesize", marker = "python_full_version == '3.9.*'" }, + { name = "importlib-metadata", version = "8.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "jinja2", marker = "python_full_version == '3.9.*'" }, + { name = "packaging", marker = "python_full_version == '3.9.*'" }, + { name = "pygments", marker = "python_full_version == '3.9.*'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.9.*'" }, + { name = "sphinxcontrib-applehelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "sphinxcontrib-devhelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "sphinxcontrib-htmlhelp", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.9.*'" }, + { name = "sphinxcontrib-qthelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "sphinxcontrib-serializinghtml", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "tomli", marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624, upload-time = "2024-07-20T14:46:52.142Z" }, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "babel", marker = "python_full_version == '3.10.*'" }, + { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "imagesize", marker = "python_full_version == '3.10.*'" }, + { name = "jinja2", marker = "python_full_version == '3.10.*'" }, + { name = "packaging", marker = "python_full_version == '3.10.*'" }, + { name = "pygments", marker = "python_full_version == '3.10.*'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-applehelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-devhelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-htmlhelp", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-qthelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-serializinghtml", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, +] + +[[package]] +name = "sphinx" +version = "8.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "babel", marker = "python_full_version >= '3.11'" }, + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "imagesize", marker = "python_full_version >= '3.11'" }, + { name = "jinja2", marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "roman-numerals-py", marker = "python_full_version >= '3.11'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-applehelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-devhelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-htmlhelp", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-qthelp", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-serializinghtml", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" }, +] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils", version = "0.20.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "sphinx", version = "7.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinxcontrib-jquery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/44/c97faec644d29a5ceddd3020ae2edffa69e7d00054a8c7a6021e82f20335/sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85", size = 7620463, upload-time = "2024-11-13T11:06:04.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/77/46e3bac77b82b4df5bb5b61f2de98637724f246b4966cfc34bc5895d852a/sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13", size = 7655561, upload-time = "2024-11-13T11:06:02.094Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/32/df/45e827f4d7e7fcc84e853bcef1d836effd762d63ccb86f43ede4e98b478c/sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e", size = 24766, upload-time = "2023-01-23T09:41:54.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/c1/5e2cafbd03105ce50d8500f9b4e8a6e8d02e22d0475b574c3b3e9451a15f/sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228", size = 120601, upload-time = "2023-01-23T09:41:52.364Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/98/33/dc28393f16385f722c893cb55539c641c9aaec8d1bc1c15b69ce0ac2dbb3/sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4", size = 17398, upload-time = "2020-02-29T04:14:43.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/09/5de5ed43a521387f18bdf5f5af31d099605c992fd25372b2b9b825ce48ee/sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", size = 84690, upload-time = "2020-02-29T04:14:40.765Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/47/64cff68ea3aa450c373301e5bebfbb9fce0a3e70aca245fcadd4af06cd75/sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff", size = 27967, upload-time = "2023-01-31T17:29:20.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/ee/a1f5e39046cbb5f8bc8fba87d1ddf1c6643fbc9194e58d26e606de4b9074/sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903", size = 99833, upload-time = "2023-01-31T17:29:18.489Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "7.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/8e/c4846e59f38a5f2b4a0e3b27af38f2fcf904d4bfd82095bf92de0b114ebd/sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", size = 21658, upload-time = "2020-02-29T04:19:10.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/14/05f9206cf4e9cfca1afb5fd224c7cd434dcc3a433d6d9e4e0264d29c6cdb/sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6", size = 90609, upload-time = "2020-02-29T04:19:08.451Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/72/835d6fadb9e5d02304cf39b18f93d227cd93abd3c41ebf58e6853eeb1455/sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952", size = 21019, upload-time = "2021-05-22T16:07:43.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/77/5464ec50dd0f1c1037e3c93249b040c8fc8078fdda97530eeb02424b6eea/sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", size = 94021, upload-time = "2021-05-22T16:07:41.627Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "sqlparse" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload-time = "2024-12-10T12:05:30.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +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 = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +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 = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "zipp" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199, upload-time = "2024-09-13T13:44:16.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200, upload-time = "2024-09-13T13:44:14.38Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]