├── .github └── workflows │ ├── ci.yml │ ├── jit-security.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── mastodon_to_sqlite ├── __init__.py ├── cli.py ├── client.py └── service.py ├── metadata.json ├── poetry.lock ├── pyproject.toml └── tests ├── __init__.py ├── conftest.py ├── fixture-auth.json ├── fixtures.py ├── test_cli.py ├── test_client.py └── test_services.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest] 16 | python-version: [3.9] 17 | 18 | steps: 19 | - id: checkout 20 | name: "Checkout 🛎" 21 | uses: actions/checkout@v2 22 | 23 | - id: setup-python 24 | name: "Setup Python ${{ matrix.python-version }} 🏗" 25 | uses: actions/setup-python@v1 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | 29 | - id: setup-poetry 30 | name: "Setup Poetry 📝" 31 | run: | 32 | curl -sSL https://install.python-poetry.org | python3 - 33 | 34 | - id: get-cache-poetry-directory 35 | name: "Get poetry's cache directory 🔎" 36 | run: | 37 | echo "::set-output name=dir::$(poetry config cache-dir)" 38 | 39 | - id: cache-poetry-directory 40 | name: "Cache poetry 📦" 41 | uses: actions/cache@v3.0.11 42 | with: 43 | path: ${{ steps.get-cache-poetry-directory.outputs.dir }} 44 | key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }} 45 | restore-keys: ${{ runner.os }}-poetry- 46 | 47 | - id: install-dependencies 48 | name: "Install dependencies 👨🏻‍💻" 49 | run: make setup 50 | 51 | - id: poetry-lock 52 | name: "Run poetry lock 📌" 53 | run: poetry lock --check 54 | 55 | - id: run-test 56 | name: "Run tests 🧪" 57 | run: make test 58 | 59 | - id: run-linters 60 | name: Run linters 🚨 61 | run: make lint 62 | 63 | - id: run-typing 64 | name: Run mypy 🏷 65 | run: make mypy 66 | -------------------------------------------------------------------------------- /.github/workflows/jit-security.yml: -------------------------------------------------------------------------------- 1 | name: Workflows generated by the MVS plan 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | client_payload: 6 | description: The Client payload 7 | required: true 8 | jobs: 9 | enrich: 10 | if: fromJSON(github.event.inputs.client_payload).payload.workflow_job_name == 'enrich' 11 | runs-on: ubuntu-20.04 12 | steps: 13 | - name: enrichment 14 | uses: jitsecurity-controls/jit-github-action@v2.0 15 | with: 16 | docker_user: jit-bot 17 | docker_password: ${{fromJSON(github.event.inputs.client_payload).payload.container_registry_token}} 18 | 19 | security_control: ghcr.io/jitsecurity-controls/control-enrichment-slim:latest 20 | security_control_args: --path \${WORK_DIR:-.} 21 | 22 | dispatch_type: workflow 23 | 24 | secret-detection: 25 | if: fromJSON(github.event.inputs.client_payload).payload.workflow_job_name == 'secret-detection' 26 | runs-on: ubuntu-20.04 27 | steps: 28 | - name: gitleaks 29 | uses: jitsecurity-controls/jit-github-action@v2.0 30 | with: 31 | docker_user: jit-bot 32 | docker_password: ${{fromJSON(github.event.inputs.client_payload).payload.container_registry_token}} 33 | 34 | security_control: ghcr.io/jitsecurity-controls/control-gitleaks-alpine:latest 35 | security_control_args: detect --config \$GITLEAKS_CONFIG_FILE_PATH --source \${WORK_DIR:-.} -v --report-format json --report-path \$REPORT_FILE --redact --no-git --exit-code 0 36 | security_control_output_file: /tmp/report.json 37 | dispatch_type: workflow 38 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | publish: 10 | runs-on: ${{ matrix.os }} 11 | 12 | strategy: 13 | fail-fast: false 14 | 15 | matrix: 16 | os: [ubuntu-latest] 17 | python-version: [3.9] 18 | 19 | steps: 20 | - id: checkout 21 | name: Checkout 🛎 22 | uses: actions/checkout@v2 23 | 24 | - id: setup-python 25 | name: Setup Python ${{ matrix.python-version }} 🏗 26 | uses: actions/setup-python@v1 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | 30 | - id: setup-poetry 31 | name: Setup Poetry 📝 32 | run: | 33 | curl -sSL https://install.python-poetry.org | python3 - 34 | 35 | - id: publish 36 | name: Publish 🚀 37 | env: 38 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 39 | run: | 40 | poetry config pypi-token.pypi $PYPI_TOKEN 41 | poetry publish --build 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | auth.json 3 | 4 | # macOS 5 | .DS_Store 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | cover/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | .pybuilder/ 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # IPython 88 | profile_default/ 89 | ipython_config.py 90 | 91 | # pyenv 92 | # For a library or package, you might want to ignore these files since the code is 93 | # intended to run in multiple environments; otherwise, check them in: 94 | # .python-version 95 | 96 | # pipenv 97 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 98 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 99 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 100 | # install all needed dependencies. 101 | #Pipfile.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/#use-with-ide 116 | .pdm.toml 117 | 118 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 119 | __pypackages__/ 120 | 121 | # Celery stuff 122 | celerybeat-schedule 123 | celerybeat.pid 124 | 125 | # SageMath parsed files 126 | *.sage.py 127 | 128 | # Environments 129 | .env 130 | .venv 131 | env/ 132 | venv/ 133 | ENV/ 134 | env.bak/ 135 | venv.bak/ 136 | 137 | # Spyder project settings 138 | .spyderproject 139 | .spyproject 140 | 141 | # Rope project settings 142 | .ropeproject 143 | 144 | # mkdocs documentation 145 | /site 146 | 147 | # mypy 148 | .mypy_cache/ 149 | .dmypy.json 150 | dmypy.json 151 | 152 | # Pyre type checker 153 | .pyre/ 154 | 155 | # pytype static type analyzer 156 | .pytype/ 157 | 158 | # Cython debug symbols 159 | cython_debug/ 160 | 161 | # ruff 162 | .ruff_cache/ 163 | 164 | # PyCharm 165 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 166 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 167 | # and can be added to the global gitignore or merged into this file. For a more nuclear 168 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 169 | .idea/ 170 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: clean setup test lint mypy 3 | 4 | .PHONY: setup 5 | setup: pyproject.toml 6 | poetry check 7 | poetry install 8 | 9 | .PHONY: test 10 | test: 11 | poetry run pytest --cov=mastodon_to_sqlite/ --cov-report=xml 12 | 13 | .PHONY: lint 14 | lint: 15 | poetry run black --check . 16 | poetry run isort --check . 17 | poetry run ruff check . 18 | 19 | .PHONY: lintfix 20 | lintfix: 21 | poetry run black . 22 | poetry run isort . 23 | poetry run ruff check . --fix 24 | 25 | .PHONY: mypy 26 | mypy: 27 | poetry run mypy mastodon_to_sqlite/ 28 | 29 | .PHONY: clean 30 | clean: 31 | rm -fr ./.mypy_cache 32 | rm -fr ./.pytest_cache 33 | rm -fr ./.ruff_cache 34 | rm -fr ./dist 35 | rm -f .coverage 36 | rm -f coverage.xml 37 | find . -type f -name '*.py[co]' -delete -o -type d -name __pycache__ -delete 38 | 39 | .PHONY: mastodon.db 40 | mastodon.db: auth.json 41 | poetry run mastodon-to-sqlite verify-auth --auth $? 42 | poetry run mastodon-to-sqlite followers $@ --auth $? 43 | poetry run mastodon-to-sqlite followings $@ --auth $? 44 | poetry run mastodon-to-sqlite statuses $@ --auth $? 45 | poetry run mastodon-to-sqlite bookmarks $@ --auth $? 46 | poetry run mastodon-to-sqlite favourites $@ --auth $? 47 | 48 | .PHONY: datasette 49 | datasette: mastodon.db 50 | poetry run datasette serve $? --metadata metadata.json 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mastodon-to-sqlite 2 | 3 | Save data from Mastodon to a SQLite database. 4 | 5 | ## Install 6 | 7 | ```console 8 | foo@bar:~$ pip install mastodon-to-sqlite 9 | ``` 10 | 11 | ## Authentication 12 | 13 | First you will need to create an application on your Mastodon server. You 14 | can find that on your Mastodon serer. 15 | 16 | ```console 17 | foo@bar:~$ mastodon-to-sqlite auth 18 | Mastodon domain: mastodon.social 19 | 20 | Create a new application here: https://mastodon.social/settings/applications/new 21 | Then navigate to newly created application and paste in the following: 22 | 23 | Your access token: xxx 24 | ``` 25 | This will write an `auth.json` file to your current directory containing your 26 | access token and Mastodon domain. Use `-a other-file.json` to save those 27 | credentials to a different location. 28 | 29 | That `-a` option is supported by all other commands. 30 | 31 | You can verify your authentication by running `mastodon-to-sqlite 32 | verify-auth`. 33 | 34 | ## Retrieving Mastodon followers 35 | 36 | The `followers` command will retrieve all the details about your Mastodon 37 | followers. 38 | 39 | ```console 40 | foo@bar:~$ mastodon-to-sqlite followers mastodon.db 41 | ``` 42 | 43 | ## Retrieving Mastodon followings 44 | 45 | The `followings` command will retrieve all the details about your Mastodon 46 | followings. 47 | 48 | ```console 49 | foo@bar:~$ mastodon-to-sqlite followings mastodon.db 50 | ``` 51 | 52 | ## Retrieving Mastodon statuses 53 | 54 | The `statuses` command will retrieve all the details about your Mastodon 55 | statuses. 56 | 57 | ```console 58 | foo@bar:~$ mastodon-to-sqlite statuses mastodon.db 59 | ``` 60 | 61 | ## Retrieving Mastodon bookmarks 62 | 63 | The `bookmarks` command will retrieve all the details about your Mastodon 64 | bookmarks. 65 | 66 | ```console 67 | foo@bar:~$ mastodon-to-sqlite bookmarks mastodon.db 68 | ``` 69 | 70 | 71 | ## Retrieving Mastodon favourites 72 | 73 | The `favourites` command will retrieve all the details about your Mastodon 74 | favourites. 75 | 76 | ```console 77 | foo@bar:~$ mastodon-to-sqlite favourites mastodon.db 78 | ``` 79 | -------------------------------------------------------------------------------- /mastodon_to_sqlite/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myles/mastodon-to-sqlite/be1db4c31fe393f96062298cd63111f5e7299bc2/mastodon_to_sqlite/__init__.py -------------------------------------------------------------------------------- /mastodon_to_sqlite/cli.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | import click 5 | 6 | from . import service 7 | 8 | 9 | @click.group() 10 | @click.version_option() 11 | def cli(): 12 | """ 13 | Save data from Mastodon to a SQLite database. 14 | """ 15 | 16 | 17 | @cli.command() 18 | @click.option( 19 | "-a", 20 | "--auth", 21 | type=click.Path(file_okay=True, dir_okay=False, allow_dash=False), 22 | default="auth.json", 23 | help="Path to save tokens to, defaults to auth.json", 24 | ) 25 | def auth(auth): 26 | """ 27 | Save Mastodon authentication credentials to a JSON file. 28 | """ 29 | auth_file_path = Path(auth).absolute() 30 | 31 | mastodon_domain = click.prompt("Mastodon domain") 32 | click.echo("") 33 | 34 | click.echo( 35 | f"Create a new application here:" 36 | f" https://{mastodon_domain}/settings/applications/new" 37 | ) 38 | click.echo( 39 | "Then navigate to newly created application and paste in the following:" 40 | ) 41 | click.echo("") 42 | 43 | access_token = click.prompt("Your access token").strip() 44 | 45 | auth_file_content = json.dumps( 46 | { 47 | "mastodon_domain": mastodon_domain, 48 | "mastodon_access_token": access_token, 49 | }, 50 | indent=4, 51 | ) 52 | 53 | with auth_file_path.open("w") as file_obj: 54 | file_obj.write(auth_file_content + "\n") 55 | 56 | 57 | @cli.command() 58 | @click.option( 59 | "-a", 60 | "--auth", 61 | type=click.Path( 62 | file_okay=True, dir_okay=False, allow_dash=True, exists=True 63 | ), 64 | default="auth.json", 65 | help="Path to auth.json token file", 66 | ) 67 | def verify_auth(auth): 68 | """ 69 | Verify the authentication to the Mastodon server. 70 | """ 71 | if service.verify_auth(auth) is True: 72 | click.echo("Successfully authenticated with the Mastodon server.") 73 | else: 74 | click.echo( 75 | "Failed to authenticated with the Mastodon server.", err=True 76 | ) 77 | 78 | 79 | @cli.command() 80 | @click.argument( 81 | "db_path", 82 | type=click.Path(file_okay=True, dir_okay=False, allow_dash=False), 83 | required=True, 84 | ) 85 | @click.option( 86 | "-a", 87 | "--auth", 88 | type=click.Path( 89 | file_okay=True, dir_okay=False, allow_dash=True, exists=True 90 | ), 91 | default="auth.json", 92 | help="Path to auth.json token file", 93 | ) 94 | def followers(db_path, auth): 95 | """ 96 | Save followers for the authenticated user. 97 | """ 98 | db = service.open_database(db_path) 99 | client = service.get_client(auth) 100 | 101 | authenticated_account = service.get_authenticated_account(client) 102 | account_id = authenticated_account["id"] 103 | 104 | service.save_accounts(db, [authenticated_account]) 105 | 106 | with click.progressbar( 107 | service.get_followers(account_id, client), 108 | label="Importing followers", 109 | show_pos=True, 110 | ) as bar: 111 | for followers in bar: 112 | service.save_accounts(db, followers, follower_id=account_id) 113 | bar.pos = bar.pos + len(followers) - 1 114 | 115 | 116 | @cli.command() 117 | @click.argument( 118 | "db_path", 119 | type=click.Path(file_okay=True, dir_okay=False, allow_dash=False), 120 | required=True, 121 | ) 122 | @click.option( 123 | "-a", 124 | "--auth", 125 | type=click.Path( 126 | file_okay=True, dir_okay=False, allow_dash=True, exists=True 127 | ), 128 | default="auth.json", 129 | help="Path to auth.json token file", 130 | ) 131 | def followings(db_path, auth): 132 | """ 133 | Save followings for the authenticated user. 134 | """ 135 | db = service.open_database(db_path) 136 | client = service.get_client(auth) 137 | 138 | authenticated_account = service.get_authenticated_account(client) 139 | account_id = authenticated_account["id"] 140 | 141 | service.save_accounts(db, [authenticated_account]) 142 | 143 | with click.progressbar( 144 | service.get_followings(account_id, client), 145 | label="Importing followings", 146 | show_pos=True, 147 | ) as bar: 148 | for followers in bar: 149 | service.save_accounts(db, followers, followed_id=account_id) 150 | bar.pos = bar.pos + len(followers) - 1 151 | 152 | 153 | @cli.command() 154 | @click.argument( 155 | "db_path", 156 | type=click.Path(file_okay=True, dir_okay=False, allow_dash=False), 157 | required=True, 158 | ) 159 | @click.option( 160 | "-a", 161 | "--auth", 162 | type=click.Path( 163 | file_okay=True, dir_okay=False, allow_dash=True, exists=True 164 | ), 165 | default="auth.json", 166 | help="Path to auth.json token file", 167 | ) 168 | @click.option( 169 | "-u", 170 | "--update", 171 | is_flag=True, 172 | show_default=True, 173 | default=False, 174 | help="Update existing statuses", 175 | ) 176 | def statuses(db_path, auth, update): 177 | """ 178 | Save statuses for the authenticated user. 179 | """ 180 | db = service.open_database(db_path) 181 | client = service.get_client(auth) 182 | 183 | authenticated_account = service.get_authenticated_account(client) 184 | account_id = authenticated_account["id"] 185 | 186 | service.save_accounts(db, [authenticated_account]) 187 | 188 | since_id = None 189 | if update: 190 | since_id = service.get_most_recent_status_id(db) 191 | 192 | with click.progressbar( 193 | service.get_statuses(account_id, client, since_id=since_id), 194 | label="Importing statuses", 195 | show_pos=True, 196 | ) as bar: 197 | for statuses in bar: 198 | service.save_statuses(db, statuses) 199 | bar.pos = bar.pos + len(statuses) - 1 200 | 201 | 202 | @cli.command() 203 | @click.argument( 204 | "db_path", 205 | type=click.Path(file_okay=True, dir_okay=False, allow_dash=False), 206 | required=True, 207 | ) 208 | @click.option( 209 | "-a", 210 | "--auth", 211 | type=click.Path( 212 | file_okay=True, dir_okay=False, allow_dash=True, exists=True 213 | ), 214 | default="auth.json", 215 | help="Path to auth.json token file", 216 | ) 217 | def bookmarks(db_path, auth): 218 | """ 219 | Save bookmarks for the authenticated user. 220 | """ 221 | db = service.open_database(db_path) 222 | client = service.get_client(auth) 223 | 224 | authenticated_account = service.get_authenticated_account(client) 225 | account_id = authenticated_account["id"] 226 | 227 | service.save_accounts(db, [authenticated_account]) 228 | 229 | with click.progressbar( 230 | service.get_bookmarks(client), 231 | label="Importing bookmarks", 232 | show_pos=True, 233 | ) as bar: 234 | for bookmarks in bar: 235 | accounts = [d["account"] for d in bookmarks] 236 | service.save_accounts(db, accounts) 237 | service.save_activities(db, account_id, "bookmarked", bookmarks) 238 | bar.pos = bar.pos + len(bookmarks) - 1 239 | 240 | 241 | @cli.command() 242 | @click.argument( 243 | "db_path", 244 | type=click.Path(file_okay=True, dir_okay=False, allow_dash=False), 245 | required=True, 246 | ) 247 | @click.option( 248 | "-a", 249 | "--auth", 250 | type=click.Path( 251 | file_okay=True, dir_okay=False, allow_dash=True, exists=True 252 | ), 253 | default="auth.json", 254 | help="Path to auth.json token file", 255 | ) 256 | def favourites(db_path, auth): 257 | """ 258 | Save favourites for the authenticated user. 259 | """ 260 | db = service.open_database(db_path) 261 | client = service.get_client(auth) 262 | 263 | authenticated_account = service.get_authenticated_account(client) 264 | account_id = authenticated_account["id"] 265 | 266 | service.save_accounts(db, [authenticated_account]) 267 | 268 | with click.progressbar( 269 | service.get_favourites(client), 270 | label="Importing favourites", 271 | show_pos=True, 272 | ) as bar: 273 | for favourites in bar: 274 | accounts = [d["account"] for d in favourites] 275 | service.save_accounts(db, accounts) 276 | service.save_activities(db, account_id, "favourited", favourites) 277 | bar.pos = bar.pos + len(favourites) - 1 278 | -------------------------------------------------------------------------------- /mastodon_to_sqlite/client.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from time import sleep 3 | from typing import Dict, Generator, Optional, Tuple 4 | 5 | from requests import PreparedRequest, Request, Response, Session 6 | from requests.auth import AuthBase 7 | 8 | 9 | def get_utc_now() -> datetime.datetime: 10 | """ 11 | Returns the current datetime in UTC. 12 | """ 13 | return datetime.datetime.now(datetime.timezone.utc) 14 | 15 | 16 | class MastodonAuth(AuthBase): 17 | def __init__(self, access_token: str): 18 | self.access_token = access_token 19 | 20 | def __call__(self, r): 21 | r.headers["Authorization"] = f"Bearer {self.access_token}" 22 | return r 23 | 24 | 25 | class MastodonClient: 26 | def __init__(self, domain: str, access_token: str): 27 | self.api_url = f"https://{domain}/api/v1" 28 | 29 | self.session = Session() 30 | self.session.auth = MastodonAuth(access_token) 31 | 32 | self.session.headers[ 33 | "User-Agent" 34 | ] = "mastodon-to-sqlite (+https://github.com/myles/mastodon-to-sqlite)" 35 | 36 | def request( 37 | self, 38 | method: str, 39 | path: str, 40 | params: Optional[Dict[str, str]] = None, 41 | timeout: Optional[Tuple[int, int]] = None, 42 | **kwargs, 43 | ) -> Tuple[PreparedRequest, Response]: 44 | full_url = f"{self.api_url}/{path}" 45 | 46 | request = Request( 47 | method=method.upper(), url=full_url, params=params, **kwargs 48 | ) 49 | prepped = self.session.prepare_request(request) 50 | response = self.session.send(prepped, timeout=timeout) 51 | 52 | return prepped, response 53 | 54 | def request_paginated( 55 | self, 56 | method: str, 57 | path: str, 58 | params: Optional[Dict[str, str]] = None, 59 | timeout: Optional[Tuple[int, int]] = None, 60 | **kwargs, 61 | ) -> Generator[Tuple[PreparedRequest, Response], None, None]: 62 | next_path: Optional[str] = path 63 | 64 | while next_path is not None: 65 | request, response = self.request( 66 | method=method, 67 | path=next_path, 68 | timeout=timeout, 69 | params=params, 70 | **kwargs, 71 | ) 72 | yield request, response 73 | 74 | # If there is no Link header or the Link header does not contain a 75 | # next link, then we know there isn't pagination this endpoint or 76 | # there is no next page. 77 | if "Link" not in response.headers or "next" not in response.links: 78 | next_path = None 79 | continue 80 | 81 | # If the Mastodon server is reporting rate limit remaining of one 82 | # more call, then we should sleep until we are free to call again. 83 | # See docs: 84 | if response.headers.get("X-RateLimit-Remaining") == "1": 85 | reset_at = datetime.datetime.fromisoformat( 86 | response.headers["X-RateLimit-Reset"] 87 | ) 88 | sleep((reset_at - get_utc_now()).seconds) 89 | 90 | next_url = response.links["next"]["url"] 91 | next_path = next_url.replace(f"{self.api_url}/", "") 92 | 93 | # Resetting the params because the next_path will provide the query 94 | # parameters. 95 | params = {} 96 | 97 | def accounts_verify_credentials(self) -> Tuple[PreparedRequest, Response]: 98 | return self.request("GET", "accounts/verify_credentials") 99 | 100 | def accounts_followers( 101 | self, account_id: str 102 | ) -> Generator[Tuple[PreparedRequest, Response], None, None]: 103 | return self.request_paginated( 104 | "GET", f"accounts/{account_id}/followers", params={"limit": "80"} 105 | ) 106 | 107 | def accounts_following( 108 | self, account_id: str 109 | ) -> Generator[Tuple[PreparedRequest, Response], None, None]: 110 | return self.request_paginated( 111 | "GET", f"accounts/{account_id}/following", params={"limit": "80"} 112 | ) 113 | 114 | def accounts_statuses( 115 | self, 116 | account_id: str, 117 | since_id: Optional[str] = None, 118 | ) -> Generator[Tuple[PreparedRequest, Response], None, None]: 119 | params = {"limit": "40"} 120 | 121 | if since_id is not None: 122 | params["since_id"] = since_id 123 | 124 | return self.request_paginated( 125 | "GET", f"accounts/{account_id}/statuses", params=params 126 | ) 127 | 128 | def bookmarks( 129 | self, 130 | ) -> Generator[Tuple[PreparedRequest, Response], None, None]: 131 | return self.request_paginated( 132 | "GET", "bookmarks", params={"limit": "40"} 133 | ) 134 | 135 | def favourites( 136 | self, 137 | ) -> Generator[Tuple[PreparedRequest, Response], None, None]: 138 | return self.request_paginated( 139 | "GET", "favourites", params={"limit": "40"} 140 | ) 141 | -------------------------------------------------------------------------------- /mastodon_to_sqlite/service.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | from pathlib import Path 4 | from typing import Any, Dict, Generator, List, Optional 5 | 6 | from sqlite_utils.db import Database, Table 7 | 8 | from .client import MastodonClient 9 | 10 | 11 | def open_database(db_file_path) -> Database: 12 | """ 13 | Open the Mastodon SQLite database. 14 | """ 15 | return Database(db_file_path) 16 | 17 | 18 | def get_table(table_name: str, db: Database) -> Table: 19 | """ 20 | Returns a Table from a given db Database object. 21 | """ 22 | return Table(db=db, name=table_name) 23 | 24 | 25 | def build_database(db: Database): 26 | """ 27 | Build the Mastodon SQLite database structure. 28 | """ 29 | accounts_table = get_table("accounts", db=db) 30 | following_table = get_table("following", db=db) 31 | statuses_table = get_table("statuses", db=db) 32 | 33 | if accounts_table.exists() is False: 34 | accounts_table.create( 35 | columns={ 36 | "id": int, 37 | "username": str, 38 | "url": str, 39 | "display_name": str, 40 | "note": str, 41 | }, 42 | pk="id", 43 | ) 44 | accounts_table.enable_fts( 45 | ["username", "display_name", "note"], create_triggers=True 46 | ) 47 | 48 | if following_table.exists() is False: 49 | following_table.create( 50 | columns={"followed_id": int, "follower_id": int, "first_seen": str}, 51 | pk=("followed_id", "follower_id"), 52 | foreign_keys=( 53 | ("followed_id", "accounts", "id"), 54 | ("follower_id", "accounts", "id"), 55 | ), 56 | ) 57 | 58 | following_indexes = {tuple(i.columns) for i in following_table.indexes} 59 | if ("followed_id",) not in following_indexes: 60 | following_table.create_index(["followed_id"]) 61 | if ("follower_id",) not in following_indexes: 62 | following_table.create_index(["follower_id"]) 63 | 64 | if statuses_table.exists() is False: 65 | statuses_table.create( 66 | columns={ 67 | "id": int, 68 | "account_id": int, 69 | "content": str, 70 | "created_at": str, 71 | "replies_count": int, 72 | "favourites_count": int, 73 | "reblogs_count": int, 74 | }, 75 | pk="id", 76 | foreign_keys=(("account_id", "accounts", "id"),), 77 | ) 78 | statuses_table.enable_fts(["content"], create_triggers=True) 79 | 80 | statuses_indexes = {tuple(i.columns) for i in statuses_table.indexes} 81 | if ("account_id",) not in statuses_indexes: 82 | statuses_table.create_index(["account_id"]) 83 | 84 | status_activities_table = get_table("status_activities", db=db) 85 | if status_activities_table.exists() is False: 86 | status_activities_table.create( 87 | columns={ 88 | "account_id": int, 89 | "activity": str, # favourited, bookmarked 90 | "status_id": int, 91 | }, 92 | pk=("account_id", "activity", "status_id"), 93 | foreign_keys=( 94 | ("account_id", "accounts", "id"), 95 | ("status_id", "statuses", "id"), 96 | ), 97 | ) 98 | 99 | status_activities_indexes = { 100 | tuple(i.columns) for i in status_activities_table.indexes 101 | } 102 | if ("account_id", "activity") not in status_activities_indexes: 103 | status_activities_table.create_index(["account_id", "activity"]) 104 | if ("status_id", "activity") not in status_activities_indexes: 105 | status_activities_table.create_index(["status_id", "activity"]) 106 | 107 | 108 | def get_client(auth_file_path: str) -> MastodonClient: 109 | """ 110 | Returns a fully authenticated MastodonClient. 111 | """ 112 | with Path(auth_file_path).absolute().open() as file_obj: 113 | raw_auth = file_obj.read() 114 | 115 | auth = json.loads(raw_auth) 116 | 117 | return MastodonClient( 118 | domain=auth["mastodon_domain"], 119 | access_token=auth["mastodon_access_token"], 120 | ) 121 | 122 | 123 | def verify_auth(auth_file_path: str) -> bool: 124 | """ 125 | Verify Mastodon authentication. 126 | """ 127 | client = get_client(auth_file_path) 128 | 129 | _, response = client.accounts_verify_credentials() 130 | 131 | if response.status_code == 200: 132 | return True 133 | 134 | return False 135 | 136 | 137 | def get_authenticated_account(client: MastodonClient) -> Dict[str, Any]: 138 | """ 139 | Returns the authenticated user's account and if the db is provided insert 140 | the account. 141 | """ 142 | _, response = client.accounts_verify_credentials() 143 | response.raise_for_status() 144 | account = response.json() 145 | 146 | return account 147 | 148 | 149 | def get_followers( 150 | account_id: str, client: MastodonClient 151 | ) -> Generator[List[Dict[str, Any]], None, None]: 152 | """ 153 | Get authenticated account's followers. 154 | """ 155 | for request, response in client.accounts_followers(account_id): 156 | yield response.json() 157 | 158 | 159 | def get_followings( 160 | account_id: str, client: MastodonClient 161 | ) -> Generator[List[Dict[str, Any]], None, None]: 162 | """ 163 | Get authenticated account's followers. 164 | """ 165 | for request, response in client.accounts_following(account_id): 166 | yield response.json() 167 | 168 | 169 | def transformer_account(account: Dict[str, Any]): 170 | """ 171 | Transformer a Mastodon account, so it can be safely saved to the SQLite 172 | database. 173 | """ 174 | to_remove = [ 175 | k 176 | for k in account.keys() 177 | if k not in ("id", "username", "url", "display_name", "note") 178 | ] 179 | for key in to_remove: 180 | del account[key] 181 | 182 | 183 | def save_accounts( 184 | db: Database, 185 | accounts: List[Dict[str, Any]], 186 | followed_id: Optional[str] = None, 187 | follower_id: Optional[str] = None, 188 | ): 189 | """ 190 | Save Mastodon Accounts to the SQLite database. 191 | """ 192 | assert not (followed_id and follower_id) 193 | 194 | build_database(db) 195 | accounts_table = get_table("accounts", db=db) 196 | following_table = get_table("following", db=db) 197 | 198 | for account in accounts: 199 | transformer_account(account) 200 | 201 | accounts_table.upsert_all(accounts, pk="id") 202 | 203 | if followed_id is not None or follower_id is not None: 204 | first_seen = datetime.datetime.now(datetime.timezone.utc).isoformat() 205 | 206 | following_table.upsert_all( 207 | ( 208 | { 209 | "followed_id": followed_id or account["id"], 210 | "follower_id": follower_id or account["id"], 211 | "first_seen": first_seen, 212 | } 213 | for account in accounts 214 | ), 215 | pk=("followed_id", "follower_id"), 216 | ) 217 | 218 | 219 | def get_statuses( 220 | account_id: str, client: MastodonClient, since_id: Optional[str] = None 221 | ) -> Generator[List[Dict[str, Any]], None, None]: 222 | """ 223 | Get authenticated account's statuses. 224 | """ 225 | for request, response in client.accounts_statuses( 226 | account_id, since_id=since_id 227 | ): 228 | yield response.json() 229 | 230 | 231 | def transformer_status(status: Dict[str, Any]): 232 | """ 233 | Transformer a Mastodon status, so it can be safely saved to the SQLite 234 | database. 235 | """ 236 | account = status.pop("account") 237 | 238 | to_keep = ( 239 | "id", 240 | "created_at", 241 | "content", 242 | "reblogs_count", 243 | "favourites_count", 244 | "replies_count", 245 | ) 246 | to_remove = [k for k in status.keys() if k not in to_keep] 247 | for key in to_remove: 248 | del status[key] 249 | 250 | status["account_id"] = account["id"] 251 | 252 | 253 | def save_statuses(db: Database, statuses: List[Dict[str, Any]]): 254 | """ 255 | Save Mastodon Statuses to the SQLite database. 256 | """ 257 | build_database(db) 258 | statuses_table = get_table("statuses", db=db) 259 | 260 | for status in statuses: 261 | transformer_status(status) 262 | 263 | statuses_table.upsert_all(statuses, pk="id") 264 | 265 | 266 | def get_bookmarks( 267 | client: MastodonClient, 268 | ) -> Generator[List[Dict[str, Any]], None, None]: 269 | """ 270 | Get authenticated account's bookmarks. 271 | """ 272 | for request, response in client.bookmarks(): 273 | yield response.json() 274 | 275 | 276 | def get_favourites( 277 | client: MastodonClient, 278 | ) -> Generator[List[Dict[str, Any]], None, None]: 279 | """ 280 | Get authenticated account's favourites. 281 | """ 282 | for request, response in client.favourites(): 283 | yield response.json() 284 | 285 | 286 | def save_activities( 287 | db: Database, account_id: str, activity: str, statuses: List[Dict[str, Any]] 288 | ): 289 | """ 290 | Save Mastodon activities to the SQLite database. 291 | """ 292 | build_database(db) 293 | statuses_table = get_table("statuses", db=db) 294 | status_activities_table = get_table("status_activities", db=db) 295 | 296 | for status in statuses: 297 | transformer_status(status) 298 | 299 | statuses_table.upsert_all(statuses, pk="id") 300 | 301 | status_activities_table.upsert_all( 302 | ( 303 | { 304 | "account_id": account_id, 305 | "activity": activity, 306 | "status_id": status["id"], 307 | } 308 | for status in statuses 309 | ), 310 | pk=("account_id", "activity", "status_id"), 311 | ) 312 | 313 | 314 | def get_most_recent_status_id(db: Database) -> Optional[int]: 315 | """ 316 | Get the most recent status ID from the SQLite database. 317 | """ 318 | table = get_table("statuses", db=db) 319 | 320 | row = next( 321 | table.rows_where(order_by="created_at desc", select="id", limit=1), 322 | None, 323 | ) 324 | 325 | if row is None: 326 | return None 327 | 328 | return row["id"] 329 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "databases": { 3 | "mastodon": { 4 | "title": "Mastodon", 5 | "tables": { 6 | "accounts": { 7 | "label_column": "username", 8 | "plugins": { 9 | "datasette-render-html": { 10 | "columns": ["note"] 11 | } 12 | } 13 | }, 14 | "following": {}, 15 | "statues": { 16 | "plugins": { 17 | "datasette-render-html": { 18 | "columns": ["content"] 19 | } 20 | } 21 | }, 22 | "status_activities": {} 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "aiofiles" 5 | version = "23.2.1" 6 | description = "File support for asyncio." 7 | optional = false 8 | python-versions = ">=3.7" 9 | files = [ 10 | {file = "aiofiles-23.2.1-py3-none-any.whl", hash = "sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107"}, 11 | {file = "aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a"}, 12 | ] 13 | 14 | [[package]] 15 | name = "anyio" 16 | version = "4.2.0" 17 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 18 | optional = false 19 | python-versions = ">=3.8" 20 | files = [ 21 | {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, 22 | {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, 23 | ] 24 | 25 | [package.dependencies] 26 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 27 | idna = ">=2.8" 28 | sniffio = ">=1.1" 29 | typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} 30 | 31 | [package.extras] 32 | doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 33 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 34 | trio = ["trio (>=0.23)"] 35 | 36 | [[package]] 37 | name = "asgi-csrf" 38 | version = "0.9" 39 | description = "ASGI middleware for protecting against CSRF attacks" 40 | optional = false 41 | python-versions = "*" 42 | files = [ 43 | {file = "asgi-csrf-0.9.tar.gz", hash = "sha256:6e9d3bddaeac1a8fd33b188fe2abc8271f9085ab7be6e1a7f4d3c9df5d7f741a"}, 44 | {file = "asgi_csrf-0.9-py3-none-any.whl", hash = "sha256:e974cffb8a4ab84a28a0088acbf7a4ecc5be4a64f08dcbe19c60dea103da01c0"}, 45 | ] 46 | 47 | [package.dependencies] 48 | itsdangerous = "*" 49 | python-multipart = "*" 50 | 51 | [package.extras] 52 | test = ["asgi-lifespan", "httpx (>=0.16)", "pytest", "pytest-asyncio", "pytest-cov", "starlette"] 53 | 54 | [[package]] 55 | name = "asgiref" 56 | version = "3.7.2" 57 | description = "ASGI specs, helper code, and adapters" 58 | optional = false 59 | python-versions = ">=3.7" 60 | files = [ 61 | {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, 62 | {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, 63 | ] 64 | 65 | [package.dependencies] 66 | typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} 67 | 68 | [package.extras] 69 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] 70 | 71 | [[package]] 72 | name = "black" 73 | version = "22.12.0" 74 | description = "The uncompromising code formatter." 75 | optional = false 76 | python-versions = ">=3.7" 77 | files = [ 78 | {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, 79 | {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, 80 | {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, 81 | {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, 82 | {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, 83 | {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, 84 | {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, 85 | {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, 86 | {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, 87 | {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, 88 | {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, 89 | {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, 90 | ] 91 | 92 | [package.dependencies] 93 | click = ">=8.0.0" 94 | mypy-extensions = ">=0.4.3" 95 | pathspec = ">=0.9.0" 96 | platformdirs = ">=2" 97 | tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} 98 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 99 | 100 | [package.extras] 101 | colorama = ["colorama (>=0.4.3)"] 102 | d = ["aiohttp (>=3.7.4)"] 103 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 104 | uvloop = ["uvloop (>=0.15.2)"] 105 | 106 | [[package]] 107 | name = "certifi" 108 | version = "2024.2.2" 109 | description = "Python package for providing Mozilla's CA Bundle." 110 | optional = false 111 | python-versions = ">=3.6" 112 | files = [ 113 | {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, 114 | {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, 115 | ] 116 | 117 | [[package]] 118 | name = "charset-normalizer" 119 | version = "3.3.2" 120 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 121 | optional = false 122 | python-versions = ">=3.7.0" 123 | files = [ 124 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 125 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 126 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 127 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 128 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 129 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 130 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 131 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 132 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 133 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 134 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 135 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 136 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 137 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 138 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 139 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 140 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 141 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 142 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 143 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 144 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 145 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 146 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 147 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 148 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 149 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 150 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 151 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 152 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 153 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 154 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 155 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 156 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 157 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 158 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 159 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 160 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 161 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 162 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 163 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 164 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 165 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 166 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 167 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 168 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 169 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 170 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 171 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 172 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 173 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 174 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 175 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 176 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 177 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 178 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 179 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 180 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 181 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 182 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 183 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 184 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 185 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 186 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 187 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 188 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 189 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 190 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 191 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 192 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 193 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 194 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 195 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 196 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 197 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 198 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 199 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 200 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 201 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 202 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 203 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 204 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 205 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 206 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 207 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 208 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 209 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 210 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 211 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 212 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 213 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 214 | ] 215 | 216 | [[package]] 217 | name = "click" 218 | version = "8.1.7" 219 | description = "Composable command line interface toolkit" 220 | optional = false 221 | python-versions = ">=3.7" 222 | files = [ 223 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 224 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 225 | ] 226 | 227 | [package.dependencies] 228 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 229 | 230 | [[package]] 231 | name = "click-default-group" 232 | version = "1.2.4" 233 | description = "click_default_group" 234 | optional = false 235 | python-versions = ">=2.7" 236 | files = [ 237 | {file = "click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f"}, 238 | {file = "click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e"}, 239 | ] 240 | 241 | [package.dependencies] 242 | click = "*" 243 | 244 | [package.extras] 245 | test = ["pytest"] 246 | 247 | [[package]] 248 | name = "colorama" 249 | version = "0.4.6" 250 | description = "Cross-platform colored terminal text." 251 | optional = false 252 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 253 | files = [ 254 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 255 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 256 | ] 257 | 258 | [[package]] 259 | name = "coverage" 260 | version = "7.4.1" 261 | description = "Code coverage measurement for Python" 262 | optional = false 263 | python-versions = ">=3.8" 264 | files = [ 265 | {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, 266 | {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, 267 | {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, 268 | {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, 269 | {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, 270 | {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, 271 | {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, 272 | {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, 273 | {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, 274 | {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, 275 | {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, 276 | {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, 277 | {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, 278 | {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, 279 | {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, 280 | {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, 281 | {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, 282 | {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, 283 | {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, 284 | {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, 285 | {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, 286 | {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, 287 | {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, 288 | {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, 289 | {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, 290 | {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, 291 | {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, 292 | {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, 293 | {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, 294 | {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, 295 | {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, 296 | {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, 297 | {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, 298 | {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, 299 | {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, 300 | {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, 301 | {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, 302 | {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, 303 | {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, 304 | {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, 305 | {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, 306 | {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, 307 | {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, 308 | {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, 309 | {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, 310 | {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, 311 | {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, 312 | {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, 313 | {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, 314 | {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, 315 | {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, 316 | {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, 317 | ] 318 | 319 | [package.dependencies] 320 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 321 | 322 | [package.extras] 323 | toml = ["tomli"] 324 | 325 | [[package]] 326 | name = "datasette" 327 | version = "0.64.6" 328 | description = "An open source multi-tool for exploring and publishing data" 329 | optional = false 330 | python-versions = ">=3.7" 331 | files = [ 332 | {file = "datasette-0.64.6-py3-none-any.whl", hash = "sha256:158dbdfab1a4c613da7757518444a7664ecd39f54a617304edff4f119dd057df"}, 333 | {file = "datasette-0.64.6.tar.gz", hash = "sha256:85ca3aabca64fd9560052042aec27d3b32a1f85303853da3550434866d0fa539"}, 334 | ] 335 | 336 | [package.dependencies] 337 | aiofiles = ">=0.4" 338 | asgi-csrf = ">=0.9" 339 | asgiref = ">=3.2.10" 340 | click = ">=7.1.1" 341 | click-default-group = ">=1.2.3" 342 | httpx = ">=0.20" 343 | hupper = ">=1.9" 344 | itsdangerous = ">=1.1" 345 | janus = ">=0.6.2" 346 | Jinja2 = ">=2.10.3" 347 | mergedeep = ">=1.1.1" 348 | pint = ">=0.9" 349 | pip = "*" 350 | pluggy = ">=1.0" 351 | PyYAML = ">=5.3" 352 | setuptools = "*" 353 | uvicorn = ">=0.11" 354 | 355 | [package.extras] 356 | docs = ["blacken-docs", "codespell", "furo (==2022.9.29)", "sphinx-autobuild", "sphinx-copybutton"] 357 | rich = ["rich"] 358 | test = ["beautifulsoup4 (>=4.8.1)", "black (==22.10.0)", "blacken-docs (==1.12.1)", "cogapp (>=3.3.0)", "pytest (>=5.2.2)", "pytest-asyncio (>=0.17)", "pytest-timeout (>=1.4.2)", "pytest-xdist (>=2.2.1)", "trustme (>=0.7)"] 359 | 360 | [[package]] 361 | name = "datasette-render-html" 362 | version = "1.0" 363 | description = "Datasette plugin that renders specified cells as HTML" 364 | optional = false 365 | python-versions = "*" 366 | files = [ 367 | {file = "datasette-render-html-1.0.tar.gz", hash = "sha256:8e7a9ab21caed72cf42ebef4cecb0e84a6d2068e4681b4eeddfc48c5379949ab"}, 368 | {file = "datasette_render_html-1.0-py3-none-any.whl", hash = "sha256:c6437c015bfb25bef983451b3840fc81fe9c1636c3158064d6ce04dde0f850f2"}, 369 | ] 370 | 371 | [package.dependencies] 372 | datasette = "*" 373 | 374 | [package.extras] 375 | test = ["pytest"] 376 | 377 | [[package]] 378 | name = "exceptiongroup" 379 | version = "1.2.0" 380 | description = "Backport of PEP 654 (exception groups)" 381 | optional = false 382 | python-versions = ">=3.7" 383 | files = [ 384 | {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, 385 | {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, 386 | ] 387 | 388 | [package.extras] 389 | test = ["pytest (>=6)"] 390 | 391 | [[package]] 392 | name = "h11" 393 | version = "0.14.0" 394 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 395 | optional = false 396 | python-versions = ">=3.7" 397 | files = [ 398 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 399 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 400 | ] 401 | 402 | [[package]] 403 | name = "httpcore" 404 | version = "1.0.2" 405 | description = "A minimal low-level HTTP client." 406 | optional = false 407 | python-versions = ">=3.8" 408 | files = [ 409 | {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, 410 | {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, 411 | ] 412 | 413 | [package.dependencies] 414 | certifi = "*" 415 | h11 = ">=0.13,<0.15" 416 | 417 | [package.extras] 418 | asyncio = ["anyio (>=4.0,<5.0)"] 419 | http2 = ["h2 (>=3,<5)"] 420 | socks = ["socksio (==1.*)"] 421 | trio = ["trio (>=0.22.0,<0.23.0)"] 422 | 423 | [[package]] 424 | name = "httpx" 425 | version = "0.26.0" 426 | description = "The next generation HTTP client." 427 | optional = false 428 | python-versions = ">=3.8" 429 | files = [ 430 | {file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"}, 431 | {file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"}, 432 | ] 433 | 434 | [package.dependencies] 435 | anyio = "*" 436 | certifi = "*" 437 | httpcore = "==1.*" 438 | idna = "*" 439 | sniffio = "*" 440 | 441 | [package.extras] 442 | brotli = ["brotli", "brotlicffi"] 443 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 444 | http2 = ["h2 (>=3,<5)"] 445 | socks = ["socksio (==1.*)"] 446 | 447 | [[package]] 448 | name = "hupper" 449 | version = "1.12.1" 450 | description = "Integrated process monitor for developing and reloading daemons." 451 | optional = false 452 | python-versions = ">=3.7" 453 | files = [ 454 | {file = "hupper-1.12.1-py3-none-any.whl", hash = "sha256:e872b959f09d90be5fb615bd2e62de89a0b57efc037bdf9637fb09cdf8552b19"}, 455 | {file = "hupper-1.12.1.tar.gz", hash = "sha256:06bf54170ff4ecf4c84ad5f188dee3901173ab449c2608ad05b9bfd6b13e32eb"}, 456 | ] 457 | 458 | [package.extras] 459 | docs = ["Sphinx", "pylons-sphinx-themes", "setuptools", "watchdog"] 460 | testing = ["mock", "pytest", "pytest-cov", "watchdog"] 461 | 462 | [[package]] 463 | name = "idna" 464 | version = "3.6" 465 | description = "Internationalized Domain Names in Applications (IDNA)" 466 | optional = false 467 | python-versions = ">=3.5" 468 | files = [ 469 | {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, 470 | {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, 471 | ] 472 | 473 | [[package]] 474 | name = "iniconfig" 475 | version = "2.0.0" 476 | description = "brain-dead simple config-ini parsing" 477 | optional = false 478 | python-versions = ">=3.7" 479 | files = [ 480 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 481 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 482 | ] 483 | 484 | [[package]] 485 | name = "isort" 486 | version = "5.13.2" 487 | description = "A Python utility / library to sort Python imports." 488 | optional = false 489 | python-versions = ">=3.8.0" 490 | files = [ 491 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 492 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 493 | ] 494 | 495 | [package.extras] 496 | colors = ["colorama (>=0.4.6)"] 497 | 498 | [[package]] 499 | name = "itsdangerous" 500 | version = "2.1.2" 501 | description = "Safely pass data to untrusted environments and back." 502 | optional = false 503 | python-versions = ">=3.7" 504 | files = [ 505 | {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, 506 | {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, 507 | ] 508 | 509 | [[package]] 510 | name = "janus" 511 | version = "1.0.0" 512 | description = "Mixed sync-async queue to interoperate between asyncio tasks and classic threads" 513 | optional = false 514 | python-versions = ">=3.7" 515 | files = [ 516 | {file = "janus-1.0.0-py3-none-any.whl", hash = "sha256:2596ea5482711c1ee3ef2df6c290aaf370a13c55a007826e8f7c32d696d1d00a"}, 517 | {file = "janus-1.0.0.tar.gz", hash = "sha256:df976f2cdcfb034b147a2d51edfc34ff6bfb12d4e2643d3ad0e10de058cb1612"}, 518 | ] 519 | 520 | [package.dependencies] 521 | typing-extensions = ">=3.7.4.3" 522 | 523 | [[package]] 524 | name = "jinja2" 525 | version = "3.1.3" 526 | description = "A very fast and expressive template engine." 527 | optional = false 528 | python-versions = ">=3.7" 529 | files = [ 530 | {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, 531 | {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, 532 | ] 533 | 534 | [package.dependencies] 535 | MarkupSafe = ">=2.0" 536 | 537 | [package.extras] 538 | i18n = ["Babel (>=2.7)"] 539 | 540 | [[package]] 541 | name = "markupsafe" 542 | version = "2.1.5" 543 | description = "Safely add untrusted strings to HTML/XML markup." 544 | optional = false 545 | python-versions = ">=3.7" 546 | files = [ 547 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, 548 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, 549 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, 550 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, 551 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, 552 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, 553 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, 554 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, 555 | {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, 556 | {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, 557 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, 558 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, 559 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, 560 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, 561 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, 562 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, 563 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, 564 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, 565 | {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, 566 | {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, 567 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, 568 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, 569 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, 570 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, 571 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, 572 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, 573 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, 574 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, 575 | {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, 576 | {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, 577 | {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, 578 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, 579 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, 580 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, 581 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, 582 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, 583 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, 584 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, 585 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, 586 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, 587 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, 588 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, 589 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, 590 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, 591 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, 592 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, 593 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, 594 | {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, 595 | {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, 596 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, 597 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, 598 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, 599 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, 600 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, 601 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, 602 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, 603 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, 604 | {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, 605 | {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, 606 | {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, 607 | ] 608 | 609 | [[package]] 610 | name = "mergedeep" 611 | version = "1.3.4" 612 | description = "A deep merge function for 🐍." 613 | optional = false 614 | python-versions = ">=3.6" 615 | files = [ 616 | {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, 617 | {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, 618 | ] 619 | 620 | [[package]] 621 | name = "mypy" 622 | version = "0.991" 623 | description = "Optional static typing for Python" 624 | optional = false 625 | python-versions = ">=3.7" 626 | files = [ 627 | {file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"}, 628 | {file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"}, 629 | {file = "mypy-0.991-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6"}, 630 | {file = "mypy-0.991-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb"}, 631 | {file = "mypy-0.991-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305"}, 632 | {file = "mypy-0.991-cp310-cp310-win_amd64.whl", hash = "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c"}, 633 | {file = "mypy-0.991-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372"}, 634 | {file = "mypy-0.991-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f"}, 635 | {file = "mypy-0.991-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33"}, 636 | {file = "mypy-0.991-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05"}, 637 | {file = "mypy-0.991-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad"}, 638 | {file = "mypy-0.991-cp311-cp311-win_amd64.whl", hash = "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297"}, 639 | {file = "mypy-0.991-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813"}, 640 | {file = "mypy-0.991-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711"}, 641 | {file = "mypy-0.991-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd"}, 642 | {file = "mypy-0.991-cp37-cp37m-win_amd64.whl", hash = "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"}, 643 | {file = "mypy-0.991-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a"}, 644 | {file = "mypy-0.991-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93"}, 645 | {file = "mypy-0.991-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf"}, 646 | {file = "mypy-0.991-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135"}, 647 | {file = "mypy-0.991-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70"}, 648 | {file = "mypy-0.991-cp38-cp38-win_amd64.whl", hash = "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243"}, 649 | {file = "mypy-0.991-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d"}, 650 | {file = "mypy-0.991-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5"}, 651 | {file = "mypy-0.991-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3"}, 652 | {file = "mypy-0.991-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648"}, 653 | {file = "mypy-0.991-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476"}, 654 | {file = "mypy-0.991-cp39-cp39-win_amd64.whl", hash = "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461"}, 655 | {file = "mypy-0.991-py3-none-any.whl", hash = "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb"}, 656 | {file = "mypy-0.991.tar.gz", hash = "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06"}, 657 | ] 658 | 659 | [package.dependencies] 660 | mypy-extensions = ">=0.4.3" 661 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 662 | typing-extensions = ">=3.10" 663 | 664 | [package.extras] 665 | dmypy = ["psutil (>=4.0)"] 666 | install-types = ["pip"] 667 | python2 = ["typed-ast (>=1.4.0,<2)"] 668 | reports = ["lxml"] 669 | 670 | [[package]] 671 | name = "mypy-extensions" 672 | version = "1.0.0" 673 | description = "Type system extensions for programs checked with the mypy type checker." 674 | optional = false 675 | python-versions = ">=3.5" 676 | files = [ 677 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 678 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 679 | ] 680 | 681 | [[package]] 682 | name = "packaging" 683 | version = "23.2" 684 | description = "Core utilities for Python packages" 685 | optional = false 686 | python-versions = ">=3.7" 687 | files = [ 688 | {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, 689 | {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, 690 | ] 691 | 692 | [[package]] 693 | name = "pathspec" 694 | version = "0.12.1" 695 | description = "Utility library for gitignore style pattern matching of file paths." 696 | optional = false 697 | python-versions = ">=3.8" 698 | files = [ 699 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 700 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 701 | ] 702 | 703 | [[package]] 704 | name = "pint" 705 | version = "0.23" 706 | description = "Physical quantities module" 707 | optional = false 708 | python-versions = ">=3.9" 709 | files = [ 710 | {file = "Pint-0.23-py3-none-any.whl", hash = "sha256:df79b6b5f1beb7ed0cd55d91a0766fc55f972f757a9364e844958c05e8eb66f9"}, 711 | {file = "Pint-0.23.tar.gz", hash = "sha256:e1509b91606dbc52527c600a4ef74ffac12fff70688aff20e9072409346ec9b4"}, 712 | ] 713 | 714 | [package.dependencies] 715 | typing-extensions = "*" 716 | 717 | [package.extras] 718 | babel = ["babel (<=2.8)"] 719 | bench = ["pytest", "pytest-codspeed"] 720 | dask = ["dask"] 721 | mip = ["mip (>=1.13)"] 722 | numpy = ["numpy (>=1.19.5)"] 723 | pandas = ["pint-pandas (>=0.3)"] 724 | test = ["pytest", "pytest-benchmark", "pytest-cov", "pytest-mpl", "pytest-subtests"] 725 | testbase = ["pytest", "pytest-benchmark", "pytest-cov", "pytest-subtests"] 726 | uncertainties = ["uncertainties (>=3.1.6)"] 727 | xarray = ["xarray"] 728 | 729 | [[package]] 730 | name = "pip" 731 | version = "24.0" 732 | description = "The PyPA recommended tool for installing Python packages." 733 | optional = false 734 | python-versions = ">=3.7" 735 | files = [ 736 | {file = "pip-24.0-py3-none-any.whl", hash = "sha256:ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc"}, 737 | {file = "pip-24.0.tar.gz", hash = "sha256:ea9bd1a847e8c5774a5777bb398c19e80bcd4e2aa16a4b301b718fe6f593aba2"}, 738 | ] 739 | 740 | [[package]] 741 | name = "platformdirs" 742 | version = "4.2.0" 743 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 744 | optional = false 745 | python-versions = ">=3.8" 746 | files = [ 747 | {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, 748 | {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, 749 | ] 750 | 751 | [package.extras] 752 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 753 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 754 | 755 | [[package]] 756 | name = "pluggy" 757 | version = "1.4.0" 758 | description = "plugin and hook calling mechanisms for python" 759 | optional = false 760 | python-versions = ">=3.8" 761 | files = [ 762 | {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, 763 | {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, 764 | ] 765 | 766 | [package.extras] 767 | dev = ["pre-commit", "tox"] 768 | testing = ["pytest", "pytest-benchmark"] 769 | 770 | [[package]] 771 | name = "pytest" 772 | version = "7.4.4" 773 | description = "pytest: simple powerful testing with Python" 774 | optional = false 775 | python-versions = ">=3.7" 776 | files = [ 777 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 778 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 779 | ] 780 | 781 | [package.dependencies] 782 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 783 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 784 | iniconfig = "*" 785 | packaging = "*" 786 | pluggy = ">=0.12,<2.0" 787 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 788 | 789 | [package.extras] 790 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 791 | 792 | [[package]] 793 | name = "pytest-cov" 794 | version = "4.1.0" 795 | description = "Pytest plugin for measuring coverage." 796 | optional = false 797 | python-versions = ">=3.7" 798 | files = [ 799 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, 800 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, 801 | ] 802 | 803 | [package.dependencies] 804 | coverage = {version = ">=5.2.1", extras = ["toml"]} 805 | pytest = ">=4.6" 806 | 807 | [package.extras] 808 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 809 | 810 | [[package]] 811 | name = "pytest-mock" 812 | version = "3.12.0" 813 | description = "Thin-wrapper around the mock package for easier use with pytest" 814 | optional = false 815 | python-versions = ">=3.8" 816 | files = [ 817 | {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, 818 | {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, 819 | ] 820 | 821 | [package.dependencies] 822 | pytest = ">=5.0" 823 | 824 | [package.extras] 825 | dev = ["pre-commit", "pytest-asyncio", "tox"] 826 | 827 | [[package]] 828 | name = "python-dateutil" 829 | version = "2.8.2" 830 | description = "Extensions to the standard Python datetime module" 831 | optional = false 832 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 833 | files = [ 834 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 835 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 836 | ] 837 | 838 | [package.dependencies] 839 | six = ">=1.5" 840 | 841 | [[package]] 842 | name = "python-multipart" 843 | version = "0.0.7" 844 | description = "A streaming multipart parser for Python" 845 | optional = false 846 | python-versions = ">=3.7" 847 | files = [ 848 | {file = "python_multipart-0.0.7-py3-none-any.whl", hash = "sha256:b1fef9a53b74c795e2347daac8c54b252d9e0df9c619712691c1cc8021bd3c49"}, 849 | {file = "python_multipart-0.0.7.tar.gz", hash = "sha256:288a6c39b06596c1b988bb6794c6fbc80e6c369e35e5062637df256bee0c9af9"}, 850 | ] 851 | 852 | [package.extras] 853 | dev = ["atomicwrites (==1.2.1)", "attrs (==19.2.0)", "coverage (==6.5.0)", "hatch", "invoke (==2.2.0)", "more-itertools (==4.3.0)", "pbr (==4.3.0)", "pluggy (==1.0.0)", "py (==1.11.0)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-timeout (==2.1.0)", "pyyaml (==5.1)"] 854 | 855 | [[package]] 856 | name = "pyyaml" 857 | version = "6.0.1" 858 | description = "YAML parser and emitter for Python" 859 | optional = false 860 | python-versions = ">=3.6" 861 | files = [ 862 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 863 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 864 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 865 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 866 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 867 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 868 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 869 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 870 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 871 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 872 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 873 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 874 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 875 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 876 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 877 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 878 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 879 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 880 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, 881 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 882 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 883 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 884 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 885 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 886 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 887 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 888 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 889 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 890 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 891 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 892 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 893 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 894 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 895 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 896 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 897 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 898 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 899 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 900 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 901 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 902 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 903 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 904 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 905 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 906 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 907 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 908 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 909 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 910 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 911 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 912 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 913 | ] 914 | 915 | [[package]] 916 | name = "requests" 917 | version = "2.31.0" 918 | description = "Python HTTP for Humans." 919 | optional = false 920 | python-versions = ">=3.7" 921 | files = [ 922 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 923 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 924 | ] 925 | 926 | [package.dependencies] 927 | certifi = ">=2017.4.17" 928 | charset-normalizer = ">=2,<4" 929 | idna = ">=2.5,<4" 930 | urllib3 = ">=1.21.1,<3" 931 | 932 | [package.extras] 933 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 934 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 935 | 936 | [[package]] 937 | name = "responses" 938 | version = "0.22.0" 939 | description = "A utility library for mocking out the `requests` Python library." 940 | optional = false 941 | python-versions = ">=3.7" 942 | files = [ 943 | {file = "responses-0.22.0-py3-none-any.whl", hash = "sha256:dcf294d204d14c436fddcc74caefdbc5764795a40ff4e6a7740ed8ddbf3294be"}, 944 | {file = "responses-0.22.0.tar.gz", hash = "sha256:396acb2a13d25297789a5866b4881cf4e46ffd49cc26c43ab1117f40b973102e"}, 945 | ] 946 | 947 | [package.dependencies] 948 | requests = ">=2.22.0,<3.0" 949 | toml = "*" 950 | types-toml = "*" 951 | urllib3 = ">=1.25.10" 952 | 953 | [package.extras] 954 | tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "types-requests"] 955 | 956 | [[package]] 957 | name = "ruff" 958 | version = "0.0.254" 959 | description = "An extremely fast Python linter, written in Rust." 960 | optional = false 961 | python-versions = ">=3.7" 962 | files = [ 963 | {file = "ruff-0.0.254-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:dd58c500d039fb381af8d861ef456c3e94fd6855c3d267d6c6718c9a9fe07be0"}, 964 | {file = "ruff-0.0.254-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:688379050ae05394a6f9f9c8471587fd5dcf22149bd4304a4ede233cc4ef89a1"}, 965 | {file = "ruff-0.0.254-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1429be6d8bd3db0bf5becac3a38bd56f8421447790c50599cd90fd53417ec4"}, 966 | {file = "ruff-0.0.254-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:059a380c08e849b6f312479b18cc63bba2808cff749ad71555f61dd930e3c9a2"}, 967 | {file = "ruff-0.0.254-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3f15d5d033fd3dcb85d982d6828ddab94134686fac2c02c13a8822aa03e1321"}, 968 | {file = "ruff-0.0.254-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8deba44fd563361c488dedec90dc330763ee0c01ba54e17df54ef5820079e7e0"}, 969 | {file = "ruff-0.0.254-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef20bf798ffe634090ad3dc2e8aa6a055f08c448810a2f800ab716cc18b80107"}, 970 | {file = "ruff-0.0.254-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0deb1d7226ea9da9b18881736d2d96accfa7f328c67b7410478cc064ad1fa6aa"}, 971 | {file = "ruff-0.0.254-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27d39d697fdd7df1f2a32c1063756ee269ad8d5345c471ee3ca450636d56e8c6"}, 972 | {file = "ruff-0.0.254-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2fc21d060a3197ac463596a97d9b5db2d429395938b270ded61dd60f0e57eb21"}, 973 | {file = "ruff-0.0.254-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f70dc93bc9db15cccf2ed2a831938919e3e630993eeea6aba5c84bc274237885"}, 974 | {file = "ruff-0.0.254-py3-none-musllinux_1_2_i686.whl", hash = "sha256:09c764bc2bd80c974f7ce1f73a46092c286085355a5711126af351b9ae4bea0c"}, 975 | {file = "ruff-0.0.254-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d4385cdd30153b7aa1d8f75dfd1ae30d49c918ead7de07e69b7eadf0d5538a1f"}, 976 | {file = "ruff-0.0.254-py3-none-win32.whl", hash = "sha256:c38291bda4c7b40b659e8952167f386e86ec29053ad2f733968ff1d78b4c7e15"}, 977 | {file = "ruff-0.0.254-py3-none-win_amd64.whl", hash = "sha256:e15742df0f9a3615fbdc1ee9a243467e97e75bf88f86d363eee1ed42cedab1ec"}, 978 | {file = "ruff-0.0.254-py3-none-win_arm64.whl", hash = "sha256:b435afc4d65591399eaf4b2af86e441a71563a2091c386cadf33eaa11064dc09"}, 979 | {file = "ruff-0.0.254.tar.gz", hash = "sha256:0eb66c9520151d3bd950ea43b3a088618a8e4e10a5014a72687881e6f3606312"}, 980 | ] 981 | 982 | [[package]] 983 | name = "setuptools" 984 | version = "69.0.3" 985 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 986 | optional = false 987 | python-versions = ">=3.8" 988 | files = [ 989 | {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, 990 | {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, 991 | ] 992 | 993 | [package.extras] 994 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 995 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 996 | testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 997 | 998 | [[package]] 999 | name = "six" 1000 | version = "1.16.0" 1001 | description = "Python 2 and 3 compatibility utilities" 1002 | optional = false 1003 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 1004 | files = [ 1005 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1006 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "sniffio" 1011 | version = "1.3.0" 1012 | description = "Sniff out which async library your code is running under" 1013 | optional = false 1014 | python-versions = ">=3.7" 1015 | files = [ 1016 | {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, 1017 | {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "sqlite-fts4" 1022 | version = "1.0.3" 1023 | description = "Python functions for working with SQLite FTS4 search" 1024 | optional = false 1025 | python-versions = "*" 1026 | files = [ 1027 | {file = "sqlite-fts4-1.0.3.tar.gz", hash = "sha256:78b05eeaf6680e9dbed8986bde011e9c086a06cb0c931b3cf7da94c214e8930c"}, 1028 | {file = "sqlite_fts4-1.0.3-py3-none-any.whl", hash = "sha256:0359edd8dea6fd73c848989e1e2b1f31a50fe5f9d7272299ff0e8dbaa62d035f"}, 1029 | ] 1030 | 1031 | [package.extras] 1032 | test = ["pytest"] 1033 | 1034 | [[package]] 1035 | name = "sqlite-utils" 1036 | version = "3.36" 1037 | description = "CLI tool and Python library for manipulating SQLite databases" 1038 | optional = false 1039 | python-versions = ">=3.7" 1040 | files = [ 1041 | {file = "sqlite-utils-3.36.tar.gz", hash = "sha256:dcc311394fe86dc16f65037b0075e238efcfd2e12e65d53ed196954502996f3c"}, 1042 | {file = "sqlite_utils-3.36-py3-none-any.whl", hash = "sha256:b71e829755c2efbdcd6931a31968dee4e8bd71b3c14f0fe648b22377027c5bec"}, 1043 | ] 1044 | 1045 | [package.dependencies] 1046 | click = "*" 1047 | click-default-group = ">=1.2.3" 1048 | pluggy = "*" 1049 | python-dateutil = "*" 1050 | sqlite-fts4 = "*" 1051 | tabulate = "*" 1052 | 1053 | [package.extras] 1054 | docs = ["beanbag-docutils (>=2.0)", "codespell", "furo", "pygments-csv-lexer", "sphinx-autobuild", "sphinx-copybutton"] 1055 | flake8 = ["flake8"] 1056 | mypy = ["data-science-types", "mypy", "types-click", "types-pluggy", "types-python-dateutil", "types-tabulate"] 1057 | test = ["black", "cogapp", "hypothesis", "pytest"] 1058 | tui = ["trogon"] 1059 | 1060 | [[package]] 1061 | name = "tabulate" 1062 | version = "0.9.0" 1063 | description = "Pretty-print tabular data" 1064 | optional = false 1065 | python-versions = ">=3.7" 1066 | files = [ 1067 | {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, 1068 | {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, 1069 | ] 1070 | 1071 | [package.extras] 1072 | widechars = ["wcwidth"] 1073 | 1074 | [[package]] 1075 | name = "toml" 1076 | version = "0.10.2" 1077 | description = "Python Library for Tom's Obvious, Minimal Language" 1078 | optional = false 1079 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 1080 | files = [ 1081 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 1082 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1083 | ] 1084 | 1085 | [[package]] 1086 | name = "tomli" 1087 | version = "2.0.1" 1088 | description = "A lil' TOML parser" 1089 | optional = false 1090 | python-versions = ">=3.7" 1091 | files = [ 1092 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1093 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1094 | ] 1095 | 1096 | [[package]] 1097 | name = "types-requests" 1098 | version = "2.31.0.20240125" 1099 | description = "Typing stubs for requests" 1100 | optional = false 1101 | python-versions = ">=3.8" 1102 | files = [ 1103 | {file = "types-requests-2.31.0.20240125.tar.gz", hash = "sha256:03a28ce1d7cd54199148e043b2079cdded22d6795d19a2c2a6791a4b2b5e2eb5"}, 1104 | {file = "types_requests-2.31.0.20240125-py3-none-any.whl", hash = "sha256:9592a9a4cb92d6d75d9b491a41477272b710e021011a2a3061157e2fb1f1a5d1"}, 1105 | ] 1106 | 1107 | [package.dependencies] 1108 | urllib3 = ">=2" 1109 | 1110 | [[package]] 1111 | name = "types-toml" 1112 | version = "0.10.8.7" 1113 | description = "Typing stubs for toml" 1114 | optional = false 1115 | python-versions = "*" 1116 | files = [ 1117 | {file = "types-toml-0.10.8.7.tar.gz", hash = "sha256:58b0781c681e671ff0b5c0319309910689f4ab40e8a2431e205d70c94bb6efb1"}, 1118 | {file = "types_toml-0.10.8.7-py3-none-any.whl", hash = "sha256:61951da6ad410794c97bec035d59376ce1cbf4453dc9b6f90477e81e4442d631"}, 1119 | ] 1120 | 1121 | [[package]] 1122 | name = "typing-extensions" 1123 | version = "4.9.0" 1124 | description = "Backported and Experimental Type Hints for Python 3.8+" 1125 | optional = false 1126 | python-versions = ">=3.8" 1127 | files = [ 1128 | {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, 1129 | {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, 1130 | ] 1131 | 1132 | [[package]] 1133 | name = "urllib3" 1134 | version = "2.2.0" 1135 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1136 | optional = false 1137 | python-versions = ">=3.8" 1138 | files = [ 1139 | {file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"}, 1140 | {file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"}, 1141 | ] 1142 | 1143 | [package.extras] 1144 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 1145 | h2 = ["h2 (>=4,<5)"] 1146 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1147 | zstd = ["zstandard (>=0.18.0)"] 1148 | 1149 | [[package]] 1150 | name = "uvicorn" 1151 | version = "0.27.0.post1" 1152 | description = "The lightning-fast ASGI server." 1153 | optional = false 1154 | python-versions = ">=3.8" 1155 | files = [ 1156 | {file = "uvicorn-0.27.0.post1-py3-none-any.whl", hash = "sha256:4b85ba02b8a20429b9b205d015cbeb788a12da527f731811b643fd739ef90d5f"}, 1157 | {file = "uvicorn-0.27.0.post1.tar.gz", hash = "sha256:54898fcd80c13ff1cd28bf77b04ec9dbd8ff60c5259b499b4b12bb0917f22907"}, 1158 | ] 1159 | 1160 | [package.dependencies] 1161 | click = ">=7.0" 1162 | h11 = ">=0.8" 1163 | typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} 1164 | 1165 | [package.extras] 1166 | standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] 1167 | 1168 | [metadata] 1169 | lock-version = "2.0" 1170 | python-versions = "^3.9" 1171 | content-hash = "242e602abcfa8e981e5b2c2afd046b8337eaa2b954fcc4120fef9ffeefa583ad" 1172 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "mastodon-to-sqlite" 3 | version = "0.2.1" 4 | description = "Save data from Mastodon to a SQLite database" 5 | authors = ["Myles Braithwaite "] 6 | license = "Apache-2.0" 7 | readme = "README.md" 8 | repository = "https://github.com/myles/mastodon-to-sqlite" 9 | keywords = ["mastodon", "sqlite", "dogsheep"] 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.9" 13 | click = "^8.1.7" 14 | requests = "^2.31.0" 15 | sqlite-utils = "^3.36" 16 | 17 | [tool.poetry.group.dev.dependencies] 18 | black = "^22.12.0" 19 | isort = "^5.13.2" 20 | mypy = "^0.991" 21 | pytest = "^7.4.4" 22 | pytest-cov = "^4.1.0" 23 | pytest-mock = "^3.12.0" 24 | responses = "^0.22.0" 25 | ruff = "^0.0.254" 26 | types-requests = "^2.31.0.20240125" 27 | 28 | [tool.poetry.group.datasette.dependencies] 29 | datasette = "^0.64.6" 30 | datasette-render-html = "^1.0" 31 | 32 | [tool.poetry.scripts] 33 | mastodon-to-sqlite = "mastodon_to_sqlite.cli:cli" 34 | 35 | [tool.ruff] 36 | line-length = 80 37 | 38 | [tool.black] 39 | line-length = 80 40 | 41 | [tool.isort] 42 | profile = "black" 43 | 44 | [build-system] 45 | requires = ["poetry-core>=1.0.0"] 46 | build-backend = "poetry.core.masonry.api" 47 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myles/mastodon-to-sqlite/be1db4c31fe393f96062298cd63111f5e7299bc2/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlite_utils.db import Database 3 | 4 | 5 | @pytest.fixture 6 | def mock_db() -> Database: 7 | db = Database(memory=True) 8 | return db 9 | -------------------------------------------------------------------------------- /tests/fixture-auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "mastodon_domain": "mastodon.ooo", 3 | "mastodon_access_token": "SBY-IAmAMastodonAccessToken", 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures.py: -------------------------------------------------------------------------------- 1 | ACCOUNT_ONE = { 2 | "id": "1", 3 | "username": "finn", 4 | "acct": "finn", 5 | "display_name": "Finn the Human", 6 | "created_at": "2019-07-04T00:00:00.000Z", 7 | "note": "Homies help homies. ALWAYS.", 8 | "url": "https://mastodon.ooo/@finn", 9 | } 10 | 11 | ACCOUNT_TWO = { 12 | "id": "2", 13 | "username": "jake", 14 | "acct": "jake", 15 | "display_name": "Jake the Human", 16 | "created_at": "2019-07-04T00:00:00.000Z", 17 | "note": "", 18 | "url": "https://mastodon.ooo/@jake", 19 | } 20 | 21 | STATUS_ONE = { 22 | "id": "1", 23 | "created_at": "2021-12-20T19:46:29.073Z", 24 | "content": ( 25 | "I'm hanging the piñatas... They are all around you! Smash the" 26 | " piñatas!" 27 | ), 28 | "account": ACCOUNT_ONE, 29 | "bookmarked": False, 30 | "favourited": True, 31 | "replies_count": 1, 32 | "reblogs_count": 3, 33 | } 34 | 35 | STATUS_TWO = { 36 | "id": "2", 37 | "created_at": "2021-12-20T20:46:29.073Z", 38 | "content": "Sleds are for suckers! Just ride on my gut!", 39 | "account": ACCOUNT_TWO, 40 | "bookmarked": True, 41 | "favourited": False, 42 | } 43 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from click.testing import CliRunner 3 | 4 | from mastodon_to_sqlite import cli 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "verify_auth_return_value, expected_stdout_startswith", 9 | ( 10 | (True, "Successfully"), 11 | (False, "Failed"), 12 | ), 13 | ) 14 | def test_verify_auth( 15 | verify_auth_return_value, expected_stdout_startswith, mocker 16 | ): 17 | mocker.patch( 18 | "mastodon_to_sqlite.cli.service.verify_auth", 19 | return_value=verify_auth_return_value, 20 | ) 21 | 22 | runner = CliRunner() 23 | result = runner.invoke( 24 | cli.verify_auth, ["--auth", "tests/fixture-auth.json"] 25 | ) 26 | 27 | assert result.stdout.startswith(expected_stdout_startswith) 28 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import responses 4 | from responses import matchers 5 | 6 | from mastodon_to_sqlite.client import MastodonClient 7 | 8 | from . import fixtures 9 | 10 | 11 | @responses.activate 12 | def test_mastodon_client__request(): 13 | domain = "mastodon.example" 14 | access_token = "IAmAnAccessToken" 15 | path = "accounts/verify_credentials" 16 | url = f"https://{domain}/api/v1/{path}" 17 | 18 | responses.add( 19 | responses.Response( 20 | method="GET", 21 | url=url, 22 | json=fixtures.ACCOUNT_ONE, 23 | ) 24 | ) 25 | 26 | client = MastodonClient(domain=domain, access_token=access_token) 27 | client.request("GET", path) 28 | 29 | assert len(responses.calls) == 1 30 | call = responses.calls[-1] 31 | 32 | assert call.request.url == url 33 | 34 | assert "Authorization" in call.request.headers 35 | assert call.request.headers["Authorization"] == f"Bearer {access_token}" 36 | 37 | assert "User-Agent" in call.request.headers 38 | assert ( 39 | call.request.headers["User-Agent"] 40 | == "mastodon-to-sqlite (+https://github.com/myles/mastodon-to-sqlite)" 41 | ) 42 | 43 | 44 | @responses.activate 45 | def test_mastodon_client__request_paginated(): 46 | domain = "mastodon.example" 47 | access_token = "IAmAnAccessToken" 48 | 49 | path = "accounts/1234567890/followers" 50 | 51 | url = f"https://{domain}/api/v1/{path}" 52 | 53 | responses.add( 54 | responses.Response( 55 | method="GET", 56 | url=url, 57 | headers={ 58 | "Link": f'<{url}?max_id=9876543210>; rel="next"', 59 | "X-RateLimit-Limit": "100", 60 | "X-RateLimit-Remaining": "99", 61 | }, 62 | json=fixtures.ACCOUNT_ONE, 63 | ) 64 | ) 65 | responses.add( 66 | responses.Response( 67 | method="GET", 68 | url=url, 69 | headers={ 70 | "Link": f'<{url}>; rel="previous"', 71 | "X-RateLimit-Limit": "100", 72 | "X-RateLimit-Remaining": "98", 73 | }, 74 | match=[matchers.query_string_matcher("max_id=9876543210")], 75 | json=fixtures.ACCOUNT_TWO, 76 | ) 77 | ) 78 | 79 | client = MastodonClient(domain=domain, access_token=access_token) 80 | list(client.request_paginated("GET", path)) 81 | 82 | assert len(responses.calls) == 2 83 | 84 | 85 | @responses.activate 86 | def test_mastodon_client__request_paginated__rate_limit(mocker): 87 | mock_now = datetime.datetime.now(datetime.timezone.utc) 88 | 89 | rate_limit_reset_at = mock_now + datetime.timedelta( 90 | hours=1, minutes=1, seconds=30 91 | ) 92 | 93 | mocker.patch( 94 | "mastodon_to_sqlite.client.get_utc_now", 95 | return_value=mock_now, 96 | ) 97 | 98 | mock_sleep = mocker.patch( 99 | "mastodon_to_sqlite.client.sleep", return_value=None 100 | ) 101 | 102 | domain = "mastodon.example" 103 | access_token = "IAmAnAccessToken" 104 | path = "accounts/verify_credentials" 105 | url = f"https://{domain}/api/v1/{path}" 106 | 107 | responses.add( 108 | responses.Response( 109 | method="GET", 110 | url=url, 111 | headers={ 112 | "Link": f'<{url}?max_id=9876543210>; rel="next"', 113 | "X-RateLimit-Limit": "100", 114 | "X-RateLimit-Remaining": "1", 115 | "X-RateLimit-Reset": rate_limit_reset_at.isoformat(), 116 | }, 117 | json=fixtures.ACCOUNT_ONE, 118 | ) 119 | ) 120 | responses.add( 121 | responses.Response( 122 | method="GET", 123 | url=url, 124 | headers={ 125 | "Link": f'<{url}>; rel="previous"', 126 | "X-RateLimit-Limit": "100", 127 | "X-RateLimit-Remaining": "99", 128 | "X-RateLimit-Reset": rate_limit_reset_at.isoformat(), 129 | }, 130 | match=[matchers.query_string_matcher("max_id=9876543210")], 131 | json=fixtures.ACCOUNT_TWO, 132 | ) 133 | ) 134 | 135 | client = MastodonClient(domain=domain, access_token=access_token) 136 | list(client.request_paginated("GET", path)) 137 | 138 | mock_sleep.assert_called_once_with(3690) 139 | -------------------------------------------------------------------------------- /tests/test_services.py: -------------------------------------------------------------------------------- 1 | from mastodon_to_sqlite import service 2 | 3 | from . import fixtures 4 | 5 | 6 | def test_build_database(mock_db): 7 | service.build_database(mock_db) 8 | 9 | assert mock_db["accounts"].exists() is True 10 | assert mock_db["following"].exists() is True 11 | assert mock_db["statuses"].exists() is True 12 | 13 | 14 | def test_transformer_account(): 15 | account = fixtures.ACCOUNT_ONE.copy() 16 | 17 | service.transformer_account(account) 18 | 19 | assert account == { 20 | "id": fixtures.ACCOUNT_ONE["id"], 21 | "username": fixtures.ACCOUNT_ONE["username"], 22 | "url": fixtures.ACCOUNT_ONE["url"], 23 | "display_name": fixtures.ACCOUNT_ONE["display_name"], 24 | "note": fixtures.ACCOUNT_ONE["note"], 25 | } 26 | 27 | 28 | def test_save_accounts(mock_db): 29 | account_one = fixtures.ACCOUNT_ONE.copy() 30 | account_two = fixtures.ACCOUNT_TWO.copy() 31 | 32 | service.save_accounts(mock_db, [account_one]) 33 | 34 | assert mock_db["accounts"].count == 1 35 | assert mock_db["following"].count == 0 36 | 37 | service.save_accounts( 38 | mock_db, 39 | [account_two], 40 | followed_id=account_one["id"], 41 | ) 42 | 43 | assert mock_db["accounts"].count == 2 44 | assert mock_db["following"].count == 1 45 | 46 | service.save_accounts( 47 | mock_db, 48 | [account_two], 49 | follower_id=account_one["id"], 50 | ) 51 | 52 | assert mock_db["accounts"].count == 2 53 | assert mock_db["following"].count == 2 54 | 55 | 56 | def test_transformer_status(): 57 | status = fixtures.STATUS_ONE.copy() 58 | 59 | service.transformer_status(status) 60 | 61 | assert status == { 62 | "id": fixtures.STATUS_ONE["id"], 63 | "created_at": fixtures.STATUS_ONE["created_at"], 64 | "content": fixtures.STATUS_ONE["content"], 65 | "account_id": fixtures.STATUS_ONE["account"]["id"], 66 | "replies_count": fixtures.STATUS_ONE["replies_count"], 67 | "reblogs_count": fixtures.STATUS_ONE["reblogs_count"], 68 | } 69 | 70 | 71 | def test_save_statuses(mock_db): 72 | status_one = fixtures.STATUS_ONE.copy() 73 | status_two = fixtures.STATUS_TWO.copy() 74 | 75 | service.save_statuses(mock_db, [status_one, status_two]) 76 | 77 | assert mock_db["statuses"].exists() is True 78 | assert mock_db["statuses"].count == 2 79 | 80 | 81 | def test_save_activities(mock_db): 82 | status_one = fixtures.STATUS_ONE.copy() 83 | status_two = fixtures.STATUS_TWO.copy() 84 | 85 | service.save_activities( 86 | mock_db, "42", "bookmarked", [status_one, status_two] 87 | ) 88 | 89 | assert mock_db["statuses"].exists() is True 90 | assert mock_db["statuses"].count == 2 91 | assert mock_db["status_activities"].exists() is True 92 | assert mock_db["status_activities"].count == 2 93 | for row in mock_db["status_activities"].rows: 94 | assert row["account_id"] == 42 95 | 96 | 97 | def test_save_multiple_activity_types(mock_db): 98 | status_one = fixtures.STATUS_ONE 99 | status_two = fixtures.STATUS_TWO 100 | 101 | service.save_activities( 102 | mock_db, "42", "bookmarked", [status_one.copy(), status_two.copy()] 103 | ) 104 | service.save_activities( 105 | mock_db, "42", "favourited", [status_one.copy(), status_two.copy()] 106 | ) 107 | 108 | assert mock_db["statuses"].exists() is True 109 | assert mock_db["statuses"].count == 2 110 | assert mock_db["status_activities"].exists() is True 111 | assert mock_db["status_activities"].count == 4 112 | for row in mock_db["status_activities"].rows: 113 | assert row["account_id"] == 42 114 | 115 | 116 | def test_get_most_recent_status_id(mock_db): 117 | result = service.get_most_recent_status_id(mock_db) 118 | assert result is None 119 | 120 | status_one = fixtures.STATUS_ONE.copy() 121 | status_two = fixtures.STATUS_TWO.copy() 122 | 123 | service.save_statuses(mock_db, [status_one, status_two]) 124 | 125 | result = service.get_most_recent_status_id(mock_db) 126 | assert result == int(status_two["id"]) 127 | --------------------------------------------------------------------------------