├── tests ├── __init__.py ├── conftest.py └── test_ext.py ├── config ├── tmpfiles.conf ├── systemd-httpd-service.conf ├── gssproxy.conf └── httpd.conf ├── .github ├── renovate.json ├── release.yml └── workflows │ └── main.yml ├── flask_mod_auth_gssapi ├── __init__.py └── ext.py ├── .gitignore ├── README.md ├── .pre-commit-config.yaml ├── tox.ini ├── LICENSE ├── pyproject.toml └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/tmpfiles.conf: -------------------------------------------------------------------------------- 1 | # 2 | # /usr/lib/tmpfiles.d/foobar.conf 3 | # 4 | 5 | d /run/foobar 0770 apache apache 6 | d /run/foobar/ccaches 0770 apache apache 7 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["local>fedora-infra/shared:renovate-config"] 4 | } 5 | -------------------------------------------------------------------------------- /config/systemd-httpd-service.conf: -------------------------------------------------------------------------------- 1 | # 2 | # /usr/lib/systemd/system/httpd.service.d/foobar.conf 3 | # 4 | 5 | [Service] 6 | Environment=KRB5CCNAME=/tmp/krb5cc-httpd 7 | Environment=GSS_USE_PROXY=yes 8 | -------------------------------------------------------------------------------- /config/gssproxy.conf: -------------------------------------------------------------------------------- 1 | # 2 | # /etc/gssproxy/90-foobar.conf 3 | # 4 | 5 | [service/foobar-httpd] 6 | mechs = krb5 7 | cred_store = keytab:/var/lib/gssproxy/httpd.keytab 8 | cred_store = client_keytab:/var/lib/gssproxy/httpd.keytab 9 | allow_constrained_delegation = true 10 | allow_client_ccache_sync = true 11 | cred_usage = both 12 | euid = apache 13 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - dependencies 5 | categories: 6 | - title: Breaking Changes 7 | labels: 8 | - breaking-change 9 | - title: New features 10 | labels: 11 | - enhancement 12 | - title: Fixes 13 | labels: 14 | - bug 15 | - title: Misc Changes 16 | labels: 17 | - '*' 18 | -------------------------------------------------------------------------------- /flask_mod_auth_gssapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .ext import FlaskModAuthGSSAPI # noqa: F401 2 | 3 | # Set the version 4 | try: 5 | import importlib.metadata 6 | 7 | __version__ = importlib.metadata.version("flask_mod_auth_gssapi") 8 | except ImportError: 9 | try: 10 | import pkg_resources 11 | 12 | try: 13 | __version__ = pkg_resources.get_distribution( 14 | "flask_mod_auth_gssapi" 15 | ).version 16 | except pkg_resources.DistributionNotFound: 17 | __version__ = None 18 | except ImportError: 19 | __version__ = None 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # Distribution / packaging 6 | build/ 7 | develop-eggs/ 8 | dist/ 9 | eggs/ 10 | .eggs/ 11 | lib/ 12 | lib64/ 13 | sdist/ 14 | var/ 15 | wheels/ 16 | *.egg-info/ 17 | .installed.cfg 18 | *.egg 19 | 20 | # Unit test / coverage reports 21 | htmlcov/ 22 | .tox/ 23 | .coverage 24 | .coverage.* 25 | .cache 26 | nosetests.xml 27 | coverage.xml 28 | 29 | # Sphinx documentation 30 | docs/_build/ 31 | docs/_source/ 32 | 33 | # pytest 34 | .pytest_cache/ 35 | 36 | # VS Code 37 | .vscode 38 | 39 | # Vagrant 40 | .vagrant 41 | 42 | # Translations 43 | *.mo 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask Mod Auth GSSAPI 2 | 3 | 4 | A Flask extention to make use of the authentication provided by the 5 | [mod_auth_gssapi](https://github.com/gssapi/mod_auth_gssapi) extention of 6 | Apache's HTTPd. See [FASJSON](https://github.com/fedora-infra/fasjson) for a 7 | usage example. 8 | 9 | If you're using sessions from `mod_session` with `mod_auth_gssapi`, set your 10 | application's `MOD_AUTH_GSSAPI_SESSION_HEADER` configuration variable to the 11 | value you used in Apache's configuration file for `SessionHeader`. This will 12 | signal `mod_session` to invalidate the session when the authentication 13 | credential has expired. 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | # Generic hooks 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v6.0.0 7 | hooks: 8 | - id: trailing-whitespace 9 | - id: end-of-file-fixer 10 | - id: check-yaml 11 | - id: check-added-large-files 12 | 13 | # Formatting 14 | - repo: https://github.com/psf/black 15 | rev: 25.12.0 16 | hooks: 17 | - id: black 18 | 19 | # Ruff 20 | - repo: https://github.com/charliermarsh/ruff-pre-commit 21 | # Ruff version. 22 | rev: v0.14.9 23 | hooks: 24 | - id: ruff 25 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from types import SimpleNamespace 2 | 3 | import pytest 4 | from flask import Flask 5 | 6 | from flask_mod_auth_gssapi import FlaskModAuthGSSAPI 7 | 8 | 9 | @pytest.fixture 10 | def app(): 11 | app = Flask("test") 12 | FlaskModAuthGSSAPI(app) 13 | app.config["TESTING"] = True 14 | return app 15 | 16 | 17 | @pytest.fixture 18 | def wsgi_env(): 19 | return {"KRB5CCNAME": "/tmp/ignore", "GSS_NAME": "dummy@EXAMPLE.TEST"} # noqa: S108 20 | 21 | 22 | @pytest.fixture 23 | def credential(mocker): 24 | creds_factory = mocker.patch("gssapi.Credentials") 25 | cred = creds_factory.return_value = SimpleNamespace(lifetime=10) 26 | return cred 27 | 28 | 29 | @pytest.fixture 30 | def expired_credential(credential): 31 | credential.lifetime = 0 32 | return credential 33 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = lint,format,{py310,py311,py312,py313,py314}-flask{2,3} 3 | isolated_build = true 4 | 5 | [testenv] 6 | passenv = HOME 7 | sitepackages = false 8 | skip_install = true 9 | allowlist_externals = 10 | poetry 11 | commands_pre = 12 | poetry install --all-extras 13 | flask1: poetry run pip install flask<2.0.0 markupsafe<2.1.0 14 | flask2: poetry run pip install flask<3.0.0 15 | flask3: poetry run pip install flask<4.0.0 16 | commands = 17 | poetry run pytest -vv --cov --cov-report html --cov-report term-missing tests {posargs} 18 | 19 | [testenv:lint] 20 | commands = 21 | poetry run ruff check {posargs:.} 22 | 23 | [testenv:format] 24 | commands = 25 | poetry run black --check --diff {posargs:.} 26 | 27 | 28 | # We use Ruff instead of flake8 but configure it appropriately so it doesn’t 29 | # complain, e.g. if it’s run via a global hook. 30 | [flake8] 31 | max-line-length = 100 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Fedora Infrastructure 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/httpd.conf: -------------------------------------------------------------------------------- 1 | # 2 | # /etc/httpd/conf.d/foobar.conf 3 | # 4 | 5 | WSGISocketPrefix /run/httpd/wsgi 6 | WSGIPythonHome /srv/venv 7 | WSGIDaemonProcess foobar processes=4 threads=1 maximum-requests=500 \ 8 | display-name=%{GROUP} socket-timeout=2147483647 \ 9 | lang=C.UTF-8 locale=C.UTF-8 10 | WSGIImportScript /srv/foobar.wsgi \ 11 | process-group=foobar application-group=foobar 12 | WSGIScriptAlias /foobar /srv/foobar.wsgi 13 | WSGIScriptReloading Off 14 | 15 | 16 | WSGIProcessGroup foobar 17 | WSGIApplicationGroup foobar 18 | 19 | AuthType GSSAPI 20 | AuthName "Kerberos Login" 21 | GssapiUseSessions On 22 | Session On 23 | SessionCookieName foobar_session path=/foobar;httponly;secure; 24 | SessionHeader X-Replace-Session 25 | GssapiSessionKey file:/run/foobar/session.key 26 | 27 | GssapiImpersonate On 28 | GssapiDelegCcacheDir /run/foobar/ccaches 29 | GssapiDelegCcachePerms mode:0660 30 | GssapiUseS4U2Proxy on 31 | GssapiAllowedMech krb5 32 | 33 | Require valid-user 34 | 35 | Header always append X-Frame-Options DENY 36 | Header always append Content-Security-Policy "frame-ancestors 'none'" 37 | Header unset Set-Cookie 38 | Header unset ETag 39 | FileETag None 40 | 41 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "flask-mod-auth-gssapi" 3 | version = "1.1.1" 4 | description = "A Flask extention to make use of the authentication provided by the mod_auth_gssapi extention of Apache's HTTPd." 5 | 6 | license = "MIT" 7 | 8 | authors = [ 9 | "Fedora Infrastructure " 10 | ] 11 | 12 | readme = "README.md" 13 | keywords = ["security", "web"] 14 | repository = "https://github.com/fedora-infra/flask-mod-auth-gssapi" 15 | homepage = "https://github.com/fedora-infra/flask-mod-auth-gssapi" 16 | 17 | include = [ 18 | "tox.ini", 19 | "config/*", 20 | "docs/*/*", 21 | "tests/*", 22 | ] 23 | 24 | classifiers = [ 25 | "Environment :: Web Environment", 26 | "Intended Audience :: Developers", 27 | "License :: OSI Approved :: MIT License", 28 | "Operating System :: OS Independent", 29 | "Programming Language :: Python", 30 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 31 | "Topic :: Software Development :: Libraries :: Python Modules" 32 | ] 33 | 34 | [tool.poetry.dependencies] 35 | python = "^3.10" 36 | flask = "^2.0.0 || ^3.0.0" 37 | gssapi = "^1.6.2" 38 | 39 | [tool.poetry.group.dev.dependencies] 40 | pytest = "*" 41 | pytest-mock = "*" 42 | pytest-cov = "*" 43 | black = "*" 44 | coverage = {extras = ["toml"], version = "*"} 45 | ruff = "*" 46 | 47 | [tool.ruff] 48 | line-length = 100 49 | 50 | [tool.ruff.lint] 51 | select = ["E", "F", "W", "I", "UP", "S", "B", "RUF"] 52 | # ignore = ["RUF010", "UP038"] 53 | 54 | [tool.ruff.lint.per-file-ignores] 55 | "tests/*" = ["S101"] 56 | 57 | [tool.pytest.ini_options] 58 | testpaths = [ 59 | "tests", 60 | ] 61 | 62 | [tool.coverage.run] 63 | branch = true 64 | source = ["flask_mod_auth_gssapi"] 65 | 66 | [tool.coverage.paths] 67 | source = ["flask_mod_auth_gssapi"] 68 | 69 | [tool.coverage.report] 70 | fail_under = 100 71 | exclude_lines = [ 72 | "pragma: no cover", 73 | "if __name__ == .__main__.:", 74 | ] 75 | omit = [ 76 | "flask_mod_auth_gssapi/__init__.py" 77 | ] 78 | 79 | 80 | [build-system] 81 | requires = ["poetry>=1.1.4"] 82 | build-backend = "poetry.masonry.api" 83 | -------------------------------------------------------------------------------- /flask_mod_auth_gssapi/ext.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import gssapi 4 | from flask import abort, current_app, g, request 5 | from werkzeug.exceptions import Unauthorized 6 | 7 | 8 | class FlaskModAuthGSSAPI: 9 | def __init__(self, app=None, abort=abort): 10 | self.abort = abort 11 | if app is not None: 12 | self.init_app(app) 13 | 14 | def init_app(self, app): 15 | app.before_request(self._gssapi_check) 16 | app.config.setdefault("MOD_AUTH_GSSAPI_SESSION_HEADER", "X-Replace-Session") 17 | 18 | def _gssapi_check(self): 19 | g.gss_name = g.gss_creds = g.principal = g.username = None 20 | 21 | wsgi_env = request.environ 22 | if wsgi_env["wsgi.multithread"]: 23 | self.abort( 24 | 500, 25 | "GSSAPI is not compatible with multi-threaded WSGI servers.", 26 | ) 27 | 28 | ccache = wsgi_env.get("KRB5CCNAME") 29 | if not ccache: 30 | return # Maybe the endpoint is not protected, stop here 31 | 32 | # The C libraries will look for the cache in the process' environment variables 33 | os.environ["KRB5CCNAME"] = ccache 34 | 35 | principal = wsgi_env.get("GSS_NAME") 36 | if not principal: 37 | return # Maybe the endpoint is not protected, stop here 38 | 39 | ccache_type, _sep, ccache_location = ccache.partition(":") 40 | if ccache_type == "FILE" and not os.path.exists(ccache_location): 41 | current_app.logger.warning( 42 | "Delegated credentials not found: %r", ccache_location 43 | ) 44 | self._authenticate() 45 | 46 | gss_name = gssapi.Name(principal, gssapi.NameType.kerberos_principal) 47 | try: 48 | creds = gssapi.Credentials( 49 | usage="initiate", name=gss_name, store={"ccache": ccache} 50 | ) 51 | except gssapi.exceptions.GSSError as e: 52 | self.abort( 53 | 403, 54 | f"Invalid credentials ({e})", 55 | ) 56 | try: 57 | lifetime = creds.lifetime 58 | except gssapi.exceptions.ExpiredCredentialsError: 59 | lifetime = 0 60 | if lifetime <= 0: 61 | current_app.logger.info("Credential lifetime has expired.") 62 | if ccache_type == "FILE": 63 | try: 64 | os.remove(ccache_location) 65 | except OSError as e: 66 | current_app.logger.warning( 67 | "Could not remove expired credential at %s: %s", 68 | ccache_location, 69 | e, 70 | ) 71 | self._authenticate() 72 | 73 | g.gss_name = gss_name 74 | g.gss_creds = creds 75 | g.principal = gss_name.display_as(gssapi.NameType.kerberos_principal) 76 | g.username = g.principal.split("@")[0] 77 | 78 | def _authenticate(self): 79 | """Unset mod_auth_gssapi's session cookie and restart GSSAPI authentication""" 80 | current_app.logger.debug( 81 | "Clearing the session and asking for re-authentication." 82 | ) 83 | exc = Unauthorized("Re-authentication is necessary.") 84 | exc.response = exc.get_response() 85 | exc.response.headers["WWW-Authenticate"] = "Negotiate" 86 | session_header = current_app.config["MOD_AUTH_GSSAPI_SESSION_HEADER"] 87 | exc.response.headers[session_header] = "MagBearerToken=" 88 | raise exc 89 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Tests & Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - stable 7 | - develop 8 | tags: 9 | - v* 10 | pull_request: 11 | branches: 12 | - stable 13 | - develop 14 | 15 | jobs: 16 | 17 | tests-misc: 18 | name: Misc tests 19 | runs-on: ubuntu-latest 20 | container: fedorapython/fedora-python-tox:latest 21 | steps: 22 | - uses: actions/checkout@v6 23 | - name: Install dependencies 24 | run: | 25 | dnf install -y krb5-devel openldap-devel 26 | pip install poetry 27 | - name: Install the project 28 | run: poetry install 29 | - name: Run tests 30 | run: tox -e ${{ matrix.tox_env }} 31 | strategy: 32 | matrix: 33 | tox_env: 34 | - lint 35 | - format 36 | 37 | tests-unit: 38 | name: Unit tests 39 | runs-on: ubuntu-latest 40 | container: fedorapython/fedora-python-tox:latest 41 | steps: 42 | - uses: actions/checkout@v6 43 | - name: Install dependencies 44 | run: | 45 | dnf install -y krb5-devel openldap-devel 46 | pip install poetry 47 | - name: Install the project 48 | run: poetry install 49 | - name: Run tests 50 | run: tox -e ${{ matrix.pyver }}-${{ matrix.flaskver }} 51 | strategy: 52 | matrix: 53 | pyver: 54 | - py310 55 | - py311 56 | - py312 57 | - py313 58 | - py314 59 | flaskver: 60 | - flask2 61 | - flask3 62 | 63 | # https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ 64 | build: 65 | name: Build distribution 📦 66 | runs-on: ubuntu-latest 67 | needs: 68 | - tests-unit 69 | - tests-misc 70 | steps: 71 | - uses: actions/checkout@v6 72 | - name: Set up Python 73 | uses: actions/setup-python@v6 74 | with: 75 | python-version: "3.x" 76 | - name: Install pypa/build 77 | run: python3 -m pip install build --user 78 | - name: Build a binary wheel and a source tarball 79 | run: python3 -m build 80 | - name: Store the distribution packages 81 | uses: actions/upload-artifact@v6 82 | with: 83 | name: python-package-distributions 84 | path: dist/ 85 | 86 | publish-to-pypi: 87 | name: Publish to PyPI 🚀 88 | if: startsWith(github.ref, 'refs/tags/') && !contains(github.ref, 'rc') # only publish to PyPI on final tag pushes 89 | needs: 90 | - build 91 | runs-on: ubuntu-latest 92 | environment: 93 | name: pypi 94 | url: https://pypi.org/p/flask-mod-auth-gssapi 95 | permissions: 96 | id-token: write # IMPORTANT: mandatory for trusted publishing 97 | steps: 98 | - name: Download all the dists 99 | uses: actions/download-artifact@v7 100 | with: 101 | name: python-package-distributions 102 | path: dist/ 103 | - name: Publish distribution to PyPI 104 | uses: pypa/gh-action-pypi-publish@release/v1 105 | 106 | github-release: 107 | name: Create a GitHub Release 📢 108 | needs: 109 | - publish-to-pypi 110 | runs-on: ubuntu-latest 111 | 112 | permissions: 113 | contents: write # IMPORTANT: mandatory for making GitHub Releases 114 | id-token: write # IMPORTANT: mandatory for sigstore 115 | 116 | steps: 117 | - name: Download all the dists 118 | uses: actions/download-artifact@v7 119 | with: 120 | name: python-package-distributions 121 | path: dist/ 122 | - name: Sign the dists with Sigstore 123 | uses: sigstore/gh-action-sigstore-python@v3.2.0 124 | with: 125 | inputs: >- 126 | ./dist/*.tar.gz 127 | ./dist/*.whl 128 | - name: Release 129 | uses: softprops/action-gh-release@v2 130 | with: 131 | draft: true 132 | files: dist/* 133 | fail_on_unmatched_files: true 134 | generate_release_notes: true 135 | -------------------------------------------------------------------------------- /tests/test_ext.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import pytest 5 | from flask import Flask, g 6 | from gssapi.raw.exceptions import ExpiredCredentialsError 7 | from werkzeug.exceptions import Forbidden, InternalServerError 8 | 9 | from flask_mod_auth_gssapi import FlaskModAuthGSSAPI 10 | 11 | 12 | def test_delayed_init(mocker): 13 | init_app = mocker.patch.object(FlaskModAuthGSSAPI, "init_app") 14 | FlaskModAuthGSSAPI(None) 15 | init_app.assert_not_called() 16 | 17 | 18 | def test_multithread(app): 19 | with app.test_request_context("/", multithread=True): 20 | with pytest.raises(InternalServerError): 21 | app.preprocess_request() 22 | 23 | 24 | def test_no_krb5ccname(app, wsgi_env): 25 | del wsgi_env["KRB5CCNAME"] 26 | with app.test_request_context("/", environ_base=wsgi_env): 27 | app.preprocess_request() 28 | assert g.principal is None 29 | assert g.username is None 30 | 31 | 32 | def test_no_gss_name(app, wsgi_env): 33 | del wsgi_env["GSS_NAME"] 34 | with app.test_request_context("/", environ_base=wsgi_env): 35 | app.preprocess_request() 36 | assert g.principal is None 37 | assert g.username is None 38 | 39 | 40 | def test_no_cache(app, wsgi_env): 41 | with app.test_request_context("/", environ_base=wsgi_env): 42 | with pytest.raises(Forbidden): 43 | app.preprocess_request() 44 | assert g.principal is None 45 | assert g.username is None 46 | 47 | 48 | def test_expired(app, wsgi_env, caplog, expired_credential, tmp_path): 49 | ccache_path = tmp_path.joinpath("ccache") 50 | open(ccache_path, "w").close() # Create the file 51 | wsgi_env["KRB5CCNAME"] = f"FILE:{ccache_path.as_posix()}" 52 | caplog.set_level(logging.INFO) 53 | client = app.test_client() 54 | response = client.get("/someplace", environ_base=wsgi_env) 55 | assert response.status_code == 401 56 | assert response.headers["www-authenticate"] == "Negotiate" 57 | assert caplog.messages == ["Credential lifetime has expired."] 58 | assert not os.path.exists(ccache_path) 59 | 60 | 61 | def test_expired_exception(app, wsgi_env, mocker, caplog): 62 | creds_factory = mocker.patch("gssapi.Credentials") 63 | 64 | class MockedCred: 65 | @property 66 | def lifetime(self): 67 | raise ExpiredCredentialsError(720896, 100001) 68 | 69 | creds_factory.return_value = MockedCred() 70 | caplog.set_level(logging.INFO) 71 | client = app.test_client() 72 | try: 73 | response = client.get("/someplace", environ_base=wsgi_env) 74 | except ExpiredCredentialsError: 75 | pytest.fail("Did not catch ExpiredCredentialsError on cred.lifetime") 76 | assert response.status_code == 401 77 | assert response.headers["www-authenticate"] == "Negotiate" 78 | assert caplog.messages == ["Credential lifetime has expired."] 79 | 80 | 81 | def test_expired_cant_remove( 82 | app, wsgi_env, caplog, expired_credential, tmp_path, mocker 83 | ): 84 | ccaches_dir = tmp_path.joinpath("ccaches") 85 | os.makedirs(ccaches_dir) 86 | ccache_path = ccaches_dir.joinpath("ccache") 87 | open(ccache_path, "w").close() # Create the file 88 | wsgi_env["KRB5CCNAME"] = f"FILE:{ccache_path.as_posix()}" 89 | client = app.test_client() 90 | mocker.patch("os.remove", side_effect=OSError("Dummy error")) 91 | response = client.get("/someplace", environ_base=wsgi_env) 92 | assert response.status_code == 401 93 | assert response.headers["www-authenticate"] == "Negotiate" 94 | assert caplog.messages == [ 95 | f"Could not remove expired credential at {ccache_path.as_posix()}: Dummy error" 96 | ] 97 | 98 | 99 | def test_nominal(app, wsgi_env, credential): 100 | with app.test_request_context("/", environ_base=wsgi_env): 101 | app.preprocess_request() 102 | assert g.principal == "dummy@EXAMPLE.TEST" 103 | assert g.username == "dummy" 104 | 105 | 106 | def test_alt_abort(app, wsgi_env, mocker): 107 | """Allow passing an alternate abort() function.""" 108 | mock_abort = mocker.Mock() 109 | mock_abort.side_effect = RuntimeError 110 | app = Flask("test") 111 | app.config["TESTING"] = True 112 | FlaskModAuthGSSAPI(app, abort=mock_abort) 113 | with app.test_request_context("/", environ_base=wsgi_env): 114 | with pytest.raises(RuntimeError): 115 | app.preprocess_request() 116 | assert g.principal is None 117 | assert g.username is None 118 | mock_abort.assert_called_once() 119 | call_args = mock_abort.call_args_list[0][0] 120 | assert call_args[0] == 403 121 | assert call_args[1].startswith("Invalid credentials ") 122 | 123 | 124 | def test_ccache_not_found(app, wsgi_env, caplog, mocker): 125 | wsgi_env["KRB5CCNAME"] = "FILE:/tmp/does-not-exist" 126 | # caplog.set_level(logging.INFO) 127 | client = app.test_client() 128 | response = client.get("/someplace", environ_base=wsgi_env) 129 | assert response.status_code == 401 130 | assert response.headers["www-authenticate"] == "Negotiate" 131 | assert caplog.messages == ["Delegated credentials not found: '/tmp/does-not-exist'"] 132 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "black" 5 | version = "25.12.0" 6 | description = "The uncompromising code formatter." 7 | optional = false 8 | python-versions = ">=3.10" 9 | groups = ["dev"] 10 | files = [ 11 | {file = "black-25.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f85ba1ad15d446756b4ab5f3044731bf68b777f8f9ac9cdabd2425b97cd9c4e8"}, 12 | {file = "black-25.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:546eecfe9a3a6b46f9d69d8a642585a6eaf348bcbbc4d87a19635570e02d9f4a"}, 13 | {file = "black-25.12.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17dcc893da8d73d8f74a596f64b7c98ef5239c2cd2b053c0f25912c4494bf9ea"}, 14 | {file = "black-25.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:09524b0e6af8ba7a3ffabdfc7a9922fb9adef60fed008c7cd2fc01f3048e6e6f"}, 15 | {file = "black-25.12.0-cp310-cp310-win_arm64.whl", hash = "sha256:b162653ed89eb942758efeb29d5e333ca5bb90e5130216f8369857db5955a7da"}, 16 | {file = "black-25.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a"}, 17 | {file = "black-25.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be"}, 18 | {file = "black-25.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b"}, 19 | {file = "black-25.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5"}, 20 | {file = "black-25.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655"}, 21 | {file = "black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a"}, 22 | {file = "black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783"}, 23 | {file = "black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59"}, 24 | {file = "black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892"}, 25 | {file = "black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43"}, 26 | {file = "black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5"}, 27 | {file = "black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f"}, 28 | {file = "black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf"}, 29 | {file = "black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d"}, 30 | {file = "black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce"}, 31 | {file = "black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5"}, 32 | {file = "black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f"}, 33 | {file = "black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f"}, 34 | {file = "black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83"}, 35 | {file = "black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b"}, 36 | {file = "black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828"}, 37 | {file = "black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7"}, 38 | ] 39 | 40 | [package.dependencies] 41 | click = ">=8.0.0" 42 | mypy-extensions = ">=0.4.3" 43 | packaging = ">=22.0" 44 | pathspec = ">=0.9.0" 45 | platformdirs = ">=2" 46 | pytokens = ">=0.3.0" 47 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 48 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 49 | 50 | [package.extras] 51 | colorama = ["colorama (>=0.4.3)"] 52 | d = ["aiohttp (>=3.10)"] 53 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 54 | uvloop = ["uvloop (>=0.15.2)"] 55 | 56 | [[package]] 57 | name = "blinker" 58 | version = "1.9.0" 59 | description = "Fast, simple object-to-object and broadcast signaling" 60 | optional = false 61 | python-versions = ">=3.9" 62 | groups = ["main"] 63 | files = [ 64 | {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, 65 | {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, 66 | ] 67 | 68 | [[package]] 69 | name = "click" 70 | version = "8.3.1" 71 | description = "Composable command line interface toolkit" 72 | optional = false 73 | python-versions = ">=3.10" 74 | groups = ["main", "dev"] 75 | files = [ 76 | {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, 77 | {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, 78 | ] 79 | 80 | [package.dependencies] 81 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 82 | 83 | [[package]] 84 | name = "colorama" 85 | version = "0.4.6" 86 | description = "Cross-platform colored terminal text." 87 | optional = false 88 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 89 | groups = ["main", "dev"] 90 | files = [ 91 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 92 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 93 | ] 94 | markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} 95 | 96 | [[package]] 97 | name = "coverage" 98 | version = "7.13.0" 99 | description = "Code coverage measurement for Python" 100 | optional = false 101 | python-versions = ">=3.10" 102 | groups = ["dev"] 103 | files = [ 104 | {file = "coverage-7.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070"}, 105 | {file = "coverage-7.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98"}, 106 | {file = "coverage-7.13.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5"}, 107 | {file = "coverage-7.13.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e"}, 108 | {file = "coverage-7.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33"}, 109 | {file = "coverage-7.13.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791"}, 110 | {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032"}, 111 | {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9"}, 112 | {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f"}, 113 | {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8"}, 114 | {file = "coverage-7.13.0-cp310-cp310-win32.whl", hash = "sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f"}, 115 | {file = "coverage-7.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303"}, 116 | {file = "coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820"}, 117 | {file = "coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f"}, 118 | {file = "coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96"}, 119 | {file = "coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259"}, 120 | {file = "coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb"}, 121 | {file = "coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9"}, 122 | {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030"}, 123 | {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833"}, 124 | {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8"}, 125 | {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753"}, 126 | {file = "coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b"}, 127 | {file = "coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe"}, 128 | {file = "coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7"}, 129 | {file = "coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf"}, 130 | {file = "coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f"}, 131 | {file = "coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb"}, 132 | {file = "coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621"}, 133 | {file = "coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74"}, 134 | {file = "coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57"}, 135 | {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8"}, 136 | {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d"}, 137 | {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b"}, 138 | {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd"}, 139 | {file = "coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef"}, 140 | {file = "coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae"}, 141 | {file = "coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080"}, 142 | {file = "coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf"}, 143 | {file = "coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a"}, 144 | {file = "coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74"}, 145 | {file = "coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6"}, 146 | {file = "coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b"}, 147 | {file = "coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232"}, 148 | {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971"}, 149 | {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d"}, 150 | {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137"}, 151 | {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511"}, 152 | {file = "coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1"}, 153 | {file = "coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a"}, 154 | {file = "coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6"}, 155 | {file = "coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a"}, 156 | {file = "coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8"}, 157 | {file = "coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053"}, 158 | {file = "coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071"}, 159 | {file = "coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e"}, 160 | {file = "coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493"}, 161 | {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0"}, 162 | {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e"}, 163 | {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c"}, 164 | {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e"}, 165 | {file = "coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46"}, 166 | {file = "coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39"}, 167 | {file = "coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e"}, 168 | {file = "coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256"}, 169 | {file = "coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a"}, 170 | {file = "coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9"}, 171 | {file = "coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19"}, 172 | {file = "coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be"}, 173 | {file = "coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb"}, 174 | {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8"}, 175 | {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b"}, 176 | {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9"}, 177 | {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927"}, 178 | {file = "coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f"}, 179 | {file = "coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc"}, 180 | {file = "coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b"}, 181 | {file = "coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28"}, 182 | {file = "coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe"}, 183 | {file = "coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657"}, 184 | {file = "coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff"}, 185 | {file = "coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3"}, 186 | {file = "coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b"}, 187 | {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d"}, 188 | {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e"}, 189 | {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940"}, 190 | {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2"}, 191 | {file = "coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7"}, 192 | {file = "coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc"}, 193 | {file = "coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a"}, 194 | {file = "coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904"}, 195 | {file = "coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936"}, 196 | ] 197 | 198 | [package.dependencies] 199 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 200 | 201 | [package.extras] 202 | toml = ["tomli ; python_full_version <= \"3.11.0a6\""] 203 | 204 | [[package]] 205 | name = "decorator" 206 | version = "5.2.1" 207 | description = "Decorators for Humans" 208 | optional = false 209 | python-versions = ">=3.8" 210 | groups = ["main"] 211 | files = [ 212 | {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, 213 | {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, 214 | ] 215 | 216 | [[package]] 217 | name = "exceptiongroup" 218 | version = "1.3.1" 219 | description = "Backport of PEP 654 (exception groups)" 220 | optional = false 221 | python-versions = ">=3.7" 222 | groups = ["dev"] 223 | markers = "python_version == \"3.10\"" 224 | files = [ 225 | {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, 226 | {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, 227 | ] 228 | 229 | [package.dependencies] 230 | typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} 231 | 232 | [package.extras] 233 | test = ["pytest (>=6)"] 234 | 235 | [[package]] 236 | name = "flask" 237 | version = "3.1.2" 238 | description = "A simple framework for building complex web applications." 239 | optional = false 240 | python-versions = ">=3.9" 241 | groups = ["main"] 242 | files = [ 243 | {file = "flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c"}, 244 | {file = "flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87"}, 245 | ] 246 | 247 | [package.dependencies] 248 | blinker = ">=1.9.0" 249 | click = ">=8.1.3" 250 | itsdangerous = ">=2.2.0" 251 | jinja2 = ">=3.1.2" 252 | markupsafe = ">=2.1.1" 253 | werkzeug = ">=3.1.0" 254 | 255 | [package.extras] 256 | async = ["asgiref (>=3.2)"] 257 | dotenv = ["python-dotenv"] 258 | 259 | [[package]] 260 | name = "gssapi" 261 | version = "1.10.1" 262 | description = "Python GSSAPI Wrapper" 263 | optional = false 264 | python-versions = ">=3.9" 265 | groups = ["main"] 266 | files = [ 267 | {file = "gssapi-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1456b60bbb999c7d2bf323c1ca9ee077dc6a59368737401c302c64bf0dd8a119"}, 268 | {file = "gssapi-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d75edd60f3105b362e4841ff86f358a9c6b9849e1327ea527b6a17b86e459207"}, 269 | {file = "gssapi-1.10.1-cp310-cp310-win32.whl", hash = "sha256:a589980c2c8c7ec7537b26b3d8d3becf26daf6f84a9534c54b3376220a9e82b5"}, 270 | {file = "gssapi-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:77e92a1bdc4c72af4f1aa850787038741bd38e3fa6c52defee50125509539ffe"}, 271 | {file = "gssapi-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44be38aef1b26270dc23c43d8f124f13cf839cadcba63f5d011793eca2ec95f2"}, 272 | {file = "gssapi-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0be7195c96968df44f3cd2b79bbfa2ca3729d4bd91374947e93fde827bdab37f"}, 273 | {file = "gssapi-1.10.1-cp311-cp311-win32.whl", hash = "sha256:048736351b013290081472b2e523251246bc96d7ea74c97189d2af31f7d20bd6"}, 274 | {file = "gssapi-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:93166ed5d3ce53af721c2a9a115ffa645900f4b71c4810a18bff10f0a9843d0e"}, 275 | {file = "gssapi-1.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5c08ae5b5fa3faae1ad5bf9d4821a27da6974df0bf994066bf8e437ff101429"}, 276 | {file = "gssapi-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec74a5e70241655b79c7de7dc750c58dae80482947973e019c67c8d53311981"}, 277 | {file = "gssapi-1.10.1-cp312-cp312-win32.whl", hash = "sha256:ed40213beec30115302bac3849134fbbfd5b0fdb60d8e4f2d9027cd44765f42b"}, 278 | {file = "gssapi-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:f0d5e5e6031e879d4050e0373cf854f5082ca234127b6553026a29c64ddf64ed"}, 279 | {file = "gssapi-1.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:952c900ced1cafe7e7938052e24d01d4ba48f234a0ca7347c854c6d96f94ae26"}, 280 | {file = "gssapi-1.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df86f1dcc2a1c19c1771565661d05dd09cb1ce7ff2c3be261b3b5312458969f3"}, 281 | {file = "gssapi-1.10.1-cp313-cp313-win32.whl", hash = "sha256:37c2abb85e76d9e4bef967a752354aa6a365bb965eb18067f1f012aad0f7a446"}, 282 | {file = "gssapi-1.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:d821d37afd61c326ba729850c9836d84e5d38ad42acec21784fb22dd467345f4"}, 283 | {file = "gssapi-1.10.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a4d2aa439bcd08cd524a6e0c566137850e681b0fed62480aa765c097344387d7"}, 284 | {file = "gssapi-1.10.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:86758d03906e10cb7feeedf26b5ead6661e844c54ef09d5e7de8e5ffb1154932"}, 285 | {file = "gssapi-1.10.1-cp314-cp314-win32.whl", hash = "sha256:2ef6e30c37676fbb2f635467e560c9a5e7b3f49ee9536ecb363939efa81c82bc"}, 286 | {file = "gssapi-1.10.1-cp314-cp314-win_amd64.whl", hash = "sha256:8f311cec5eabe0ce417908bcf50f60afa91a5b455884794eb02eb35a41d410c7"}, 287 | {file = "gssapi-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a2b539dc10c46968a558a92f7ca49a53f1215b9c16fc25a55980824a9123c241"}, 288 | {file = "gssapi-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f1622e0a03ec736456b84ddd8b54808f90dca3cf3528d1ff4203f1e8f014a293"}, 289 | {file = "gssapi-1.10.1-cp39-cp39-win32.whl", hash = "sha256:2a71b089589ff949ffd557c795ac0c3f80eaa0cb56062c39b1363cf4af413e38"}, 290 | {file = "gssapi-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:fea5fa50b36502665d6d2f2479c198307adf21afbc523017cf115e5c4769258a"}, 291 | {file = "gssapi-1.10.1.tar.gz", hash = "sha256:7b54335dc9a3c55d564624fb6e25fcf9cfc0b80296a5c51e9c7cf9781c7d295b"}, 292 | ] 293 | 294 | [package.dependencies] 295 | decorator = "*" 296 | 297 | [[package]] 298 | name = "iniconfig" 299 | version = "2.3.0" 300 | description = "brain-dead simple config-ini parsing" 301 | optional = false 302 | python-versions = ">=3.10" 303 | groups = ["dev"] 304 | files = [ 305 | {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, 306 | {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, 307 | ] 308 | 309 | [[package]] 310 | name = "itsdangerous" 311 | version = "2.2.0" 312 | description = "Safely pass data to untrusted environments and back." 313 | optional = false 314 | python-versions = ">=3.8" 315 | groups = ["main"] 316 | files = [ 317 | {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, 318 | {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, 319 | ] 320 | 321 | [[package]] 322 | name = "jinja2" 323 | version = "3.1.6" 324 | description = "A very fast and expressive template engine." 325 | optional = false 326 | python-versions = ">=3.7" 327 | groups = ["main"] 328 | files = [ 329 | {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, 330 | {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, 331 | ] 332 | 333 | [package.dependencies] 334 | MarkupSafe = ">=2.0" 335 | 336 | [package.extras] 337 | i18n = ["Babel (>=2.7)"] 338 | 339 | [[package]] 340 | name = "markupsafe" 341 | version = "3.0.3" 342 | description = "Safely add untrusted strings to HTML/XML markup." 343 | optional = false 344 | python-versions = ">=3.9" 345 | groups = ["main"] 346 | files = [ 347 | {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, 348 | {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, 349 | {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, 350 | {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, 351 | {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, 352 | {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, 353 | {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, 354 | {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, 355 | {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, 356 | {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, 357 | {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, 358 | {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, 359 | {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, 360 | {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, 361 | {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, 362 | {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, 363 | {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, 364 | {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, 365 | {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, 366 | {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, 367 | {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, 368 | {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, 369 | {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, 370 | {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, 371 | {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, 372 | {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, 373 | {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, 374 | {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, 375 | {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, 376 | {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, 377 | {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, 378 | {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, 379 | {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, 380 | {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, 381 | {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, 382 | {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, 383 | {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, 384 | {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, 385 | {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, 386 | {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, 387 | {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, 388 | {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, 389 | {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, 390 | {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, 391 | {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, 392 | {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, 393 | {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, 394 | {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, 395 | {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, 396 | {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, 397 | {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, 398 | {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, 399 | {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, 400 | {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, 401 | {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, 402 | {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, 403 | {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, 404 | {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, 405 | {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, 406 | {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, 407 | {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, 408 | {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, 409 | {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, 410 | {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, 411 | {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, 412 | {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, 413 | {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, 414 | {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, 415 | {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, 416 | {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, 417 | {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, 418 | {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, 419 | {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, 420 | {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, 421 | {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, 422 | {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, 423 | {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, 424 | {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, 425 | {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, 426 | {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, 427 | {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, 428 | {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, 429 | {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, 430 | {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, 431 | {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, 432 | {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, 433 | {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, 434 | {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, 435 | {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, 436 | ] 437 | 438 | [[package]] 439 | name = "mypy-extensions" 440 | version = "1.1.0" 441 | description = "Type system extensions for programs checked with the mypy type checker." 442 | optional = false 443 | python-versions = ">=3.8" 444 | groups = ["dev"] 445 | files = [ 446 | {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, 447 | {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, 448 | ] 449 | 450 | [[package]] 451 | name = "packaging" 452 | version = "25.0" 453 | description = "Core utilities for Python packages" 454 | optional = false 455 | python-versions = ">=3.8" 456 | groups = ["dev"] 457 | files = [ 458 | {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, 459 | {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, 460 | ] 461 | 462 | [[package]] 463 | name = "pathspec" 464 | version = "0.12.1" 465 | description = "Utility library for gitignore style pattern matching of file paths." 466 | optional = false 467 | python-versions = ">=3.8" 468 | groups = ["dev"] 469 | files = [ 470 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 471 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 472 | ] 473 | 474 | [[package]] 475 | name = "platformdirs" 476 | version = "4.5.1" 477 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 478 | optional = false 479 | python-versions = ">=3.10" 480 | groups = ["dev"] 481 | files = [ 482 | {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, 483 | {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, 484 | ] 485 | 486 | [package.extras] 487 | docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"] 488 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] 489 | type = ["mypy (>=1.18.2)"] 490 | 491 | [[package]] 492 | name = "pluggy" 493 | version = "1.6.0" 494 | description = "plugin and hook calling mechanisms for python" 495 | optional = false 496 | python-versions = ">=3.9" 497 | groups = ["dev"] 498 | files = [ 499 | {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, 500 | {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, 501 | ] 502 | 503 | [package.extras] 504 | dev = ["pre-commit", "tox"] 505 | testing = ["coverage", "pytest", "pytest-benchmark"] 506 | 507 | [[package]] 508 | name = "pygments" 509 | version = "2.19.2" 510 | description = "Pygments is a syntax highlighting package written in Python." 511 | optional = false 512 | python-versions = ">=3.8" 513 | groups = ["dev"] 514 | files = [ 515 | {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, 516 | {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, 517 | ] 518 | 519 | [package.extras] 520 | windows-terminal = ["colorama (>=0.4.6)"] 521 | 522 | [[package]] 523 | name = "pytest" 524 | version = "9.0.2" 525 | description = "pytest: simple powerful testing with Python" 526 | optional = false 527 | python-versions = ">=3.10" 528 | groups = ["dev"] 529 | files = [ 530 | {file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"}, 531 | {file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"}, 532 | ] 533 | 534 | [package.dependencies] 535 | colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} 536 | exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} 537 | iniconfig = ">=1.0.1" 538 | packaging = ">=22" 539 | pluggy = ">=1.5,<2" 540 | pygments = ">=2.7.2" 541 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 542 | 543 | [package.extras] 544 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] 545 | 546 | [[package]] 547 | name = "pytest-cov" 548 | version = "7.0.0" 549 | description = "Pytest plugin for measuring coverage." 550 | optional = false 551 | python-versions = ">=3.9" 552 | groups = ["dev"] 553 | files = [ 554 | {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, 555 | {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, 556 | ] 557 | 558 | [package.dependencies] 559 | coverage = {version = ">=7.10.6", extras = ["toml"]} 560 | pluggy = ">=1.2" 561 | pytest = ">=7" 562 | 563 | [package.extras] 564 | testing = ["process-tests", "pytest-xdist", "virtualenv"] 565 | 566 | [[package]] 567 | name = "pytest-mock" 568 | version = "3.15.1" 569 | description = "Thin-wrapper around the mock package for easier use with pytest" 570 | optional = false 571 | python-versions = ">=3.9" 572 | groups = ["dev"] 573 | files = [ 574 | {file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"}, 575 | {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, 576 | ] 577 | 578 | [package.dependencies] 579 | pytest = ">=6.2.5" 580 | 581 | [package.extras] 582 | dev = ["pre-commit", "pytest-asyncio", "tox"] 583 | 584 | [[package]] 585 | name = "pytokens" 586 | version = "0.3.0" 587 | description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." 588 | optional = false 589 | python-versions = ">=3.8" 590 | groups = ["dev"] 591 | files = [ 592 | {file = "pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3"}, 593 | {file = "pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a"}, 594 | ] 595 | 596 | [package.extras] 597 | dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] 598 | 599 | [[package]] 600 | name = "ruff" 601 | version = "0.14.9" 602 | description = "An extremely fast Python linter and code formatter, written in Rust." 603 | optional = false 604 | python-versions = ">=3.7" 605 | groups = ["dev"] 606 | files = [ 607 | {file = "ruff-0.14.9-py3-none-linux_armv6l.whl", hash = "sha256:f1ec5de1ce150ca6e43691f4a9ef5c04574ad9ca35c8b3b0e18877314aba7e75"}, 608 | {file = "ruff-0.14.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ed9d7417a299fc6030b4f26333bf1117ed82a61ea91238558c0268c14e00d0c2"}, 609 | {file = "ruff-0.14.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d5dc3473c3f0e4a1008d0ef1d75cee24a48e254c8bed3a7afdd2b4392657ed2c"}, 610 | {file = "ruff-0.14.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84bf7c698fc8f3cb8278830fb6b5a47f9bcc1ed8cb4f689b9dd02698fa840697"}, 611 | {file = "ruff-0.14.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa733093d1f9d88a5d98988d8834ef5d6f9828d03743bf5e338bf980a19fce27"}, 612 | {file = "ruff-0.14.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a1cfb04eda979b20c8c19550c8b5f498df64ff8da151283311ce3199e8b3648"}, 613 | {file = "ruff-0.14.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1e5cb521e5ccf0008bd74d5595a4580313844a42b9103b7388eca5a12c970743"}, 614 | {file = "ruff-0.14.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd429a8926be6bba4befa8cdcf3f4dd2591c413ea5066b1e99155ed245ae42bb"}, 615 | {file = "ruff-0.14.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab208c1b7a492e37caeaf290b1378148f75e13c2225af5d44628b95fd7834273"}, 616 | {file = "ruff-0.14.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72034534e5b11e8a593f517b2f2f2b273eb68a30978c6a2d40473ad0aaa4cb4a"}, 617 | {file = "ruff-0.14.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:712ff04f44663f1b90a1195f51525836e3413c8a773574a7b7775554269c30ed"}, 618 | {file = "ruff-0.14.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a111fee1db6f1d5d5810245295527cda1d367c5aa8f42e0fca9a78ede9b4498b"}, 619 | {file = "ruff-0.14.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8769efc71558fecc25eb295ddec7d1030d41a51e9dcf127cbd63ec517f22d567"}, 620 | {file = "ruff-0.14.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:347e3bf16197e8a2de17940cd75fd6491e25c0aa7edf7d61aa03f146a1aa885a"}, 621 | {file = "ruff-0.14.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7715d14e5bccf5b660f54516558aa94781d3eb0838f8e706fb60e3ff6eff03a8"}, 622 | {file = "ruff-0.14.9-py3-none-win32.whl", hash = "sha256:df0937f30aaabe83da172adaf8937003ff28172f59ca9f17883b4213783df197"}, 623 | {file = "ruff-0.14.9-py3-none-win_amd64.whl", hash = "sha256:c0b53a10e61df15a42ed711ec0bda0c582039cf6c754c49c020084c55b5b0bc2"}, 624 | {file = "ruff-0.14.9-py3-none-win_arm64.whl", hash = "sha256:8e821c366517a074046d92f0e9213ed1c13dbc5b37a7fc20b07f79b64d62cc84"}, 625 | {file = "ruff-0.14.9.tar.gz", hash = "sha256:35f85b25dd586381c0cc053f48826109384c81c00ad7ef1bd977bfcc28119d5b"}, 626 | ] 627 | 628 | [[package]] 629 | name = "tomli" 630 | version = "2.3.0" 631 | description = "A lil' TOML parser" 632 | optional = false 633 | python-versions = ">=3.8" 634 | groups = ["dev"] 635 | markers = "python_full_version <= \"3.11.0a6\"" 636 | files = [ 637 | {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, 638 | {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, 639 | {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, 640 | {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, 641 | {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, 642 | {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, 643 | {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, 644 | {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, 645 | {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, 646 | {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, 647 | {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, 648 | {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, 649 | {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, 650 | {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, 651 | {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, 652 | {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, 653 | {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, 654 | {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, 655 | {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, 656 | {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, 657 | {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, 658 | {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, 659 | {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, 660 | {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, 661 | {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, 662 | {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, 663 | {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, 664 | {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, 665 | {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, 666 | {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, 667 | {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, 668 | {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, 669 | {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, 670 | {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, 671 | {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, 672 | {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, 673 | {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, 674 | {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, 675 | {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, 676 | {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, 677 | {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, 678 | {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, 679 | ] 680 | 681 | [[package]] 682 | name = "typing-extensions" 683 | version = "4.15.0" 684 | description = "Backported and Experimental Type Hints for Python 3.9+" 685 | optional = false 686 | python-versions = ">=3.9" 687 | groups = ["dev"] 688 | markers = "python_version == \"3.10\"" 689 | files = [ 690 | {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, 691 | {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, 692 | ] 693 | 694 | [[package]] 695 | name = "werkzeug" 696 | version = "3.1.4" 697 | description = "The comprehensive WSGI web application library." 698 | optional = false 699 | python-versions = ">=3.9" 700 | groups = ["main"] 701 | files = [ 702 | {file = "werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905"}, 703 | {file = "werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e"}, 704 | ] 705 | 706 | [package.dependencies] 707 | markupsafe = ">=2.1.1" 708 | 709 | [package.extras] 710 | watchdog = ["watchdog (>=2.3)"] 711 | 712 | [metadata] 713 | lock-version = "2.1" 714 | python-versions = "^3.10" 715 | content-hash = "1d5e7453bf5cd5dfc4dc153f6d1479c82cb2b5dfbb59fc3aa73f5ee86575b1de" 716 | --------------------------------------------------------------------------------