diff --git a/README.md b/README.md
new file mode 100644
index 0000000..becdf4c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,43 @@
+# Intuit OAuth Python Client
+
+A Python package to manage OAuth2 authentication with Intuits API.
+
+## Rationale
+
+This package is a fork from the official Intuit OAuth [repository](https://github.com/intuit/oauth-pythonclient).
+
+It was forked after 2 years of inactivity and lack of maintenance on the original project.
+I wanted to use it and was frustrated with how it was designed; it made it particularly costly on unit tests.
+PRs that would have fixed this were closed with no action or response by the maintainers.
+
+The lack of engagement from Intuit is telling and it's better to have an engaged community-driven fork, than a
+poorly maintained official project.
+
+## Installation
+
+`pip install intuit-oauth-client`
+
+## Usage
+
+See original [repository](https://github.com/intuit/oauth-pythonclient)
+
+## Changes
+
+Notable changes from the official repository are:
+
+1. Remove HTTP requests on AuthClient __init__ constructor
+2. Lazy load the discovery doc dictionary and all corresponding attributes that were being set on __init__
+3. Remove subclass on `requests.Session`. A separate session can be passed on __init__ or one is created
+4. Update project to `poetry` with a pyproject.toml file
+5. Improve unit tests fixtures. Tests run much faster now issuing far fewer actual HTTP requests
+6. Responsive unpaid maintainer
+
+## Contributions
+
+Feel free to submit an issue or pull request, I promise to respond
+
+## License
+
+This library is provided under Apache 2.0 which is found [here](https://github.com/SunPowered/intuit-oauth-pythonclient/blob/master/LICENSE)
+
+
diff --git a/README.rst b/README.rst
deleted file mode 100644
index 6d1b879..0000000
--- a/README.rst
+++ /dev/null
@@ -1,56 +0,0 @@
-.. image:: views/SDK.png
- :target: https://help.developer.intuit.com/s/samplefeedback?cid=1110&repoName=oauth-pythonclient
-
-Intuit's OAuth2 and OpenID Python Client
-========================================
-
-|build| |coverage| |docs|
-
-.. |build| image:: https://travis-ci.com/intuit/oauth-pythonclient.svg?branch=master
- :target: https://travis-ci.com/intuit/oauth-pythonclient
-
-.. |coverage| image:: https://coveralls.io/repos/github/intuit/oauth-pythonclient/badge.svg?branch=master
- :target: https://coveralls.io/github/intuit/oauth-pythonclient?branch=master
-
-.. |docs| image:: https://readthedocs.org/projects/oauth-pythonclient/badge/?version=latest
- :target: https://oauth-pythonclient.readthedocs.io/en/latest/?badge=latest
- :alt: Documentation Status
-
-This client library is meant to work with Intuit's OAuth and OpenID implementation. The `AuthClient` object response can be used for User Info API, Accounting API and Payments API. This library supports:
-
-- Generating Authorization URL
-- Getting OAuth2 Bearer Token
-- Getting User Info
-- Validating OpenID token
-- Refreshing OAuth2 Token
-- Revoking OAuth2 Token
-- Migrating tokens from OAuth1.0 to OAuth2
-
-Install
--------
-
-Using `pip `_: ::
-
- $ pip install intuit-oauth
-
-Documentation
--------------
-
-Usage and Reference Documentation can be found at `oauth-pythonclient.readthedocs.io `_
-
-Sample App
-----------
-
-Sample app for this library can be found at `IntuitDeveloper GitHub Org `_
-
-Issues and Contributions
-------------------------
-
-Please open an `issue `_ on GitHub if you have a problem, suggestion, or other comment.
-
-Pull requests are welcome and encouraged! Any contributions should include new or updated unit tests as necessary to maintain thorough test coverage.
-
-License
--------
-
-This library is provided under Apache 2.0 which is found `here `__
diff --git a/intuitlib/__init__.py b/intuitlib/__init__.py
index 0865c74..f7d5a7b 100644
--- a/intuitlib/__init__.py
+++ b/intuitlib/__init__.py
@@ -12,10 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-try:
- import pkg_resources
- pkg_resources.declare_namespace(__name__)
-except ImportError:
- import pkgutil
- __path__ = pkgutil.extend_path(__path__, __name__)
diff --git a/intuitlib/client.py b/intuitlib/client.py
index fc3ae6a..07f37ee 100644
--- a/intuitlib/client.py
+++ b/intuitlib/client.py
@@ -12,11 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from __future__ import absolute_import
-
import json
import requests
-from future.moves.urllib.parse import urlencode
+from urllib.parse import urlencode
from intuitlib.utils import (
get_discovery_doc,
@@ -26,11 +24,11 @@
send_request,
)
-class AuthClient(requests.Session):
+class AuthClient:
"""Handles OAuth 2.0 and OpenID Connect flows to get access to User Info API, Accounting APIs and Payments APIs
"""
- def __init__(self, client_id, client_secret, redirect_uri, environment, state_token=None, access_token=None, refresh_token=None, id_token=None, realm_id=None):
+ def __init__(self, client_id, client_secret, redirect_uri, environment, state_token=None, access_token=None, refresh_token=None, id_token=None, realm_id=None, session=None):
"""Constructor for AuthClient
:param client_id: Client ID found in developer account Keys tab
@@ -42,10 +40,9 @@ def __init__(self, client_id, client_secret, redirect_uri, environment, state_to
:param refresh_token: Refresh Token for refresh or revoke functionality, defaults to None
:param id_token: ID Token for OpenID flow, defaults to None
:param realm_id: QBO Realm/Company ID, defaults to None
+ :param session: Optional requests.Session object to use
"""
- super(AuthClient, self).__init__()
-
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
@@ -53,13 +50,13 @@ def __init__(self, client_id, client_secret, redirect_uri, environment, state_to
self.state_token = state_token
# Discovery doc contains endpoints based on environment specified
- discovery_doc = get_discovery_doc(self.environment, session=self)
- self.auth_endpoint = discovery_doc['authorization_endpoint']
- self.token_endpoint = discovery_doc['token_endpoint']
- self.revoke_endpoint = discovery_doc['revocation_endpoint']
- self.issuer_uri = discovery_doc['issuer']
- self.jwks_uri = discovery_doc['jwks_uri']
- self.user_info_url = discovery_doc['userinfo_endpoint']
+ self._discovery_doc = None
+ self._auth_endpoint = None
+ self._token_endpoint = None
+ self._revoke_endpoint = None
+ self._issuer_uri = None
+ self._jwks_uri = None
+ self._user_info_url = None
# response values
self.realm_id = realm_id
@@ -68,7 +65,62 @@ def __init__(self, client_id, client_secret, redirect_uri, environment, state_to
self.refresh_token = refresh_token
self.x_refresh_token_expires_in = None
self.id_token = id_token
+
+ # Session
+ if session is not None and not isinstance(session, requests.Session):
+ raise TypeError(f"Bad parameter type for session. Expected requests.Session, got {type(session)}")
+ self.session = session or requests.Session()
+
+ @property
+ def discovery_doc(self):
+ """Cache the discovery doc dictionary returned from get_discovery_doc"""
+ if self._discovery_doc is None:
+ self._discovery_doc = get_discovery_doc(self.environment, session=self)
+ return self._discovery_doc
+
+ @property
+ def auth_endpoint(self):
+ """Cache authorization_endpoint from the discovery doc"""
+ if self._auth_endpoint is None:
+ self._auth_endpoint = self.discovery_doc['authorization_endpoint']
+ return self._auth_endpoint
+
+ @property
+ def token_endpoint(self):
+ """Cache token_endpoint from the discovery doc"""
+ if self._token_endpoint is None:
+ self._token_endpoint = self.discovery_doc['token_endpoint']
+ return self._token_endpoint
+
+ @property
+ def revoke_endpoint(self):
+ """Cache revocation_endpoint from the discovery doc"""
+ if self._revoke_endpoint is None:
+ self._revoke_endpoint = self.discovery_doc['revocation_endpoint']
+ return self._revoke_endpoint
+
+ @property
+ def issuer_uri(self):
+ """Cache issuer from the discovery doc"""
+ if self._issuer_uri is None:
+ self._issuer_uri = self.discovery_doc['issuer']
+ return self._issuer_uri
+
+ @property
+ def jwks_uri(self):
+ """Cache jwks_uri from the discovery doc"""
+ if self._jwks_uri is None:
+ self._jwks_uri = self.discovery_doc['jwks_uri']
+ return self._jwks_uri
+
+ @property
+ def user_info_url(self):
+ """Cache userinfo_endpoint from the discovery doc"""
+ if self._user_info_url is None:
+ self._user_info_url = self.discovery_doc['userinfo_endpoint']
+ return self._user_info_url
+
def setAuthorizeURLs(self, urlObject):
"""Set authorization url using custom values passed in the data dict
:param **data: data dict for custom authorizationURLS
diff --git a/intuitlib/migration.py b/intuitlib/migration.py
index da142cb..abe0edd 100644
--- a/intuitlib/migration.py
+++ b/intuitlib/migration.py
@@ -14,8 +14,6 @@
"""This module helps in migrating OAuth 1.0a tokens to OAuth 2.0
"""
-from __future__ import absolute_import
-
import json
from requests_oauthlib import OAuth1
diff --git a/intuitlib/utils.py b/intuitlib/utils.py
index f3fb700..ef564e0 100644
--- a/intuitlib/utils.py
+++ b/intuitlib/utils.py
@@ -14,8 +14,6 @@
"""This module contains utility methods used by this library
"""
-from __future__ import absolute_import
-
import json
from base64 import b64encode, b64decode, urlsafe_b64decode
from datetime import datetime
@@ -24,7 +22,6 @@
from jose import jwk
import requests
from requests.sessions import Session
-import six
from requests_oauthlib import OAuth1
@@ -109,9 +106,7 @@ def get_auth_header(client_id, client_secret):
:return: Authorization header
"""
- auth_header = '{0}:{1}'.format(client_id, client_secret)
- if six.PY3:
- auth_header = auth_header.encode('utf-8')
+ auth_header = '{0}:{1}'.format(client_id, client_secret).encode('utf-8')
return ' '.join(['Basic', b64encode(auth_header).decode('utf-8')])
def scopes_to_string(scopes):
diff --git a/intuitlib/version.py b/intuitlib/version.py
index d2811fe..025a95d 100644
--- a/intuitlib/version.py
+++ b/intuitlib/version.py
@@ -12,4 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-__version__ = '1.2.4'
+__version__ = '2.0.1'
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..83348f9
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,516 @@
+[[package]]
+name = "attrs"
+version = "22.1.0"
+description = "Classes Without Boilerplate"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.extras]
+dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
+docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
+tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
+tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
+
+[[package]]
+name = "certifi"
+version = "2022.9.24"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "charset-normalizer"
+version = "2.1.1"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "main"
+optional = false
+python-versions = ">=3.6.0"
+
+[package.extras]
+unicode-backport = ["unicodedata2"]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+
+[[package]]
+name = "coverage"
+version = "6.5.0"
+description = "Code coverage measurement for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "ecdsa"
+version = "0.18.0"
+description = "ECDSA cryptographic signature library (pure python)"
+category = "main"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[package.dependencies]
+six = ">=1.9.0"
+
+[package.extras]
+gmpy = ["gmpy"]
+gmpy2 = ["gmpy2"]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.0.4"
+description = "Backport of PEP 654 (exception groups)"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "idna"
+version = "3.4"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "iniconfig"
+version = "1.1.1"
+description = "iniconfig: brain-dead simple config-ini parsing"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "mock"
+version = "4.0.3"
+description = "Rolling backport of unittest.mock for all Pythons"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+build = ["blurb", "twine", "wheel"]
+docs = ["sphinx"]
+test = ["pytest (<5.4)", "pytest-cov"]
+
+[[package]]
+name = "mypy"
+version = "0.991"
+description = "Optional static typing for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+mypy-extensions = ">=0.4.3"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typing-extensions = ">=3.10"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+install-types = ["pip"]
+python2 = ["typed-ast (>=1.4.0,<2)"]
+reports = ["lxml"]
+
+[[package]]
+name = "mypy-extensions"
+version = "0.4.3"
+description = "Experimental type system extensions for programs checked with the mypy typechecker."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "oauthlib"
+version = "3.2.2"
+description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+rsa = ["cryptography (>=3.0.0)"]
+signals = ["blinker (>=1.4.0)"]
+signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
+
+[[package]]
+name = "packaging"
+version = "21.3"
+description = "Core utilities for Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
+
+[[package]]
+name = "pluggy"
+version = "1.0.0"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pyasn1"
+version = "0.4.8"
+description = "ASN.1 types and codecs"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pyparsing"
+version = "3.0.9"
+description = "pyparsing module - Classes and methods to define and execute parsing grammars"
+category = "dev"
+optional = false
+python-versions = ">=3.6.8"
+
+[package.extras]
+diagrams = ["jinja2", "railroad-diagrams"]
+
+[[package]]
+name = "pytest"
+version = "7.2.0"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+attrs = ">=19.2.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+
+[[package]]
+name = "pytest-cov"
+version = "4.0.0"
+description = "Pytest plugin for measuring coverage."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+coverage = {version = ">=5.2.1", extras = ["toml"]}
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
+
+[[package]]
+name = "python-jose"
+version = "3.3.0"
+description = "JOSE implementation in Python"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+ecdsa = "!=0.15"
+pyasn1 = "*"
+rsa = "*"
+
+[package.extras]
+cryptography = ["cryptography (>=3.4.0)"]
+pycrypto = ["pyasn1", "pycrypto (>=2.6.0,<2.7.0)"]
+pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"]
+
+[[package]]
+name = "requests"
+version = "2.28.1"
+description = "Python HTTP for Humans."
+category = "main"
+optional = false
+python-versions = ">=3.7, <4"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<3"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<1.27"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
+name = "requests-oauthlib"
+version = "1.3.1"
+description = "OAuthlib authentication support for Requests."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+oauthlib = ">=3.0.0"
+requests = ">=2.0.0"
+
+[package.extras]
+rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
+
+[[package]]
+name = "rsa"
+version = "4.9"
+description = "Pure-Python RSA implementation"
+category = "main"
+optional = false
+python-versions = ">=3.6,<4"
+
+[package.dependencies]
+pyasn1 = ">=0.1.3"
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "typing-extensions"
+version = "4.4.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "urllib3"
+version = "1.26.13"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
+secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+
+[metadata]
+lock-version = "1.1"
+python-versions = "^3.10"
+content-hash = "793d77edae00b65718473c862cee30345b593b874c16e9aa933bb190b41b16d0"
+
+[metadata.files]
+attrs = [
+ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
+ {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
+]
+certifi = [
+ {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
+ {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
+]
+charset-normalizer = [
+ {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
+ {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
+]
+colorama = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+coverage = [
+ {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"},
+ {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"},
+ {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"},
+ {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"},
+ {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"},
+ {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"},
+ {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"},
+ {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"},
+ {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"},
+ {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"},
+ {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"},
+ {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"},
+ {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"},
+ {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"},
+ {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"},
+ {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"},
+ {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"},
+ {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"},
+ {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"},
+ {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"},
+ {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"},
+ {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"},
+ {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"},
+ {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"},
+ {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"},
+ {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"},
+ {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"},
+ {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"},
+ {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"},
+ {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"},
+ {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"},
+ {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"},
+ {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"},
+ {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"},
+ {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"},
+ {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"},
+ {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"},
+ {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"},
+ {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"},
+ {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"},
+ {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"},
+ {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"},
+ {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"},
+ {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"},
+ {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"},
+ {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"},
+ {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"},
+ {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"},
+ {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"},
+ {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"},
+]
+ecdsa = [
+ {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"},
+ {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"},
+]
+exceptiongroup = [
+ {file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"},
+ {file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"},
+]
+idna = [
+ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
+ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
+]
+iniconfig = [
+ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
+ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+]
+mock = [
+ {file = "mock-4.0.3-py3-none-any.whl", hash = "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62"},
+ {file = "mock-4.0.3.tar.gz", hash = "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc"},
+]
+mypy = [
+ {file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"},
+ {file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"},
+ {file = "mypy-0.991-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6"},
+ {file = "mypy-0.991-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb"},
+ {file = "mypy-0.991-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305"},
+ {file = "mypy-0.991-cp310-cp310-win_amd64.whl", hash = "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c"},
+ {file = "mypy-0.991-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372"},
+ {file = "mypy-0.991-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f"},
+ {file = "mypy-0.991-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33"},
+ {file = "mypy-0.991-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05"},
+ {file = "mypy-0.991-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad"},
+ {file = "mypy-0.991-cp311-cp311-win_amd64.whl", hash = "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297"},
+ {file = "mypy-0.991-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813"},
+ {file = "mypy-0.991-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711"},
+ {file = "mypy-0.991-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd"},
+ {file = "mypy-0.991-cp37-cp37m-win_amd64.whl", hash = "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"},
+ {file = "mypy-0.991-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a"},
+ {file = "mypy-0.991-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93"},
+ {file = "mypy-0.991-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf"},
+ {file = "mypy-0.991-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135"},
+ {file = "mypy-0.991-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70"},
+ {file = "mypy-0.991-cp38-cp38-win_amd64.whl", hash = "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243"},
+ {file = "mypy-0.991-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d"},
+ {file = "mypy-0.991-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5"},
+ {file = "mypy-0.991-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3"},
+ {file = "mypy-0.991-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648"},
+ {file = "mypy-0.991-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476"},
+ {file = "mypy-0.991-cp39-cp39-win_amd64.whl", hash = "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461"},
+ {file = "mypy-0.991-py3-none-any.whl", hash = "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb"},
+ {file = "mypy-0.991.tar.gz", hash = "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06"},
+]
+mypy-extensions = [
+ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
+ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
+]
+oauthlib = [
+ {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"},
+ {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"},
+]
+packaging = [
+ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
+ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
+]
+pluggy = [
+ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
+ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+]
+pyasn1 = [
+ {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
+ {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
+]
+pyparsing = [
+ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
+ {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
+]
+pytest = [
+ {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
+ {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
+]
+pytest-cov = [
+ {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"},
+ {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"},
+]
+python-jose = [
+ {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"},
+ {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"},
+]
+requests = [
+ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
+ {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
+]
+requests-oauthlib = [
+ {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"},
+ {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"},
+]
+rsa = [
+ {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"},
+ {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"},
+]
+six = [
+ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+tomli = [
+ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+typing-extensions = [
+ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
+ {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
+]
+urllib3 = [
+ {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"},
+ {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"},
+]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..2229ecd
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,31 @@
+[tool.poetry]
+name = "intuit-oauth-client"
+version = "2.0.1"
+description = "An updated fork of Intuit's python-oauthclient library"
+authors = ["SunPowered "]
+homepage = "https://github.com/SunPowered/intuit-oauth-pythonclient"
+license = "Apache 2.0"
+readme = "README.md"
+packages = [{include = "intuitlib"}]
+
+[tool.poetry.dependencies]
+python = "^3.10"
+requests = "^2.13.0"
+requests_oauthlib = "^1.0.0"
+python_jose = "^3.3.0"
+
+[tool.poetry.group.dev.dependencies]
+mypy = "^0.991"
+pytest = "^7.2.0"
+coverage = "^6.5.0"
+pytest-cov = "^4.0.0"
+mock = "^4.0.3"
+
+[tool.pytest.ini_options]
+markers = [
+ "http: marks tests that perform HTTP calls",
+]
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 87b7bb0..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-python_jose>=2.0.2
-future>=0.16.0
-requests>=2.13.0
-mock>=2.0.0
-requests_oauthlib>=1.0.0
-coverage==4.4
-python-coveralls>=2.9.0
-pytest>=3.8.0
-pytest-cov==2.5.0
-six>=1.10.0
-enum-compat
-
-
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 6ffc152..0000000
--- a/setup.py
+++ /dev/null
@@ -1,42 +0,0 @@
- # Copyright (c) 2018 Intuit
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
-
-from setuptools import setup
-from setuptools import find_packages
-
-version = {}
-with open("./intuitlib/version.py") as fp:
- exec(fp.read(), version)
-
-setup(
- name='intuit-oauth',
- version=version['__version__'],
- description='Intuit OAuth Client',
- long_description=open('README.rst').read().strip(),
- author='Intuit Inc',
- author_email='IDGSDK@intuit.com',
- url='https://github.com/intuit/oauth-pythonclient',
- packages=find_packages(exclude=('tests*',)),
- namespace_packages=('intuitlib',),
- install_requires=[
- 'python_jose>=2.0.2',
- 'future>=0.16.0',
- 'requests>=2.13.0',
- 'requests_oauthlib>=1.0.0',
- 'six>=1.10.0',
- 'enum-compat',
- ],
- license='Apache 2.0',
- keywords='intuit quickbooks oauth auth openid client'
-)
diff --git a/tests/test_client.py b/tests/test_client.py
index 5ebc106..4f2f241 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -15,7 +15,6 @@
"""Test module for intuitlib.client
"""
-from __future__ import unicode_literals
import pytest
import mock
@@ -29,37 +28,42 @@
from intuitlib.exceptions import AuthClientError
from tests.helper import MockResponse
-class TestClient():
-
- auth_client = AuthClient('clientId','secret','https://www.mydemoapp.com/oauth-redirect','sandbox')
+@pytest.fixture()
+def auth_client_default():
+ return AuthClient('clientId','secret','https://www.mydemoapp.com/oauth-redirect','sandbox')
- client_mock_discovery_urls = {
- 'authorization_endpoint': 'test',
- 'token_endpoint': 'test',
- 'revocation_endpoint': 'test',
- 'issuer': 'test',
- 'jwks_uri': 'test',
- 'userinfo_endpoint': 'test',
+@pytest.fixture()
+def auth_client(auth_client_default):
+ auth_client_default._discovery_doc = {
+ 'authorization_endpoint': 'http://test',
+ 'token_endpoint': 'http://test',
+ 'revocation_endpoint': 'http://test',
+ 'issuer': 'http://test',
+ 'jwks_uri': 'http://test',
+ 'userinfo_endpoint': 'http://test',
}
+ return auth_client_default
+class TestClient():
+
def mock_request(self, status=200, content=None):
return MockResponse(status=status, content=content)
- def test_input_all(self):
+ def test_input_all(self, auth_client):
- self.auth_client.access_token = None
+ auth_client.access_token = None
with pytest.raises(ValueError):
- self.auth_client.refresh()
+ auth_client.refresh()
with pytest.raises(ValueError):
- self.auth_client.revoke()
+ auth_client.revoke()
with pytest.raises(ValueError):
- self.auth_client.get_user_info()
+ auth_client.get_user_info()
- def test_get_authorization_url_without_csrf(self):
- uri = self.auth_client.get_authorization_url([Scopes.ACCOUNTING])
+ def test_get_authorization_url_without_csrf(self, auth_client):
+ uri = auth_client.get_authorization_url([Scopes.ACCOUNTING])
params = parse_qs(urlsplit(uri).query)
param_values_to_string = {k: v[0] for k, v in params.items()}
@@ -68,43 +72,44 @@ def test_get_authorization_url_without_csrf(self):
'response_type': 'code',
'scope': 'com.intuit.quickbooks.accounting',
'redirect_uri': 'https://www.mydemoapp.com/oauth-redirect',
- 'state': self.auth_client.state_token
+ 'state': auth_client.state_token
}
assert auth_params == param_values_to_string
- @mock.patch('intuitlib.utils.requests.Session')
- def test_exceptions_all_bad_request(self, mock_post):
+ @mock.patch('intuitlib.utils.requests.Session.request')
+ def test_exceptions_all_bad_request(self, mock_post, auth_client):
mock_resp = self.mock_request(status=400)
mock_post.return_value = mock_resp
+ # auth_client = auth_client_default
with pytest.raises(AuthClientError):
- self.auth_client.get_bearer_token('test_code', realm_id='realm')
+ auth_client.get_bearer_token('test_code', realm_id='realm')
with pytest.raises(AuthClientError):
- self.auth_client.refresh(refresh_token='test_token')
+ auth_client.refresh(refresh_token='test_token')
with pytest.raises(AuthClientError):
- self.auth_client.revoke(token='test_token')
+ auth_client.revoke(token='test_token')
with pytest.raises(AuthClientError):
- self.auth_client.get_user_info(access_token='token')
+ auth_client.get_user_info(access_token='token')
@mock.patch('intuitlib.utils.requests.Session.request')
- def test_get_user_info_ok(self, mock_session):
+ def test_get_user_info_ok(self, mock_session, auth_client):
mock_resp = self.mock_request(status=200, content={
'givenName': 'Test'
})
mock_session.return_value = mock_resp
- response = self.auth_client.get_user_info(access_token='token')
+ response = auth_client.get_user_info(access_token='token')
assert response.json()['givenName'] == 'Test'
@mock.patch('intuitlib.utils.requests.Session.request')
- def test_revoke_ok(self, mock_session):
+ def test_revoke_ok(self, mock_session, auth_client):
mock_resp = self.mock_request(status=200)
mock_session.return_value = mock_resp
- response = self.auth_client.revoke(token='token')
+ response = auth_client.revoke(token='token')
assert response
if __name__ == '__main__':
diff --git a/tests/test_migration.py b/tests/test_migration.py
index 520f0ae..5b3712d 100644
--- a/tests/test_migration.py
+++ b/tests/test_migration.py
@@ -31,49 +31,54 @@
from intuitlib.migration import migrate
from tests.helper import MockResponse
-class TestMigration():
-
- auth_client = AuthClient('clientId','secret','https://www.mydemoapp.com/oauth-redirect','sandbox')
-
- client_mock_discovery_urls = {
- 'authorization_endpoint': 'test',
- 'token_endpoint': 'test',
- 'revocation_endpoint': 'test',
- 'issuer': 'test',
- 'jwks_uri': 'test',
- 'userinfo_endpoint': 'test',
+@pytest.fixture()
+def auth_client_default():
+ return AuthClient('clientId','secret','https://www.mydemoapp.com/oauth-redirect','sandbox')
+
+@pytest.fixture()
+def auth_client(auth_client_default):
+ auth_client_default._discovery_doc = {
+ 'authorization_endpoint': 'http://test',
+ 'token_endpoint': 'http://test',
+ 'revocation_endpoint': 'http://test',
+ 'issuer': 'http://test',
+ 'jwks_uri': 'http://test',
+ 'userinfo_endpoint': 'http://test',
}
+ return auth_client_default
+
+class TestMigration():
def mock_request(self, status=200, content=None):
return MockResponse(status=status, content=content)
@mock.patch('intuitlib.utils.requests.request')
- def test_migrate_bad_request(self, mock_post):
+ def test_migrate_bad_request(self, mock_post, auth_client):
mock_resp = self.mock_request(status=400)
mock_post.return_value = mock_resp
with pytest.raises(AuthClientError):
- migrate('consumer_key', 'consumer_secret', 'access_token', 'access_secret', self.auth_client, [Scopes.ACCOUNTING])
+ migrate('consumer_key', 'consumer_secret', 'access_token', 'access_secret', auth_client, [Scopes.ACCOUNTING])
@mock.patch('intuitlib.utils.requests.request')
- def test_migrate_200(self, mock_post):
+ def test_migrate_200(self, mock_post, auth_client):
mock_resp = self.mock_request(status=200, content={
'access_token': 'testaccess'
})
mock_post.return_value = mock_resp
- migrate('consumer_key', 'consumer_secret', 'access_token', 'access_secret', self.auth_client, [Scopes.ACCOUNTING])
+ migrate('consumer_key', 'consumer_secret', 'access_token', 'access_secret', auth_client, [Scopes.ACCOUNTING])
- assert self.auth_client.access_token == 'testaccess'
+ assert auth_client.access_token == 'testaccess'
@mock.patch('intuitlib.utils.requests.request')
- def test_migrate_prod(self, mock_post):
+ def test_migrate_prod(self, mock_post, auth_client):
mock_resp = self.mock_request(status=400)
mock_post.return_value = mock_resp
- self.auth_client.environment = 'production'
+ auth_client.environment = 'production'
with pytest.raises(AuthClientError):
- migrate('consumer_key', 'consumer_secret', 'access_token', 'access_secret', self.auth_client, [Scopes.ACCOUNTING])
+ migrate('consumer_key', 'consumer_secret', 'access_token', 'access_secret', auth_client, [Scopes.ACCOUNTING])
if __name__ == '__main__':
pytest.main()
\ No newline at end of file
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 9cf2059..a16f845 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -33,6 +33,23 @@
from intuitlib.exceptions import AuthClientError
from tests.helper import MockResponse
+@pytest.fixture()
+def auth_client_default():
+ return AuthClient('clientId','secret','https://www.mydemoapp.com/oauth-redirect','sandbox')
+
+@pytest.fixture()
+def auth_client(auth_client_default):
+ auth_client_default._discovery_doc = {
+ 'authorization_endpoint': 'http://test',
+ 'token_endpoint': 'http://test',
+ 'revocation_endpoint': 'http://test',
+ 'issuer': 'http://test',
+ 'jwks_uri': 'http://test',
+ 'userinfo_endpoint': 'http://test',
+ }
+ return auth_client_default
+
+
class TestUtils():
auth_client = AuthClient('client_id','client_secret','redirect_uri','sandbox')
@@ -40,18 +57,21 @@ class TestUtils():
def mock_request(self, status=200, content=None):
return MockResponse(status=status, content=content)
+ @pytest.mark.http
def test_get_discovery_doc_sandbox(self):
discovery_doc = get_discovery_doc('sandbox')
assert discovery_doc['issuer'] == 'https://oauth.platform.intuit.com/op/v1'
assert discovery_doc['userinfo_endpoint'] == 'https://sandbox-accounts.platform.intuit.com/v1/openid_connect/userinfo'
+ @pytest.mark.http
def test_get_discovery_doc_production(self):
discovery_doc = get_discovery_doc('production')
assert discovery_doc['issuer'] == 'https://oauth.platform.intuit.com/op/v1'
assert discovery_doc['userinfo_endpoint'] == 'https://accounts.platform.intuit.com/v1/openid_connect/userinfo'
+ @pytest.mark.http
def test_get_discovery_doc_custom_url_input(self):
discovery_doc = get_discovery_doc('https://developer.intuit.com/.well-known/openid_sandbox_configuration/')
@@ -79,18 +99,18 @@ def test_scopes_to_string_input_correct(self):
assert scope == 'openid email'
- def test_set_attributes(self):
+ def test_set_attributes(self, auth_client):
response = {
'refresh_token': 'testrefresh',
'access_token': 'testaccess',
'test': 'testing',
'id_token': 'token'
}
- set_attributes(self.auth_client, response)
+ set_attributes(auth_client, response)
- assert self.auth_client.refresh_token == response['refresh_token']
- assert self.auth_client.access_token == response['access_token']
- assert not self.auth_client.id_token
+ assert auth_client.refresh_token == response['refresh_token']
+ assert auth_client.access_token == response['access_token']
+ assert not auth_client.id_token
@mock.patch('intuitlib.utils.requests.request')
def test_send_request_bad_request(self, mock_post):
@@ -101,30 +121,30 @@ def test_send_request_bad_request(self, mock_post):
send_request('POST', 'url', {}, '', body={})
@mock.patch('intuitlib.utils.requests.request')
- def test_send_request_ok(self, mock_post):
+ def test_send_request_ok(self, mock_post, auth_client):
mock_resp = self.mock_request(status=200, content={'access_token': 'testaccess'})
mock_post.return_value = mock_resp
- send_request('POST', 'url', {}, self.auth_client, body={})
- assert self.auth_client.access_token == 'testaccess'
+ send_request('POST', 'url', {}, auth_client, body={})
+ assert auth_client.access_token == 'testaccess'
@mock.patch('intuitlib.utils.Session.request')
- def test_send_request_session_ok(self, mock_post):
+ def test_send_request_session_ok(self, mock_post, auth_client):
mock_resp = self.mock_request(status=200, content={'access_token': 'testaccess'})
mock_post.return_value = mock_resp
session = requests.Session()
- send_request('POST', 'url', {}, self.auth_client, body={}, session=session)
- assert self.auth_client.access_token == 'testaccess'
+ send_request('POST', 'url', {}, auth_client, body={}, session=session)
+ assert auth_client.access_token == 'testaccess'
@mock.patch('intuitlib.utils.Session.request')
- def test_send_request_session_bad(self, mock_post):
+ def test_send_request_session_bad(self, mock_post, auth_client):
mock_resp = self.mock_request(status=400, content={'access_token': 'testaccess'})
mock_post.return_value = mock_resp
session = requests.Session()
with pytest.raises(AuthClientError):
- send_request('POST', 'url', {}, self.auth_client, body={}, session=session)
+ send_request('POST', 'url', {}, auth_client, body={}, session=session)
def test_generate_token(self):
token = generate_token()