├── .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 |
--------------------------------------------------------------------------------