├── oidc
├── __init__.py
├── templates
│ └── oidc
│ │ └── configure.html
├── apps.py
├── constants.py
├── views.py
└── provider.py
├── tests
├── __init__.py
└── test_provider.py
├── .python-version
├── .gitmodules
├── .gitignore
├── renovate.json
├── setup.cfg
├── docker-compose.yml
├── .github
└── workflows
│ ├── publish.yml
│ └── test.yml
├── Makefile
├── pyproject.toml
├── README.rst
├── LICENSE
└── poetry.lock
/oidc/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
1 | 3.13.5
2 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "deps/sentry"]
2 | path = deps/sentry
3 | url = https://github.com/getsentry/sentry.git
4 | # This is here for renovate
5 | branch = 25.6.2
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.egg-info/
3 | *.eggs
4 | /dist
5 | /build
6 | venv
7 | .venv
8 | .coverage
9 | .vscode/
10 | # Fetched from upstream
11 | tests/conftest.py
12 |
--------------------------------------------------------------------------------
/oidc/templates/oidc/configure.html:
--------------------------------------------------------------------------------
1 |
Domain
2 |
3 | Users will be allowed to authenticate if they have an account under the from issuer {{ provider_name }}.
4 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended"
5 | ],
6 | "git-submodules": {
7 | "enabled": true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [wheel]
2 | universal = 1
3 |
4 | [tool:pytest]
5 | python_files = test*.py
6 | addopts = --tb=native -p no:doctest
7 | norecursedirs = bin dist docs htmlcov script hooks node_modules .* {args}
8 |
9 | [flake8]
10 | ignore = F999,E501,E128,E124,E402,W503,E731,C901
11 | max-line-length = 100
12 | exclude = .tox,.git,*/migrations/*,node_modules/*,docs/*
13 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | postgres:
3 | image: postgres:14.11
4 | environment:
5 | POSTGRES_HOST_AUTH_METHOD: trust
6 | # Set health checks to wait until postgres has started
7 | healthcheck:
8 | test: ["CMD-SHELL", “pg_isready”]
9 | interval: 10s
10 | timeout: 5s
11 | retries: 5
12 | ports:
13 | - 5432:5432
14 | memcached:
15 | image: memcached:1.6.23-alpine
16 | ports:
17 | - 11211:11211
18 | redis:
19 | image: redis:6.2.14-alpine
20 | ports:
21 | - 6379:6379
22 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | env:
9 | PY_COLORS: 1
10 |
11 | jobs:
12 | publish:
13 | runs-on: ubuntu-latest
14 | permissions:
15 | id-token: write
16 | steps:
17 | - uses: actions/checkout@v4
18 | - uses: actions/setup-python@v4
19 | with:
20 | python-version: "3.13"
21 | - uses: abatilo/actions-poetry@v2
22 | - name: Build package
23 | run: poetry build
24 | - name: Publish package to PyPI
25 | uses: pypa/gh-action-pypi-publish@release/v1
26 |
--------------------------------------------------------------------------------
/oidc/apps.py:
--------------------------------------------------------------------------------
1 | from inspect import signature
2 |
3 | from django.apps import AppConfig
4 |
5 |
6 | class Config(AppConfig):
7 | name = "oidc"
8 |
9 | def ready(self):
10 | from sentry import auth
11 |
12 | from .provider import OIDCProvider
13 |
14 | # In Sentry 25.3.0, the signature of `ProviderManager.register()` changed:
15 | # Instead of providing the key as a parameter, it is now expected to be a
16 | # property of the provider class.
17 | if len(signature(auth.register).parameters) == 1:
18 | auth.register(OIDCProvider)
19 | else:
20 | auth.register("oidc", OIDCProvider)
21 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: clean deps
2 |
3 | # Upstream no longer tracks its own dependencies in the package as dev extras,
4 | # so we cannot resolve them here as transitive dependencies. Instead we fetch
5 | # their locked development dependencies.
6 | # Likewise, their root-level conftest is not provided as a pytest plugin for
7 | # use outside their own tests, but we need their fixtures. We fetch them into
8 | # our own namespace here.
9 | deps:
10 | git submodule update --init
11 | poetry run pip install -r deps/sentry/requirements-dev-frozen.txt -e deps/sentry
12 | cp -f deps/sentry/tests/conftest.py tests/conftest.py
13 |
14 | clean:
15 | rm -rf *.egg-info src/*.egg-info
16 | rm -rf dist build
17 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "sentry-auth-oidc"
3 | version = "9.1.0"
4 | description = "OpenID Connect authentication provider for Sentry"
5 | authors = [
6 | "Max Wittig ",
7 | "Diego Louzán ",
8 | "Nejc Habjan ",
9 | ]
10 | license = "Apache 2.0"
11 | readme = "README.rst"
12 | classifiers = [
13 | "Intended Audience :: Developers",
14 | "Intended Audience :: System Administrators",
15 | "Operating System :: OS Independent",
16 | "Topic :: Software Development",
17 | ]
18 | packages = [
19 | { include = "oidc" }
20 | ]
21 |
22 | [tool.black]
23 | extend-exclude = "deps"
24 |
25 | [tool.poetry.dependencies]
26 | python = "^3.11"
27 |
28 | [tool.poetry.dev-dependencies]
29 | black = "^24.3.0"
30 | isort = "^5.7.0"
31 | flake8 = "^3.8.4"
32 |
33 | [tool.poetry.group.test.dependencies]
34 | codecov = "^2.1.12"
35 | pytest = "^7.1.3"
36 | fixtures = "^4.1.0"
37 |
38 | [tool.isort]
39 | profile = "black"
40 |
41 | [tool.poetry.plugins."sentry.apps"]
42 | "oidc" = "oidc.apps.Config"
43 |
44 | [tool.pytest.ini_options]
45 | testpaths = ["tests"]
46 |
47 | [build-system]
48 | requires = ["poetry-core>=1.0.0"]
49 | build-backend = "poetry.core.masonry.api"
50 |
--------------------------------------------------------------------------------
/oidc/constants.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from django.conf import settings
3 |
4 | AUTHORIZATION_ENDPOINT = getattr(settings, "OIDC_AUTHORIZATION_ENDPOINT", None)
5 | TOKEN_ENDPOINT = getattr(settings, "OIDC_TOKEN_ENDPOINT", None)
6 | CLIENT_ID = getattr(settings, "OIDC_CLIENT_ID", None)
7 | CLIENT_SECRET = getattr(settings, "OIDC_CLIENT_SECRET", None)
8 | USERINFO_ENDPOINT = getattr(settings, "OIDC_USERINFO_ENDPOINT", None)
9 | SCOPE = getattr(settings, "OIDC_SCOPE", "openid email")
10 | WELL_KNOWN_SCHEME = "/.well-known/openid-configuration"
11 | ERR_INVALID_RESPONSE = (
12 | "Unable to fetch user information from provider. Please check the log."
13 | )
14 | ISSUER = None
15 |
16 | DATA_VERSION = "1"
17 |
18 | OIDC_DOMAIN = getattr(settings, "OIDC_DOMAIN", None)
19 | if OIDC_DOMAIN:
20 | WELL_KNOWN_URL = OIDC_DOMAIN.strip("/") + WELL_KNOWN_SCHEME
21 | well_known_values = requests.get(WELL_KNOWN_URL, timeout=2.0).json()
22 | if well_known_values:
23 | USERINFO_ENDPOINT = well_known_values["userinfo_endpoint"]
24 | AUTHORIZATION_ENDPOINT = well_known_values["authorization_endpoint"]
25 | TOKEN_ENDPOINT = well_known_values["token_endpoint"]
26 | ISSUER = well_known_values["issuer"]
27 |
28 |
29 | config_issuer = getattr(settings, "OIDC_ISSUER", None)
30 | if config_issuer:
31 | ISSUER = config_issuer
32 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | env:
12 | PY_COLORS: 1
13 |
14 | jobs:
15 | black:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: actions/setup-python@v4
20 | with:
21 | python-version: "3.13"
22 | - uses: psf/black@stable
23 | with:
24 | options: "--check ."
25 | commitlint:
26 | runs-on: ubuntu-latest
27 | steps:
28 | - uses: actions/checkout@v4
29 | with:
30 | fetch-depth: 0
31 | - uses: wagoid/commitlint-github-action@v3
32 | test:
33 | runs-on: ubuntu-latest
34 | services:
35 | postgres:
36 | image: postgres:14.11
37 | env:
38 | POSTGRES_HOST_AUTH_METHOD: trust
39 | # Set health checks to wait until postgres has started
40 | options: >-
41 | --health-cmd pg_isready
42 | --health-interval 10s
43 | --health-timeout 5s
44 | --health-retries 5
45 | ports:
46 | - 5432:5432
47 | memcached:
48 | image: memcached:1.6.23-alpine
49 | ports:
50 | - 11211:11211
51 | redis:
52 | image: redis:6.2.14-alpine
53 | ports:
54 | - 6379:6379
55 | steps:
56 | - uses: actions/checkout@v4
57 | - uses: actions/setup-python@v4
58 | with:
59 | python-version: "3.13"
60 | - uses: abatilo/actions-poetry@v2
61 | - uses: actions/setup-node@v3
62 | with:
63 | node-version: '18'
64 | - name: Setup required upstream yarn version
65 | run: yarn set version 1.22.21
66 | - run: |
67 | sudo apt-get update && sudo apt-get install -y libxmlsec1-dev libmaxminddb-dev
68 | poetry install --with test
69 | make deps
70 | poetry run pytest
71 |
--------------------------------------------------------------------------------
/tests/test_provider.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from sentry import auth
3 | from sentry.auth.exceptions import IdentityNotValid
4 | from sentry.models import AuthIdentity, AuthProvider
5 | from sentry.testutils.cases import TestCase
6 | from sentry.testutils.silo import control_silo_test
7 |
8 | from oidc.constants import DATA_VERSION
9 | from oidc.provider import OIDCProvider
10 |
11 |
12 | @control_silo_test
13 | class OIDCProviderTest(TestCase):
14 | def setUp(self):
15 | self.auth_provider_inst = AuthProvider.objects.create(
16 | provider="oidc", organization_id=self.organization.id
17 | )
18 | auth.register(OIDCProvider)
19 | super().setUp()
20 |
21 | def test_refresh_identity_without_refresh_token(self):
22 | auth_identity = AuthIdentity.objects.create(
23 | auth_provider=self.auth_provider_inst,
24 | user=self.user,
25 | data={"access_token": "access_token"},
26 | )
27 |
28 | provider = self.auth_provider_inst.get_provider()
29 |
30 | with pytest.raises(IdentityNotValid):
31 | provider.refresh_identity(auth_identity)
32 |
33 | def test_handles_multiple_domains(self):
34 | self.auth_provider_inst.update(config={"domains": ["example.com"]})
35 |
36 | provider = self.auth_provider_inst.get_provider()
37 | assert provider.domains == ["example.com"]
38 |
39 | def test_handles_legacy_single_domain(self):
40 | self.auth_provider_inst.update(config={"domain": "example.com"})
41 |
42 | provider = self.auth_provider_inst.get_provider()
43 | assert provider.domains == ["example.com"]
44 |
45 | def test_build_config(self):
46 | provider = self.auth_provider_inst.get_provider()
47 | state = {
48 | "domain": "example.com",
49 | "user": {
50 | "iss": "accounts.google.com",
51 | "at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q",
52 | "email_verified": "true",
53 | "sub": "10769150350006150715113082367",
54 | "azp": "1234987819200.apps.googleusercontent.com",
55 | "email": "jsmith@example.com",
56 | "aud": "1234987819200.apps.googleusercontent.com",
57 | "iat": 1353601026,
58 | "exp": 1353604926,
59 | "hd": "example.com",
60 | },
61 | }
62 | result = provider.build_config(state)
63 | assert result == {"domains": ["example.com"], "version": DATA_VERSION}
64 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | OpenIDConnect Auth for Sentry
2 | =============================
3 |
4 | An SSO provider for Sentry which enables `OpenID Connect `_ Apps authentication.
5 |
6 | This is a fork of `sentry-auth-google `_.
7 |
8 | Why fork, instead of adapting sentry-auth-google to work with every OpenID Connect provider?
9 | --------------------------------------------------------------------------------------------
10 | The maintainer has different ideas with sentry-auth-google. See:
11 |
12 | * https://github.com/getsentry/sentry-auth-google/pull/29
13 | * https://github.com/getsentry/sentry/issues/5650
14 |
15 | Install
16 | -------
17 |
18 | ::
19 |
20 | $ pip install sentry-auth-oidc
21 |
22 | Example Setup for Google
23 | ------------------------
24 |
25 | Start by `creating a project in the Google Developers Console `_.
26 |
27 | In the **Authorized redirect URIs** add the SSO endpoint for your installation::
28 |
29 | https://sentry.example.com/auth/sso/
30 |
31 | Naturally other providers, that are supporting OpenID-Connect can also be used (like GitLab).
32 |
33 | Finally, obtain the API keys and the well-known account URL and plug them into your ``sentry.conf.py``:
34 |
35 | .. code-block:: python
36 |
37 | OIDC_CLIENT_ID = ""
38 |
39 | OIDC_CLIENT_SECRET = ""
40 |
41 | OIDC_SCOPE = "openid email"
42 |
43 | OIDC_DOMAIN = "https://accounts.google.com" # e.g. for Google
44 |
45 | The ``OIDC_DOMAIN`` defines where the OIDC configuration is going to be pulled from.
46 | Basically it specifies the OIDC server and adds the path ``.well-known/openid-configuration`` to it.
47 | That's where different endpoint paths can be found.
48 |
49 | Detailed information can be found in the `ProviderConfig `_ specification.
50 |
51 | You can also define ``OIDC_ISSUER`` to change the default provider name in the UI, even when the ``OIDC_DOMAIN`` is set.
52 |
53 | If your provider doesn't support the ``OIDC_DOMAIN``, then you have to set these
54 | required endpoints by yourself (autorization_endpoint, token_endpoint, userinfo_endpoint, issuer).
55 |
56 | .. code-block:: python
57 |
58 | OIDC_AUTHORIZATION_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth" # e.g. for Google
59 |
60 | OIDC_TOKEN_ENDPOINT = "https://www.googleapis.com/oauth2/v4/token" # e.g. for Google
61 |
62 | OIDC_USERINFO_ENDPOINT = "https://www.googleapis.com/oauth2/v3/userinfo" # e.g. for Google
63 |
64 | OIDC_ISSUER = "Google"
65 |
66 | Development
67 | -----------
68 |
69 | FAQ
70 | ~~~~~
71 |
72 | - If you are using macOS brew's openssl and you get a psycopg build error such as:
73 | ::
74 |
75 | ld: library not found for -lssl
76 |
77 | Please setup the following environment variables:
78 | .. code-block:: bash
79 |
80 | export LDFLAGS="-L/usr/local/opt/openssl/lib"
81 | export CPPFLAGS="-I/usr/local/opt/openssl/include"
82 |
--------------------------------------------------------------------------------
/oidc/views.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import logging
4 |
5 | from django.http import HttpRequest
6 | from rest_framework.response import Response
7 |
8 | from sentry.auth.services.auth.model import RpcAuthProvider
9 | from sentry.auth.view import AuthView
10 | from sentry.utils import json
11 | from sentry.organizations.services.organization.model import RpcOrganization
12 | from sentry.plugins.base.response import DeferredResponse
13 | from sentry.utils.signing import urlsafe_b64decode
14 |
15 | from .constants import ERR_INVALID_RESPONSE, ISSUER
16 |
17 | logger = logging.getLogger("sentry.auth.oidc")
18 |
19 |
20 | class FetchUser(AuthView):
21 | def __init__(self, domains, version, *args, **kwargs):
22 | self.domains = domains
23 | self.version = version
24 | super().__init__(*args, **kwargs)
25 |
26 | def dispatch(self, request: HttpRequest, **kwargs) -> Response: # type: ignore
27 | # Until Sentry 25.6.0, the second argument to this function was called `helper`
28 | # and was then renamed to `pipeline`.
29 | if "pipeline" in kwargs:
30 | pipeline = kwargs["pipeline"]
31 | elif "helper" in kwargs:
32 | pipeline = kwargs["helper"]
33 | else:
34 | raise TypeError(
35 | f"FetchUser.dispatch() is missing either the `pipeline` or the `helper` keyword argument."
36 | )
37 |
38 | data = pipeline.fetch_state("data")
39 |
40 | try:
41 | id_token = data["id_token"]
42 | except KeyError:
43 | logger.error("Missing id_token in OAuth response: %s" % data)
44 | return pipeline.error(ERR_INVALID_RESPONSE)
45 |
46 | try:
47 | _, payload, _ = map(urlsafe_b64decode, id_token.split(".", 2))
48 | except Exception as exc:
49 | logger.error("Unable to decode id_token: %s" % exc, exc_info=True)
50 | return pipeline.error(ERR_INVALID_RESPONSE)
51 |
52 | try:
53 | payload = json.loads(payload)
54 | except Exception as exc:
55 | logger.error("Unable to decode id_token payload: %s" % exc, exc_info=True)
56 | return pipeline.error(ERR_INVALID_RESPONSE)
57 |
58 | if not payload.get("email"):
59 | logger.error("Missing email in id_token payload: %s" % id_token)
60 | return pipeline.error(ERR_INVALID_RESPONSE)
61 |
62 | # support legacy style domains with pure domain regexp
63 | if self.version is None:
64 | domain = extract_domain(payload["email"])
65 | else:
66 | domain = payload.get("hd")
67 |
68 | pipeline.bind_state("domain", domain)
69 | pipeline.bind_state("user", payload)
70 |
71 | return pipeline.next_step()
72 |
73 |
74 | def oidc_configure_view(
75 | request: HttpRequest, organization: RpcOrganization, auth_provider: RpcAuthProvider
76 | ) -> DeferredResponse:
77 | config = auth_provider.config
78 | if config.get("domain"):
79 | domains: list[str] | None
80 | domains = [config["domain"]]
81 | else:
82 | domains = config.get("domains")
83 | return DeferredResponse(
84 | "oidc/configure.html", {"provider_name": ISSUER or "", "domains": domains or []}
85 | )
86 |
87 |
88 | def extract_domain(email):
89 | return email.rsplit("@", 1)[-1]
90 |
--------------------------------------------------------------------------------
/oidc/provider.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from collections.abc import Callable
4 |
5 | from django.http import HttpRequest
6 |
7 | import time
8 |
9 | import requests
10 | from sentry.auth.provider import MigratingIdentityId
11 | from sentry.auth.providers.oauth2 import OAuth2Callback, OAuth2Login, OAuth2Provider
12 | from sentry.auth.services.auth.model import RpcAuthProvider
13 | from sentry.organizations.services.organization.model import RpcOrganization
14 | from sentry.plugins.base.response import DeferredResponse
15 |
16 | from .constants import (
17 | AUTHORIZATION_ENDPOINT,
18 | CLIENT_ID,
19 | CLIENT_SECRET,
20 | DATA_VERSION,
21 | ISSUER,
22 | SCOPE,
23 | TOKEN_ENDPOINT,
24 | USERINFO_ENDPOINT,
25 | )
26 | from .views import FetchUser, oidc_configure_view
27 |
28 |
29 | class OIDCLogin(OAuth2Login):
30 | authorize_url = AUTHORIZATION_ENDPOINT
31 | client_id = CLIENT_ID
32 | scope = SCOPE
33 |
34 | def __init__(self, client_id, domains=None):
35 | self.domains = domains
36 | super().__init__(client_id=client_id)
37 |
38 | def get_authorize_params(self, state, redirect_uri):
39 | params = super().get_authorize_params(state, redirect_uri)
40 | # TODO(dcramer): ideally we could look at the current resulting state
41 | # when an existing auth happens, and if they're missing a refresh_token
42 | # we should re-prompt them a second time with ``approval_prompt=force``
43 | params["approval_prompt"] = "force"
44 | params["access_type"] = "offline"
45 | return params
46 |
47 |
48 | class OIDCProvider(OAuth2Provider):
49 | name = ISSUER
50 | key = "oidc"
51 |
52 | def __init__(self, domain=None, domains=None, version=None, **config):
53 | if domain:
54 | if domains:
55 | domains.append(domain)
56 | else:
57 | domains = [domain]
58 | self.domains = domains
59 | # if a domain is not configured this is part of the setup pipeline
60 | # this is a bit complex in Sentry's SSO implementation as we don't
61 | # provide a great way to get initial state for new setup pipelines
62 | # vs missing state in case of migrations.
63 | if domains is None:
64 | version = DATA_VERSION
65 | else:
66 | version = None
67 | self.version = version
68 | super().__init__(**config)
69 |
70 | def get_client_id(self):
71 | return CLIENT_ID
72 |
73 | def get_client_secret(self):
74 | return CLIENT_SECRET
75 |
76 | def get_configure_view(
77 | self,
78 | ) -> Callable[[HttpRequest, RpcOrganization, RpcAuthProvider], DeferredResponse]:
79 | return oidc_configure_view
80 |
81 | def get_auth_pipeline(self):
82 | return [
83 | OIDCLogin(domains=self.domains, client_id=self.get_client_id()),
84 | OAuth2Callback(
85 | access_token_url=TOKEN_ENDPOINT,
86 | client_id=self.get_client_id(),
87 | client_secret=self.get_client_secret(),
88 | ),
89 | FetchUser(domains=self.domains, version=self.version),
90 | ]
91 |
92 | def get_refresh_token_url(self):
93 | return TOKEN_ENDPOINT
94 |
95 | def build_config(self, state):
96 | return {"domains": [state["domain"]], "version": DATA_VERSION}
97 |
98 | def get_user_info(self, bearer_token):
99 | endpoint = USERINFO_ENDPOINT
100 | bearer_auth = "Bearer " + bearer_token
101 | retry_codes = [429, 500, 502, 503, 504]
102 | for retry in range(10):
103 | if 10 < retry:
104 | return {}
105 | r = requests.get(
106 | endpoint + "?schema=openid",
107 | headers={"Authorization": bearer_auth},
108 | timeout=20.0,
109 | )
110 | if r.status_code in retry_codes:
111 | wait_time = 2**retry * 0.1
112 | time.sleep(wait_time)
113 | continue
114 | return r.json()
115 |
116 | def build_identity(self, state):
117 | data = state["data"]
118 | user_data = state["user"]
119 |
120 | bearer_token = data["access_token"]
121 | user_info = self.get_user_info(bearer_token)
122 |
123 | # XXX(epurkhiser): We initially were using the email as the id key.
124 | # This caused account dupes on domain changes. Migrate to the
125 | # account-unique sub key.
126 | user_id = MigratingIdentityId(id=user_data["sub"], legacy_id=user_data["email"])
127 |
128 | return {
129 | "id": user_id,
130 | "email": user_info.get("email"),
131 | "name": user_info.get("name"),
132 | "data": self.get_oauth_data(data),
133 | "email_verified": user_info.get("email_verified"),
134 | }
135 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2016 Functional Software, Inc.
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/poetry.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
2 |
3 | [[package]]
4 | name = "black"
5 | version = "24.3.0"
6 | description = "The uncompromising code formatter."
7 | optional = false
8 | python-versions = ">=3.8"
9 | files = [
10 | {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"},
11 | {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"},
12 | {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"},
13 | {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"},
14 | {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"},
15 | {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"},
16 | {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"},
17 | {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"},
18 | {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"},
19 | {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"},
20 | {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"},
21 | {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"},
22 | {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"},
23 | {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"},
24 | {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"},
25 | {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"},
26 | {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"},
27 | {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"},
28 | {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"},
29 | {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"},
30 | {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"},
31 | {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"},
32 | ]
33 |
34 | [package.dependencies]
35 | click = ">=8.0.0"
36 | mypy-extensions = ">=0.4.3"
37 | packaging = ">=22.0"
38 | pathspec = ">=0.9.0"
39 | platformdirs = ">=2"
40 |
41 | [package.extras]
42 | colorama = ["colorama (>=0.4.3)"]
43 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
44 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
45 | uvloop = ["uvloop (>=0.15.2)"]
46 |
47 | [[package]]
48 | name = "certifi"
49 | version = "2024.8.30"
50 | description = "Python package for providing Mozilla's CA Bundle."
51 | optional = false
52 | python-versions = ">=3.6"
53 | files = [
54 | {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
55 | {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
56 | ]
57 |
58 | [[package]]
59 | name = "charset-normalizer"
60 | version = "3.3.2"
61 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
62 | optional = false
63 | python-versions = ">=3.7.0"
64 | files = [
65 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
66 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
67 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
68 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
69 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
70 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
71 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
72 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
73 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
74 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
75 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
76 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
77 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
78 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
79 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
80 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
81 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
82 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
83 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
84 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
85 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
86 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
87 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
88 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
89 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
90 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
91 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
92 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
93 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
94 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
95 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
96 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
97 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
98 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
99 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
100 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
101 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
102 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
103 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
104 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
105 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
106 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
107 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
108 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
109 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
110 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
111 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
112 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
113 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
114 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
115 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
116 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
117 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
118 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
119 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
120 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
121 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
122 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
123 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
124 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
125 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
126 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
127 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
128 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
129 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
130 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
131 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
132 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
133 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
134 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
135 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
136 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
137 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
138 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
139 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
140 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
141 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
142 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
143 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
144 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
145 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
146 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
147 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
148 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
149 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
150 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
151 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
152 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
153 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
154 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
155 | ]
156 |
157 | [[package]]
158 | name = "click"
159 | version = "8.1.7"
160 | description = "Composable command line interface toolkit"
161 | optional = false
162 | python-versions = ">=3.7"
163 | files = [
164 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
165 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
166 | ]
167 |
168 | [package.dependencies]
169 | colorama = {version = "*", markers = "platform_system == \"Windows\""}
170 |
171 | [[package]]
172 | name = "codecov"
173 | version = "2.1.13"
174 | description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab"
175 | optional = false
176 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
177 | files = [
178 | {file = "codecov-2.1.13-py2.py3-none-any.whl", hash = "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5"},
179 | {file = "codecov-2.1.13.tar.gz", hash = "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c"},
180 | ]
181 |
182 | [package.dependencies]
183 | coverage = "*"
184 | requests = ">=2.7.9"
185 |
186 | [[package]]
187 | name = "colorama"
188 | version = "0.4.6"
189 | description = "Cross-platform colored terminal text."
190 | optional = false
191 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
192 | files = [
193 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
194 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
195 | ]
196 |
197 | [[package]]
198 | name = "coverage"
199 | version = "7.6.1"
200 | description = "Code coverage measurement for Python"
201 | optional = false
202 | python-versions = ">=3.8"
203 | files = [
204 | {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"},
205 | {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"},
206 | {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"},
207 | {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"},
208 | {file = "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"},
209 | {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"},
210 | {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"},
211 | {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"},
212 | {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"},
213 | {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"},
214 | {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"},
215 | {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"},
216 | {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"},
217 | {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"},
218 | {file = "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"},
219 | {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"},
220 | {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"},
221 | {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"},
222 | {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"},
223 | {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"},
224 | {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"},
225 | {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"},
226 | {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"},
227 | {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"},
228 | {file = "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"},
229 | {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"},
230 | {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"},
231 | {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"},
232 | {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"},
233 | {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"},
234 | {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"},
235 | {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"},
236 | {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"},
237 | {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"},
238 | {file = "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"},
239 | {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"},
240 | {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"},
241 | {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"},
242 | {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"},
243 | {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"},
244 | {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"},
245 | {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"},
246 | {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"},
247 | {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"},
248 | {file = "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"},
249 | {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"},
250 | {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"},
251 | {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"},
252 | {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"},
253 | {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"},
254 | {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"},
255 | {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"},
256 | {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"},
257 | {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"},
258 | {file = "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"},
259 | {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"},
260 | {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"},
261 | {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"},
262 | {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"},
263 | {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"},
264 | {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"},
265 | {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"},
266 | {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"},
267 | {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"},
268 | {file = "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"},
269 | {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"},
270 | {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"},
271 | {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"},
272 | {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"},
273 | {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"},
274 | {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"},
275 | {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"},
276 | ]
277 |
278 | [package.extras]
279 | toml = ["tomli"]
280 |
281 | [[package]]
282 | name = "fixtures"
283 | version = "4.1.0"
284 | description = "Fixtures, reusable state for writing clean tests and more."
285 | optional = false
286 | python-versions = ">=3.7"
287 | files = [
288 | {file = "fixtures-4.1.0-py3-none-any.whl", hash = "sha256:a43a55da406c37651aa86dd1ba6c3983a09d36d60fe5f72242872c8a4eeeb710"},
289 | {file = "fixtures-4.1.0.tar.gz", hash = "sha256:82b1c5e69f615526ef6c067188a1e6c6067df7f88332509c99f8b8fdbb9776f3"},
290 | ]
291 |
292 | [package.dependencies]
293 | pbr = ">=5.7.0"
294 |
295 | [package.extras]
296 | docs = ["docutils"]
297 | streams = ["testtools"]
298 | test = ["mock", "testtools"]
299 |
300 | [[package]]
301 | name = "flake8"
302 | version = "3.9.2"
303 | description = "the modular source code checker: pep8 pyflakes and co"
304 | optional = false
305 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
306 | files = [
307 | {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
308 | {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
309 | ]
310 |
311 | [package.dependencies]
312 | mccabe = ">=0.6.0,<0.7.0"
313 | pycodestyle = ">=2.7.0,<2.8.0"
314 | pyflakes = ">=2.3.0,<2.4.0"
315 |
316 | [[package]]
317 | name = "idna"
318 | version = "3.8"
319 | description = "Internationalized Domain Names in Applications (IDNA)"
320 | optional = false
321 | python-versions = ">=3.6"
322 | files = [
323 | {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"},
324 | {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"},
325 | ]
326 |
327 | [[package]]
328 | name = "iniconfig"
329 | version = "2.0.0"
330 | description = "brain-dead simple config-ini parsing"
331 | optional = false
332 | python-versions = ">=3.7"
333 | files = [
334 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
335 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
336 | ]
337 |
338 | [[package]]
339 | name = "isort"
340 | version = "5.13.2"
341 | description = "A Python utility / library to sort Python imports."
342 | optional = false
343 | python-versions = ">=3.8.0"
344 | files = [
345 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
346 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
347 | ]
348 |
349 | [package.extras]
350 | colors = ["colorama (>=0.4.6)"]
351 |
352 | [[package]]
353 | name = "mccabe"
354 | version = "0.6.1"
355 | description = "McCabe checker, plugin for flake8"
356 | optional = false
357 | python-versions = "*"
358 | files = [
359 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
360 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
361 | ]
362 |
363 | [[package]]
364 | name = "mypy-extensions"
365 | version = "1.0.0"
366 | description = "Type system extensions for programs checked with the mypy type checker."
367 | optional = false
368 | python-versions = ">=3.5"
369 | files = [
370 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
371 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
372 | ]
373 |
374 | [[package]]
375 | name = "packaging"
376 | version = "24.1"
377 | description = "Core utilities for Python packages"
378 | optional = false
379 | python-versions = ">=3.8"
380 | files = [
381 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
382 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
383 | ]
384 |
385 | [[package]]
386 | name = "pathspec"
387 | version = "0.12.1"
388 | description = "Utility library for gitignore style pattern matching of file paths."
389 | optional = false
390 | python-versions = ">=3.8"
391 | files = [
392 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
393 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
394 | ]
395 |
396 | [[package]]
397 | name = "pbr"
398 | version = "6.1.0"
399 | description = "Python Build Reasonableness"
400 | optional = false
401 | python-versions = ">=2.6"
402 | files = [
403 | {file = "pbr-6.1.0-py2.py3-none-any.whl", hash = "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a"},
404 | {file = "pbr-6.1.0.tar.gz", hash = "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24"},
405 | ]
406 |
407 | [[package]]
408 | name = "platformdirs"
409 | version = "4.3.2"
410 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
411 | optional = false
412 | python-versions = ">=3.8"
413 | files = [
414 | {file = "platformdirs-4.3.2-py3-none-any.whl", hash = "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617"},
415 | {file = "platformdirs-4.3.2.tar.gz", hash = "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c"},
416 | ]
417 |
418 | [package.extras]
419 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
420 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
421 | type = ["mypy (>=1.11.2)"]
422 |
423 | [[package]]
424 | name = "pluggy"
425 | version = "1.5.0"
426 | description = "plugin and hook calling mechanisms for python"
427 | optional = false
428 | python-versions = ">=3.8"
429 | files = [
430 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
431 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
432 | ]
433 |
434 | [package.extras]
435 | dev = ["pre-commit", "tox"]
436 | testing = ["pytest", "pytest-benchmark"]
437 |
438 | [[package]]
439 | name = "pycodestyle"
440 | version = "2.7.0"
441 | description = "Python style guide checker"
442 | optional = false
443 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
444 | files = [
445 | {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
446 | {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
447 | ]
448 |
449 | [[package]]
450 | name = "pyflakes"
451 | version = "2.3.1"
452 | description = "passive checker of Python programs"
453 | optional = false
454 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
455 | files = [
456 | {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
457 | {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
458 | ]
459 |
460 | [[package]]
461 | name = "pytest"
462 | version = "7.4.4"
463 | description = "pytest: simple powerful testing with Python"
464 | optional = false
465 | python-versions = ">=3.7"
466 | files = [
467 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
468 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
469 | ]
470 |
471 | [package.dependencies]
472 | colorama = {version = "*", markers = "sys_platform == \"win32\""}
473 | iniconfig = "*"
474 | packaging = "*"
475 | pluggy = ">=0.12,<2.0"
476 |
477 | [package.extras]
478 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
479 |
480 | [[package]]
481 | name = "requests"
482 | version = "2.32.3"
483 | description = "Python HTTP for Humans."
484 | optional = false
485 | python-versions = ">=3.8"
486 | files = [
487 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
488 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
489 | ]
490 |
491 | [package.dependencies]
492 | certifi = ">=2017.4.17"
493 | charset-normalizer = ">=2,<4"
494 | idna = ">=2.5,<4"
495 | urllib3 = ">=1.21.1,<3"
496 |
497 | [package.extras]
498 | socks = ["PySocks (>=1.5.6,!=1.5.7)"]
499 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
500 |
501 | [[package]]
502 | name = "urllib3"
503 | version = "2.2.3"
504 | description = "HTTP library with thread-safe connection pooling, file post, and more."
505 | optional = false
506 | python-versions = ">=3.8"
507 | files = [
508 | {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"},
509 | {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"},
510 | ]
511 |
512 | [package.extras]
513 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
514 | h2 = ["h2 (>=4,<5)"]
515 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
516 | zstd = ["zstandard (>=0.18.0)"]
517 |
518 | [metadata]
519 | lock-version = "2.0"
520 | python-versions = "^3.11"
521 | content-hash = "ae8987209203ae540aa1d19b1b6b1d2ab08f6ad084e49a7cc304ccd3f0a3febd"
522 |
--------------------------------------------------------------------------------