├── .editorconfig ├── .gitattributes ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── pr-checks.yml │ └── publish.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── poetry.lock ├── pyproject.toml ├── src └── fds │ └── sdk │ └── utils │ ├── __init__.py │ └── authentication │ ├── __init__.py │ ├── confidential.py │ ├── constants.py │ ├── exceptions.py │ └── oauth2client.py ├── tests └── fds │ └── sdk │ └── utils │ └── authentication │ ├── test_confidential.py │ └── test_oauth2client.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # @see http://editorconfig.org/ 2 | 3 | # This is the top-most .editorconfig file; do not search in parent directories. 4 | root = true 5 | 6 | # All files. 7 | [*] 8 | end_of_line = LF 9 | indent_style = space 10 | indent_size = 2 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [*.py] 16 | indent_size = 4 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.png binary 3 | *.pdf binary 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # global code owners 2 | 3 | * @FactSet/enterprise-sdk 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | - **Package Name**: 11 | - **Package Version**: 12 | - **Package Language**: 13 | - **Operating System**: 14 | 15 | **Describe the bug** 16 | A clear and concise description of what the bug is. 17 | 18 | **To Reproduce** 19 | Steps to reproduce the behavior: 20 | 21 | 1. ... 22 | 2. ... 23 | 3. ... 24 | 25 | **Expected behavior** 26 | A clear and concise description of what you expected to happen. 27 | 28 | **Screenshots** 29 | If applicable, add screenshots to help explain your problem. 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | commit-message: 8 | prefix: fix 9 | prefix-development: chore 10 | include: scope 11 | - package-ecosystem: github-actions 12 | directory: '/' 13 | schedule: 14 | interval: daily 15 | commit-message: 16 | prefix: fix 17 | prefix-development: chore 18 | include: scope 19 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 18 | 19 | ### Links 20 | 21 | 28 | 29 | ### Testing 30 | 31 | 35 | 36 | ### Checklist 37 | 38 | Ensure the following things have been met before requesting a review: 39 | 40 | * [ ] Follows all project developer guide and coding standards. 41 | * [ ] Tests have been written for the change, when applicable. 42 | * [ ] Confidential information (credentials, auth tokens, etc...) is not included. 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | push: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-24.04 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.11" 22 | 23 | - name: Install Poetry 24 | uses: snok/install-poetry@v1 25 | 26 | - name: Install dependencies 27 | run: poetry install 28 | 29 | - name: Lint 30 | run: poetry run black --check . 31 | 32 | - name: Build 33 | run: poetry build 34 | 35 | # the `coverage xml -i` command is needed to re-write the 36 | # coverage report with relative paths 37 | - name: Test 38 | run: | 39 | poetry run pytest --cov src --cov-report xml tests 40 | poetry run coverage xml -i 41 | 42 | test: 43 | name: Test 44 | runs-on: ubuntu-24.04 45 | strategy: 46 | fail-fast: false 47 | matrix: 48 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 49 | poetry-version: ["1.8.2"] 50 | 51 | steps: 52 | - name: Checkout repository 53 | uses: actions/checkout@v4 54 | 55 | - name: Setup python 56 | uses: actions/setup-python@v5 57 | with: 58 | python-version: "${{ matrix.python-version }}" 59 | 60 | - name: Install Poetry 61 | uses: snok/install-poetry@v1 62 | with: 63 | version: "${{ matrix.poetry-version }}" 64 | 65 | - name: Install dependencies 66 | run: poetry install 67 | 68 | - name: Build 69 | run: poetry build 70 | 71 | - name: Test 72 | run: poetry run tox 73 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [main] 9 | schedule: 10 | - cron: "15 18 * * 5" 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-24.04 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: ["python"] 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v3 33 | with: 34 | languages: ${{ matrix.language }} 35 | 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v3 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v3 41 | -------------------------------------------------------------------------------- /.github/workflows/pr-checks.yml: -------------------------------------------------------------------------------- 1 | name: Pull request checks 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | - reopened 10 | 11 | jobs: 12 | check-title: 13 | if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' }} 14 | name: Check title 15 | runs-on: ubuntu-24.04 16 | 17 | steps: 18 | - uses: naveenk1223/action-pr-title@v1.0.0 19 | with: 20 | regex: '^(chore|demo|deprecate|docs|feat|fix|perf|refactor|revert|style|test)(\(.+\))?: .+$' 21 | max_length: 60 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | name: Deploy to package index 10 | runs-on: ubuntu-24.04 11 | env: 12 | PYTHON_VERSION: 3.9 13 | REPOSITORY_USERNAME: ${{ secrets.PYPI_USERNAME }} 14 | REPOSITORY_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 15 | REPOSITORY_URL: ${{ secrets.PYPI_PUBLISH_URL }} 16 | ANACONDA_TOKEN: ${{ secrets.ANACONDA_TOKEN }} 17 | CONDA_ENV_NAME: conda-env 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup Python 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: ${{ env.PYTHON_VERSION }} 27 | 28 | - name: Setup Conda 29 | uses: conda-incubator/setup-miniconda@v3 30 | with: 31 | miniconda-version: "latest" 32 | activate-environment: ${{ env.CONDA_ENV_NAME }} 33 | python-version: ${{ env.PYTHON_VERSION }} 34 | 35 | - name: Install Poetry 36 | uses: snok/install-poetry@v1 37 | 38 | - name: Configure Poetry 39 | run: | 40 | poetry config repositories.publish $REPOSITORY_URL 41 | poetry config http-basic.publish $REPOSITORY_USERNAME $REPOSITORY_PASSWORD 42 | 43 | - name: Build 44 | run: | 45 | poetry build 46 | 47 | - name: Publish 48 | run: | 49 | poetry publish -r publish 50 | 51 | - name: Publish to Anaconda 52 | shell: bash -el {0} 53 | run: | 54 | conda install grayskull conda-build anaconda-client 55 | conda info 56 | conda list 57 | grayskull --version 58 | anaconda --version 59 | 60 | count=0 61 | max_retries=5 62 | tag=${{ github.event.release.tag_name }} 63 | version=${tag#v} 64 | while [ $count -lt $max_retries ]; do 65 | # Create meta.yaml recipe for the package pulled from PyPi 66 | grayskull pypi fds.sdk.utils==${version} 67 | 68 | if [ -f ./fds.sdk.utils/meta.yaml ]; then 69 | echo "Version ${version} of fds.sdk.utils is available on PyPI." 70 | 71 | # Modify the meta.yaml recipe-maintainers property to include all maintainers of this repo 72 | sed -i "/recipe-maintainers:/,/extra:/ s/- .*/- gdulafactset/" fds.sdk.utils/meta.yaml 73 | echo " - mima0815" >> fds.sdk.utils/meta.yaml 74 | echo " - eschmidtfds" >> fds.sdk.utils/meta.yaml 75 | echo " - Filip1x9" >> fds.sdk.utils/meta.yaml 76 | echo " - dgawande12" >> fds.sdk.utils/meta.yaml 77 | 78 | # Modify meta.yaml to include description and dev_url 79 | sed -i "/about:/a \\ 80 | dev_url: \"https://github.com/factset/enterprise-sdk-utils-python\"\\ 81 | description: \"This repository contains a collection of utilities that supports FactSet's SDK in Python and facilitate usage of FactSet APIs.\" 82 | " "fds.sdk.utils/meta.yaml" 83 | 84 | # Build conda package 85 | conda config --set anaconda_upload no 86 | package_file=$(conda build fds.sdk.utils --output) 87 | conda build -c conda-forge fds.sdk.utils 88 | 89 | anaconda -t $ANACONDA_TOKEN upload -u factset -l main ${package_file} 90 | break 91 | else 92 | echo "Version ${version} not found, rechecking in $((2 ** count)) seconds..." 93 | sleep $((2 ** count)) 94 | count=$((count + 1)) 95 | fi 96 | done 97 | 98 | if [ $count -eq $max_retries ]; then 99 | echo "Maximum retries reached, package with that version was not found, publish failed." 100 | exit 1 101 | fi 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # vscode 132 | .vscode/ 133 | 134 | .idea/ 135 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | ## Bug Reports 4 | 5 | Our project isn't always perfect, but we strive to always improve on that work. Please report any bugs by [filing an issue](https://github.com/factset/enterprise-sdk-utils-python/issues/new). 6 | 7 | ## Feature Requests 8 | 9 | We're always looking for suggestions to improve this project. If you have a suggestion for improving an existing feature, or would like to suggest a completely new feature, please [file an issue](https://github.com/factset/enterprise-sdk-utils-python/issues/new). 10 | 11 | ## Pull Requests 12 | 13 | Along with our desire to hear your feedback and suggestions, we're also interested in accepting direct assistance in the form of new code or documentation. 14 | 15 | We ask that you please file a [bug report](#bug-reports) or [feature request](#feature-requests) first to make sure your change isn't already being worked on, then [open a pull request](https://github.com/factset/enterprise-sdk-utils-python/compare) with the code change. 16 | 17 | ## Testing 18 | 19 | Tests will be run automatically with each pull request, or you can run them locally with `pytest` or `tox`. Make sure you're in an appropriate virtual environment before running the above commands. 20 | 21 | ```bash 22 | $ pytest 23 | ``` 24 | 25 | ```bash 26 | $ tox 27 | ``` 28 | 29 | With `tox` a unit test coverage HTML page will appear within the `.htmlcov` directory of your workspace root directory. 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FactSet 2 | 3 | # FactSet SDK Utilities for Python 4 | 5 | [![PyPi](https://img.shields.io/pypi/v/fds.sdk.utils)](https://pypi.org/project/fds.sdk.utils/) 6 | [![Anaconda-Server Badge](https://anaconda.org/factset/fds.sdk.utils/badges/version.svg)](https://anaconda.org/factset/fds.sdk.utils) 7 | [![Apache-2 license](https://img.shields.io/badge/license-Apache2-brightgreen.svg)](https://www.apache.org/licenses/LICENSE-2.0) 8 | 9 | This repository contains a collection of utilities that supports FactSet's SDK in Python and facilitate usage of FactSet 10 | APIs. 11 | 12 | ## Installation 13 | 14 | ### Poetry 15 | 16 | ```sh 17 | poetry add fds.sdk.utils 18 | ``` 19 | 20 | ### pip 21 | 22 | ```sh 23 | pip install fds.sdk.utils 24 | ``` 25 | 26 | ### Conda 27 | 28 | ```sh 29 | conda install factset::fds.sdk.utils 30 | ``` 31 | 32 | ## Usage 33 | 34 | This library contains multiple modules, sample usage of each module is below. 35 | 36 | ### Authentication 37 | 38 | First, you need to create the OAuth 2.0 client configuration that will be used to authenticate against FactSet's APIs: 39 | 40 | 1. [Create a new application](https://developer.factset.com/learn/authentication-oauth2#creating-an-application) on 41 | FactSet's Developer Portal. 42 | 2. When prompted, download the configuration file and move it to your development environment. 43 | 44 | ```python 45 | from fds.sdk.utils.authentication import ConfidentialClient 46 | import requests 47 | 48 | # The ConfidentialClient instance should be reused in production environments. 49 | client = ConfidentialClient( 50 | config_path='/path/to/config.json' 51 | ) 52 | res = requests.get( 53 | 'https://api.factset.com/analytics/lookups/v3/currencies', 54 | headers={ 55 | 'Authorization': 'Bearer ' + client.get_access_token() 56 | }) 57 | 58 | print(res.text) 59 | ``` 60 | 61 | ### Configure a Proxy 62 | 63 | You can pass proxy settings to the ConfidentialClient if necessary. 64 | The `proxy` parameter takes a URL to tell the request library which proxy should be used. 65 | 66 | If necessary it is possible to set custom `proxy_headers` as dictionary. 67 | 68 | ```python 69 | from fds.sdk.utils.authentication import ConfidentialClient 70 | 71 | client = ConfidentialClient( 72 | config_path='/path/to/config.json', 73 | proxy="http://secret:password@localhost:5050", 74 | proxy_headers={ 75 | "Custom-Proxy-Header": "Custom-Proxy-Header-Value" 76 | } 77 | ) 78 | ``` 79 | 80 | ### Custom SSL Certificate 81 | 82 | If you have proxies or firewalls which are using custom TLS certificates, 83 | you are able to pass a custom pem file (`ssl_ca_cert` parameter) so that the 84 | request library is able to verify the validity of that certificate. If a 85 | ca cert is passed it is validated regardless if `verify_ssl` is set to false. 86 | 87 | With `verify_ssl` it is possible to disable the verifications of certificates. 88 | Disabling the verification is not recommended, but it might be useful during 89 | local development or testing 90 | 91 | ```python 92 | from fds.sdk.utils.authentication import ConfidentialClient 93 | 94 | client = ConfidentialClient( 95 | config_path='/path/to/config.json', 96 | verify_ssl=True, 97 | ssl_ca_cert='/path/to/ca.pem' 98 | ) 99 | ``` 100 | 101 | ### Request Retries 102 | 103 | In case the request retry behaviour should be customized, it is possible to pass a `urllib3.Retry` object to 104 | the `ConfidentialClient`. 105 | 106 | ```python 107 | from urllib3 import Retry 108 | from fds.sdk.utils.authentication import ConfidentialClient 109 | 110 | client = ConfidentialClient( 111 | config_path='/path/to/config.json', 112 | retry=Retry( 113 | total=5, 114 | backoff_factor=0.1, 115 | status_forcelist=[500, 502, 503, 504] 116 | ) 117 | ) 118 | ``` 119 | 120 | ## Modules 121 | 122 | Information about the various utility modules contained in this library can be found below. 123 | 124 | ### Authentication 125 | 126 | The [authentication module](src/fds/sdk/utils/authentication) provides helper classes that 127 | facilitate [OAuth 2.0](https://developer.factset.com/learn/authentication-oauth2) authentication and authorization with 128 | FactSet's APIs. Currently the module has support for 129 | the [client credentials flow](https://github.com/factset/oauth2-guidelines#client-credentials-flow-1). 130 | 131 | Each helper class in the module has the following features: 132 | 133 | * Accepts a configuration file or `dict` that contains information about the OAuth 2.0 client, including the client ID 134 | and private key. 135 | * Performs authentication with FactSet's OAuth 2.0 authorization server and retrieves an access token. 136 | * Caches the access token for reuse and requests a new access token as needed when one expires. 137 | * In order for this to work correctly, the helper class instance should be reused in production environments. 138 | 139 | #### Configuration 140 | 141 | Classes in the authentication module require OAuth 2.0 client configuration information to be passed to constructors 142 | through a JSON-formatted file or a `dict`. In either case the format is the same: 143 | 144 | ```json 145 | { 146 | "name": "Application name registered with FactSet's Developer Portal", 147 | "clientId": "OAuth 2.0 Client ID registered with FactSet's Developer Portal", 148 | "clientAuthType": "Confidential", 149 | "owners": [ 150 | "USERNAME-SERIAL" 151 | ], 152 | "jwk": { 153 | "kty": "RSA", 154 | "use": "sig", 155 | "alg": "RS256", 156 | "kid": "Key ID", 157 | "d": "ECC Private Key", 158 | "n": "Modulus", 159 | "e": "Exponent", 160 | "p": "First Prime Factor", 161 | "q": "Second Prime Factor", 162 | "dp": "First Factor CRT Exponent", 163 | "dq": "Second Factor CRT Exponent", 164 | "qi": "First CRT Coefficient" 165 | } 166 | } 167 | ``` 168 | 169 | If you're just starting out, you can visit FactSet's Developer Portal 170 | to [create a new application](https://developer.factset.com/applications) and download a configuration file in this 171 | format. 172 | 173 | If you're creating and managing your signing key pair yourself, see the 174 | required [JWK parameters](https://github.com/factset/oauth2-guidelines#jwk-parameters) for public-private key pairs. 175 | 176 | ## Debugging 177 | 178 | This library uses the [logging module](https://docs.python.org/3/howto/logging.html) to log various messages that will 179 | help you understand what it's doing. You can increase the log level to see additional debug information using standard 180 | conventions. For example: 181 | 182 | ```python 183 | logging.getLogger('fds.sdk.utils').setLevel(logging.DEBUG) 184 | ``` 185 | 186 | or 187 | 188 | ```python 189 | logging.getLogger('fds.sdk.utils.authentication').setLevel(logging.DEBUG) 190 | ``` 191 | 192 | # Contributing 193 | 194 | Please refer to the [contributing guide](CONTRIBUTING.md). 195 | 196 | # Copyright 197 | 198 | Copyright 2022 FactSet Research Systems Inc 199 | 200 | Licensed under the Apache License, Version 2.0 (the "License"); 201 | you may not use this file except in compliance with the License. 202 | You may obtain a copy of the License at 203 | 204 | http://www.apache.org/licenses/LICENSE-2.0 205 | 206 | Unless required by applicable law or agreed to in writing, software 207 | distributed under the License is distributed on an "AS IS" BASIS, 208 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 209 | See the License for the specific language governing permissions and 210 | limitations under the License. 211 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "black" 5 | version = "24.8.0" 6 | description = "The uncompromising code formatter." 7 | optional = false 8 | python-versions = ">=3.8" 9 | groups = ["dev"] 10 | files = [ 11 | {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, 12 | {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, 13 | {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, 14 | {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, 15 | {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, 16 | {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, 17 | {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, 18 | {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, 19 | {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, 20 | {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, 21 | {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, 22 | {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, 23 | {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, 24 | {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, 25 | {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, 26 | {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, 27 | {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, 28 | {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, 29 | {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, 30 | {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, 31 | {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, 32 | {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, 33 | ] 34 | 35 | [package.dependencies] 36 | click = ">=8.0.0" 37 | mypy-extensions = ">=0.4.3" 38 | packaging = ">=22.0" 39 | pathspec = ">=0.9.0" 40 | platformdirs = ">=2" 41 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 42 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 43 | 44 | [package.extras] 45 | colorama = ["colorama (>=0.4.3)"] 46 | d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""] 47 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 48 | uvloop = ["uvloop (>=0.15.2)"] 49 | 50 | [[package]] 51 | name = "cachetools" 52 | version = "5.5.2" 53 | description = "Extensible memoizing collections and decorators" 54 | optional = false 55 | python-versions = ">=3.7" 56 | groups = ["dev"] 57 | files = [ 58 | {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, 59 | {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, 60 | ] 61 | 62 | [[package]] 63 | name = "certifi" 64 | version = "2024.7.4" 65 | description = "Python package for providing Mozilla's CA Bundle." 66 | optional = false 67 | python-versions = ">=3.6" 68 | groups = ["main"] 69 | files = [ 70 | {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, 71 | {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, 72 | ] 73 | 74 | [[package]] 75 | name = "cffi" 76 | version = "1.16.0" 77 | description = "Foreign Function Interface for Python calling C code." 78 | optional = false 79 | python-versions = ">=3.8" 80 | groups = ["main"] 81 | markers = "platform_python_implementation != \"PyPy\"" 82 | files = [ 83 | {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, 84 | {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, 85 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, 86 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, 87 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, 88 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, 89 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, 90 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, 91 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, 92 | {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, 93 | {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, 94 | {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, 95 | {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, 96 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, 97 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, 98 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, 99 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, 100 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, 101 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, 102 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, 103 | {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, 104 | {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, 105 | {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, 106 | {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, 107 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, 108 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, 109 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, 110 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, 111 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, 112 | {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, 113 | {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, 114 | {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, 115 | {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, 116 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, 117 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, 118 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, 119 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, 120 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, 121 | {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, 122 | {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, 123 | {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, 124 | {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, 125 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, 126 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, 127 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, 128 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, 129 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, 130 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, 131 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, 132 | {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, 133 | {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, 134 | {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, 135 | ] 136 | 137 | [package.dependencies] 138 | pycparser = "*" 139 | 140 | [[package]] 141 | name = "chardet" 142 | version = "5.2.0" 143 | description = "Universal encoding detector for Python 3" 144 | optional = false 145 | python-versions = ">=3.7" 146 | groups = ["dev"] 147 | files = [ 148 | {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, 149 | {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, 150 | ] 151 | 152 | [[package]] 153 | name = "charset-normalizer" 154 | version = "3.3.2" 155 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 156 | optional = false 157 | python-versions = ">=3.7.0" 158 | groups = ["main"] 159 | files = [ 160 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 161 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 162 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 163 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 164 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 165 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 166 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 167 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 168 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 169 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 170 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 171 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 172 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 173 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 174 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 175 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 176 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 177 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 178 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 179 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 180 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 181 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 182 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 183 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 184 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 185 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 186 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 187 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 188 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 189 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 190 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 191 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 192 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 193 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 194 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 195 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 196 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 197 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 198 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 199 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 200 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 201 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 202 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 203 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 204 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 205 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 206 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 207 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 208 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 209 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 210 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 211 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 212 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 213 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 214 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 215 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 216 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 217 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 218 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 219 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 220 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 221 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 222 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 223 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 224 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 225 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 226 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 227 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 228 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 229 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 230 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 231 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 232 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 233 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 234 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 235 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 236 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 237 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 238 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 239 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 240 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 241 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 242 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 243 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 244 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 245 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 246 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 247 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 248 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 249 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 250 | ] 251 | 252 | [[package]] 253 | name = "click" 254 | version = "8.1.7" 255 | description = "Composable command line interface toolkit" 256 | optional = false 257 | python-versions = ">=3.7" 258 | groups = ["dev"] 259 | files = [ 260 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 261 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 262 | ] 263 | 264 | [package.dependencies] 265 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 266 | 267 | [[package]] 268 | name = "colorama" 269 | version = "0.4.6" 270 | description = "Cross-platform colored terminal text." 271 | optional = false 272 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 273 | groups = ["dev"] 274 | files = [ 275 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 276 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 277 | ] 278 | 279 | [[package]] 280 | name = "coverage" 281 | version = "7.5.1" 282 | description = "Code coverage measurement for Python" 283 | optional = false 284 | python-versions = ">=3.8" 285 | groups = ["dev"] 286 | files = [ 287 | {file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"}, 288 | {file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"}, 289 | {file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"}, 290 | {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"}, 291 | {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"}, 292 | {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"}, 293 | {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"}, 294 | {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"}, 295 | {file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"}, 296 | {file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"}, 297 | {file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"}, 298 | {file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"}, 299 | {file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"}, 300 | {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"}, 301 | {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"}, 302 | {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"}, 303 | {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"}, 304 | {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"}, 305 | {file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"}, 306 | {file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"}, 307 | {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"}, 308 | {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"}, 309 | {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"}, 310 | {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"}, 311 | {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"}, 312 | {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"}, 313 | {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"}, 314 | {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"}, 315 | {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"}, 316 | {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"}, 317 | {file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"}, 318 | {file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"}, 319 | {file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"}, 320 | {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"}, 321 | {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"}, 322 | {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"}, 323 | {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"}, 324 | {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"}, 325 | {file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"}, 326 | {file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"}, 327 | {file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"}, 328 | {file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"}, 329 | {file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"}, 330 | {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"}, 331 | {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"}, 332 | {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"}, 333 | {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"}, 334 | {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"}, 335 | {file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"}, 336 | {file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"}, 337 | {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"}, 338 | {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"}, 339 | ] 340 | 341 | [package.dependencies] 342 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 343 | 344 | [package.extras] 345 | toml = ["tomli ; python_full_version <= \"3.11.0a6\""] 346 | 347 | [[package]] 348 | name = "cryptography" 349 | version = "42.0.6" 350 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 351 | optional = false 352 | python-versions = ">=3.7" 353 | groups = ["main"] 354 | files = [ 355 | {file = "cryptography-42.0.6-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:073104df012fc815eed976cd7d0a386c8725d0d0947cf9c37f6c36a6c20feb1b"}, 356 | {file = "cryptography-42.0.6-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:5967e3632f42b0c0f9dc2c9da88c79eabdda317860b246d1fbbde4a8bbbc3b44"}, 357 | {file = "cryptography-42.0.6-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99831397fdc6e6e0aa088b060c278c6e635d25c0d4d14bdf045bf81792fda0a"}, 358 | {file = "cryptography-42.0.6-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:089aeb297ff89615934b22c7631448598495ffd775b7d540a55cfee35a677bf4"}, 359 | {file = "cryptography-42.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:97eeacae9aa526ddafe68b9202a535f581e21d78f16688a84c8dcc063618e121"}, 360 | {file = "cryptography-42.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f4cece02478d73dacd52be57a521d168af64ae03d2a567c0c4eb6f189c3b9d79"}, 361 | {file = "cryptography-42.0.6-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb6f56b004e898df5530fa873e598ec78eb338ba35f6fa1449970800b1d97c2"}, 362 | {file = "cryptography-42.0.6-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:8b90c57b3cd6128e0863b894ce77bd36fcb5f430bf2377bc3678c2f56e232316"}, 363 | {file = "cryptography-42.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d16a310c770cc49908c500c2ceb011f2840674101a587d39fa3ea828915b7e83"}, 364 | {file = "cryptography-42.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3442601d276bd9e961d618b799761b4e5d892f938e8a4fe1efbe2752be90455"}, 365 | {file = "cryptography-42.0.6-cp37-abi3-win32.whl", hash = "sha256:00c0faa5b021457848d031ecff041262211cc1e2bce5f6e6e6c8108018f6b44a"}, 366 | {file = "cryptography-42.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:b16b90605c62bcb3aa7755d62cf5e746828cfc3f965a65211849e00c46f8348d"}, 367 | {file = "cryptography-42.0.6-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:eecca86813c6a923cabff284b82ff4d73d9e91241dc176250192c3a9b9902a54"}, 368 | {file = "cryptography-42.0.6-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d93080d2b01b292e7ee4d247bf93ed802b0100f5baa3fa5fd6d374716fa480d4"}, 369 | {file = "cryptography-42.0.6-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ff75b88a4d273c06d968ad535e6cb6a039dd32db54fe36f05ed62ac3ef64a44"}, 370 | {file = "cryptography-42.0.6-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c05230d8aaaa6b8ab3ab41394dc06eb3d916131df1c9dcb4c94e8f041f704b74"}, 371 | {file = "cryptography-42.0.6-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9184aff0856261ecb566a3eb26a05dfe13a292c85ce5c59b04e4aa09e5814187"}, 372 | {file = "cryptography-42.0.6-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:4bdb39ecbf05626e4bfa1efd773bb10346af297af14fb3f4c7cb91a1d2f34a46"}, 373 | {file = "cryptography-42.0.6-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e85f433230add2aa26b66d018e21134000067d210c9c68ef7544ba65fc52e3eb"}, 374 | {file = "cryptography-42.0.6-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:65d529c31bd65d54ce6b926a01e1b66eacf770b7e87c0622516a840e400ec732"}, 375 | {file = "cryptography-42.0.6-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f1e933b238978ccfa77b1fee0a297b3c04983f4cb84ae1c33b0ea4ae08266cc9"}, 376 | {file = "cryptography-42.0.6-cp39-abi3-win32.whl", hash = "sha256:bc954251edcd8a952eeaec8ae989fec7fe48109ab343138d537b7ea5bb41071a"}, 377 | {file = "cryptography-42.0.6-cp39-abi3-win_amd64.whl", hash = "sha256:9f1a3bc2747166b0643b00e0b56cd9b661afc9d5ff963acaac7a9c7b2b1ef638"}, 378 | {file = "cryptography-42.0.6-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:945a43ebf036dd4b43ebfbbd6b0f2db29ad3d39df824fb77476ca5777a9dde33"}, 379 | {file = "cryptography-42.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f567a82b7c2b99257cca2a1c902c1b129787278ff67148f188784245c7ed5495"}, 380 | {file = "cryptography-42.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3b750279f3e7715df6f68050707a0cee7cbe81ba2eeb2f21d081bd205885ffed"}, 381 | {file = "cryptography-42.0.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6981acac509cc9415344cb5bfea8130096ea6ebcc917e75503143a1e9e829160"}, 382 | {file = "cryptography-42.0.6-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:076c92b08dd1ab88108bc84545187e10d3693a9299c593f98c4ea195a0b0ead7"}, 383 | {file = "cryptography-42.0.6-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81dbe47e28b703bc4711ac74a64ef8b758a0cf056ce81d08e39116ab4bc126fa"}, 384 | {file = "cryptography-42.0.6-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e1f5f15c5ddadf6ee4d1d624a2ae940f14bd74536230b0056ccb28bb6248e42a"}, 385 | {file = "cryptography-42.0.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:43e521f21c2458038d72e8cdfd4d4d9f1d00906a7b6636c4272e35f650d1699b"}, 386 | {file = "cryptography-42.0.6.tar.gz", hash = "sha256:f987a244dfb0333fbd74a691c36000a2569eaf7c7cc2ac838f85f59f0588ddc9"}, 387 | ] 388 | 389 | [package.dependencies] 390 | cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} 391 | 392 | [package.extras] 393 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] 394 | docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] 395 | nox = ["nox"] 396 | pep8test = ["check-sdist", "click", "mypy", "ruff"] 397 | sdist = ["build"] 398 | ssh = ["bcrypt (>=3.1.5)"] 399 | test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] 400 | test-randomorder = ["pytest-randomly"] 401 | 402 | [[package]] 403 | name = "distlib" 404 | version = "0.3.8" 405 | description = "Distribution utilities" 406 | optional = false 407 | python-versions = "*" 408 | groups = ["dev"] 409 | files = [ 410 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 411 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 412 | ] 413 | 414 | [[package]] 415 | name = "exceptiongroup" 416 | version = "1.2.1" 417 | description = "Backport of PEP 654 (exception groups)" 418 | optional = false 419 | python-versions = ">=3.7" 420 | groups = ["dev"] 421 | markers = "python_version < \"3.11\"" 422 | files = [ 423 | {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, 424 | {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, 425 | ] 426 | 427 | [package.extras] 428 | test = ["pytest (>=6)"] 429 | 430 | [[package]] 431 | name = "filelock" 432 | version = "3.16.1" 433 | description = "A platform independent file lock." 434 | optional = false 435 | python-versions = ">=3.8" 436 | groups = ["dev"] 437 | files = [ 438 | {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, 439 | {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, 440 | ] 441 | 442 | [package.extras] 443 | docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] 444 | testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] 445 | typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] 446 | 447 | [[package]] 448 | name = "idna" 449 | version = "3.7" 450 | description = "Internationalized Domain Names in Applications (IDNA)" 451 | optional = false 452 | python-versions = ">=3.5" 453 | groups = ["main"] 454 | files = [ 455 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 456 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 457 | ] 458 | 459 | [[package]] 460 | name = "iniconfig" 461 | version = "2.0.0" 462 | description = "brain-dead simple config-ini parsing" 463 | optional = false 464 | python-versions = ">=3.7" 465 | groups = ["dev"] 466 | files = [ 467 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 468 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 469 | ] 470 | 471 | [[package]] 472 | name = "joserfc" 473 | version = "1.1.0" 474 | description = "The ultimate Python library for JOSE RFCs, including JWS, JWE, JWK, JWA, JWT" 475 | optional = false 476 | python-versions = ">=3.8" 477 | groups = ["main"] 478 | files = [ 479 | {file = "joserfc-1.1.0-py3-none-any.whl", hash = "sha256:9493512cfffb9bc3001e8f609fe0eb7e95b71f3d3b374ede93de94b4b6b520f5"}, 480 | {file = "joserfc-1.1.0.tar.gz", hash = "sha256:a8f3442b04c233f742f7acde0d0dcd926414e9542a6337096b2b4e5f435f36c1"}, 481 | ] 482 | 483 | [package.dependencies] 484 | cryptography = "*" 485 | 486 | [package.extras] 487 | drafts = ["pycryptodome"] 488 | 489 | [[package]] 490 | name = "mypy-extensions" 491 | version = "1.0.0" 492 | description = "Type system extensions for programs checked with the mypy type checker." 493 | optional = false 494 | python-versions = ">=3.5" 495 | groups = ["dev"] 496 | files = [ 497 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 498 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 499 | ] 500 | 501 | [[package]] 502 | name = "oauthlib" 503 | version = "3.2.2" 504 | description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" 505 | optional = false 506 | python-versions = ">=3.6" 507 | groups = ["main"] 508 | files = [ 509 | {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, 510 | {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, 511 | ] 512 | 513 | [package.extras] 514 | rsa = ["cryptography (>=3.0.0)"] 515 | signals = ["blinker (>=1.4.0)"] 516 | signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] 517 | 518 | [[package]] 519 | name = "packaging" 520 | version = "24.2" 521 | description = "Core utilities for Python packages" 522 | optional = false 523 | python-versions = ">=3.8" 524 | groups = ["dev"] 525 | files = [ 526 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 527 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 528 | ] 529 | 530 | [[package]] 531 | name = "pathspec" 532 | version = "0.12.1" 533 | description = "Utility library for gitignore style pattern matching of file paths." 534 | optional = false 535 | python-versions = ">=3.8" 536 | groups = ["dev"] 537 | files = [ 538 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 539 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 540 | ] 541 | 542 | [[package]] 543 | name = "platformdirs" 544 | version = "4.3.6" 545 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 546 | optional = false 547 | python-versions = ">=3.8" 548 | groups = ["dev"] 549 | files = [ 550 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 551 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 552 | ] 553 | 554 | [package.extras] 555 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 556 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 557 | type = ["mypy (>=1.11.2)"] 558 | 559 | [[package]] 560 | name = "pluggy" 561 | version = "1.5.0" 562 | description = "plugin and hook calling mechanisms for python" 563 | optional = false 564 | python-versions = ">=3.8" 565 | groups = ["dev"] 566 | files = [ 567 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 568 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 569 | ] 570 | 571 | [package.extras] 572 | dev = ["pre-commit", "tox"] 573 | testing = ["pytest", "pytest-benchmark"] 574 | 575 | [[package]] 576 | name = "pycparser" 577 | version = "2.22" 578 | description = "C parser in Python" 579 | optional = false 580 | python-versions = ">=3.8" 581 | groups = ["main"] 582 | markers = "platform_python_implementation != \"PyPy\"" 583 | files = [ 584 | {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, 585 | {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, 586 | ] 587 | 588 | [[package]] 589 | name = "pyproject-api" 590 | version = "1.8.0" 591 | description = "API to interact with the python pyproject.toml based projects" 592 | optional = false 593 | python-versions = ">=3.8" 594 | groups = ["dev"] 595 | files = [ 596 | {file = "pyproject_api-1.8.0-py3-none-any.whl", hash = "sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228"}, 597 | {file = "pyproject_api-1.8.0.tar.gz", hash = "sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496"}, 598 | ] 599 | 600 | [package.dependencies] 601 | packaging = ">=24.1" 602 | tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} 603 | 604 | [package.extras] 605 | docs = ["furo (>=2024.8.6)", "sphinx-autodoc-typehints (>=2.4.1)"] 606 | testing = ["covdefaults (>=2.3)", "pytest (>=8.3.3)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] 607 | 608 | [[package]] 609 | name = "pytest" 610 | version = "8.3.5" 611 | description = "pytest: simple powerful testing with Python" 612 | optional = false 613 | python-versions = ">=3.8" 614 | groups = ["dev"] 615 | files = [ 616 | {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, 617 | {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, 618 | ] 619 | 620 | [package.dependencies] 621 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 622 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 623 | iniconfig = "*" 624 | packaging = "*" 625 | pluggy = ">=1.5,<2" 626 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 627 | 628 | [package.extras] 629 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 630 | 631 | [[package]] 632 | name = "pytest-cov" 633 | version = "5.0.0" 634 | description = "Pytest plugin for measuring coverage." 635 | optional = false 636 | python-versions = ">=3.8" 637 | groups = ["dev"] 638 | files = [ 639 | {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, 640 | {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, 641 | ] 642 | 643 | [package.dependencies] 644 | coverage = {version = ">=5.2.1", extras = ["toml"]} 645 | pytest = ">=4.6" 646 | 647 | [package.extras] 648 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] 649 | 650 | [[package]] 651 | name = "pytest-mock" 652 | version = "3.14.1" 653 | description = "Thin-wrapper around the mock package for easier use with pytest" 654 | optional = false 655 | python-versions = ">=3.8" 656 | groups = ["dev"] 657 | files = [ 658 | {file = "pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0"}, 659 | {file = "pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e"}, 660 | ] 661 | 662 | [package.dependencies] 663 | pytest = ">=6.2.5" 664 | 665 | [package.extras] 666 | dev = ["pre-commit", "pytest-asyncio", "tox"] 667 | 668 | [[package]] 669 | name = "requests" 670 | version = "2.32.3" 671 | description = "Python HTTP for Humans." 672 | optional = false 673 | python-versions = ">=3.8" 674 | groups = ["main"] 675 | files = [ 676 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 677 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 678 | ] 679 | 680 | [package.dependencies] 681 | certifi = ">=2017.4.17" 682 | charset-normalizer = ">=2,<4" 683 | idna = ">=2.5,<4" 684 | urllib3 = ">=1.21.1,<3" 685 | 686 | [package.extras] 687 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 688 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 689 | 690 | [[package]] 691 | name = "requests-oauthlib" 692 | version = "2.0.0" 693 | description = "OAuthlib authentication support for Requests." 694 | optional = false 695 | python-versions = ">=3.4" 696 | groups = ["main"] 697 | files = [ 698 | {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, 699 | {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, 700 | ] 701 | 702 | [package.dependencies] 703 | oauthlib = ">=3.0.0" 704 | requests = ">=2.0.0" 705 | 706 | [package.extras] 707 | rsa = ["oauthlib[signedtoken] (>=3.0.0)"] 708 | 709 | [[package]] 710 | name = "tomli" 711 | version = "2.2.1" 712 | description = "A lil' TOML parser" 713 | optional = false 714 | python-versions = ">=3.8" 715 | groups = ["dev"] 716 | markers = "python_full_version <= \"3.11.0a6\"" 717 | files = [ 718 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 719 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 720 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 721 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 722 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 723 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 724 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 725 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 726 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 727 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 728 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 729 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 730 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 731 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 732 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 733 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 734 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 735 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 736 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 737 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 738 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 739 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 740 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 741 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 742 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 743 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 744 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 745 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 746 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 747 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 748 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 749 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 750 | ] 751 | 752 | [[package]] 753 | name = "tox" 754 | version = "4.25.0" 755 | description = "tox is a generic virtualenv management and test command line tool" 756 | optional = false 757 | python-versions = ">=3.8" 758 | groups = ["dev"] 759 | files = [ 760 | {file = "tox-4.25.0-py3-none-any.whl", hash = "sha256:4dfdc7ba2cc6fdc6688dde1b21e7b46ff6c41795fb54586c91a3533317b5255c"}, 761 | {file = "tox-4.25.0.tar.gz", hash = "sha256:dd67f030317b80722cf52b246ff42aafd3ed27ddf331c415612d084304cf5e52"}, 762 | ] 763 | 764 | [package.dependencies] 765 | cachetools = ">=5.5.1" 766 | chardet = ">=5.2" 767 | colorama = ">=0.4.6" 768 | filelock = ">=3.16.1" 769 | packaging = ">=24.2" 770 | platformdirs = ">=4.3.6" 771 | pluggy = ">=1.5" 772 | pyproject-api = ">=1.8" 773 | tomli = {version = ">=2.2.1", markers = "python_version < \"3.11\""} 774 | typing-extensions = {version = ">=4.12.2", markers = "python_version < \"3.11\""} 775 | virtualenv = ">=20.29.1" 776 | 777 | [package.extras] 778 | test = ["devpi-process (>=1.0.2)", "pytest (>=8.3.4)", "pytest-mock (>=3.14)"] 779 | 780 | [[package]] 781 | name = "tox-gh-actions" 782 | version = "3.3.0" 783 | description = "Seamless integration of tox into GitHub Actions" 784 | optional = false 785 | python-versions = ">=3.7" 786 | groups = ["dev"] 787 | files = [ 788 | {file = "tox_gh_actions-3.3.0-py2.py3-none-any.whl", hash = "sha256:0e1f9db7a775d04b6d94ab801c60d2d482929a934136262969791eb0ccac65a4"}, 789 | {file = "tox_gh_actions-3.3.0.tar.gz", hash = "sha256:6933775dd7ab98649de5134283277e604fecfd4eb44bf31150c1c6ba2b1092ef"}, 790 | ] 791 | 792 | [package.dependencies] 793 | tox = ">=4,<5" 794 | 795 | [package.extras] 796 | testing = ["black ; platform_python_implementation == \"CPython\"", "devpi-process", "flake8 (>=6,<7) ; python_version >= \"3.8\"", "mypy ; platform_python_implementation == \"CPython\"", "pytest (>=7)", "pytest-cov (>=4)", "pytest-mock (>=3)", "pytest-randomly (>=3)"] 797 | 798 | [[package]] 799 | name = "typing-extensions" 800 | version = "4.12.2" 801 | description = "Backported and Experimental Type Hints for Python 3.8+" 802 | optional = false 803 | python-versions = ">=3.8" 804 | groups = ["dev"] 805 | markers = "python_version < \"3.11\"" 806 | files = [ 807 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 808 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 809 | ] 810 | 811 | [[package]] 812 | name = "urllib3" 813 | version = "2.2.2" 814 | description = "HTTP library with thread-safe connection pooling, file post, and more." 815 | optional = false 816 | python-versions = ">=3.8" 817 | groups = ["main"] 818 | files = [ 819 | {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, 820 | {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, 821 | ] 822 | 823 | [package.extras] 824 | brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] 825 | h2 = ["h2 (>=4,<5)"] 826 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 827 | zstd = ["zstandard (>=0.18.0)"] 828 | 829 | [[package]] 830 | name = "virtualenv" 831 | version = "20.29.2" 832 | description = "Virtual Python Environment builder" 833 | optional = false 834 | python-versions = ">=3.8" 835 | groups = ["dev"] 836 | files = [ 837 | {file = "virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a"}, 838 | {file = "virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728"}, 839 | ] 840 | 841 | [package.dependencies] 842 | distlib = ">=0.3.7,<1" 843 | filelock = ">=3.12.2,<4" 844 | platformdirs = ">=3.9.1,<5" 845 | 846 | [package.extras] 847 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 848 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] 849 | 850 | [metadata] 851 | lock-version = "2.1" 852 | python-versions = "^3.8.0" 853 | content-hash = "1aef842d5fdd696e5e5c3ac98d62d11b212886002d3f5c0b9197b79b57288756" 854 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fds.sdk.utils" 3 | version = "2.1.1" 4 | description = "Utilities for interacting with FactSet APIs." 5 | authors = ["FactSet Research Systems"] 6 | license = "Apache-2.0" 7 | readme = "README.md" 8 | homepage = "https://developer.factset.com" 9 | keywords = [ 10 | "FactSet", 11 | "API", 12 | "SDK" 13 | ] 14 | packages = [ 15 | { include = "fds", from = "src" } 16 | ] 17 | exclude = ["tests"] 18 | 19 | [tool.poetry.dependencies] 20 | python = "^3.8.0" 21 | requests-oauthlib = "^2.0.0" 22 | requests = "^2.28.2" 23 | oauthlib = "^3.2.2" 24 | joserfc = ">=0.9,<1.2" 25 | 26 | [tool.poetry.dev-dependencies] 27 | pytest = "^8.3.5" 28 | black = "^24.8.0" 29 | pytest-cov = "^5.0.0" 30 | pytest-mock = "^3.14.1" 31 | tox = "^4.25.0" 32 | tox-gh-actions = "^3.3.0" 33 | 34 | [tool.black] 35 | line-length = 120 36 | 37 | [build-system] 38 | requires = ["poetry-core"] 39 | build-backend = "poetry.core.masonry.api" 40 | -------------------------------------------------------------------------------- /src/fds/sdk/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging import NullHandler 3 | 4 | # Set default logging handler to avoid "No handler found" warnings. 5 | # See: https://docs.python.org/3/howto/logging.html#library-config 6 | logging.getLogger(__name__).addHandler(NullHandler()) 7 | del NullHandler 8 | -------------------------------------------------------------------------------- /src/fds/sdk/utils/authentication/__init__.py: -------------------------------------------------------------------------------- 1 | from .confidential import ConfidentialClient 2 | from .oauth2client import OAuth2Client 3 | from .exceptions import ( 4 | AccessTokenError, 5 | AuthServerMetadataError, 6 | AuthServerMetadataContentError, 7 | ConfidentialClientError, 8 | ConfigurationError, 9 | JWSSigningError, 10 | ) 11 | -------------------------------------------------------------------------------- /src/fds/sdk/utils/authentication/confidential.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import time 4 | import uuid 5 | 6 | from joserfc import jwt 7 | from joserfc.jwk import RSAKey 8 | from oauthlib.oauth2 import BackendApplicationClient 9 | from requests import Session 10 | from requests.adapters import HTTPAdapter 11 | from requests_oauthlib import OAuth2Session 12 | from urllib3 import Retry 13 | 14 | from .constants import CONSTS 15 | from .oauth2client import OAuth2Client 16 | from .exceptions import ( 17 | AccessTokenError, 18 | ConfidentialClientError, 19 | ConfigurationError, 20 | JWSSigningError, 21 | AuthServerMetadataError, 22 | AuthServerMetadataContentError, 23 | ) 24 | 25 | # Set up logger 26 | log = logging.getLogger(__name__) 27 | 28 | 29 | class ConfidentialClient(OAuth2Client): 30 | """ 31 | Helper class that supports FactSet's implementation of the OAuth 2.0 32 | client credentials flow. 33 | 34 | The main purpose of this class is to provide an access token that can 35 | be used to authenticate against FactSet's APIs. It takes care of fetching 36 | the access token, caching it and refreshing it as needed. 37 | """ 38 | 39 | def __init__( 40 | self, 41 | config_path: str = None, 42 | config: dict = None, 43 | proxy: str = None, 44 | proxy_headers: dict = None, 45 | verify_ssl: bool = True, 46 | ssl_ca_cert: str = None, 47 | retry: Retry = None, 48 | ) -> None: 49 | """ 50 | Creates a new ConfidentialClient. 51 | 52 | When setting up the OAuth 2.0 client, this constructor reaches out to 53 | FactSet's well-known URI to retrieve metadata about its authorization 54 | server. This information along with information about the OAuth 2.0 55 | client is stored and used whenever a new access token is fetched. 56 | 57 | Args: 58 | `NB`: Either `config_path` OR `config` should be sent, not both. 59 | 60 | `config_path` (str) : Path to credentials configuration file. 61 | `config` (dict) : Dictionary containing authorization credentials. 62 | 63 | `Example config` 64 | { 65 | "name": "Application Name registered with FactSet:Developer", 66 | "clientId": "Client ID registered with FactSet:Developer", 67 | "clientAuthType": "Confidential", 68 | "owners": ["Owner ID(s) of this configuration"], 69 | "jwk": { 70 | "kty": "RSA", 71 | "use": "sig", 72 | "alg": "RS256", 73 | "kid": "Key ID", 74 | "d": "ECC Private Key", 75 | "n": "Modulus", 76 | "e": "Exponent", 77 | "p": "First Prime Factor", 78 | "q": "Second Prime Factor", 79 | "dp": "First Factor CRT Exponent", 80 | "dq": "Second Factor CRT Exponent", 81 | "qi": "First CRT Coefficient", 82 | } 83 | } 84 | 85 | `NB`: Within the JWK parameters kty, alg, use, kid, n, e, d, p, q, dp, dq, qi are 86 | required for authorization. 87 | 88 | `proxy` (str) : Proxy URL 89 | 90 | `proxy_headers` (dict) : Sometimes it is necessary to add custom headers to http requests to be able to 91 | use a proxy or firewall 92 | 93 | `verify_ssl` (bool): Set this to ``False`` to skip verifying SSL certificate when calling API from 94 | https server. When set to ``False``, requests will accept any TLS certificate presented by the server, 95 | and will ignore hostname mismatches and/or expired certificates, which will make your application 96 | vulnerable to man-in-the-middle (MitM) attacks. Setting verify to ``False`` may be useful during 97 | local development or testing. 98 | 99 | `ssl_ca_cert` (str): Set this to customize the certificate file to verify the peer. If ``ssl_ca_cert`` is 100 | set, the ca_cert will be verified whether ``verify_ssl`` is enabled 101 | 102 | `retry` (Retry): Set this to custommize the retry policy for the requests. If not set, the default is used. 103 | 104 | 105 | Raises: 106 | AuthServerMetadataError: Raised if there's an issue retrieving the authorization server metadata 107 | AuthServerMetadataContentError: Raised if the authorization server metadata is incomplete 108 | ConfidentialClientError: Raised if instantiation errors occur 109 | ConfigurationError: Raised if there's an issue reading the configuration file 110 | KeyError: Raised if configuration is missing a required property 111 | ValueError: Raised if `config_path` or `config` are not provided properly 112 | """ 113 | 114 | if not config_path and not config: 115 | raise ValueError("Either 'config_path' or 'config' must be set.") 116 | 117 | if config_path and config: 118 | raise ValueError("Either 'config_path' or 'config' must be set. Not Both.") 119 | 120 | if config_path: 121 | try: 122 | with open(config_path, "r") as config_file: 123 | self._config = json.load(config_file) 124 | log.debug("Retrieved configuration from file: %s", config_path) 125 | except Exception as e: 126 | raise ConfigurationError(f"Error retrieving contents of {config_path}") from e 127 | 128 | if config: 129 | self._config = config 130 | 131 | if proxy: 132 | self._proxy = {"http": proxy, "https": proxy} 133 | else: 134 | self._proxy = None 135 | 136 | self._verify_ssl = verify_ssl 137 | self._proxy_headers = proxy_headers 138 | self._ssl_ca_cert = ssl_ca_cert 139 | 140 | if retry is not None: 141 | self._retry = retry 142 | else: 143 | self._retry = Retry( 144 | total=3, 145 | backoff_factor=1, 146 | status_forcelist=[413, 429, 500, 502, 503, 504], 147 | allowed_methods={"DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT", "TRACE"}, 148 | ) 149 | 150 | try: 151 | self._oauth_session = OAuth2Session( 152 | client=BackendApplicationClient(client_id=self._config[CONSTS.CONFIG_CLIENT_ID]) 153 | ) 154 | self._oauth_session.mount("https://", HTTPAdapter(max_retries=self._retry)) 155 | except Exception as e: 156 | raise ConfidentialClientError( 157 | f"Error instantiating OAuth2 session with {CONSTS.CONFIG_CLIENT_ID}:{self._config[CONSTS.CONFIG_CLIENT_ID]}" 158 | ) from e 159 | 160 | log.debug("Reviewing credentials format and completeness") 161 | 162 | self._config.setdefault(CONSTS.CONFIG_WELL_KNOWN_URI, CONSTS.FACTSET_WELL_KNOWN_URI) 163 | 164 | if CONSTS.CONFIG_JWK not in self._config: 165 | raise KeyError(f"'{CONSTS.CONFIG_JWK}' must be contained within configuration") 166 | 167 | if not set(CONSTS.CONFIG_JWK_REQUIRED_KEYS).issubset(set(self._config[CONSTS.CONFIG_JWK].keys())): 168 | raise KeyError(f"JWK must contain the following items: '{CONSTS.CONFIG_JWK_REQUIRED_KEYS}'") 169 | 170 | log.debug("Credentials are complete and formatted correctly") 171 | 172 | self._requests_session = Session() 173 | self._requests_session.mount("https://", HTTPAdapter(max_retries=self._retry)) 174 | 175 | self._init_auth_server_metadata() 176 | 177 | self._cached_token = {} 178 | 179 | def _init_auth_server_metadata(self) -> None: 180 | try: 181 | log.debug( 182 | "Attempting metadata retrieval from well_known_uri: %s", self._config[CONSTS.CONFIG_WELL_KNOWN_URI] 183 | ) 184 | 185 | verify = self._verify_ssl 186 | 187 | if self._ssl_ca_cert: 188 | verify = self._ssl_ca_cert 189 | 190 | headers = { 191 | "User-Agent": CONSTS.USER_AGENT, 192 | **(self._proxy_headers if self._proxy_headers else {}), 193 | } 194 | 195 | res = self._requests_session.get( 196 | url=self._config[CONSTS.CONFIG_WELL_KNOWN_URI], 197 | proxies=self._proxy, 198 | verify=verify, 199 | headers=headers, 200 | ) 201 | log.debug("Request from well_known_uri completed with status: %s", res.status_code) 202 | log.debug("Response headers from well_known_uri were %s", res.headers) 203 | self._well_known_uri_metadata = res.json() 204 | except Exception as e: 205 | raise AuthServerMetadataError( 206 | f"Error retrieving contents from the well_known_uri: {self._config[CONSTS.CONFIG_WELL_KNOWN_URI]}" 207 | ) from e 208 | 209 | if ( 210 | CONSTS.META_ISSUER not in self._well_known_uri_metadata 211 | or CONSTS.META_TOKEN_ENDPOINT not in self._well_known_uri_metadata 212 | ): 213 | raise AuthServerMetadataContentError( 214 | f"Both '{CONSTS.META_ISSUER}' and '{CONSTS.META_TOKEN_ENDPOINT}' are required within contents of well_known_uri: {self._config[CONSTS.CONFIG_WELL_KNOWN_URI]}" 215 | ) 216 | log.debug( 217 | "Retrieved issuer: %s and token_endpoint: %s from well_known_uri", 218 | self._well_known_uri_metadata[CONSTS.META_ISSUER], 219 | self._well_known_uri_metadata[CONSTS.META_TOKEN_ENDPOINT], 220 | ) 221 | 222 | def _get_client_assertion_jws(self) -> str: 223 | issued_at = time.time() 224 | key = RSAKey.import_key(self._config["jwk"]) 225 | 226 | try: 227 | return jwt.encode( 228 | claims={ 229 | "sub": self._config[CONSTS.CONFIG_CLIENT_ID], 230 | "iss": self._config[CONSTS.CONFIG_CLIENT_ID], 231 | "aud": [self._well_known_uri_metadata[CONSTS.META_ISSUER]], 232 | "nbf": issued_at - CONSTS.CC_JWT_NOT_BEFORE_SECS, 233 | "iat": issued_at, 234 | "exp": issued_at + CONSTS.CC_JWT_EXPIRE_AFTER_SECS, 235 | "jti": str(uuid.uuid4()), 236 | }, 237 | key=key, 238 | header={ 239 | "kid": self._config[CONSTS.CONFIG_JWK][CONSTS.JWK_KID], 240 | "alg": self._config[CONSTS.CONFIG_JWK][CONSTS.JWK_ALG], 241 | }, 242 | ) 243 | except ValueError as value_error: 244 | raise JWSSigningError("Error attempting to sign JWS") from value_error 245 | 246 | def _is_cached_token_valid(self) -> bool: 247 | if not self._cached_token: 248 | log.debug("Access Token cache is empty") 249 | return False 250 | if time.time() < self._cached_token[CONSTS.TOKEN_EXPIRES_AT] - CONSTS.TOKEN_EXPIRY_OFFSET_SECS: 251 | return True 252 | else: 253 | log.debug("Cached access token has expired at %s", self._cached_token[CONSTS.TOKEN_EXPIRES_AT]) 254 | return False 255 | 256 | def get_access_token(self) -> str: 257 | """ 258 | Returns an access token that can be used for authentication. 259 | 260 | If the cache contains a valid access token, it's returned. Otherwise 261 | a new access token is retrieved from FactSet's authorization server. 262 | 263 | The access token should be used immediately and not stored to avoid 264 | any issues with token expiry. 265 | 266 | The access token is used in the Authorization header when when accessing 267 | FactSet's APIs. Example: `{"Authorization": "Bearer access-token"}` 268 | 269 | Returns: 270 | str: access token for protected resource requests 271 | 272 | Raises: 273 | AccessTokenError: Raised if there's an issue retrieving the access token 274 | JWSSigningError: Raised if there's an issue signing the JWS 275 | """ 276 | if self._is_cached_token_valid(): 277 | log.debug( 278 | "Retrieving cached token. Expires in '%s' seconds.", 279 | int(self._cached_token[CONSTS.TOKEN_EXPIRES_AT] - time.time()), 280 | ) 281 | return self._cached_token[CONSTS.TOKEN_ACCESS_TOKEN] 282 | 283 | try: 284 | log.debug("Fetching new access token") 285 | 286 | headers = { 287 | "Accept": "application/json", 288 | "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", 289 | "User-Agent": CONSTS.USER_AGENT, 290 | } 291 | 292 | if self._proxy_headers: 293 | headers |= self._proxy_headers 294 | 295 | verify = self._verify_ssl 296 | if self._ssl_ca_cert: 297 | verify = self._ssl_ca_cert 298 | 299 | token = self._oauth_session.fetch_token( 300 | token_url=self._well_known_uri_metadata[CONSTS.META_TOKEN_ENDPOINT], 301 | client_id=self._config[CONSTS.CONFIG_CLIENT_ID], 302 | client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer", 303 | client_assertion=self._get_client_assertion_jws(), 304 | verify=verify, 305 | proxies=self._proxy, 306 | headers=headers, 307 | ) 308 | self._cached_token = token 309 | log.info("Caching token that expires at %s", token[CONSTS.TOKEN_EXPIRES_AT]) 310 | return token[CONSTS.TOKEN_ACCESS_TOKEN] 311 | except Exception as e: 312 | raise AccessTokenError("Error attempting to get access token") from e 313 | -------------------------------------------------------------------------------- /src/fds/sdk/utils/authentication/constants.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | 4 | class CONSTS: 5 | # confidential client assertion JWT 6 | CC_JWT_NOT_BEFORE_SECS = 5 7 | CC_JWT_EXPIRE_AFTER_SECS = 300 8 | 9 | # JSON Web Key 10 | JWK_ALG = "alg" 11 | JWK_KID = "kid" 12 | 13 | # auth server metadata 14 | META_ISSUER = "issuer" 15 | META_TOKEN_ENDPOINT = "token_endpoint" 16 | 17 | # access token 18 | TOKEN_ACCESS_TOKEN = "access_token" 19 | TOKEN_EXPIRES_AT = "expires_at" 20 | TOKEN_EXPIRY_OFFSET_SECS = 30 21 | 22 | # config 23 | CONFIG_CLIENT_ID = "clientId" 24 | CONFIG_WELL_KNOWN_URI = "wellKnownUri" 25 | CONFIG_JWK = "jwk" 26 | CONFIG_JWK_REQUIRED_KEYS = ["kty", "alg", "use", "kid", "n", "e", "d", "p", "q", "dp", "dq", "qi"] 27 | 28 | # default values 29 | FACTSET_WELL_KNOWN_URI = "https://auth.factset.com/.well-known/openid-configuration" 30 | 31 | USER_AGENT = f"fds-sdk/python/utils/2.1.1 ({platform.system()}; Python {platform.python_version()})" 32 | 33 | 34 | CONSTS = CONSTS() 35 | -------------------------------------------------------------------------------- /src/fds/sdk/utils/authentication/exceptions.py: -------------------------------------------------------------------------------- 1 | class AccessTokenError(Exception): 2 | """Raise when there's an issue retrieving an access token""" 3 | 4 | pass 5 | 6 | 7 | class AuthServerMetadataError(Exception): 8 | """Raise when there is an issue retrieving metadata from the authorization server""" 9 | 10 | pass 11 | 12 | 13 | class AuthServerMetadataContentError(AuthServerMetadataError): 14 | """Raise when there is an issue with the authorization server metadata content""" 15 | 16 | pass 17 | 18 | 19 | class ConfidentialClientError(Exception): 20 | """Raise for catch-all exceptions""" 21 | 22 | pass 23 | 24 | 25 | class ConfigurationError(Exception): 26 | """Raise when Credentials cannot be interrogated""" 27 | 28 | pass 29 | 30 | 31 | class JWSSigningError(Exception): 32 | """Raise during any exceptions during signing of the JWS""" 33 | 34 | pass 35 | -------------------------------------------------------------------------------- /src/fds/sdk/utils/authentication/oauth2client.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class OAuth2Client(ABC): 5 | """ 6 | Abstract base class for OAuth 2.0 clients. 7 | """ 8 | 9 | @abstractmethod 10 | def get_access_token(self) -> str: 11 | """ 12 | Retrieve Access Token 13 | Returns: 14 | str: access token for protected resource requests 15 | """ 16 | pass 17 | -------------------------------------------------------------------------------- /tests/fds/sdk/utils/authentication/test_confidential.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import platform 4 | from unittest.mock import ANY, mock_open 5 | 6 | import pytest 7 | 8 | from fds.sdk.utils.authentication import ( 9 | AccessTokenError, 10 | AuthServerMetadataError, 11 | AuthServerMetadataContentError, 12 | ConfidentialClient, 13 | ConfidentialClientError, 14 | ConfigurationError, 15 | OAuth2Client, 16 | ) 17 | 18 | 19 | @pytest.fixture() 20 | def example_config(): 21 | return { 22 | "name": "ExampleApp", 23 | "clientId": "test-clientid", 24 | "clientAuthType": "Confidential", 25 | "owners": ["owner_id"], 26 | "jwk": { 27 | "kty": "RSA", 28 | "use": "sig", 29 | "alg": "RS256", 30 | "kid": "jwk_kid", 31 | "d": "d", 32 | "n": "n", 33 | "e": "e", 34 | "p": "p", 35 | "q": "q", 36 | "dp": "dp", 37 | "dq": "dq", 38 | "qi": "qi", 39 | }, 40 | } 41 | 42 | 43 | @pytest.fixture() 44 | def client(mocker, example_config): 45 | mocker.patch("fds.sdk.utils.authentication.confidential.BackendApplicationClient") 46 | mocker.patch( 47 | "fds.sdk.utils.authentication.confidential.OAuth2Session.fetch_token", 48 | return_value={"access_token": "test-token", "expires_at": 10}, 49 | ) 50 | 51 | mock_get = mocker.patch("requests.Session.get") 52 | mock_get.return_value.json.return_value = { 53 | "issuer": "test-issuer", 54 | "token_endpoint": "https://test.token.endpoint", 55 | } 56 | 57 | mocker.patch("joserfc.jwk.RSAKey.import_key", return_value="jwk") 58 | 59 | return ConfidentialClient(config=example_config) 60 | 61 | 62 | def test_confidential_client_inheritance(client): 63 | assert issubclass(ConfidentialClient, OAuth2Client) 64 | assert isinstance(client, OAuth2Client) 65 | 66 | 67 | def test_constructor_with_config(mocker, example_config, caplog): 68 | mocker.patch("fds.sdk.utils.authentication.confidential.BackendApplicationClient") 69 | mocker.patch( 70 | "fds.sdk.utils.authentication.confidential.OAuth2Session.fetch_token", 71 | return_value={"access_token": "test", "expires_at": 10}, 72 | ) 73 | 74 | class AuthServerMetadataRes: 75 | status_code = 200 76 | headers = {"header": "value"} 77 | 78 | def json(self): 79 | return {"issuer": "test", "token_endpoint": "http://test.test"} 80 | 81 | caplog.set_level(logging.DEBUG) 82 | mocker.patch("requests.Session.get", return_value=AuthServerMetadataRes()) 83 | 84 | client = ConfidentialClient(config=example_config) 85 | 86 | assert client 87 | 88 | assert "Credentials are complete" in caplog.text 89 | assert "Attempting metadata retrieval" in caplog.text 90 | assert "Request from well_known_uri completed with status: 200" in caplog.text 91 | assert "headers from well_known_uri" in caplog.text 92 | assert "Retrieved issuer" in caplog.text 93 | assert "and token_endpoint" in caplog.text 94 | 95 | 96 | def test_constructor_with_file(mocker, example_config, caplog): 97 | mocker.patch("fds.sdk.utils.authentication.confidential.BackendApplicationClient") 98 | mocker.patch( 99 | "fds.sdk.utils.authentication.confidential.OAuth2Session.fetch_token", 100 | return_value={"access_token": "test", "expires_at": 10}, 101 | ) 102 | 103 | class AuthServerMetadataRes: 104 | status_code = 200 105 | headers = {"header": "value"} 106 | 107 | def json(self): 108 | return {"issuer": "test", "token_endpoint": "http://test.test"} 109 | 110 | caplog.set_level(logging.DEBUG) 111 | mocker.patch("requests.Session.get", return_value=AuthServerMetadataRes()) 112 | mocker.patch("json.load", return_value=example_config) 113 | fake_file_path = "/my/fake/path/creds.json" 114 | 115 | mocked_open = mocker.patch( 116 | "fds.sdk.utils.authentication.confidential.open", mock_open(read_data=json.dumps(example_config)) 117 | ) 118 | client = ConfidentialClient(fake_file_path) 119 | 120 | assert client 121 | 122 | mocked_open.assert_called_once_with(fake_file_path, "r") 123 | assert "Attempting metadata retrieval" in caplog.text 124 | assert "Request from well_known_uri completed with status: 200" in caplog.text 125 | assert "headers from well_known_uri" in caplog.text 126 | assert "Retrieved issuer" in caplog.text 127 | assert "and token_endpoint" in caplog.text 128 | 129 | 130 | def test_constructor_bad_params(): 131 | with pytest.raises(ValueError): 132 | ConfidentialClient() 133 | 134 | with pytest.raises(ValueError): 135 | ConfidentialClient(config_path="my_path", config=example_config) 136 | 137 | with pytest.raises(ValueError): 138 | ConfidentialClient(config={}) 139 | 140 | with pytest.raises(ValueError): 141 | ConfidentialClient("") 142 | 143 | 144 | def test_constructor_with_bad_file(): 145 | with pytest.raises(ConfigurationError): 146 | ConfidentialClient("/my/fake/file/path") 147 | 148 | 149 | def test_constructor_bad_session_instantiation(mocker, example_config): 150 | mocker.patch("fds.sdk.utils.authentication.confidential.BackendApplicationClient") 151 | mocker.patch("fds.sdk.utils.authentication.confidential.OAuth2Session", side_effect=Exception("fail!")) 152 | 153 | with pytest.raises(ConfidentialClientError): 154 | ConfidentialClient(config=example_config) 155 | 156 | 157 | def test_constructor_session_instantiation(mocker, example_config): 158 | test_client_id = "good_test" 159 | backend_result = "good_mock_backend" 160 | example_config["clientId"] = test_client_id 161 | mock_oauth_backend = mocker.patch( 162 | "fds.sdk.utils.authentication.confidential.BackendApplicationClient", return_value=backend_result 163 | ) 164 | 165 | mock_oauth2_session = mocker.patch("fds.sdk.utils.authentication.confidential.OAuth2Session") 166 | 167 | ConfidentialClient(config=example_config) 168 | 169 | mock_oauth_backend.assert_called_with(client_id=test_client_id) 170 | mock_oauth2_session.assert_called_with(client=backend_result) 171 | 172 | 173 | def test_constructor_session_instantiation_with_additional_parameters(mocker, example_config): 174 | test_client_id = "good_test" 175 | backend_result = "good_mock_backend" 176 | example_config["clientId"] = test_client_id 177 | mock_oauth_backend = mocker.patch( 178 | "fds.sdk.utils.authentication.confidential.BackendApplicationClient", return_value=backend_result 179 | ) 180 | 181 | mock_oauth2_session = mocker.patch("fds.sdk.utils.authentication.confidential.OAuth2Session") 182 | 183 | additional_parameters = { 184 | "proxy": "http://my:pass@test.test.test", 185 | "verify_ssl": False, 186 | "proxy_headers": {}, 187 | } 188 | 189 | class AuthServerMetadataRes: 190 | status_code = 200 191 | headers = {"header": "value"} 192 | 193 | def json(self): 194 | return {"issuer": "test", "token_endpoint": "http://test.test"} 195 | 196 | get_mock = mocker.patch("requests.Session.get", return_value=AuthServerMetadataRes()) 197 | 198 | ConfidentialClient(config=example_config, **additional_parameters) 199 | 200 | mock_oauth_backend.assert_called_with(client_id=test_client_id) 201 | mock_oauth2_session.assert_called_with(client=backend_result) 202 | get_mock.assert_called_with( 203 | url="https://auth.factset.com/.well-known/openid-configuration", 204 | proxies={"http": "http://my:pass@test.test.test", "https": "http://my:pass@test.test.test"}, 205 | verify=False, 206 | headers={"User-Agent": f"fds-sdk/python/utils/2.1.1 ({platform.system()}; Python {platform.python_version()})"}, 207 | ) 208 | 209 | 210 | def test_constructor_custom_well_known_uri(mocker, example_config, caplog): 211 | caplog.set_level(logging.DEBUG) 212 | mocker.patch("fds.sdk.utils.authentication.confidential.BackendApplicationClient") 213 | mocker.patch( 214 | "fds.sdk.utils.authentication.confidential.OAuth2Session.fetch_token", 215 | return_value={"access_token": "test", "expires_at": 10}, 216 | ) 217 | 218 | class AuthServerMetadataRes: 219 | status_code = 200 220 | headers = {"header": "value"} 221 | 222 | def json(self): 223 | return {"issuer": "test", "token_endpoint": "http://test.test"} 224 | 225 | get_mock = mocker.patch("requests.Session.get", return_value=AuthServerMetadataRes()) 226 | auth_test = "https://auth.test" 227 | 228 | example_config["wellKnownUri"] = auth_test 229 | 230 | client = ConfidentialClient(config=example_config) 231 | 232 | get_mock.assert_called_with( 233 | url=auth_test, 234 | proxies=None, 235 | verify=True, 236 | headers={"User-Agent": f"fds-sdk/python/utils/2.1.1 ({platform.system()}; Python {platform.python_version()})"}, 237 | ) 238 | assert client 239 | 240 | assert "Attempting metadata retrieval from well_known_uri: https://auth.test" in caplog.text 241 | 242 | 243 | def test_constructor_metadata_error(mocker, example_config): 244 | mocker.patch("fds.sdk.utils.authentication.confidential.BackendApplicationClient") 245 | mocker.patch( 246 | "fds.sdk.utils.authentication.confidential.OAuth2Session.fetch_token", 247 | return_value={"access_token": "test", "expires_at": 10}, 248 | ) 249 | mocker.patch("requests.Session.get", side_effect=Exception("error")) 250 | with pytest.raises(AuthServerMetadataError): 251 | ConfidentialClient(config=example_config) 252 | 253 | 254 | def test_constructor_missing_metadata(mocker, example_config): 255 | mocker.patch("fds.sdk.utils.authentication.confidential.BackendApplicationClient") 256 | mocker.patch( 257 | "fds.sdk.utils.authentication.confidential.OAuth2Session.fetch_token", 258 | return_value={"access_token": "test", "expires_at": 10}, 259 | ) 260 | 261 | class AuthServerMetadataRes: 262 | status_code = 200 263 | headers = {"header": "value"} 264 | 265 | @staticmethod 266 | def json(): 267 | return {} 268 | 269 | mocker.patch("requests.Session.get", return_value=AuthServerMetadataRes) 270 | 271 | with pytest.raises(AuthServerMetadataContentError): 272 | ConfidentialClient(config=example_config) 273 | 274 | 275 | def test_missing_jwk(mocker, example_config): 276 | mocker.patch("fds.sdk.utils.authentication.confidential.BackendApplicationClient") 277 | mocker.patch( 278 | "fds.sdk.utils.authentication.confidential.OAuth2Session.fetch_token", 279 | return_value={"access_token": "test", "expires_at": 10}, 280 | ) 281 | del example_config["jwk"] 282 | 283 | with pytest.raises(KeyError): 284 | ConfidentialClient(config=example_config) 285 | 286 | 287 | def test_missing_jwk_data(mocker, example_config): 288 | mocker.patch("fds.sdk.utils.authentication.confidential.BackendApplicationClient") 289 | mocker.patch( 290 | "fds.sdk.utils.authentication.confidential.OAuth2Session.fetch_token", 291 | return_value={"access_token": "test", "expires_at": 10}, 292 | ) 293 | del example_config["jwk"]["p"] 294 | 295 | with pytest.raises(KeyError): 296 | ConfidentialClient(config=example_config) 297 | 298 | 299 | def test_get_access_token(client, mocker, caplog): 300 | caplog.set_level(logging.INFO) 301 | mocker.patch("joserfc.jwt.encode", return_value="jws") 302 | 303 | assert client.get_access_token() == "test-token" 304 | 305 | assert "Caching token that expires at" in caplog.text 306 | 307 | 308 | def test_get_access_token_jws_sign(client, example_config, mocker): 309 | mocker.patch("fds.sdk.utils.authentication.confidential.time.time", return_value=0) 310 | mocker.patch("fds.sdk.utils.authentication.confidential.CONSTS.CC_JWT_NOT_BEFORE_SECS", 1000) 311 | mocker.patch( 312 | "fds.sdk.utils.authentication.confidential.CONSTS.CC_JWT_EXPIRE_AFTER_SECS", 313 | 2000, 314 | ) 315 | mocker.patch("fds.sdk.utils.authentication.confidential.uuid.uuid4", return_value="uuid") 316 | mock_jws_sign = mocker.patch("joserfc.jwt.encode", return_value="jws") 317 | 318 | client.get_access_token() 319 | 320 | mock_jws_sign.assert_called_once_with( 321 | claims={ 322 | "sub": "test-clientid", 323 | "iss": "test-clientid", 324 | "aud": ["test-issuer"], 325 | "nbf": -1000, 326 | "iat": 0, 327 | "exp": 2000, 328 | "jti": "uuid", 329 | }, 330 | key="jwk", 331 | header={"kid": "jwk_kid", "alg": "RS256"}, 332 | ) 333 | 334 | 335 | def test_get_access_token_jws_sign_error(client, mocker, caplog): 336 | mocker.patch("fds.sdk.utils.authentication.confidential.time.time", return_value=0) 337 | mocker.patch("fds.sdk.utils.authentication.confidential.CONSTS.CC_JWT_NOT_BEFORE_SECS", 1000) 338 | mocker.patch( 339 | "fds.sdk.utils.authentication.confidential.CONSTS.CC_JWT_EXPIRE_AFTER_SECS", 340 | 2000, 341 | ) 342 | mocker.patch("fds.sdk.utils.authentication.confidential.uuid.uuid4", return_value="uuid") 343 | 344 | mocker.patch( 345 | "joserfc.jwt.encode", 346 | side_effect=Exception("fail!"), 347 | ) 348 | 349 | with pytest.raises(AccessTokenError): 350 | client.get_access_token() 351 | 352 | 353 | def test_get_access_token_fetch(client, mocker): 354 | mocker.patch("fds.sdk.utils.authentication.confidential.BackendApplicationClient") 355 | mock_oauth2_session = mocker.patch( 356 | "fds.sdk.utils.authentication.confidential.OAuth2Session.fetch_token", 357 | return_value={"access_token": "test", "expires_at": 10}, 358 | ) 359 | mocker.patch("fds.sdk.utils.authentication.confidential.time.time", return_value=0) 360 | mocker.patch("fds.sdk.utils.authentication.confidential.CONSTS.CC_JWT_NOT_BEFORE_SECS", 1000) 361 | mocker.patch( 362 | "fds.sdk.utils.authentication.confidential.CONSTS.CC_JWT_EXPIRE_AFTER_SECS", 363 | 2000, 364 | ) 365 | mocker.patch("fds.sdk.utils.authentication.confidential.uuid.uuid4", return_value="uuid") 366 | mocker.patch("joserfc.jwt.encode", return_value="jws") 367 | mocker.patch("joserfc.jwk.RSAKey.import_key", return_value="jwk") 368 | 369 | client.get_access_token() 370 | 371 | mock_oauth2_session.assert_called_once_with( 372 | token_url="https://test.token.endpoint", 373 | client_id="test-clientid", 374 | client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer", 375 | client_assertion="jws", 376 | proxies=None, 377 | verify=True, 378 | headers={ 379 | "Accept": "application/json", 380 | "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", 381 | "User-Agent": f"fds-sdk/python/utils/2.1.1 ({platform.system()}; Python {platform.python_version()})", 382 | }, 383 | ) 384 | 385 | 386 | def test_get_access_token_fetch_error(client, mocker, caplog): 387 | caplog.set_level(logging.DEBUG) 388 | mocker.patch("fds.sdk.utils.authentication.confidential.BackendApplicationClient") 389 | mocker.patch("fds.sdk.utils.authentication.confidential.OAuth2Session.fetch_token", side_effect=Exception("fail!")) 390 | mocker.patch("fds.sdk.utils.authentication.confidential.time.time", return_value=0) 391 | mocker.patch("fds.sdk.utils.authentication.confidential.CONSTS.CC_JWT_NOT_BEFORE_SECS", 1000) 392 | mocker.patch( 393 | "fds.sdk.utils.authentication.confidential.CONSTS.CC_JWT_EXPIRE_AFTER_SECS", 394 | 2000, 395 | ) 396 | mocker.patch("fds.sdk.utils.authentication.confidential.uuid.uuid4", return_value="uuid") 397 | mocker.patch("joserfc.jwt.encode", return_value="jws") 398 | mocker.patch("joserfc.jwk.RSAKey.import_key", return_value="jwk") 399 | 400 | with pytest.raises(AccessTokenError): 401 | client.get_access_token() 402 | 403 | assert "Access Token cache is empty" in caplog.text 404 | 405 | 406 | def test_get_access_token_cached(example_config, mocker, caplog): 407 | caplog.set_level(logging.DEBUG) 408 | mock_get = mocker.patch("requests.Session.get") 409 | mock_get.return_value.json.return_value = { 410 | "issuer": "test-issuer", 411 | "token_endpoint": "https://test.token.endpoint", 412 | } 413 | mocker.patch("joserfc.jwt.encode", return_value="jws") 414 | mocker.patch("joserfc.jwk.RSAKey.import_key", return_value="jwk") 415 | mocker.patch("fds.sdk.utils.authentication.confidential.BackendApplicationClient") 416 | mock_oauth2_session = mocker.patch("fds.sdk.utils.authentication.confidential.OAuth2Session") 417 | mock_oauth2_session.return_value.fetch_token.return_value = { 418 | "access_token": "test", 419 | "expires_at": 40, 420 | } 421 | mocker.patch("fds.sdk.utils.authentication.confidential.time.time", return_value=0) 422 | 423 | client = ConfidentialClient(config=example_config) 424 | 425 | assert client.get_access_token() == client.get_access_token() 426 | mock_oauth2_session.return_value.fetch_token.assert_called_once() 427 | 428 | assert "Retrieving cached token. Expires in '40' seconds" in caplog.text 429 | 430 | 431 | def test_get_access_token_cache_expired(client, mocker, caplog): 432 | caplog.set_level(logging.DEBUG) 433 | mocker.patch("joserfc.jwt.encode", return_value="jws") 434 | mock_oauth2_session = mocker.patch( 435 | "fds.sdk.utils.authentication.confidential.OAuth2Session.fetch_token", 436 | return_value={ 437 | "access_token": "test", 438 | "expires_at": 30, 439 | }, 440 | ) 441 | 442 | mocker.patch("fds.sdk.utils.authentication.confidential.time.time", return_value=20) 443 | 444 | client.get_access_token() 445 | client.get_access_token() 446 | 447 | assert mock_oauth2_session.call_count == 2 448 | 449 | assert "Cached access token has expired" in caplog.text 450 | -------------------------------------------------------------------------------- /tests/fds/sdk/utils/authentication/test_oauth2client.py: -------------------------------------------------------------------------------- 1 | from typing import Type 2 | import pytest 3 | from fds.sdk.utils.authentication import OAuth2Client 4 | 5 | 6 | def test_cannot_instantiate(): 7 | with pytest.raises(TypeError): 8 | OAuth2Client() 9 | 10 | 11 | def test_bad_instantiation(): 12 | class bad_class(OAuth2Client): 13 | pass 14 | 15 | with pytest.raises(TypeError): 16 | bad_class() 17 | 18 | 19 | def test_good_instantiation(): 20 | ret_val = "good_token" 21 | 22 | class good_class(OAuth2Client): 23 | def get_access_token(self): 24 | return ret_val 25 | 26 | my_good_instance = good_class() 27 | assert callable(my_good_instance.get_access_token) 28 | assert issubclass(good_class, OAuth2Client) 29 | assert isinstance(my_good_instance, OAuth2Client) 30 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # tox (https://tox.readthedocs.io/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py37,py38,py39,py310,py311 8 | isolated_build = true 9 | 10 | [gh-actions] 11 | python = 12 | 3.7: py37 13 | 3.8: py38 14 | 3.9: py39 15 | 3.10: py310 16 | 3.11: py311 17 | 18 | [testenv] 19 | allowlist_externals = poetry 20 | commands = 21 | poetry install -v 22 | poetry run pytest 23 | --------------------------------------------------------------------------------