├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGELOG.md ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── docs ├── Makefile ├── api_reference.rst ├── conf.py ├── index.rst ├── make.bat ├── quick.rst ├── requirements.txt ├── testing.rst └── typing.rst ├── pyproject.toml ├── tests ├── __init__.py ├── test_api_client.py ├── test_mypy_plugin.ini └── test_mypy_plugin.yml └── tiny_api_client ├── __init__.py ├── mypy.py └── py.typed /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: sanjacob 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Test and Deploy 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ "master" ] 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python-version: ["3.10", "3.11", "3.12"] 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v3 21 | 22 | - name: Set up Python 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - name: Set up Python environment 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install pipenv 31 | pipenv requirements --dev > requirements.txt 32 | pip install -r requirements.txt 33 | 34 | - name: Run linter 35 | run: flake8 tiny_api_client 36 | 37 | - name: Run type checker 38 | run: mypy --strict tiny_api_client 39 | 40 | - name: Run test suite 41 | run: pytest --mypy-ini-file=tests/test_mypy_plugin.ini --mypy-only-local-stub -v 42 | 43 | pypi: 44 | runs-on: ubuntu-latest 45 | if: startsWith(github.ref, 'refs/tags/') 46 | needs: [ test ] 47 | steps: 48 | - name: Checkout repository 49 | uses: actions/checkout@v3 50 | 51 | - name: Set up Python 52 | uses: actions/setup-python@v4 53 | with: 54 | python-version: "3.10" 55 | 56 | - name: Set up Python environment 57 | run: | 58 | python -m pip install --upgrade pip 59 | pip install pipenv 60 | pipenv requirements --dev > requirements.txt 61 | pip install -r requirements.txt 62 | 63 | - name: Build and upload to PyPI 64 | env: 65 | TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} 66 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} 67 | run: | 68 | python -m build 69 | twine upload dist/* 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python,vim,linux,macos,windows 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,vim,linux,macos,windows 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### macOS ### 20 | # General 21 | .DS_Store 22 | .AppleDouble 23 | .LSOverride 24 | 25 | # Icon must end with two \r 26 | Icon 27 | 28 | 29 | # Thumbnails 30 | ._* 31 | 32 | # Files that might appear in the root of a volume 33 | .DocumentRevisions-V100 34 | .fseventsd 35 | .Spotlight-V100 36 | .TemporaryItems 37 | .Trashes 38 | .VolumeIcon.icns 39 | .com.apple.timemachine.donotpresent 40 | 41 | # Directories potentially created on remote AFP share 42 | .AppleDB 43 | .AppleDesktop 44 | Network Trash Folder 45 | Temporary Items 46 | .apdisk 47 | 48 | ### macOS Patch ### 49 | # iCloud generated files 50 | *.icloud 51 | 52 | ### Python ### 53 | # Byte-compiled / optimized / DLL files 54 | __pycache__/ 55 | *.py[cod] 56 | *$py.class 57 | 58 | # C extensions 59 | *.so 60 | 61 | # Distribution / packaging 62 | .Python 63 | build/ 64 | develop-eggs/ 65 | dist/ 66 | downloads/ 67 | eggs/ 68 | .eggs/ 69 | lib/ 70 | lib64/ 71 | parts/ 72 | sdist/ 73 | var/ 74 | wheels/ 75 | share/python-wheels/ 76 | *.egg-info/ 77 | .installed.cfg 78 | *.egg 79 | MANIFEST 80 | 81 | # PyInstaller 82 | # Usually these files are written by a python script from a template 83 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 84 | *.manifest 85 | *.spec 86 | 87 | # Installer logs 88 | pip-log.txt 89 | pip-delete-this-directory.txt 90 | 91 | # Unit test / coverage reports 92 | htmlcov/ 93 | .tox/ 94 | .nox/ 95 | .coverage 96 | .coverage.* 97 | .cache 98 | nosetests.xml 99 | coverage.xml 100 | *.cover 101 | *.py,cover 102 | .hypothesis/ 103 | .pytest_cache/ 104 | cover/ 105 | 106 | # Translations 107 | *.mo 108 | *.pot 109 | 110 | # Django stuff: 111 | *.log 112 | local_settings.py 113 | db.sqlite3 114 | db.sqlite3-journal 115 | 116 | # Flask stuff: 117 | instance/ 118 | .webassets-cache 119 | 120 | # Scrapy stuff: 121 | .scrapy 122 | 123 | # Sphinx documentation 124 | docs/_build/ 125 | 126 | # PyBuilder 127 | .pybuilder/ 128 | target/ 129 | 130 | # Jupyter Notebook 131 | .ipynb_checkpoints 132 | 133 | # IPython 134 | profile_default/ 135 | ipython_config.py 136 | 137 | # pyenv 138 | # For a library or package, you might want to ignore these files since the code is 139 | # intended to run in multiple environments; otherwise, check them in: 140 | # .python-version 141 | 142 | # pipenv 143 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 144 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 145 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 146 | # install all needed dependencies. 147 | #Pipfile.lock 148 | 149 | # poetry 150 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 151 | # This is especially recommended for binary packages to ensure reproducibility, and is more 152 | # commonly ignored for libraries. 153 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 154 | #poetry.lock 155 | 156 | # pdm 157 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 158 | #pdm.lock 159 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 160 | # in version control. 161 | # https://pdm.fming.dev/#use-with-ide 162 | .pdm.toml 163 | 164 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 165 | __pypackages__/ 166 | 167 | # Celery stuff 168 | celerybeat-schedule 169 | celerybeat.pid 170 | 171 | # SageMath parsed files 172 | *.sage.py 173 | 174 | # Environments 175 | .env 176 | .venv 177 | env/ 178 | venv/ 179 | ENV/ 180 | env.bak/ 181 | venv.bak/ 182 | 183 | # Spyder project settings 184 | .spyderproject 185 | .spyproject 186 | 187 | # Rope project settings 188 | .ropeproject 189 | 190 | # mkdocs documentation 191 | /site 192 | 193 | # mypy 194 | .mypy_cache/ 195 | .dmypy.json 196 | dmypy.json 197 | 198 | # Pyre type checker 199 | .pyre/ 200 | 201 | # pytype static type analyzer 202 | .pytype/ 203 | 204 | # Cython debug symbols 205 | cython_debug/ 206 | 207 | # PyCharm 208 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 209 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 210 | # and can be added to the global gitignore or merged into this file. For a more nuclear 211 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 212 | #.idea/ 213 | 214 | ### Python Patch ### 215 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 216 | poetry.toml 217 | 218 | # ruff 219 | .ruff_cache/ 220 | 221 | # LSP config files 222 | pyrightconfig.json 223 | 224 | ### Vim ### 225 | # Swap 226 | [._]*.s[a-v][a-z] 227 | !*.svg # comment out if you don't need vector files 228 | [._]*.sw[a-p] 229 | [._]s[a-rt-v][a-z] 230 | [._]ss[a-gi-z] 231 | [._]sw[a-p] 232 | 233 | # Session 234 | Session.vim 235 | Sessionx.vim 236 | 237 | # Temporary 238 | .netrwhist 239 | # Auto-generated tag files 240 | tags 241 | # Persistent undo 242 | [._]*.un~ 243 | 244 | ### Windows ### 245 | # Windows thumbnail cache files 246 | Thumbs.db 247 | Thumbs.db:encryptable 248 | ehthumbs.db 249 | ehthumbs_vista.db 250 | 251 | # Dump file 252 | *.stackdump 253 | 254 | # Folder config file 255 | [Dd]esktop.ini 256 | 257 | # Recycle Bin used on file shares 258 | $RECYCLE.BIN/ 259 | 260 | # Windows Installer files 261 | *.cab 262 | *.msi 263 | *.msix 264 | *.msm 265 | *.msp 266 | 267 | # Windows shortcuts 268 | *.lnk 269 | 270 | # End of https://www.toptal.com/developers/gitignore/api/python,vim,linux,macos,windows 271 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v3.2.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | 12 | - repo: https://github.com/pycqa/flake8 13 | rev: 6.0.0 14 | hooks: 15 | - id: flake8 16 | additional_dependencies: [Flake8-pyproject] 17 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the OS, Python version and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.10" 13 | # You can also specify other tool versions: 14 | # nodejs: "19" 15 | # rust: "1.64" 16 | # golang: "1.19" 17 | 18 | # Build documentation in the "docs/" directory with Sphinx 19 | sphinx: 20 | configuration: docs/conf.py 21 | 22 | # Optionally build your docs in additional formats such as PDF and ePub 23 | # formats: 24 | # - pdf 25 | # - epub 26 | 27 | # Optional but recommended, declare the Python requirements required 28 | # to build your documentation 29 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 30 | python: 31 | install: 32 | - requirements: docs/requirements.txt 33 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [1.4.0] - 2025-02-24 10 | 11 | ### Added 12 | - Max retries client parameter applied to the requests adapter 13 | 14 | ### Changed 15 | - Use `collections.defaultdict` for route parameter substitution 16 | 17 | ## [1.3.1] - 2024-08-19 18 | 19 | ### Fixed 20 | - Type annotation of status handler to include client parameter 21 | 22 | ## [1.3.0] - 2024-08-11 23 | 24 | ### Changed 25 | - Status handler is now called with status and entire response 26 | 27 | ## [1.2.2] - 2024-01-11 28 | 29 | ### Changed 30 | - mypy plugin now treats most route params as required 31 | - mypy route parsing slightly improved 32 | 33 | ## [1.2.1] - 2024-01-06 34 | 35 | ### Added 36 | - New mypy plugin for route parameter type checking support 37 | 38 | ## [1.2.0] - 2024-01-04 39 | 40 | ### Changed 41 | - Improved type checking support 42 | - Refactored module to divide work in functions 43 | 44 | ## [1.1.0] - 2023-11-23 45 | 46 | ### Added 47 | - Support for connection pooling within the same client instance 48 | 49 | ### Changed 50 | - Member `_session` has been renamed `_cookies` to avoid confusion 51 | 52 | ## [1.0.0] - 2023-11-23 53 | 54 | ### Added 55 | - Initial release 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | requests = "*" 8 | 9 | [dev-packages] 10 | mypy = "1.8.0" 11 | pytest = "*" 12 | sphinx = "*" 13 | build = "*" 14 | sphinx-rtd-theme = "*" 15 | exceptiongroup = "*" 16 | pytest-mock = "*" 17 | types-requests = "*" 18 | isort = "*" 19 | flake8 = "*" 20 | flake8-pyproject = "*" 21 | pre-commit = "*" 22 | twine = "*" 23 | pytest-mypy-plugins = "*" 24 | types-urllib3 = "*" 25 | 26 | [requires] 27 | python_version = "3.10" 28 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "953e604fa56251cce395adf581e090eef90f3b79a3889e73868219444540b898" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.10" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", 22 | "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" 23 | ], 24 | "markers": "python_version >= '3.6'", 25 | "version": "==2025.1.31" 26 | }, 27 | "charset-normalizer": { 28 | "hashes": [ 29 | "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", 30 | "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa", 31 | "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", 32 | "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", 33 | "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", 34 | "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", 35 | "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", 36 | "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", 37 | "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", 38 | "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", 39 | "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", 40 | "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", 41 | "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", 42 | "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", 43 | "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", 44 | "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", 45 | "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", 46 | "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", 47 | "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", 48 | "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", 49 | "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e", 50 | "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a", 51 | "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", 52 | "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", 53 | "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", 54 | "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", 55 | "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", 56 | "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", 57 | "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", 58 | "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", 59 | "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", 60 | "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", 61 | "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", 62 | "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", 63 | "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", 64 | "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", 65 | "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", 66 | "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", 67 | "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", 68 | "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", 69 | "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", 70 | "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", 71 | "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", 72 | "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf", 73 | "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487", 74 | "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d", 75 | "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd", 76 | "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", 77 | "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534", 78 | "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", 79 | "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", 80 | "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", 81 | "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", 82 | "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", 83 | "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", 84 | "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", 85 | "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", 86 | "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d", 87 | "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", 88 | "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", 89 | "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", 90 | "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", 91 | "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", 92 | "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", 93 | "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", 94 | "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", 95 | "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", 96 | "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", 97 | "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", 98 | "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", 99 | "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", 100 | "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", 101 | "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", 102 | "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", 103 | "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", 104 | "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", 105 | "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", 106 | "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e", 107 | "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", 108 | "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", 109 | "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", 110 | "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", 111 | "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", 112 | "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", 113 | "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", 114 | "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", 115 | "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", 116 | "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089", 117 | "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", 118 | "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e", 119 | "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", 120 | "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616" 121 | ], 122 | "markers": "python_version >= '3.7'", 123 | "version": "==3.4.1" 124 | }, 125 | "idna": { 126 | "hashes": [ 127 | "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", 128 | "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" 129 | ], 130 | "markers": "python_version >= '3.6'", 131 | "version": "==3.10" 132 | }, 133 | "requests": { 134 | "hashes": [ 135 | "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", 136 | "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" 137 | ], 138 | "index": "pypi", 139 | "markers": "python_version >= '3.8'", 140 | "version": "==2.32.3" 141 | }, 142 | "urllib3": { 143 | "hashes": [ 144 | "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", 145 | "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d" 146 | ], 147 | "markers": "python_version >= '3.9'", 148 | "version": "==2.3.0" 149 | } 150 | }, 151 | "develop": { 152 | "alabaster": { 153 | "hashes": [ 154 | "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", 155 | "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b" 156 | ], 157 | "markers": "python_version >= '3.10'", 158 | "version": "==1.0.0" 159 | }, 160 | "attrs": { 161 | "hashes": [ 162 | "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", 163 | "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a" 164 | ], 165 | "markers": "python_version >= '3.8'", 166 | "version": "==25.1.0" 167 | }, 168 | "babel": { 169 | "hashes": [ 170 | "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", 171 | "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2" 172 | ], 173 | "markers": "python_version >= '3.8'", 174 | "version": "==2.17.0" 175 | }, 176 | "backports.tarfile": { 177 | "hashes": [ 178 | "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", 179 | "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991" 180 | ], 181 | "markers": "python_version < '3.12'", 182 | "version": "==1.2.0" 183 | }, 184 | "build": { 185 | "hashes": [ 186 | "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", 187 | "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7" 188 | ], 189 | "index": "pypi", 190 | "markers": "python_version >= '3.8'", 191 | "version": "==1.2.2.post1" 192 | }, 193 | "certifi": { 194 | "hashes": [ 195 | "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", 196 | "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" 197 | ], 198 | "markers": "python_version >= '3.6'", 199 | "version": "==2025.1.31" 200 | }, 201 | "cffi": { 202 | "hashes": [ 203 | "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", 204 | "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", 205 | "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", 206 | "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", 207 | "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", 208 | "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", 209 | "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", 210 | "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", 211 | "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", 212 | "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", 213 | "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", 214 | "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", 215 | "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", 216 | "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", 217 | "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", 218 | "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", 219 | "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", 220 | "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", 221 | "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", 222 | "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", 223 | "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", 224 | "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", 225 | "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", 226 | "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", 227 | "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", 228 | "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", 229 | "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", 230 | "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", 231 | "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", 232 | "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", 233 | "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", 234 | "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", 235 | "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", 236 | "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", 237 | "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", 238 | "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", 239 | "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", 240 | "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", 241 | "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", 242 | "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", 243 | "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", 244 | "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", 245 | "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", 246 | "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", 247 | "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", 248 | "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", 249 | "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", 250 | "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", 251 | "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", 252 | "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", 253 | "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", 254 | "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", 255 | "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", 256 | "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", 257 | "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", 258 | "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", 259 | "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", 260 | "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", 261 | "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", 262 | "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", 263 | "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", 264 | "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", 265 | "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", 266 | "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", 267 | "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", 268 | "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", 269 | "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" 270 | ], 271 | "markers": "platform_python_implementation != 'PyPy'", 272 | "version": "==1.17.1" 273 | }, 274 | "cfgv": { 275 | "hashes": [ 276 | "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", 277 | "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560" 278 | ], 279 | "markers": "python_version >= '3.8'", 280 | "version": "==3.4.0" 281 | }, 282 | "charset-normalizer": { 283 | "hashes": [ 284 | "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", 285 | "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa", 286 | "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", 287 | "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", 288 | "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", 289 | "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", 290 | "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", 291 | "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", 292 | "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", 293 | "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", 294 | "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", 295 | "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", 296 | "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", 297 | "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", 298 | "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", 299 | "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", 300 | "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", 301 | "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", 302 | "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", 303 | "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", 304 | "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e", 305 | "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a", 306 | "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", 307 | "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", 308 | "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", 309 | "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", 310 | "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", 311 | "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", 312 | "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", 313 | "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", 314 | "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", 315 | "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", 316 | "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", 317 | "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", 318 | "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", 319 | "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", 320 | "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", 321 | "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", 322 | "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", 323 | "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", 324 | "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", 325 | "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", 326 | "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", 327 | "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf", 328 | "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487", 329 | "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d", 330 | "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd", 331 | "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", 332 | "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534", 333 | "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", 334 | "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", 335 | "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", 336 | "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", 337 | "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", 338 | "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", 339 | "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", 340 | "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", 341 | "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d", 342 | "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", 343 | "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", 344 | "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", 345 | "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", 346 | "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", 347 | "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", 348 | "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", 349 | "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", 350 | "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", 351 | "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", 352 | "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", 353 | "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", 354 | "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", 355 | "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", 356 | "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", 357 | "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", 358 | "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", 359 | "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", 360 | "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", 361 | "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e", 362 | "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", 363 | "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", 364 | "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", 365 | "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", 366 | "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", 367 | "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", 368 | "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", 369 | "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", 370 | "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", 371 | "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089", 372 | "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", 373 | "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e", 374 | "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", 375 | "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616" 376 | ], 377 | "markers": "python_version >= '3.7'", 378 | "version": "==3.4.1" 379 | }, 380 | "cryptography": { 381 | "hashes": [ 382 | "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7", 383 | "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3", 384 | "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183", 385 | "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69", 386 | "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a", 387 | "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62", 388 | "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911", 389 | "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7", 390 | "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a", 391 | "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41", 392 | "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83", 393 | "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12", 394 | "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864", 395 | "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf", 396 | "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c", 397 | "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2", 398 | "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b", 399 | "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0", 400 | "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4", 401 | "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9", 402 | "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008", 403 | "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862", 404 | "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009", 405 | "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7", 406 | "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f", 407 | "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026", 408 | "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f", 409 | "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd", 410 | "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420", 411 | "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14", 412 | "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00" 413 | ], 414 | "markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'", 415 | "version": "==44.0.1" 416 | }, 417 | "decorator": { 418 | "hashes": [ 419 | "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", 420 | "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a" 421 | ], 422 | "markers": "python_version >= '3.8'", 423 | "version": "==5.2.1" 424 | }, 425 | "distlib": { 426 | "hashes": [ 427 | "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", 428 | "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403" 429 | ], 430 | "version": "==0.3.9" 431 | }, 432 | "docutils": { 433 | "hashes": [ 434 | "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", 435 | "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2" 436 | ], 437 | "markers": "python_version >= '3.9'", 438 | "version": "==0.21.2" 439 | }, 440 | "exceptiongroup": { 441 | "hashes": [ 442 | "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", 443 | "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" 444 | ], 445 | "index": "pypi", 446 | "markers": "python_version >= '3.7'", 447 | "version": "==1.2.2" 448 | }, 449 | "filelock": { 450 | "hashes": [ 451 | "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", 452 | "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de" 453 | ], 454 | "markers": "python_version >= '3.9'", 455 | "version": "==3.18.0" 456 | }, 457 | "flake8": { 458 | "hashes": [ 459 | "sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343", 460 | "sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426" 461 | ], 462 | "index": "pypi", 463 | "markers": "python_version >= '3.9'", 464 | "version": "==7.2.0" 465 | }, 466 | "flake8-pyproject": { 467 | "hashes": [ 468 | "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a" 469 | ], 470 | "index": "pypi", 471 | "markers": "python_version >= '3.6'", 472 | "version": "==1.2.3" 473 | }, 474 | "id": { 475 | "hashes": [ 476 | "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", 477 | "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658" 478 | ], 479 | "markers": "python_version >= '3.8'", 480 | "version": "==1.5.0" 481 | }, 482 | "identify": { 483 | "hashes": [ 484 | "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150", 485 | "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf" 486 | ], 487 | "markers": "python_version >= '3.9'", 488 | "version": "==2.6.9" 489 | }, 490 | "idna": { 491 | "hashes": [ 492 | "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", 493 | "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" 494 | ], 495 | "markers": "python_version >= '3.6'", 496 | "version": "==3.10" 497 | }, 498 | "imagesize": { 499 | "hashes": [ 500 | "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", 501 | "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a" 502 | ], 503 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 504 | "version": "==1.4.1" 505 | }, 506 | "importlib-metadata": { 507 | "hashes": [ 508 | "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", 509 | "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580" 510 | ], 511 | "markers": "python_version < '3.12'", 512 | "version": "==8.6.1" 513 | }, 514 | "iniconfig": { 515 | "hashes": [ 516 | "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", 517 | "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" 518 | ], 519 | "markers": "python_version >= '3.7'", 520 | "version": "==2.0.0" 521 | }, 522 | "isort": { 523 | "hashes": [ 524 | "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", 525 | "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615" 526 | ], 527 | "index": "pypi", 528 | "markers": "python_full_version >= '3.9.0'", 529 | "version": "==6.0.1" 530 | }, 531 | "jaraco.classes": { 532 | "hashes": [ 533 | "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", 534 | "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" 535 | ], 536 | "markers": "python_version >= '3.8'", 537 | "version": "==3.4.0" 538 | }, 539 | "jaraco.context": { 540 | "hashes": [ 541 | "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", 542 | "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4" 543 | ], 544 | "markers": "python_version >= '3.8'", 545 | "version": "==6.0.1" 546 | }, 547 | "jaraco.functools": { 548 | "hashes": [ 549 | "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", 550 | "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649" 551 | ], 552 | "markers": "python_version >= '3.8'", 553 | "version": "==4.1.0" 554 | }, 555 | "jeepney": { 556 | "hashes": [ 557 | "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", 558 | "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755" 559 | ], 560 | "markers": "sys_platform == 'linux'", 561 | "version": "==0.8.0" 562 | }, 563 | "jinja2": { 564 | "hashes": [ 565 | "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", 566 | "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb" 567 | ], 568 | "markers": "python_version >= '3.7'", 569 | "version": "==3.1.5" 570 | }, 571 | "jsonschema": { 572 | "hashes": [ 573 | "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", 574 | "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566" 575 | ], 576 | "markers": "python_version >= '3.8'", 577 | "version": "==4.23.0" 578 | }, 579 | "jsonschema-specifications": { 580 | "hashes": [ 581 | "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", 582 | "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf" 583 | ], 584 | "markers": "python_version >= '3.9'", 585 | "version": "==2024.10.1" 586 | }, 587 | "keyring": { 588 | "hashes": [ 589 | "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", 590 | "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd" 591 | ], 592 | "markers": "platform_machine != 'ppc64le' and platform_machine != 's390x'", 593 | "version": "==25.6.0" 594 | }, 595 | "markdown-it-py": { 596 | "hashes": [ 597 | "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", 598 | "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" 599 | ], 600 | "markers": "python_version >= '3.8'", 601 | "version": "==3.0.0" 602 | }, 603 | "markupsafe": { 604 | "hashes": [ 605 | "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", 606 | "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", 607 | "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", 608 | "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", 609 | "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", 610 | "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", 611 | "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", 612 | "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", 613 | "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", 614 | "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", 615 | "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", 616 | "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", 617 | "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", 618 | "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", 619 | "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", 620 | "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", 621 | "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", 622 | "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", 623 | "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", 624 | "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", 625 | "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", 626 | "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", 627 | "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", 628 | "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", 629 | "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", 630 | "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", 631 | "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", 632 | "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", 633 | "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", 634 | "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", 635 | "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", 636 | "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", 637 | "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", 638 | "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", 639 | "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", 640 | "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", 641 | "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", 642 | "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", 643 | "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", 644 | "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", 645 | "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", 646 | "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", 647 | "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", 648 | "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", 649 | "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", 650 | "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", 651 | "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", 652 | "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", 653 | "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", 654 | "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", 655 | "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", 656 | "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", 657 | "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", 658 | "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", 659 | "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", 660 | "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", 661 | "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", 662 | "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", 663 | "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", 664 | "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", 665 | "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" 666 | ], 667 | "markers": "python_version >= '3.9'", 668 | "version": "==3.0.2" 669 | }, 670 | "mccabe": { 671 | "hashes": [ 672 | "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", 673 | "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" 674 | ], 675 | "markers": "python_version >= '3.6'", 676 | "version": "==0.7.0" 677 | }, 678 | "mdurl": { 679 | "hashes": [ 680 | "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", 681 | "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" 682 | ], 683 | "markers": "python_version >= '3.7'", 684 | "version": "==0.1.2" 685 | }, 686 | "more-itertools": { 687 | "hashes": [ 688 | "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b", 689 | "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89" 690 | ], 691 | "markers": "python_version >= '3.9'", 692 | "version": "==10.6.0" 693 | }, 694 | "mypy": { 695 | "hashes": [ 696 | "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6", 697 | "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d", 698 | "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02", 699 | "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d", 700 | "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3", 701 | "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3", 702 | "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3", 703 | "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66", 704 | "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259", 705 | "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835", 706 | "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd", 707 | "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d", 708 | "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8", 709 | "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07", 710 | "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b", 711 | "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e", 712 | "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6", 713 | "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae", 714 | "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9", 715 | "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d", 716 | "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a", 717 | "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592", 718 | "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218", 719 | "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817", 720 | "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4", 721 | "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410", 722 | "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55" 723 | ], 724 | "index": "pypi", 725 | "markers": "python_version >= '3.8'", 726 | "version": "==1.8.0" 727 | }, 728 | "mypy-extensions": { 729 | "hashes": [ 730 | "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", 731 | "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" 732 | ], 733 | "markers": "python_version >= '3.5'", 734 | "version": "==1.0.0" 735 | }, 736 | "nh3": { 737 | "hashes": [ 738 | "sha256:09f037c02fc2c43b211ff1523de32801dcfb0918648d8e651c36ef890f1731ec", 739 | "sha256:0ae9cbd713524cdb81e64663d0d6aae26f678db9f2cd9db0bf162606f1f9f20c", 740 | "sha256:10317cd96fe4bbd4eb6b95f3920b71c902157ad44fed103fdcde43e3b8ee8be6", 741 | "sha256:181063c581defe683bd4bb78188ac9936d208aebbc74c7f7c16b6a32ae2ebb38", 742 | "sha256:1b9a8340a0aab991c68a5ca938d35ef4a8a3f4bf1b455da8855a40bee1fa0ace", 743 | "sha256:231addb7643c952cd6d71f1c8702d703f8fe34afcb20becb3efb319a501a12d7", 744 | "sha256:3eb04b9c3deb13c3a375ea39fd4a3c00d1f92e8fb2349f25f1e3e4506751774b", 745 | "sha256:47b2946c0e13057855209daeffb45dc910bd0c55daf10190bb0b4b60e2999784", 746 | "sha256:4fd2e9248725ebcedac3997a8d3da0d90a12a28c9179c6ba51f1658938ac30d0", 747 | "sha256:6ed834c68452a600f517dd3e1534dbfaff1f67f98899fecf139a055a25d99150", 748 | "sha256:76e2f603b30c02ff6456b233a83fc377dedab6a50947b04e960a6b905637b776", 749 | "sha256:813f1c8012dd64c990514b795508abb90789334f76a561fa0fd4ca32d2275330", 750 | "sha256:8698db4c04b140800d1a1cd3067fda399e36e1e2b8fc1fe04292a907350a3e9b", 751 | "sha256:92f3f1c4f47a2c6f3ca7317b1d5ced05bd29556a75d3a4e2715652ae9d15c05d", 752 | "sha256:9705c42d7ff88a0bea546c82d7fe5e59135e3d3f057e485394f491248a1f8ed5", 753 | "sha256:ac4d27dc836a476efffc6eb661994426b8b805c951b29c9cf2ff36bc9ad58bc5", 754 | "sha256:ce3731c8f217685d33d9268362e5b4f770914e922bba94d368ab244a59a6c397", 755 | "sha256:d2a176fd4306b6f0f178a3f67fac91bd97a3a8d8fafb771c9b9ef675ba5c8886", 756 | "sha256:da87573f03084edae8eb87cfe811ec338606288f81d333c07d2a9a0b9b976c0b", 757 | "sha256:ddefa9fd6794a87e37d05827d299d4b53a3ec6f23258101907b96029bfef138a", 758 | "sha256:e1061a4ab6681f6bdf72b110eea0c4e1379d57c9de937db3be4202f7ad6043db", 759 | "sha256:e1f7370b4e14cc03f5ae141ef30a1caf81fa5787711f80be9081418dd9eb79d2", 760 | "sha256:eb4254b1dac4a1ee49919a5b3f1caf9803ea8dada1816d9e8289e63d3cd0dd9a", 761 | "sha256:f7d564871833ddbe54df3aa59053b1110729d3a800cb7628ae8f42adb3d75208" 762 | ], 763 | "markers": "python_version >= '3.8'", 764 | "version": "==0.2.20" 765 | }, 766 | "nodeenv": { 767 | "hashes": [ 768 | "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", 769 | "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9" 770 | ], 771 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", 772 | "version": "==1.9.1" 773 | }, 774 | "packaging": { 775 | "hashes": [ 776 | "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", 777 | "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" 778 | ], 779 | "markers": "python_version >= '3.8'", 780 | "version": "==24.2" 781 | }, 782 | "platformdirs": { 783 | "hashes": [ 784 | "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", 785 | "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351" 786 | ], 787 | "markers": "python_version >= '3.9'", 788 | "version": "==4.3.7" 789 | }, 790 | "pluggy": { 791 | "hashes": [ 792 | "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", 793 | "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" 794 | ], 795 | "markers": "python_version >= '3.8'", 796 | "version": "==1.5.0" 797 | }, 798 | "pre-commit": { 799 | "hashes": [ 800 | "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", 801 | "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd" 802 | ], 803 | "index": "pypi", 804 | "markers": "python_version >= '3.9'", 805 | "version": "==4.2.0" 806 | }, 807 | "pycodestyle": { 808 | "hashes": [ 809 | "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9", 810 | "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae" 811 | ], 812 | "markers": "python_version >= '3.9'", 813 | "version": "==2.13.0" 814 | }, 815 | "pycparser": { 816 | "hashes": [ 817 | "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", 818 | "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" 819 | ], 820 | "markers": "python_version >= '3.8'", 821 | "version": "==2.22" 822 | }, 823 | "pyflakes": { 824 | "hashes": [ 825 | "sha256:8752eee11d4ef3a4be642d774863047864b47406cba906fabf8dd892cf98d5b3", 826 | "sha256:af4d63344d478524956e9950a9ae11da51414622479b8c148647fe9722e96837" 827 | ], 828 | "markers": "python_version >= '3.9'", 829 | "version": "==3.3.1" 830 | }, 831 | "pygments": { 832 | "hashes": [ 833 | "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", 834 | "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c" 835 | ], 836 | "markers": "python_version >= '3.8'", 837 | "version": "==2.19.1" 838 | }, 839 | "pyproject-hooks": { 840 | "hashes": [ 841 | "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", 842 | "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913" 843 | ], 844 | "markers": "python_version >= '3.7'", 845 | "version": "==1.2.0" 846 | }, 847 | "pytest": { 848 | "hashes": [ 849 | "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", 850 | "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845" 851 | ], 852 | "index": "pypi", 853 | "markers": "python_version >= '3.8'", 854 | "version": "==8.3.5" 855 | }, 856 | "pytest-mock": { 857 | "hashes": [ 858 | "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", 859 | "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0" 860 | ], 861 | "index": "pypi", 862 | "markers": "python_version >= '3.8'", 863 | "version": "==3.14.0" 864 | }, 865 | "pytest-mypy-plugins": { 866 | "hashes": [ 867 | "sha256:46e24e8d9eaeabcddd0a5dc5fb089c021903d5952e0c9d8af79383db99b9ffae", 868 | "sha256:68bd95400c8f128327acd9a16c737dbec18b20fced3184ad97f391b07d4662f4" 869 | ], 870 | "index": "pypi", 871 | "markers": "python_version >= '3.9'", 872 | "version": "==3.2.0" 873 | }, 874 | "pyyaml": { 875 | "hashes": [ 876 | "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", 877 | "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", 878 | "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", 879 | "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", 880 | "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", 881 | "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", 882 | "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", 883 | "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", 884 | "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", 885 | "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", 886 | "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", 887 | "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", 888 | "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", 889 | "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", 890 | "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", 891 | "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", 892 | "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", 893 | "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", 894 | "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", 895 | "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", 896 | "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", 897 | "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", 898 | "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", 899 | "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", 900 | "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", 901 | "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", 902 | "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", 903 | "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", 904 | "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", 905 | "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", 906 | "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", 907 | "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", 908 | "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", 909 | "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", 910 | "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", 911 | "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", 912 | "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", 913 | "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", 914 | "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", 915 | "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", 916 | "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", 917 | "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", 918 | "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", 919 | "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", 920 | "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", 921 | "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", 922 | "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", 923 | "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", 924 | "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", 925 | "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", 926 | "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", 927 | "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", 928 | "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" 929 | ], 930 | "markers": "python_version >= '3.8'", 931 | "version": "==6.0.2" 932 | }, 933 | "readme-renderer": { 934 | "hashes": [ 935 | "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", 936 | "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1" 937 | ], 938 | "markers": "python_version >= '3.9'", 939 | "version": "==44.0" 940 | }, 941 | "referencing": { 942 | "hashes": [ 943 | "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", 944 | "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0" 945 | ], 946 | "markers": "python_version >= '3.9'", 947 | "version": "==0.36.2" 948 | }, 949 | "regex": { 950 | "hashes": [ 951 | "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", 952 | "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", 953 | "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", 954 | "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", 955 | "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", 956 | "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773", 957 | "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", 958 | "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef", 959 | "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", 960 | "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", 961 | "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", 962 | "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", 963 | "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", 964 | "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", 965 | "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", 966 | "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", 967 | "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", 968 | "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", 969 | "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e", 970 | "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", 971 | "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", 972 | "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", 973 | "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0", 974 | "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", 975 | "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b", 976 | "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", 977 | "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd", 978 | "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57", 979 | "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", 980 | "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", 981 | "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f", 982 | "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b", 983 | "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", 984 | "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", 985 | "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", 986 | "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", 987 | "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b", 988 | "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839", 989 | "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", 990 | "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf", 991 | "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", 992 | "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", 993 | "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f", 994 | "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95", 995 | "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4", 996 | "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", 997 | "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13", 998 | "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", 999 | "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", 1000 | "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", 1001 | "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9", 1002 | "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc", 1003 | "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48", 1004 | "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", 1005 | "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", 1006 | "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", 1007 | "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", 1008 | "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b", 1009 | "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd", 1010 | "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", 1011 | "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", 1012 | "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", 1013 | "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", 1014 | "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", 1015 | "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3", 1016 | "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983", 1017 | "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", 1018 | "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", 1019 | "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", 1020 | "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", 1021 | "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467", 1022 | "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", 1023 | "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001", 1024 | "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", 1025 | "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", 1026 | "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", 1027 | "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf", 1028 | "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6", 1029 | "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", 1030 | "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", 1031 | "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", 1032 | "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df", 1033 | "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", 1034 | "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5", 1035 | "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", 1036 | "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2", 1037 | "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", 1038 | "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", 1039 | "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c", 1040 | "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f", 1041 | "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", 1042 | "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", 1043 | "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", 1044 | "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91" 1045 | ], 1046 | "markers": "python_version >= '3.8'", 1047 | "version": "==2024.11.6" 1048 | }, 1049 | "requests": { 1050 | "hashes": [ 1051 | "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", 1052 | "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" 1053 | ], 1054 | "index": "pypi", 1055 | "markers": "python_version >= '3.8'", 1056 | "version": "==2.32.3" 1057 | }, 1058 | "requests-toolbelt": { 1059 | "hashes": [ 1060 | "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", 1061 | "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" 1062 | ], 1063 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 1064 | "version": "==1.0.0" 1065 | }, 1066 | "rfc3986": { 1067 | "hashes": [ 1068 | "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", 1069 | "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" 1070 | ], 1071 | "markers": "python_version >= '3.7'", 1072 | "version": "==2.0.0" 1073 | }, 1074 | "rich": { 1075 | "hashes": [ 1076 | "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", 1077 | "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90" 1078 | ], 1079 | "markers": "python_full_version >= '3.8.0'", 1080 | "version": "==13.9.4" 1081 | }, 1082 | "rpds-py": { 1083 | "hashes": [ 1084 | "sha256:09cd7dbcb673eb60518231e02874df66ec1296c01a4fcd733875755c02014b19", 1085 | "sha256:0f3288930b947cbebe767f84cf618d2cbe0b13be476e749da0e6a009f986248c", 1086 | "sha256:0fced9fd4a07a1ded1bac7e961ddd9753dd5d8b755ba8e05acba54a21f5f1522", 1087 | "sha256:112b8774b0b4ee22368fec42749b94366bd9b536f8f74c3d4175d4395f5cbd31", 1088 | "sha256:11dd60b2ffddba85715d8a66bb39b95ddbe389ad2cfcf42c833f1bcde0878eaf", 1089 | "sha256:178f8a60fc24511c0eb756af741c476b87b610dba83270fce1e5a430204566a4", 1090 | "sha256:1b08027489ba8fedde72ddd233a5ea411b85a6ed78175f40285bd401bde7466d", 1091 | "sha256:1bf5be5ba34e19be579ae873da515a2836a2166d8d7ee43be6ff909eda42b72b", 1092 | "sha256:1ed7de3c86721b4e83ac440751329ec6a1102229aa18163f84c75b06b525ad7e", 1093 | "sha256:1eedaaccc9bb66581d4ae7c50e15856e335e57ef2734dbc5fd8ba3e2a4ab3cb6", 1094 | "sha256:243241c95174b5fb7204c04595852fe3943cc41f47aa14c3828bc18cd9d3b2d6", 1095 | "sha256:26bb3e8de93443d55e2e748e9fd87deb5f8075ca7bc0502cfc8be8687d69a2ec", 1096 | "sha256:271fa2184cf28bdded86bb6217c8e08d3a169fe0bbe9be5e8d96e8476b707122", 1097 | "sha256:28358c54fffadf0ae893f6c1050e8f8853e45df22483b7fff2f6ab6152f5d8bf", 1098 | "sha256:285019078537949cecd0190f3690a0b0125ff743d6a53dfeb7a4e6787af154f5", 1099 | "sha256:2893d778d4671ee627bac4037a075168b2673c57186fb1a57e993465dbd79a93", 1100 | "sha256:2a54027554ce9b129fc3d633c92fa33b30de9f08bc61b32c053dc9b537266fed", 1101 | "sha256:2c6ae11e6e93728d86aafc51ced98b1658a0080a7dd9417d24bfb955bb09c3c2", 1102 | "sha256:2cfa07c346a7ad07019c33fb9a63cf3acb1f5363c33bc73014e20d9fe8b01cdd", 1103 | "sha256:35d5631ce0af26318dba0ae0ac941c534453e42f569011585cb323b7774502a5", 1104 | "sha256:3614d280bf7aab0d3721b5ce0e73434acb90a2c993121b6e81a1c15c665298ac", 1105 | "sha256:3902df19540e9af4cc0c3ae75974c65d2c156b9257e91f5101a51f99136d834c", 1106 | "sha256:3aaf141d39f45322e44fc2c742e4b8b4098ead5317e5f884770c8df0c332da70", 1107 | "sha256:3d8abf7896a91fb97e7977d1aadfcc2c80415d6dc2f1d0fca5b8d0df247248f3", 1108 | "sha256:3e77febf227a1dc3220159355dba68faa13f8dca9335d97504abf428469fb18b", 1109 | "sha256:3e9212f52074fc9d72cf242a84063787ab8e21e0950d4d6709886fb62bcb91d5", 1110 | "sha256:3ee9d6f0b38efb22ad94c3b68ffebe4c47865cdf4b17f6806d6c674e1feb4246", 1111 | "sha256:4233df01a250b3984465faed12ad472f035b7cd5240ea3f7c76b7a7016084495", 1112 | "sha256:4263320ed887ed843f85beba67f8b2d1483b5947f2dc73a8b068924558bfeace", 1113 | "sha256:4ab923167cfd945abb9b51a407407cf19f5bee35001221f2911dc85ffd35ff4f", 1114 | "sha256:4caafd1a22e5eaa3732acb7672a497123354bef79a9d7ceed43387d25025e935", 1115 | "sha256:50fb62f8d8364978478b12d5f03bf028c6bc2af04082479299139dc26edf4c64", 1116 | "sha256:55ff4151cfd4bc635e51cfb1c59ac9f7196b256b12e3a57deb9e5742e65941ad", 1117 | "sha256:5b98b6c953e5c2bda51ab4d5b4f172617d462eebc7f4bfdc7c7e6b423f6da957", 1118 | "sha256:5c9ff044eb07c8468594d12602291c635da292308c8c619244e30698e7fc455a", 1119 | "sha256:5e9c206a1abc27e0588cf8b7c8246e51f1a16a103734f7750830a1ccb63f557a", 1120 | "sha256:5fb89edee2fa237584e532fbf78f0ddd1e49a47c7c8cfa153ab4849dc72a35e6", 1121 | "sha256:633462ef7e61d839171bf206551d5ab42b30b71cac8f10a64a662536e057fdef", 1122 | "sha256:66f8d2a17e5838dd6fb9be6baaba8e75ae2f5fa6b6b755d597184bfcd3cb0eba", 1123 | "sha256:6959bb9928c5c999aba4a3f5a6799d571ddc2c59ff49917ecf55be2bbb4e3722", 1124 | "sha256:698a79d295626ee292d1730bc2ef6e70a3ab135b1d79ada8fde3ed0047b65a10", 1125 | "sha256:721f9c4011b443b6e84505fc00cc7aadc9d1743f1c988e4c89353e19c4a968ee", 1126 | "sha256:72e680c1518733b73c994361e4b06441b92e973ef7d9449feec72e8ee4f713da", 1127 | "sha256:75307599f0d25bf6937248e5ac4e3bde5ea72ae6618623b86146ccc7845ed00b", 1128 | "sha256:754fba3084b70162a6b91efceee8a3f06b19e43dac3f71841662053c0584209a", 1129 | "sha256:759462b2d0aa5a04be5b3e37fb8183615f47014ae6b116e17036b131985cb731", 1130 | "sha256:7938c7b0599a05246d704b3f5e01be91a93b411d0d6cc62275f025293b8a11ce", 1131 | "sha256:7b77e07233925bd33fc0022b8537774423e4c6680b6436316c5075e79b6384f4", 1132 | "sha256:7e5413d2e2d86025e73f05510ad23dad5950ab8417b7fc6beaad99be8077138b", 1133 | "sha256:7f3240dcfa14d198dba24b8b9cb3b108c06b68d45b7babd9eefc1038fdf7e707", 1134 | "sha256:7f9682a8f71acdf59fd554b82b1c12f517118ee72c0f3944eda461606dfe7eb9", 1135 | "sha256:8d67beb6002441faef8251c45e24994de32c4c8686f7356a1f601ad7c466f7c3", 1136 | "sha256:9441af1d25aed96901f97ad83d5c3e35e6cd21a25ca5e4916c82d7dd0490a4fa", 1137 | "sha256:98b257ae1e83f81fb947a363a274c4eb66640212516becaff7bef09a5dceacaa", 1138 | "sha256:9e9f3a3ac919406bc0414bbbd76c6af99253c507150191ea79fab42fdb35982a", 1139 | "sha256:a1c66e71ecfd2a4acf0e4bd75e7a3605afa8f9b28a3b497e4ba962719df2be57", 1140 | "sha256:a1e17d8dc8e57d8e0fd21f8f0f0a5211b3fa258b2e444c2053471ef93fe25a00", 1141 | "sha256:a20cb698c4a59c534c6701b1c24a968ff2768b18ea2991f886bd8985ce17a89f", 1142 | "sha256:a970bfaf130c29a679b1d0a6e0f867483cea455ab1535fb427566a475078f27f", 1143 | "sha256:a98f510d86f689fcb486dc59e6e363af04151e5260ad1bdddb5625c10f1e95f8", 1144 | "sha256:a9d3b728f5a5873d84cba997b9d617c6090ca5721caaa691f3b1a78c60adc057", 1145 | "sha256:ad76f44f70aac3a54ceb1813ca630c53415da3a24fd93c570b2dfb4856591017", 1146 | "sha256:ae28144c1daa61366205d32abd8c90372790ff79fc60c1a8ad7fd3c8553a600e", 1147 | "sha256:b03a8d50b137ee758e4c73638b10747b7c39988eb8e6cd11abb7084266455165", 1148 | "sha256:b5a96fcac2f18e5a0a23a75cd27ce2656c66c11c127b0318e508aab436b77428", 1149 | "sha256:b5ef909a37e9738d146519657a1aab4584018746a18f71c692f2f22168ece40c", 1150 | "sha256:b79f5ced71efd70414a9a80bbbfaa7160da307723166f09b69773153bf17c590", 1151 | "sha256:b91cceb5add79ee563bd1f70b30896bd63bc5f78a11c1f00a1e931729ca4f1f4", 1152 | "sha256:b92f5654157de1379c509b15acec9d12ecf6e3bc1996571b6cb82a4302060447", 1153 | "sha256:c04ca91dda8a61584165825907f5c967ca09e9c65fe8966ee753a3f2b019fe1e", 1154 | "sha256:c1f8afa346ccd59e4e5630d5abb67aba6a9812fddf764fd7eb11f382a345f8cc", 1155 | "sha256:c5334a71f7dc1160382d45997e29f2637c02f8a26af41073189d79b95d3321f1", 1156 | "sha256:c617d7453a80e29d9973b926983b1e700a9377dbe021faa36041c78537d7b08c", 1157 | "sha256:c632419c3870507ca20a37c8f8f5352317aca097639e524ad129f58c125c61c6", 1158 | "sha256:c6760211eee3a76316cf328f5a8bd695b47b1626d21c8a27fb3b2473a884d597", 1159 | "sha256:c698d123ce5d8f2d0cd17f73336615f6a2e3bdcedac07a1291bb4d8e7d82a05a", 1160 | "sha256:c76b32eb2ab650a29e423525e84eb197c45504b1c1e6e17b6cc91fcfeb1a4b1d", 1161 | "sha256:c8f7e90b948dc9dcfff8003f1ea3af08b29c062f681c05fd798e36daa3f7e3e8", 1162 | "sha256:c9e799dac1ffbe7b10c1fd42fe4cd51371a549c6e108249bde9cd1200e8f59b4", 1163 | "sha256:cafa48f2133d4daa028473ede7d81cd1b9f9e6925e9e4003ebdf77010ee02f35", 1164 | "sha256:ce473a2351c018b06dd8d30d5da8ab5a0831056cc53b2006e2a8028172c37ce5", 1165 | "sha256:d31ed4987d72aabdf521eddfb6a72988703c091cfc0064330b9e5f8d6a042ff5", 1166 | "sha256:d550d7e9e7d8676b183b37d65b5cd8de13676a738973d330b59dc8312df9c5dc", 1167 | "sha256:d6adb81564af0cd428910f83fa7da46ce9ad47c56c0b22b50872bc4515d91966", 1168 | "sha256:d6f6512a90bd5cd9030a6237f5346f046c6f0e40af98657568fa45695d4de59d", 1169 | "sha256:d7031d493c4465dbc8d40bd6cafefef4bd472b17db0ab94c53e7909ee781b9ef", 1170 | "sha256:d9f75a06ecc68f159d5d7603b734e1ff6daa9497a929150f794013aa9f6e3f12", 1171 | "sha256:db7707dde9143a67b8812c7e66aeb2d843fe33cc8e374170f4d2c50bd8f2472d", 1172 | "sha256:e0397dd0b3955c61ef9b22838144aa4bef6f0796ba5cc8edfc64d468b93798b4", 1173 | "sha256:e0df046f2266e8586cf09d00588302a32923eb6386ced0ca5c9deade6af9a149", 1174 | "sha256:e14f86b871ea74c3fddc9a40e947d6a5d09def5adc2076ee61fb910a9014fb35", 1175 | "sha256:e5963ea87f88bddf7edd59644a35a0feecf75f8985430124c253612d4f7d27ae", 1176 | "sha256:e768267cbe051dd8d1c5305ba690bb153204a09bf2e3de3ae530de955f5b5580", 1177 | "sha256:e9cb79ecedfc156c0692257ac7ed415243b6c35dd969baa461a6888fc79f2f07", 1178 | "sha256:ed6f011bedca8585787e5082cce081bac3d30f54520097b2411351b3574e1219", 1179 | "sha256:f3429fb8e15b20961efca8c8b21432623d85db2228cc73fe22756c6637aa39e7", 1180 | "sha256:f35eff113ad430b5272bbfc18ba111c66ff525828f24898b4e146eb479a2cdda", 1181 | "sha256:f3a6cb95074777f1ecda2ca4fa7717caa9ee6e534f42b7575a8f0d4cb0c24013", 1182 | "sha256:f7356a6da0562190558c4fcc14f0281db191cdf4cb96e7604c06acfcee96df15", 1183 | "sha256:f88626e3f5e57432e6191cd0c5d6d6b319b635e70b40be2ffba713053e5147dd", 1184 | "sha256:fad784a31869747df4ac968a351e070c06ca377549e4ace94775aaa3ab33ee06", 1185 | "sha256:fc869af5cba24d45fb0399b0cfdbcefcf6910bf4dee5d74036a57cf5264b3ff4", 1186 | "sha256:fee513135b5a58f3bb6d89e48326cd5aa308e4bcdf2f7d59f67c861ada482bf8" 1187 | ], 1188 | "markers": "python_version >= '3.9'", 1189 | "version": "==0.23.1" 1190 | }, 1191 | "secretstorage": { 1192 | "hashes": [ 1193 | "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", 1194 | "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99" 1195 | ], 1196 | "markers": "sys_platform == 'linux'", 1197 | "version": "==3.3.3" 1198 | }, 1199 | "snowballstemmer": { 1200 | "hashes": [ 1201 | "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", 1202 | "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" 1203 | ], 1204 | "version": "==2.2.0" 1205 | }, 1206 | "sphinx": { 1207 | "hashes": [ 1208 | "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", 1209 | "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927" 1210 | ], 1211 | "index": "pypi", 1212 | "markers": "python_version >= '3.10'", 1213 | "version": "==8.1.3" 1214 | }, 1215 | "sphinx-rtd-theme": { 1216 | "hashes": [ 1217 | "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13", 1218 | "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85" 1219 | ], 1220 | "index": "pypi", 1221 | "markers": "python_version >= '3.8'", 1222 | "version": "==3.0.2" 1223 | }, 1224 | "sphinxcontrib-applehelp": { 1225 | "hashes": [ 1226 | "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", 1227 | "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5" 1228 | ], 1229 | "markers": "python_version >= '3.9'", 1230 | "version": "==2.0.0" 1231 | }, 1232 | "sphinxcontrib-devhelp": { 1233 | "hashes": [ 1234 | "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", 1235 | "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2" 1236 | ], 1237 | "markers": "python_version >= '3.9'", 1238 | "version": "==2.0.0" 1239 | }, 1240 | "sphinxcontrib-htmlhelp": { 1241 | "hashes": [ 1242 | "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", 1243 | "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9" 1244 | ], 1245 | "markers": "python_version >= '3.9'", 1246 | "version": "==2.1.0" 1247 | }, 1248 | "sphinxcontrib-jquery": { 1249 | "hashes": [ 1250 | "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", 1251 | "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae" 1252 | ], 1253 | "markers": "python_version >= '2.7'", 1254 | "version": "==4.1" 1255 | }, 1256 | "sphinxcontrib-jsmath": { 1257 | "hashes": [ 1258 | "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", 1259 | "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" 1260 | ], 1261 | "markers": "python_version >= '3.5'", 1262 | "version": "==1.0.1" 1263 | }, 1264 | "sphinxcontrib-qthelp": { 1265 | "hashes": [ 1266 | "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", 1267 | "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb" 1268 | ], 1269 | "markers": "python_version >= '3.9'", 1270 | "version": "==2.0.0" 1271 | }, 1272 | "sphinxcontrib-serializinghtml": { 1273 | "hashes": [ 1274 | "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", 1275 | "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d" 1276 | ], 1277 | "markers": "python_version >= '3.9'", 1278 | "version": "==2.0.0" 1279 | }, 1280 | "tomli": { 1281 | "hashes": [ 1282 | "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", 1283 | "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", 1284 | "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", 1285 | "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", 1286 | "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", 1287 | "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", 1288 | "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", 1289 | "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", 1290 | "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", 1291 | "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", 1292 | "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", 1293 | "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", 1294 | "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", 1295 | "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", 1296 | "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", 1297 | "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", 1298 | "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", 1299 | "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", 1300 | "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", 1301 | "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", 1302 | "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", 1303 | "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", 1304 | "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", 1305 | "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", 1306 | "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", 1307 | "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", 1308 | "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", 1309 | "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", 1310 | "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", 1311 | "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", 1312 | "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", 1313 | "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7" 1314 | ], 1315 | "markers": "python_version < '3.11'", 1316 | "version": "==2.2.1" 1317 | }, 1318 | "tomlkit": { 1319 | "hashes": [ 1320 | "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", 1321 | "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79" 1322 | ], 1323 | "markers": "python_version >= '3.8'", 1324 | "version": "==0.13.2" 1325 | }, 1326 | "twine": { 1327 | "hashes": [ 1328 | "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384", 1329 | "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd" 1330 | ], 1331 | "index": "pypi", 1332 | "markers": "python_version >= '3.8'", 1333 | "version": "==6.1.0" 1334 | }, 1335 | "types-requests": { 1336 | "hashes": [ 1337 | "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2", 1338 | "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32" 1339 | ], 1340 | "index": "pypi", 1341 | "markers": "python_version >= '3.9'", 1342 | "version": "==2.32.0.20250328" 1343 | }, 1344 | "types-urllib3": { 1345 | "hashes": [ 1346 | "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f", 1347 | "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e" 1348 | ], 1349 | "index": "pypi", 1350 | "version": "==1.26.25.14" 1351 | }, 1352 | "typing-extensions": { 1353 | "hashes": [ 1354 | "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", 1355 | "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" 1356 | ], 1357 | "markers": "python_version >= '3.8'", 1358 | "version": "==4.12.2" 1359 | }, 1360 | "urllib3": { 1361 | "hashes": [ 1362 | "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", 1363 | "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813" 1364 | ], 1365 | "markers": "python_version >= '3.9'", 1366 | "version": "==2.4.0" 1367 | }, 1368 | "virtualenv": { 1369 | "hashes": [ 1370 | "sha256:3e3d00f5807e83b234dfb6122bf37cfadf4be216c53a49ac059d02414f819170", 1371 | "sha256:95e39403fcf3940ac45bc717597dba16110b74506131845d9b687d5e73d947ac" 1372 | ], 1373 | "markers": "python_version >= '3.8'", 1374 | "version": "==20.29.3" 1375 | }, 1376 | "zipp": { 1377 | "hashes": [ 1378 | "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", 1379 | "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931" 1380 | ], 1381 | "markers": "python_version >= '3.9'", 1382 | "version": "==3.21.0" 1383 | } 1384 | } 1385 | } 1386 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tiny API Client 🐝 2 | 3 | [![License: GPL v2][license-shield]][gnu] 4 | 5 | Write JSON API Clients in Python without the fluff, pumped full of 6 | syntactic sugar 7 | 8 | ```python 9 | from tiny_api_client import api_client, get, post, delete 10 | 11 | @api_client('https://example.org/api/public/v{version}') 12 | class MyAPIClient: 13 | @get('/users/{user_id}') 14 | def find_user(self, response): 15 | return response 16 | 17 | @post('/notes') 18 | def create_note(self, response): 19 | return response 20 | 21 | @delete('/notes/{note_id}/attachment/{attachment_id}', version=3) 22 | def delete_note_attachment(self, response): 23 | return response 24 | 25 | >>> client = MyClient() 26 | >>> client.find_user(user_id='PeterParker') 27 | {'name': 'Peter', 'surname': 'Parker', ...} 28 | >>> client.create_note(data={'title': 'New Note', 'content': 'Hello World!'}) 29 | {'id': ...} 30 | >>> client.delete_note_attachment(node_id=...) 31 | ``` 32 | 33 | 34 | 35 | ## Features 36 | 37 | - Instance-scoped `requests.Session()` with connection pooling and 38 | cookie preservation 39 | - JSON is king, but XML and raw responses are fine too 40 | - Endpoints can use GET, POST, PUT, PATCH, DELETE 41 | - Route parameters are optional 42 | - Easy integration with your custom API classes 43 | - Declare endpoints under different API versions 44 | - Can define the API URL at runtime if not available before 45 | - Can set a custom CookieJar to pass with all requests 46 | - Pass along any parameters you would usually pass to requests 47 | - Custom JSON status error handling 48 | - Installable [pytest plugin][pytest-plugin] for easy testing 49 | - Excellent support for type checking thanks to a built-in mypy plugin 50 | 51 | 52 | 53 | ## Installation 54 | 55 | ```bash 56 | pip install tiny-api-client 57 | ``` 58 | 59 | 60 | 61 | ## Documentation 62 | 63 | You can find the documentation at 64 | https://tiny-api-client.readthedocs.io 65 | 66 | 67 | 68 | ## License 69 | 70 | [![License: LGPL v2.1][license-shield]][gnu] 71 | 72 | This software is distributed under the 73 | [Lesser General Public License v2.1][license], 74 | more information available at the [Free Software Foundation][gnu]. 75 | 76 | 77 | 78 | 79 | 80 | [pytest-plugin]: https://github.com/sanjacob/pytest-tiny-api-client 81 | 82 | 83 | 84 | 85 | 86 | [license]: LICENSE "Lesser General Public License v2.1" 87 | [gnu]: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html "Free Software Foundation" 88 | [license-shield]: https://img.shields.io/github/license/sanjacob/tiny-api-client 89 | 90 | 91 | 92 | 93 | 94 | [pypi]: https://pypi.org/project/tiny-api-client 95 | 96 | 97 | 98 | 99 | 100 | [pypi-shield]: https://img.shields.io/pypi/v/tiny-api-client 101 | [build-shield]: https://img.shields.io/github/actions/workflow/status/sanjacob/tiny-api-client/build.yml?branch=master 102 | [docs-shield]: https://img.shields.io/readthedocs/tiny-api-client 103 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/api_reference.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. automodule:: tiny_api_client 5 | :members: 6 | 7 | Exceptions 8 | ---------- 9 | 10 | .. autoclass:: tiny_api_client.APIClientError 11 | :members: 12 | 13 | .. autoclass:: tiny_api_client.APIEmptyResponseError 14 | :members: 15 | 16 | .. autoclass:: tiny_api_client.APIStatusError 17 | :members: 18 | 19 | .. autoclass:: tiny_api_client.APINoURLError 20 | :members: 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | import os 10 | import sys 11 | sys.path.insert(0, os.path.abspath('..')) 12 | 13 | project = 'Tiny API Client' 14 | copyright = '2023, Jacob S.P.' 15 | author = 'Jacob S.P.' 16 | 17 | # -- General configuration --------------------------------------------------- 18 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 19 | 20 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] 21 | intersphinx_mapping = { 22 | 'python': ('https://docs.python.org/3', None), 23 | 'pydantic': ('https://docs.pydantic.dev/latest', None), 24 | 'requests': ('https://requests.readthedocs.io/en/stable', None), 25 | 'urllib3': ('https://urllib3.readthedocs.io/en/stable', None), 26 | } 27 | 28 | templates_path = ['_templates'] 29 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 30 | 31 | 32 | # -- Options for HTML output ------------------------------------------------- 33 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 34 | 35 | html_theme = 'sphinx_rtd_theme' 36 | html_static_path = ['_static'] 37 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Tiny API Client documentation master file, created by 2 | sphinx-quickstart on Mon Nov 20 16:04:27 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 🐝 Tiny API Client 7 | ================== 8 | 9 | The short and sweet way to create an API client 10 | ----------------------------------------------- 11 | 12 | 13 | :: 14 | 15 | from tiny_api_client import api_client, get 16 | 17 | @api_client("https://example.org/api/v{version}", timeout=10) 18 | class MyClient: 19 | @get("/posts/{post_id}", version=2) 20 | def get_posts(self, response): 21 | return response 22 | 23 | >>> client = MyClient() 24 | >>> client.get_posts() # route parameters are optional 25 | 26 | 27 | Tiny API Client is a wrapper for `requests` that enables you to succintly write API clients 28 | without much effort. Calls on each instance of a client class will share a `requests.Session` 29 | with cookie preservation and improved performance due to request pooling. 30 | 31 | To get started, see the :ref:`basics` first. 32 | 33 | To learn how to test your own API clients, see the :doc:`testing`. 34 | 35 | To learn how to use the mypy integration, see the :doc:`typing` section. 36 | 37 | .. toctree:: 38 | :maxdepth: 2 39 | :caption: Contents: 40 | 41 | quick 42 | testing 43 | typing 44 | api_reference 45 | 46 | 47 | 48 | Indices and tables 49 | ================== 50 | 51 | * :ref:`genindex` 52 | * :ref:`modindex` 53 | * :ref:`search` 54 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/quick.rst: -------------------------------------------------------------------------------- 1 | .. py:currentmodule:: tiny_api_client 2 | 3 | Quick Guide 4 | =========== 5 | 6 | 7 | Installation 8 | ------------ 9 | 10 | :: 11 | 12 | pip install tiny-api-client 13 | 14 | .. _basics: 15 | 16 | 17 | Basics 18 | ------ 19 | 20 | To begin, import the class decorator, and any http methods you will use 21 | 22 | :: 23 | 24 | from tiny_api_client import api_client, get 25 | 26 | 27 | Then, create a class for your very own API client 28 | 29 | :: 30 | 31 | @api_client("https://example.org/api") 32 | class MyAPIClient: 33 | ... 34 | 35 | Finally, declare your endpoints one by one, using one of the valid HTTP methods 36 | 37 | :: 38 | 39 | @get('/profile/{user_id}/comments/{comment_id}') 40 | def fetch_comments(self, response): 41 | return response 42 | 43 | That's it, you are done creating your API client 44 | 45 | :: 46 | 47 | client = MyAPIClient() 48 | client.fetch_comments(user_id='me') # parameters are optional 49 | [{'id': '001', 'content': 'This is my first comment'}, ...] 50 | 51 | client.fetch_comments(user_id='me', comment_id='001') 52 | {'id': '001', 'content': 'This is my first comment'} 53 | 54 | 55 | In its entirety, the client looks like this, short and sweet 56 | 57 | :: 58 | 59 | from tiny_api_client import api_client, get 60 | 61 | @api_client("https://example.org/api") 62 | class MyAPIClient: 63 | @get('/profile/{user_id}/comments/{comment_id}') 64 | def fetch_comments(self, response): 65 | return response 66 | 67 | 68 | To pass along a request body, do so as you would normally when calling 69 | :py:func:`requests.post`. 70 | 71 | :: 72 | 73 | >>> client.create_comment(data={...}) 74 | >>> client.create_comment(json={...}) 75 | 76 | 77 | You can either return the JSON response directly as seen before, 78 | or use custom classes to parse and structure the API responses 79 | (for example, with :py:mod:`pydantic`) 80 | 81 | :: 82 | 83 | from pydantic import BaseModel 84 | 85 | class Kitten(BaseModel): 86 | ... 87 | 88 | @api_client('https://example.org/api') 89 | class KittenAPIClient: 90 | @get("/kitten/{kitten_name}") 91 | def find_kitten(self, response) -> list[Kitten] | Kitten: 92 | if isinstance(response, list): 93 | return [Kitten(**item) for item in response] 94 | else: 95 | return Kitten(**response) 96 | 97 | 98 | Advanced 99 | -------- 100 | 101 | - Retry mechanism for network errors 102 | 103 | You can directly set a retry policy which will be passed to the 104 | :py:class:`~requests.adapters.HTTPAdapter` used by the client 105 | :py:class:`~requests.Session`. 106 | For this, use either a number of attempts or a 107 | :py:class:`urllib3.util.retry.Retry` instance. 108 | 109 | :: 110 | 111 | @api_client('https://example.org/api', max_retries=5) 112 | class MyClient: 113 | ... 114 | 115 | from urllib3.util.retry import Retry 116 | policy = Retry(total=5, redirect=2) 117 | 118 | @api_client('https://example.org/api', max_retries=policy) 119 | class MyClient: 120 | ... 121 | 122 | 123 | - Handle non-JSON data and streams 124 | 125 | The library will call `.json()` on the server response for you by default. 126 | But you can also turn this off on an endpoint basis 127 | 128 | 129 | :: 130 | 131 | @get("/comments/{comment_id}", json=False) 132 | def fetch_comment(self, response): 133 | return response.text() 134 | 135 | >>> client.fetch_comment(comment_id=...) 136 | A plaintext HTTP response 137 | 138 | 139 | - Parse XML response 140 | 141 | If one of your endpoints is still using XML you can let the library parse 142 | the response for you with :py:mod:`xml.etree.ElementTree`. 143 | Note that as with JSON parsing, you must handle any errors produced from this. 144 | 145 | :: 146 | 147 | @get("/xml/comments/{comment_id}", json=False, xml=True) 148 | def fetch_xml_comment(self, response): 149 | return response 150 | 151 | 152 | - Custom *requests* parameters 153 | 154 | Any keyword parameters included in either the endpoint declaration or the call to it will be passed to requests when called. 155 | 156 | :: 157 | 158 | @get("/file/{file_hash}", json=False, stream=True) # in endpoint declaration 159 | def download_file(self, response): 160 | for chunk in r.iter_content(chunk_size=1024): 161 | # Handle file content 162 | 163 | 164 | >>> client.download_file(file_hash='...', auth=..., headers=...) # passed at runtime 165 | 166 | For the full list of accepted parameters, see the `requests`_ documentation. 167 | 168 | .. _requests: https://requests.readthedocs.io/en/latest/api/#requests.request 169 | 170 | 171 | - Dynamic API URL 172 | 173 | Don't know the URL at import time? No problem, define a `_url` member at runtime instead. 174 | 175 | .. note:: 176 | 177 | Please do not use a `@property` for this 178 | 179 | :: 180 | 181 | @api_client() 182 | class ContinentAPIClient: 183 | def __init__(api_url: str): 184 | self._url = api_url 185 | 186 | @get("/countries") 187 | def fetch_countries(self, response): 188 | return response 189 | 190 | 191 | >>> africa = ContinentAPIClient("https://africa.example.org/api") 192 | >>> europe = ContinentAPIClient("https://europe.example.org/api") 193 | 194 | This technique is useful in situations where there is a common API with different 195 | instances hosted independently, and you don't know beforehand which instance you 196 | are connecting to. 197 | 198 | 199 | - Pass arguments to the endpoint handler 200 | 201 | Any positional parameters will be passed to the response handler, which can 202 | aid in post-request validation or parsing, if desired. 203 | 204 | :: 205 | 206 | @get('/photos/{photo_id}') 207 | def fetch_photo(self, response, expected_format): 208 | if response['format'] != expected_format: 209 | raise ValueError() 210 | 211 | >>> client.fetch_photo('jpeg', photo_id='PHOTO_001') 212 | 213 | 214 | - Unpack results from response dict 215 | 216 | If the server responds with the result inside a dictionary, you can directly retrieve the result instead 217 | 218 | :: 219 | 220 | @get("/quotes/{quote_id}", results_key='results') 221 | def fetch_quotes(self, response) -> list[str]: 222 | return response 223 | 224 | >>> client.fetch_quote(quote_id=...) # Server response: {'results': ['An apple a day...', ...]} 225 | ['An apple a day...', ...] 226 | 227 | 228 | - Include an optional `{version}` placeholder on an endpoint basis 229 | 230 | :: 231 | 232 | @api_client('https://example.org/api/public/v{version}') 233 | class MyAPIClient: 234 | @get('/users/{user_id}', version=3): # will call https://example.org/api/public/v3/users/{user_id} 235 | ... 236 | 237 | 238 | Error Handling 239 | -------------- 240 | 241 | Exceptions 242 | ^^^^^^^^^^ 243 | 244 | The library can throw :py:exc:`APIEmptyResponseError` and 245 | :py:exc:`APIStatusError`, both of which are subclassed from 246 | :py:exc:`APIClientError`. 247 | Independent of this, it will not catch any error thrown by requests or the 248 | conversion of the response to JSON, so you will need to decide on a strategy 249 | to handle such errors. 250 | 251 | :: 252 | 253 | from tiny_api_client import APIEmptyResponseError, APIStatusError 254 | from requests import RequestException 255 | from json import JSONDecodeError 256 | 257 | try: 258 | client.fetch_users() 259 | except APIEmptyResponseError: 260 | print("The API returned an empty string") 261 | except APIStatusError: 262 | print("The JSON response contained a status code") 263 | except RequestException: 264 | print("The request could not be completed") 265 | except JSONDecodeError: 266 | print("The server response could not be parsed into JSON") 267 | 268 | Status Codes 269 | ^^^^^^^^^^^^ 270 | 271 | If your API can return an error code in the JSON response itself, the library 272 | can make use of this. You can either declare an error handler, or let the library 273 | throw an :py:exc:`APIStatusError`. 274 | 275 | .. note:: 276 | 277 | `status_handler` is called with three arguments: 278 | the client instance, the status code, and the entire 279 | `response.json()` object. 280 | 281 | :: 282 | 283 | def my_handler(client, error_code, response): 284 | raise ValueError(error_code) 285 | 286 | 287 | @api_client('https://example.org', status_key='status', 288 | status_handler=my_handler) 289 | class MyClient: 290 | ... 291 | 292 | >>> client = MyClient() 293 | >>> client.fetch_profile() # Server response: {'status': '404'} 294 | Traceback (most recent call last): 295 | File "", line 1, in 296 | ValueError(404) 297 | 298 | 299 | Session/Cookies 300 | --------------- 301 | 302 | - Define a `_cookies` property and all requests will include this cookie jar 303 | 304 | :: 305 | 306 | from http.cookiejar import CookieJar 307 | 308 | @api_client('https://example.org') 309 | class MyAPIClient: 310 | def __init__(self, cookies: CookieJar | dict): 311 | self._cookies = cookies 312 | 313 | 314 | .. note:: 315 | 316 | Please do not use a `@property` for this 317 | 318 | 319 | .. deprecated:: 1.1.0 320 | 321 | self._session (which served the same purpose) is deprecated 322 | 323 | - Make a request to a different server 324 | 325 | There might come a time when you wish to make a request to a different server 326 | within the same session, without implementing your own logic 327 | 328 | :: 329 | 330 | @get("{external_url}", use_api=False) 331 | def fetch_external_resource(self, response): 332 | return response 333 | 334 | >>> client.fetch_external_resource(external_url="https://example.org/api/...") 335 | 336 | 337 | Reserved Names 338 | -------------- 339 | 340 | The following are meant to be set by the developer if needed 341 | 342 | - `self._cookies` 343 | - `self._url` 344 | 345 | .. deprecated:: 1.1.0 346 | 347 | self._session 348 | 349 | 350 | Tiny API Client reserves the use of the following member names, where * is a wildcard. 351 | 352 | - `self.__client_*`: For client instance attributes 353 | - `self.__api_*`: For class wide client attributes 354 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | -i https://pypi.org/simple 2 | recommonmark==0.7.1 3 | sphinx==8.1.3; python_version >= '3.9' 4 | sphinx-rtd-theme==1.3.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' 5 | requests 6 | typing-extensions 7 | -------------------------------------------------------------------------------- /docs/testing.rst: -------------------------------------------------------------------------------- 1 | Testing Guide 2 | ============= 3 | 4 | Testing an API client written with ``tiny-api-client`` is not straightforward out of the box. 5 | The reason for this is that decorators cannot be patched after the functions they decorate 6 | have been imported. To remediate this issue, a companion pytest plugin is available. 7 | 8 | Installation 9 | ------------ 10 | 11 | :: 12 | 13 | pip install pytest-tiny-api-client 14 | 15 | Testing Fixture 16 | --------------- 17 | 18 | This plugin exposes a pytest fixture ``api_call``, which replaces 19 | the usual implementation of the api decorators. 20 | Thus, your api endpoints will receive the return value of this fixture as 21 | their response argument, instead of the actual api result. 22 | 23 | The fixture is an instance of ``unittest.mock.Mock``. 24 | This mock accepts an *endpoint* argument, and arbitrary keyword arguments, which 25 | are passed directly from the method decorator of your endpoint definition. 26 | Thus, you can use the `mock`_ assertions to check that your endpoint is passing 27 | the right arguments to the decorator. 28 | 29 | .. _mock: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock 30 | 31 | 32 | Usage 33 | ----- 34 | 35 | :: 36 | 37 | from my_api import MyClient 38 | 39 | def test_my_client(api_call): 40 | # set your fake api response 41 | api_call.return_value = [{"id": 0, "name": "Mary Jane"}, ...] 42 | # make your calls 43 | client = MyClient() 44 | users = client.fetch_users() 45 | # make assertions 46 | assert users[0].name == "Mary Jane" 47 | 48 | 49 | Using a Context Manager 50 | ----------------------- 51 | 52 | In some circumstances it might not be possible to use a function-scoped fixture. 53 | One example of this is when using the property-based testing library ``hypothesis``. 54 | It is still possible to use a context manager to temporarily patch the api call. 55 | 56 | :: 57 | 58 | from unittest.mock import patch 59 | 60 | def test_my_client(): 61 | with patch('pytest_tiny_api_client._api_call') as api_call: 62 | ... 63 | 64 | 65 | Not Using Pytest 66 | ---------------- 67 | 68 | If you are not using pytest, or you don't want to use the plugin, all you have to 69 | do is to patch ``tiny_api_client.{method}`` where method is one of the http method 70 | decorators. This patch must occur before your api module is imported. 71 | Another option is to call ``importlib.reload(module)`` where module is your api module. 72 | 73 | For more information on how to do this, check this `stackoverflow`_ question. 74 | By far easier, though, is to see how this plugin is implemented, and replicate this 75 | behaviour in your own project. 76 | 77 | .. _stackoverflow: https://stackoverflow.com/questions/7667567/can-i-patch-a-python-decorator-before-it-wraps-a-function 78 | -------------------------------------------------------------------------------- /docs/typing.rst: -------------------------------------------------------------------------------- 1 | Type Checking 2 | ============= 3 | 4 | Due to the way route parameters are declared with tiny-api-client, 5 | type checking them would be impossible for tools like *mypy* without 6 | knowledge of how this library works. 7 | Therefore, it was necessary to go beyond ordinary type annotations. 8 | 9 | 10 | mypy Plugin 11 | ----------- 12 | 13 | As of version ``>=1.2.1``, the library ships with a mypy plugin that 14 | can parse positional route parameters declared in your endpoints and 15 | treat them as keyword-only optional string parameters. 16 | 17 | Therefore, errors like this will be highlighted: 18 | 19 | :: 20 | 21 | @get('/users/{user_id}/') 22 | def find_user(self, response: dict[str, str]) -> User: 23 | ... 24 | 25 | client.find_user(user_name="username") 26 | 27 | >>> error: Unexpected keyword argument "user_name" for "find_user" 28 | of "MyClient" [call-arg] 29 | 30 | .. note:: 31 | The mypy plugin is still in early development, and may not have all 32 | expected features. Additional requests parameters in endpoint calls 33 | are not supported yet. 34 | 35 | **Required and optional parameters** 36 | 37 | By design, only the last positional parameter can be optional. For instance: 38 | 39 | :: 40 | 41 | @get('/users/{user_id}/comments/{comment_id}') 42 | def get_comment(self, response: dict[str, str]) -> Comment: 43 | ... 44 | 45 | client.get_comment(comment_id="...") # forgot to add user id 46 | 47 | >>> error: Missing named argument... 48 | 49 | .. note:: 50 | If you want to leave the starting parameters empty, you will have to 51 | explicitly pass an empty string. 52 | 53 | The parameter can only be marked optional if the route ends on that 54 | parameter. This implies the route cannot end in a slash, for instance. 55 | The endpoint below has no optional route parameters. 56 | 57 | :: 58 | 59 | @get('/users/{user_id}/comments/{comment_id}/likes') 60 | def get_likes(self, response: int) -> int: 61 | ... 62 | 63 | **Enabling the plugin** 64 | 65 | To enable the plugin, add this to your pyproject.toml, or check the 66 | `mypy_config`_ documentation if you are using a different file format. 67 | 68 | :: 69 | 70 | [mypy] 71 | plugins = ["tiny_api_client.mypy"] 72 | 73 | 74 | .. _mypy_config: https://mypy.readthedocs.io/en/latest/config_file.html 75 | 76 | 77 | Without Plugin 78 | -------------- 79 | 80 | It is possible to type check an API client without the plugin, but be 81 | warned that you won't have positional route parameter checking 82 | whatsoever. 83 | 84 | The major issue you will run into is the following: 85 | 86 | :: 87 | 88 | client.my_call(my_arg="...") 89 | 90 | error: Unexpected keyword argument "my_arg" for "my_call" 91 | of "Client" [call-arg] 92 | 93 | Due to inherent limitations with the typing spec as of Python 3.12, it 94 | is not possible to add arbitrary keyword-only arguments to a decorated 95 | function for type checking purposes. For more information, see 96 | `pep`_ 612. 97 | 98 | .. _pep: https://peps.python.org/pep-0612/#concatenating-keyword-parameters 99 | 100 | One way around this is to include arbitrary keyword-only arguments in your 101 | endpoint definition. This will let mypy know that the wrapper function can 102 | also accept arbitrary keyword-only arguments. The obvious downside is that 103 | it does not look very clean and if you have multiple endpoints it can get 104 | tiring to write them like this. 105 | 106 | :: 107 | 108 | from typing import Any 109 | 110 | @get('/my_endpoint/{item_id}') 111 | def my_call(self, response, /, **_: Any) -> str: 112 | return response 113 | 114 | The other way is to manually silence this error for a certain scope. 115 | For more information, see the `mypy`_ docs. 116 | 117 | .. _mypy: https://mypy.readthedocs.io/en/stable/error_codes.html 118 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0", "setuptools_scm>=8"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "tiny-api-client" 7 | dynamic = ["version"] 8 | authors = [ 9 | { name="Jacob Sánchez", email="jacobszpz@protonmail.com" }, 10 | ] 11 | description = "The short and sweet way to create an API client" 12 | readme = "README.md" 13 | requires-python = ">=3.10" 14 | classifiers = [ 15 | "Development Status :: 5 - Production/Stable", 16 | "Topic :: Internet :: WWW/HTTP", 17 | "Programming Language :: Python :: 3.10", 18 | "Programming Language :: Python :: 3.11", 19 | "Programming Language :: Python :: 3.12", 20 | "License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)", 21 | "Intended Audience :: Developers", 22 | "Operating System :: OS Independent", 23 | "Topic :: Software Development :: Libraries" 24 | ] 25 | dependencies = ["requests", "typing_extensions"] 26 | [tool.setuptools_scm] 27 | 28 | [project.optional-dependencies] 29 | test = ["pytest", "pytest-mock", "exceptiongroup", "mypy"] 30 | docs = ["sphinx", "sphinx-rtd-theme"] 31 | 32 | [project.urls] 33 | "Repository" = "https://github.com/sanjacob/tiny-api-client" 34 | "Bug Tracker" = "https://github.com/sanjacob/tiny-api-client/issues" 35 | 36 | [tool.isort] 37 | no_sections = true 38 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanjacob/tiny-api-client/c7a90a7661e2b6c84f92eb3e1442d63124ba7a6f/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_api_client.py: -------------------------------------------------------------------------------- 1 | """Test the tiny_api_client module""" 2 | 3 | # Copyright (C) 2023, Jacob Sánchez Pérez 4 | 5 | # This library is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU Lesser General Public 7 | # License as published by the Free Software Foundation; either 8 | # version 2.1 of the License, or (at your option) any later version. 9 | # 10 | # This library is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public 16 | # License along with this library; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 18 | # MA 02110-1301 USA 19 | 20 | import pytest 21 | 22 | from tiny_api_client import api_client, get, post, put, patch, delete 23 | from tiny_api_client import APIEmptyResponseError, APIStatusError 24 | 25 | 26 | @pytest.fixture 27 | def example_url(): 28 | return 'https://example.org/api/public' 29 | 30 | 31 | @pytest.fixture 32 | def example_timeout(): 33 | return 1977 34 | 35 | 36 | @pytest.fixture 37 | def example_note(): 38 | return {'title': 'My Note', 39 | 'content': 'The beat goes round and round', 40 | 'custom_error': '200'} 41 | 42 | 43 | @pytest.fixture 44 | def mock_requests(mocker, example_note): 45 | mocked_requests = mocker.patch('tiny_api_client.requests') 46 | 47 | mock_response = mocker.Mock() 48 | mock_response.json.return_value = example_note 49 | 50 | mocked_requests.Session().request.return_value = mock_response 51 | return mocked_requests 52 | 53 | 54 | def get_request_fn(mock_requests): 55 | return mock_requests.Session().request 56 | 57 | 58 | def test_get(mock_requests, example_url, example_note): 59 | @api_client(example_url) 60 | class MyClient: 61 | @get('/my-endpoint') 62 | def get_my_endpoint(self, response): 63 | return response 64 | 65 | client = MyClient() 66 | r = client.get_my_endpoint() 67 | get_request_fn(mock_requests).assert_called_with( 68 | 'GET', f'{example_url}/my-endpoint', timeout=None, cookies=None 69 | ) 70 | assert r == example_note 71 | 72 | 73 | def test_post(mock_requests, example_url, example_note): 74 | @api_client(example_url) 75 | class MyClient: 76 | @post('/my-endpoint') 77 | def post_my_endpoint(self, response): 78 | return response 79 | 80 | client = MyClient() 81 | r = client.post_my_endpoint() 82 | get_request_fn(mock_requests).assert_called_with( 83 | 'POST', f'{example_url}/my-endpoint', timeout=None, cookies=None 84 | ) 85 | assert r == example_note 86 | 87 | 88 | def test_put(mock_requests, example_url, example_note): 89 | @api_client(example_url) 90 | class MyClient: 91 | @put('/my-endpoint') 92 | def put_my_endpoint(self, response): 93 | return response 94 | 95 | client = MyClient() 96 | r = client.put_my_endpoint() 97 | get_request_fn(mock_requests).assert_called_with( 98 | 'PUT', f'{example_url}/my-endpoint', timeout=None, cookies=None 99 | ) 100 | assert r == example_note 101 | 102 | 103 | def test_patch(mock_requests, example_url, example_note): 104 | @api_client(example_url) 105 | class MyClient: 106 | @patch('/my-endpoint') 107 | def patch_my_endpoint(self, response): 108 | return response 109 | 110 | client = MyClient() 111 | r = client.patch_my_endpoint() 112 | get_request_fn(mock_requests).assert_called_with( 113 | 'PATCH', f'{example_url}/my-endpoint', timeout=None, cookies=None 114 | ) 115 | assert r == example_note 116 | 117 | 118 | def test_delete(mock_requests, example_url, example_note): 119 | @api_client(example_url) 120 | class MyClient: 121 | @delete('/my-endpoint') 122 | def delete_my_endpoint(self, response): 123 | return response 124 | 125 | client = MyClient() 126 | r = client.delete_my_endpoint() 127 | get_request_fn(mock_requests).assert_called_with( 128 | 'DELETE', f'{example_url}/my-endpoint', timeout=None, cookies=None 129 | ) 130 | assert r == example_note 131 | 132 | 133 | def test_non_json(mocker, example_url): 134 | mock_requests = mocker.patch('tiny_api_client.requests') 135 | get_request_fn(mock_requests).return_value = 'This is a plaintext message' 136 | 137 | @api_client(example_url) 138 | class MyClient: 139 | @get('/my-endpoint', json=False) 140 | def get_my_endpoint(self, response): 141 | return response 142 | 143 | client = MyClient() 144 | r = client.get_my_endpoint() 145 | assert r == 'This is a plaintext message' 146 | 147 | 148 | def test_non_json_xml(mocker, example_url): 149 | mock_requests = mocker.patch('tiny_api_client.requests') 150 | mock_response = mocker.Mock() 151 | mock_response.text = """ 152 | 153 | First 154 | 155 | """ 156 | 157 | get_request_fn(mock_requests).return_value = mock_response 158 | 159 | @api_client(example_url) 160 | class MyClient: 161 | @get('/my-endpoint', json=False, xml=True) 162 | def get_my_endpoint(self, response): 163 | return response 164 | 165 | client = MyClient() 166 | root = client.get_my_endpoint() 167 | song = root.find('title') 168 | assert song.text == 'First' 169 | 170 | 171 | def test_optional_parameter(mock_requests, example_url): 172 | @api_client(example_url) 173 | class MyClient: 174 | @get('/my-endpoint/{optional_id}') 175 | def get_my_endpoint(self, response): 176 | return response 177 | 178 | client = MyClient() 179 | client.get_my_endpoint() 180 | get_request_fn(mock_requests).assert_called_with( 181 | 'GET', f'{example_url}/my-endpoint', timeout=None, cookies=None 182 | ) 183 | 184 | client.get_my_endpoint(optional_id='MY_OPTIONAL_ID') 185 | get_request_fn(mock_requests).assert_called_with( 186 | 'GET', f'{example_url}/my-endpoint/MY_OPTIONAL_ID', 187 | timeout=None, cookies=None 188 | ) 189 | 190 | 191 | def test_multiple_route_parameters(mock_requests, example_url): 192 | @api_client(example_url) 193 | class MyClient: 194 | @get('/my-endpoint/{first_id}/child/{second_id}/child/{third_id}') 195 | def get_my_endpoint(self, response): 196 | return response 197 | 198 | client = MyClient() 199 | client.get_my_endpoint(first_id='1', second_id='22', third_id='333') 200 | get_request_fn(mock_requests).assert_called_with( 201 | 'GET', f'{example_url}/my-endpoint/1/child/22/child/333', 202 | timeout=None, cookies=None 203 | ) 204 | 205 | 206 | def test_positional_endpoint_parameters(mock_requests, example_url): 207 | @api_client(example_url) 208 | class MyClient: 209 | @get('/my-endpoint') 210 | def get_my_endpoint(self, response, cheese): 211 | self.cheese = cheese 212 | return response 213 | 214 | client = MyClient() 215 | client.get_my_endpoint('my cheese') 216 | assert client.cheese == 'my cheese' 217 | 218 | 219 | def test_extra_requests_parameter(mock_requests, example_url): 220 | @api_client(example_url) 221 | class MyClient: 222 | @get('/my-endpoint') 223 | def get_my_endpoint(self, response): 224 | return response 225 | 226 | client = MyClient() 227 | client.get_my_endpoint(my_extra_param='hello world') 228 | get_request_fn(mock_requests).assert_called_with( 229 | 'GET', f'{example_url}/my-endpoint', timeout=None, cookies=None, 230 | my_extra_param='hello world' 231 | ) 232 | 233 | 234 | def test_extra_requests_parameter_endpoint(mock_requests, example_url): 235 | @api_client(example_url) 236 | class MyClient: 237 | @get('/my-endpoint', my_extra_param='hello world') 238 | def get_my_endpoint(self, response): 239 | return response 240 | 241 | client = MyClient() 242 | client.get_my_endpoint() 243 | get_request_fn(mock_requests).assert_called_with( 244 | 'GET', f'{example_url}/my-endpoint', timeout=None, cookies=None, 245 | my_extra_param='hello world' 246 | ) 247 | 248 | 249 | def test_unpacking(mock_requests, example_url, example_note): 250 | @api_client(example_url, results_key='content') 251 | class MyClient: 252 | @get('/my-endpoint') 253 | def get_my_endpoint(self, response): 254 | return response 255 | 256 | client = MyClient() 257 | r = client.get_my_endpoint() 258 | get_request_fn(mock_requests).assert_called_with( 259 | 'GET', f'{example_url}/my-endpoint', timeout=None, cookies=None 260 | ) 261 | assert r == example_note['content'] 262 | 263 | 264 | def test_empty_response_error(mocker, example_url): 265 | mock_requests = mocker.patch('tiny_api_client.requests') 266 | mock_response = mocker.Mock() 267 | mock_response.json.return_value = "" 268 | 269 | get_request_fn(mock_requests).return_value = mock_response 270 | 271 | @api_client(example_url) 272 | class MyClient: 273 | @get('/my-endpoint') 274 | def get_my_endpoint(self, response): 275 | return response 276 | 277 | client = MyClient() 278 | with pytest.raises(APIEmptyResponseError): 279 | client.get_my_endpoint() 280 | 281 | 282 | def test_endpoint_versions(mock_requests, example_url, example_note): 283 | @api_client(f"{example_url}/v{{version}}") 284 | class MyClient: 285 | @put('/my-endpoint') 286 | def put_my_endpoint(self, response): 287 | return response 288 | 289 | @get('/my-endpoint', version=3) 290 | def get_my_endpoint(self, response): 291 | return response 292 | 293 | @post('/my-endpoint', version=2) 294 | def post_my_endpoint(self, response): 295 | return response 296 | 297 | client = MyClient() 298 | client.get_my_endpoint() 299 | get_request_fn(mock_requests).assert_called_with( 300 | 'GET', f'{example_url}/v3/my-endpoint', timeout=None, cookies=None 301 | ) 302 | client.post_my_endpoint() 303 | get_request_fn(mock_requests).assert_called_with( 304 | 'POST', f'{example_url}/v2/my-endpoint', timeout=None, cookies=None 305 | ) 306 | client.put_my_endpoint() 307 | get_request_fn(mock_requests).assert_called_with( 308 | 'PUT', f'{example_url}/v1/my-endpoint', timeout=None, cookies=None 309 | ) 310 | 311 | 312 | def test_class_decorator_parameter_max_retries(mocker, mock_requests, 313 | example_url): 314 | mock_adapter = mocker.patch("tiny_api_client.HTTPAdapter") 315 | http_adapter = mocker.Mock() 316 | mock_adapter.return_value = http_adapter 317 | 318 | @api_client(example_url, max_retries=5) 319 | class MyClient: 320 | @get('/my-endpoint') 321 | def get_my_endpoint(self, response): 322 | return response 323 | 324 | client = MyClient() 325 | client.get_my_endpoint() 326 | 327 | mock_adapter.assert_has_calls([ 328 | mocker.call(max_retries=5), 329 | ]) 330 | 331 | mock_requests.Session().mount.assert_has_calls([ 332 | mocker.call("http://", http_adapter), 333 | mocker.call("https://", http_adapter) 334 | ]) 335 | 336 | 337 | def test_class_decorator_parameter_timeout(mock_requests, example_url, 338 | example_timeout): 339 | @api_client(example_url, timeout=example_timeout) 340 | class MyClient: 341 | @get('/my-endpoint') 342 | def get_my_endpoint(self, response): 343 | return response 344 | 345 | client = MyClient() 346 | client.get_my_endpoint() 347 | get_request_fn(mock_requests).assert_called_with( 348 | 'GET', f'{example_url}/my-endpoint', 349 | timeout=example_timeout, cookies=None 350 | ) 351 | 352 | 353 | def test_class_decorator_parameter_status_no_handler(mock_requests, 354 | example_url): 355 | @api_client(example_url, status_key='custom_error') 356 | class MyClient: 357 | @get('/my-endpoint') 358 | def get_my_endpoint(self, response): 359 | return response 360 | 361 | client = MyClient() 362 | with pytest.raises(APIStatusError) as e: 363 | client.get_my_endpoint() 364 | assert str(e.value) == '200' 365 | 366 | 367 | def test_class_decorator_parameter_status_handler_throws(mock_requests, 368 | example_url): 369 | def throw_custom(client, error_code, response): 370 | raise ValueError(error_code) 371 | 372 | @api_client(example_url, status_key='custom_error', 373 | status_handler=throw_custom) 374 | class MyClient: 375 | @get('/my-endpoint') 376 | def get_my_endpoint(self, response): 377 | return response 378 | 379 | client = MyClient() 380 | with pytest.raises(ValueError) as e: 381 | client.get_my_endpoint() 382 | assert str(e.value) == '200' 383 | 384 | 385 | def test_deferred_url_parameter(mock_requests, example_url, example_note): 386 | @api_client() 387 | class MyClient: 388 | def __init__(self, url: str): 389 | self._url = url 390 | 391 | @get('/my-endpoint') 392 | def fetch_my_endpoint(self, response): 393 | return response 394 | 395 | client = MyClient(example_url) 396 | r = client.fetch_my_endpoint() 397 | 398 | assert r == example_note 399 | get_request_fn(mock_requests).assert_called_once_with( 400 | 'GET', f'{example_url}/my-endpoint', timeout=None, cookies=None 401 | ) 402 | 403 | 404 | def test_session_member(mock_requests, example_url, example_note): 405 | @api_client(example_url) 406 | class MyClient: 407 | def __init__(self, session: dict[str, str]): 408 | self._session = session 409 | 410 | @get('/my-endpoint') 411 | def fetch_my_endpoint(self, response): 412 | return response 413 | 414 | example_session = {'session_cookie': 'MY_COOKIE'} 415 | client = MyClient(example_session) 416 | r = client.fetch_my_endpoint() 417 | 418 | assert r == example_note 419 | get_request_fn(mock_requests).assert_called_once_with( 420 | 'GET', f'{example_url}/my-endpoint', timeout=None, 421 | cookies=example_session 422 | ) 423 | -------------------------------------------------------------------------------- /tests/test_mypy_plugin.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | plugins = tiny_api_client.mypy 3 | -------------------------------------------------------------------------------- /tests/test_mypy_plugin.yml: -------------------------------------------------------------------------------- 1 | - case: mypy_plugin_no_placeholders 2 | main: | 3 | from tiny_api_client import get, api_client 4 | 5 | @api_client('https://api.example.org') 6 | class MyClient: 7 | @get('/users') 8 | def get_users(self, response: list[str]) -> list[str]: 9 | return response 10 | 11 | client = MyClient() 12 | client.get_users() 13 | env: 14 | - PYTHONPATH=$(pwd)/../ 15 | 16 | - case: mypy_plugin_correct_route_param 17 | main: | 18 | from tiny_api_client import get, api_client 19 | 20 | @api_client('https://api.example.org') 21 | class MyClient: 22 | @get('/users/{user_id}') 23 | def get_users(self, response: list[str]) -> list[str]: 24 | return response 25 | 26 | client = MyClient() 27 | client.get_users(user_id='peterparker') 28 | env: 29 | - PYTHONPATH=$(pwd)/../ 30 | 31 | 32 | - case: mypy_plugin_multiple_route_params 33 | main: | 34 | from tiny_api_client import get, api_client 35 | 36 | @api_client('https://api.example.org') 37 | class MyClient: 38 | @get('/users/{user_id}/comments/{comment_id}') 39 | def get_comment(self, response: str) -> str: 40 | return response 41 | 42 | client = MyClient() 43 | client.get_comment(user_id='peterparker', comment_id='001') 44 | env: 45 | - PYTHONPATH=$(pwd)/../ 46 | 47 | 48 | - case: mypy_plugin_optional_route_param 49 | main: | 50 | from tiny_api_client import get, api_client 51 | 52 | @api_client('https://api.example.org') 53 | class MyClient: 54 | @get('/users/{user_id}') 55 | def get_users(self, response: list[str]) -> list[str]: 56 | return response 57 | 58 | client = MyClient() 59 | client.get_users() 60 | env: 61 | - PYTHONPATH=$(pwd)/../ 62 | 63 | - case: mypy_plugin_wrong_route_param 64 | main: | 65 | from tiny_api_client import get, api_client 66 | 67 | @api_client('https://api.example.org') 68 | class MyClient: 69 | @get('/users/{user_id}') 70 | def get_users(self, response: list[str]) -> list[str]: 71 | return response 72 | 73 | client = MyClient() 74 | client.get_users(unknown_id='idk') 75 | env: 76 | - PYTHONPATH=$(pwd)/../ 77 | out: | 78 | main:10: error: Unexpected keyword argument "unknown_id" for "get_users" of "MyClient" [call-arg] 79 | 80 | 81 | - case: mypy_plugin_wrong_extra_route_param 82 | main: | 83 | from tiny_api_client import get, api_client 84 | 85 | @api_client('https://api.example.org') 86 | class MyClient: 87 | @get('/users/{user_id}') 88 | def get_users(self, response: list[str]) -> list[str]: 89 | return response 90 | 91 | client = MyClient() 92 | client.get_users(user_id='peterparker', unknown_id='idk') 93 | env: 94 | - PYTHONPATH=$(pwd)/../ 95 | out: | 96 | main:10: error: Unexpected keyword argument "unknown_id" for "get_users" of "MyClient" [call-arg] 97 | 98 | 99 | - case: mypy_plugin_non_optional_args 100 | main: | 101 | from tiny_api_client import get, api_client 102 | 103 | @api_client('https://api.example.org') 104 | class MyClient: 105 | @get('/category/{category_id}/product/{product_id}') 106 | def get_product(self, response: str) -> str: 107 | return response 108 | 109 | client = MyClient() 110 | client.get_product(product_id='peterparker') 111 | env: 112 | - PYTHONPATH=$(pwd)/../ 113 | out: | 114 | main:10: error: Missing named argument "category_id" for "get_product" of "MyClient" [call-arg] 115 | -------------------------------------------------------------------------------- /tiny_api_client/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tiny API Client 3 | 4 | The short and sweet way to create an API client 5 | 6 | Basic usage: 7 | >>> from tiny_api_client import api_client, get 8 | >>> @api_client("https://example.org/api") 9 | ... class MyClient: 10 | ... @get("/profile/{user_id}") 11 | ... def fetch_profile(response): 12 | ... return response 13 | >>> client = MyClient() 14 | >>> client.fetch_profile(user_id=...) 15 | 16 | """ 17 | 18 | # Copyright (C) 2024, Jacob Sánchez Pérez 19 | 20 | # This library is free software; you can redistribute it and/or 21 | # modify it under the terms of the GNU Lesser General Public 22 | # License as published by the Free Software Foundation; either 23 | # version 2.1 of the License, or (at your option) any later version. 24 | # 25 | # This library is distributed in the hope that it will be useful, 26 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 27 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 28 | # Lesser General Public License for more details. 29 | # 30 | # You should have received a copy of the GNU Lesser General Public 31 | # License along with this library; if not, write to the Free Software 32 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 33 | # 02110-1301 USA 34 | 35 | import logging 36 | import requests 37 | import string 38 | from collections import defaultdict 39 | from collections.abc import Callable 40 | from dataclasses import dataclass 41 | from functools import wraps 42 | from requests.adapters import HTTPAdapter 43 | from typing import Any, Concatenate, ParamSpec, Protocol, TypeVar 44 | from urllib3.util.retry import Retry 45 | from xml.etree import ElementTree 46 | 47 | __all__ = ['api_client', 'get', 'post', 'put', 'patch', 'delete'] 48 | 49 | _logger = logging.getLogger(__name__) 50 | _logger.addHandler(logging.NullHandler()) 51 | 52 | # Typing 53 | 54 | P = ParamSpec('P') 55 | T = TypeVar('T') 56 | 57 | APIStatusHandler = Callable[[Any, Any, Any], None] | None 58 | _StatusHandler = Callable[[Any, Any], None] | None 59 | 60 | APIClient = TypeVar('APIClient', bound=type[Any]) 61 | 62 | 63 | class RequestDecorator(Protocol): 64 | def __call__( 65 | self, func: Callable[Concatenate[Any, Any, P], T] 66 | ) -> Callable[Concatenate[Any, P], T]: ... 67 | 68 | 69 | class DecoratorFactory(Protocol): 70 | def __call__( 71 | self, route: str, *, version: int = 1, use_api: bool = True, 72 | json: bool = True, xml: bool = False, **g_kwargs: Any 73 | ) -> RequestDecorator: ... 74 | 75 | 76 | # Exceptions 77 | 78 | class APIClientError(Exception): 79 | """Base error class for the API Client""" 80 | pass 81 | 82 | 83 | class APIEmptyResponseError(APIClientError): 84 | """The API response is empty""" 85 | pass 86 | 87 | 88 | class APIStatusError(APIClientError): 89 | """The API returned an error status""" 90 | pass 91 | 92 | 93 | class APINoURLError(APIClientError): 94 | """The API has no URL declared""" 95 | pass 96 | 97 | 98 | # Tiny API Client 99 | 100 | @dataclass 101 | class Endpoint: 102 | route: str 103 | version: int 104 | use_api: bool 105 | json: bool 106 | xml: bool 107 | kwargs: dict[str, Any] 108 | 109 | 110 | def _format_endpoint(url: str, endpoint: str, use_api: bool, 111 | positional_args: dict[str, Any]) -> str: 112 | """Build final endpoint URL for an API call.""" 113 | param_map = defaultdict(lambda: '', positional_args) 114 | route_params = endpoint.format_map(param_map) 115 | endpoint_url = f"{url}{route_params}" if use_api else route_params 116 | return endpoint_url.rstrip('/') 117 | 118 | 119 | def _pop_api_kwargs(endpoint: str, kwargs: dict[str, Any]) -> dict[str, Any]: 120 | """Remove positional endpoint arguments from kwargs before passing 121 | additional arguments to `requests`. 122 | """ 123 | formatter = string.Formatter() 124 | for x in formatter.parse(endpoint): 125 | if x[1] is not None: 126 | kwargs.pop(x[1], None) 127 | return kwargs 128 | 129 | 130 | def _make_request(client: Any, method: str, endpoint: str, 131 | **kwargs: Any) -> Any: 132 | """Use `requests` to send out a request to the API endpoint.""" 133 | if not hasattr(client, '__client_session'): 134 | # Create a session to reuse connections 135 | _logger.info("Creating new requests session") 136 | client.__client_session = requests.Session() 137 | # Set custom adapter for retries 138 | adapter = HTTPAdapter(max_retries=client.__api_max_retries) 139 | client.__client_session.mount("http://", adapter) 140 | client.__client_session.mount("https://", adapter) 141 | 142 | # The following assertion causes issues in testing 143 | # since MagicMock is not an instance of Session 144 | # Thus, the return type has to be Any for now 145 | # assert isinstance(client.__client_session, requests.Session) 146 | 147 | _logger.debug(f"Making request to {endpoint}") 148 | 149 | cookies = None 150 | if hasattr(client, '_cookies'): 151 | cookies = client._cookies 152 | elif hasattr(client, '_session'): 153 | _logger.warning("_session is deprecated.") 154 | cookies = client._session 155 | 156 | return client.__client_session.request( 157 | method, endpoint, 158 | timeout=client.__api_timeout, 159 | cookies=cookies, **kwargs 160 | ) 161 | 162 | 163 | def _handle_response(response: Any, 164 | json: bool, xml: bool, 165 | status_key: str, results_key: str, 166 | status_handler: _StatusHandler) -> Any: 167 | """Parse json or XML response after request is complete""" 168 | endpoint_response: Any = response 169 | 170 | if json: 171 | endpoint_response = response.json() 172 | 173 | if not endpoint_response: 174 | raise APIEmptyResponseError() 175 | 176 | if status_key in endpoint_response: 177 | status_code = endpoint_response[status_key] 178 | _logger.warning(f"Code {status_code} from {response.url}") 179 | 180 | if status_handler is not None: 181 | status_handler(status_code, endpoint_response) 182 | else: 183 | raise APIStatusError('Server responded with an error code') 184 | 185 | if results_key in endpoint_response: 186 | endpoint_response = endpoint_response[results_key] 187 | elif xml: 188 | endpoint_response = ElementTree.fromstring(response.text) 189 | 190 | return endpoint_response 191 | 192 | 193 | def make_api_call(method: str, client: Any, 194 | endpoint: Endpoint, **kwargs: Any) -> Any: 195 | """Calls the API endpoint and handles result.""" 196 | if client._url is None: 197 | raise APINoURLError() 198 | 199 | # Build final API endpoint URL 200 | url = client._url.format(version=endpoint.version) 201 | route = _format_endpoint(url, endpoint.route, endpoint.use_api, kwargs) 202 | 203 | # Remove parameters meant for endpoint formatting 204 | kwargs = _pop_api_kwargs(endpoint.route, kwargs) 205 | 206 | response = _make_request(client, method, route, 207 | **kwargs, **endpoint.kwargs) 208 | endpoint_response = _handle_response( 209 | response, 210 | endpoint.json, 211 | endpoint.xml, 212 | client.__api_status_key, 213 | client.__api_results_key, 214 | client.__api_status_handler) 215 | 216 | return endpoint_response 217 | 218 | 219 | def api_client_method(method: str) -> DecoratorFactory: 220 | """Create a decorator factory for one of the http methods 221 | 222 | This superfactory can create factories for arbitrary http verbs 223 | (GET, POST, etc.). Unless specifying an http verb not already 224 | covered by this library, this function should not be called 225 | directly. 226 | 227 | Basic usage: 228 | >>> get = api_client_method('GET') 229 | >>> @get("/profile/{user_id}") 230 | ... def fetch_profile(response): 231 | ... return response 232 | >>> client.fetch_profile(user_id=...) 233 | 234 | :param str method: The HTTP verb for the decorator 235 | """ 236 | 237 | def request(route: str, *, version: int = 1, use_api: bool = True, 238 | json: bool = True, xml: bool = False, 239 | **request_kwargs: Any) -> RequestDecorator: 240 | """Declare an endpoint with the given HTTP method and parameters 241 | 242 | Basic usage: 243 | >>> from tiny_api_client import get, post 244 | >>> @get("/posts") 245 | ... def get_posts(self, response): 246 | ... return response 247 | >>> @post("/posts") 248 | ... def create_post(self, response): 249 | ... return response 250 | 251 | :param str endpoint: Endpoint including positional placeholders 252 | :param int version: Replaces version placeholder in API URL 253 | :param bool json: Toggle JSON parsing of response 254 | :param bool xml: Toggle XML parsing of response 255 | :param dict request_kwargs: Any keyword arguments passed to requests 256 | """ 257 | endpoint = Endpoint(route, version, use_api, json, xml, request_kwargs) 258 | 259 | def request_decorator(func: Callable[Concatenate[Any, Any, P], T] 260 | ) -> Callable[Concatenate[Any, P], T]: 261 | """Decorator created when calling @get(...) and others. 262 | 263 | :param function func: Function to decorate 264 | """ 265 | 266 | @wraps(func) 267 | def request_wrapper(self: Any, /, 268 | *args: P.args, **kwargs: P.kwargs) -> T: 269 | """Replace endpoint parameters and call API endpoint, 270 | then execute user-defined API endpoint handler. 271 | 272 | :param list args: Passed to the function being wrapped 273 | :param dict kwargs: Any kwargs are passed to requests 274 | """ 275 | response = make_api_call(method, self, endpoint, **kwargs) 276 | return func(self, response, *args) 277 | return request_wrapper 278 | return request_decorator 279 | return request 280 | 281 | 282 | def api_client(url: str | None = None, /, *, 283 | max_retries: int | Retry = 0, 284 | timeout: int | None = None, 285 | status_handler: APIStatusHandler = None, 286 | status_key: str = 'status', 287 | results_key: str = 'results' 288 | ) -> Callable[[APIClient], APIClient]: 289 | """Annotate a class to use the api client method decorators 290 | 291 | Basic usage: 292 | >>> @api_client("https://example.org/api") 293 | >>> class MyClient: 294 | ... ... 295 | 296 | :param str url: The root URL of the API server 297 | :param int max_retries: Max number of retries for network errors 298 | :param int timeout: Timeout for requests in seconds 299 | :param Callable status_handler: Error handler for status codes 300 | :param str status_key: Key of response that contains status codes 301 | :param str results_key: Key of response that contains results 302 | """ 303 | 304 | def wrap(cls: APIClient) -> APIClient: 305 | cls._url = url 306 | cls.__api_max_retries = max_retries 307 | cls.__api_timeout = timeout 308 | cls.__api_status_handler = status_handler 309 | cls.__api_status_key = status_key 310 | cls.__api_results_key = results_key 311 | return cls 312 | 313 | return wrap 314 | 315 | 316 | get = api_client_method('GET') 317 | post = api_client_method('POST') 318 | put = api_client_method('PUT') 319 | patch = api_client_method('PATCH') 320 | delete = api_client_method('DELETE') 321 | -------------------------------------------------------------------------------- /tiny_api_client/mypy.py: -------------------------------------------------------------------------------- 1 | """ 2 | mypy plugin for tiny-api-client. 3 | 4 | Please activate in your mypy configuration file. 5 | """ 6 | 7 | # Copyright (C) 2024, Jacob Sánchez Pérez 8 | 9 | # This library is free software; you can redistribute it and/or 10 | # modify it under the terms of the GNU Lesser General Public 11 | # License as published by the Free Software Foundation; either 12 | # version 2.1 of the License, or (at your option) any later version. 13 | # 14 | # This library is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | # Lesser General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Lesser General Public 20 | # License along with this library; if not, write to the Free Software 21 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 22 | # 02110-1301 USA 23 | 24 | import string 25 | from typing import NamedTuple 26 | from collections.abc import Callable, Iterable 27 | 28 | from mypy.nodes import ARG_NAMED, ARG_NAMED_OPT, StrExpr 29 | from mypy.options import Options 30 | from mypy.plugin import MethodContext, Plugin 31 | from mypy.types import Type, CallableType 32 | 33 | 34 | class RouteParser: 35 | formatter = string.Formatter() 36 | 37 | class FormatTuple(NamedTuple): 38 | literal_text: str | None 39 | field_name: str | None 40 | format_spec: str | None 41 | conversion: str | None 42 | 43 | def __init__(self, route: str): 44 | parsed = self.formatter.parse(route) 45 | self.params = [] 46 | 47 | for t in parsed: 48 | self.params.append(self.FormatTuple(*t)) 49 | 50 | @property 51 | def fields(self) -> Iterable[str]: 52 | return (x.field_name for x in self.params if x.field_name is not None) 53 | 54 | @property 55 | def has_optional(self) -> bool: 56 | if not len(self.params): 57 | return False 58 | return self.params[-1].field_name is not None 59 | 60 | 61 | class TinyAPIClientPlugin(Plugin): 62 | """Companion mypy plugin for tiny-api-client. 63 | 64 | Normally, it isn't possible to type check route parameters since 65 | they are defined at runtime, and typing primitives are not capable 66 | of introspection. 67 | 68 | This plugin captures the route parameters of every endpoint and 69 | modifies the decorated signature to include said parameters as if 70 | they were factual ones. 71 | """ 72 | def __init__(self, options: Options) -> None: 73 | self._ctx_cache: dict[str, RouteParser] = {} 74 | super().__init__(options) 75 | 76 | def get_method_hook(self, fullname: str 77 | ) -> Callable[[MethodContext], Type] | None: 78 | if fullname == "tiny_api_client.DecoratorFactory.__call__": 79 | return self._factory_callback 80 | if fullname == "tiny_api_client.RequestDecorator.__call__": 81 | return self._decorator_callback 82 | return None 83 | 84 | def _factory_callback(self, ctx: MethodContext) -> Type: 85 | """Capture route positional params passed in decorator factory. 86 | 87 | The route argument is captured and parsed for positional 88 | parameters. These parameters are stored in a dictionary 89 | with the line and column as its key. 90 | 91 | The parameters are later retrieved in a subsequent call 92 | to the returned decorator. 93 | """ 94 | if len(ctx.args) and len(ctx.args[0]): 95 | pos = f"{ctx.context.line},{ctx.context.column}" 96 | route = ctx.args[0][0] 97 | assert isinstance(route, StrExpr) 98 | self._ctx_cache[pos] = RouteParser(route.value) 99 | return ctx.default_return_type 100 | 101 | def _decorator_callback(self, ctx: MethodContext) -> Type: 102 | """Append route positional parameters to function kw-only args. 103 | 104 | The route parameters are retrieved from memory according to the 105 | context, and they are included in the decorated function type 106 | as optional keyword-only parameters. 107 | """ 108 | pos = f"{ctx.context.line},{ctx.context.column}" 109 | default_ret = ctx.default_return_type 110 | # need this to access properties without a warning 111 | assert isinstance(default_ret, CallableType) 112 | 113 | # Modify default return type in place (probably fine) 114 | route_parser = self._ctx_cache[pos] 115 | for p in route_parser.fields: 116 | default_ret.arg_types.append( 117 | # API endpoint URL params must be strings 118 | ctx.api.named_generic_type("builtins.str", []) 119 | ) 120 | default_ret.arg_kinds.append(ARG_NAMED) 121 | default_ret.arg_names.append(p) 122 | 123 | if route_parser.has_optional: 124 | default_ret.arg_kinds[-1] = ARG_NAMED_OPT 125 | 126 | return ctx.default_return_type 127 | 128 | 129 | def plugin(version: str) -> type[Plugin]: 130 | return TinyAPIClientPlugin 131 | -------------------------------------------------------------------------------- /tiny_api_client/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanjacob/tiny-api-client/c7a90a7661e2b6c84f92eb3e1442d63124ba7a6f/tiny_api_client/py.typed --------------------------------------------------------------------------------