pax_global_header00006660000000000000000000000064143662354400014521gustar00rootroot0000000000000052 comment=a16b00f3ca174de7fb128ee587f3c90a039cfe5d flask-sqlalchemy-3.0.3/000077500000000000000000000000001436623544000147645ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/.editorconfig000066400000000000000000000003311436623544000174360ustar00rootroot00000000000000root = true [*] indent_style = space indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true end_of_line = lf charset = utf-8 max_line_length = 88 [*.{yml,yaml,json,js,css,html}] indent_size = 2 flask-sqlalchemy-3.0.3/.flake8000066400000000000000000000007201436623544000161360ustar00rootroot00000000000000[flake8] extend-select = # bugbear B # bugbear opinions B9 # implicit str concat ISC extend-ignore = # slice notation whitespace, invalid E203 # line length, handled by bugbear B950 E501 # bare except, handled by bugbear B001 E722 # zip with strict=, requires python >= 3.10 B905 # string formatting opinion, B028 renamed to B907 B028 B907 # up to 88 allowed by bugbear B950 max-line-length = 80 flask-sqlalchemy-3.0.3/.github/000077500000000000000000000000001436623544000163245ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/.github/ISSUE_TEMPLATE/000077500000000000000000000000001436623544000205075ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/.github/ISSUE_TEMPLATE/bug-report.md000066400000000000000000000013701436623544000231200ustar00rootroot00000000000000--- name: Bug report about: Report a bug in Flask-SQLAlchemy (not SQLAlchemy, not other projects which depend on Flask-SQLAlchemy) --- Environment: - Python version: - Flask-SQLAlchemy version: - SQLAlchemy version: flask-sqlalchemy-3.0.3/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000007641436623544000225060ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: Security issue url: security@palletsprojects.com about: Do not report security issues publicly. Email our security contact. - name: Questions url: https://stackoverflow.com/questions/tagged/flask-sqlalchemy?tab=Frequent about: Search for and ask questions about your code on Stack Overflow. - name: Questions and discussions url: https://discord.gg/pallets about: Discuss questions about your code on our Discord chat. flask-sqlalchemy-3.0.3/.github/ISSUE_TEMPLATE/feature-request.md000066400000000000000000000006531436623544000241560ustar00rootroot00000000000000--- name: Feature request about: Suggest a new feature for Flask-SQLAlchemy --- flask-sqlalchemy-3.0.3/.github/dependabot.yml000066400000000000000000000002471436623544000211570ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" day: "monday" time: "16:00" timezone: "UTC" flask-sqlalchemy-3.0.3/.github/pull_request_template.md000066400000000000000000000017131436623544000232670ustar00rootroot00000000000000 - fixes # Checklist: - [ ] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change. - [ ] Add or update relevant docs, in the docs folder and in code. - [ ] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue. - [ ] Add `.. versionchanged::` entries in any relevant code docs. - [ ] Run `pre-commit` hooks and fix any issues. - [ ] Run `pytest` and `tox`, no tests failed. flask-sqlalchemy-3.0.3/.github/workflows/000077500000000000000000000000001436623544000203615ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/.github/workflows/lock.yaml000066400000000000000000000011501436623544000221720ustar00rootroot00000000000000name: 'Lock threads' # Lock closed issues that have not received any further activity for # two weeks. This does not close open issues, only humans may do that. # We find that it is easier to respond to new issues with fresh examples # rather than continuing discussions on old issues. on: schedule: - cron: '0 0 * * *' permissions: issues: write pull-requests: write concurrency: group: lock jobs: lock: runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@c1b35aecc5cdb1a34539d14196df55838bb2f836 with: issue-inactive-days: 14 pr-inactive-days: 14 flask-sqlalchemy-3.0.3/.github/workflows/publish.yaml000066400000000000000000000054371436623544000227240ustar00rootroot00000000000000name: Publish on: push: tags: - '*' jobs: build: runs-on: ubuntu-latest outputs: hash: ${{ steps.hash.outputs.hash }} steps: - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c - uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 with: python-version: '3.x' cache: 'pip' cache-dependency-path: 'pdm.lock' - uses: actions/cache@58c146cc91c5b9e778e71775dfe9bf1442ad9a12 with: path: ~/.cache/pdm key: ${{ matrix.python }}-pdm-${{ hashFiles('pdm.lock') }} restore-keys: ${{ matrix.python }}-pdm- - run: pip install pdm # Use the commit date instead of the current date during the build. - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV - run: pdm build # Generate hashes used for provenance. - name: generate hash id: hash run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce with: path: ./dist provenance: needs: ['build'] permissions: actions: read id-token: write contents: write # Can't pin with hash due to how this workflow works. uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.4.0 with: base64-subjects: ${{ needs.build.outputs.hash }} create-release: # Upload the sdist, wheels, and provenance to a GitHub release. They remain # available as build artifacts for a while as well. needs: ['provenance'] runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a - name: create release run: > gh release create --draft --repo ${{ github.repository }} ${{ github.ref_name }} *.intoto.jsonl/* artifact/* env: GH_TOKEN: ${{ github.token }} publish-pypi: needs: ['provenance'] # Wait for approval before attempting to upload to PyPI. This allows reviewing the # files in the draft release. environment: 'publish' runs-on: ubuntu-latest steps: - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # Try uploading to Test PyPI first, in case something fails. - uses: pypa/gh-action-pypi-publish@c7f29f7adef1a245bd91520e94867e5c6eedddcc with: password: ${{ secrets.TEST_PYPI_TOKEN }} repository_url: https://test.pypi.org/legacy/ packages_dir: artifact/ - uses: pypa/gh-action-pypi-publish@c7f29f7adef1a245bd91520e94867e5c6eedddcc with: password: ${{ secrets.PYPI_TOKEN }} packages_dir: artifact/ flask-sqlalchemy-3.0.3/.github/workflows/tests.yaml000066400000000000000000000032731436623544000224140ustar00rootroot00000000000000name: Tests on: push: branches: - main - '*.x' paths-ignore: - 'docs/**' - '*.md' - '*.rst' pull_request: branches: - main - '*.x' paths-ignore: - 'docs/**' - '*.md' - '*.rst' jobs: tests: name: ${{ matrix.name }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - {name: '3.11', python: '3.11', tox: py311} - {name: 'Lowest', python: '3.11', tox: py311-lowest} - {name: '3.10', python: '3.10', tox: py310} - {name: '3.9', python: '3.9', tox: py39} - {name: '3.8', python: '3.8', tox: py38} - {name: '3.7', python: '3.7', tox: py37} - {name: 'Typing', python: '3.11', os: ubuntu-latest, tox: typing} steps: - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c - uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 with: python-version: ${{ matrix.python }} cache: 'pip' cache-dependency-path: 'pdm.lock' - uses: actions/cache@58c146cc91c5b9e778e71775dfe9bf1442ad9a12 with: path: ~/.cache/pdm key: ${{ matrix.python }}-pdm-${{ hashFiles('pdm.lock') }} restore-keys: ${{ matrix.python }}-pdm- - name: cache mypy uses: actions/cache@58c146cc91c5b9e778e71775dfe9bf1442ad9a12 with: path: ./.mypy_cache key: mypy|${{ matrix.python }}|${{ hashFiles('pyproject.toml') }} if: matrix.tox == 'typing' - run: | pip install pdm pdm config install.cache true pdm sync -dG tox - run: pdm run tox -e ${{ matrix.tox }} flask-sqlalchemy-3.0.3/.gitignore000066400000000000000000000002511436623544000167520ustar00rootroot00000000000000.DS_Store *.pyc *.pyo *.egg-info/ dist/ build/ docs/_build/ .tox/ .idea/ .pytest_cache/ .mypy_cache/ htmlcov/ .coverage .coverage.* .vscode/ env/ venv/ .venv/ .pdm.toml flask-sqlalchemy-3.0.3/.pre-commit-config.yaml000066400000000000000000000015641436623544000212530ustar00rootroot00000000000000ci: autoupdate_schedule: monthly repos: - repo: https://github.com/asottile/pyupgrade rev: v3.3.1 hooks: - id: pyupgrade args: ["--py37-plus"] - repo: https://github.com/asottile/reorder_python_imports rev: v3.9.0 hooks: - id: reorder-python-imports files: "^(?!examples/)" args: ["--py37-plus", "--application-directories", "src"] - repo: https://github.com/psf/black rev: 22.12.0 hooks: - id: black args: ["--target-version", "py37"] - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 hooks: - id: flake8 additional_dependencies: - flake8-bugbear - flake8-implicit-str-concat - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - id: fix-byte-order-marker - id: trailing-whitespace - id: end-of-file-fixer flask-sqlalchemy-3.0.3/.readthedocs.yaml000066400000000000000000000005211436623544000202110ustar00rootroot00000000000000version: 2 build: os: "ubuntu-22.04" tools: python: "3.10" jobs: post_install: - "curl -sSL https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py | python3 -" - "VIRTUAL_ENV=$(dirname $(dirname $(which python))) ~/.local/bin/pdm sync -dG docs" sphinx: builder: dirhtml fail_on_warning: true flask-sqlalchemy-3.0.3/CHANGES.rst000066400000000000000000000331361436623544000165740ustar00rootroot00000000000000Version 3.0.3 ------------- Released 2023-01-31 - Show helpful errors when mistakenly using multiple ``SQLAlchemy`` instances for the same app, or without calling ``init_app``. :pr:`1151` - Fix issue with getting the engine associated with a model that uses polymorphic table inheritance. :issue:`1155` Version 3.0.2 ------------- Released 2022-10-14 - Update compatibility with SQLAlchemy 2. :issue:`1122` Version 3.0.1 ------------- Released 2022-10-11 - Export typing information instead of using external typeshed definitions. :issue:`1112` - If default engine options are set, but ``SQLALCHEMY_DATABASE_URI`` is not set, an invalid default bind will not be configured. :issue:`1117` Version 3.0.0 ------------- Released 2022-10-04 - Drop support for Python 2, 3.4, 3.5, and 3.6. - Bump minimum version of Flask to 2.2. - Bump minimum version of SQLAlchemy to 1.4.18. - Remove previously deprecated code. - The session is scoped to the current app context instead of the thread. This requires that an app context is active. This ensures that the session is cleaned up after every request. - An active Flask application context is always required to access ``session`` and ``engine``, regardless of if an application was passed to the constructor. :issue:`508, 944` - Different bind keys use different SQLAlchemy ``MetaData`` registries, allowing tables in different databases to have the same name. Bind keys are stored and looked up on the resulting metadata rather than the model or table. - ``SQLALCHEMY_DATABASE_URI`` does not default to ``sqlite:///:memory:``. An error is raised if neither it nor ``SQLALCHEMY_BINDS`` define any engines. :pr:`731` - Configuring SQLite with a relative path is relative to ``app.instance_path`` instead of ``app.root_path``. The instance folder is created if necessary. :issue:`462` - Added ``get_or_404``, ``first_or_404``, ``one_or_404``, and ``paginate`` methods to the extension object. These use SQLAlchemy's preferred ``session.execute(select())`` pattern instead of the legacy query interface. :issue:`1088` - Setup methods that create the engines and session are renamed with a leading underscore. They are considered internal interfaces which may change at any time. - All parameters to ``SQLAlchemy`` except ``app`` are keyword-only. - Renamed the ``bind`` parameter to ``bind_key`` and removed the ``app`` parameter from various ``SQLAlchemy`` methods. - The extension object uses ``__getattr__`` to alias names from the SQLAlchemy package, rather than copying them as attributes. - The extension object is stored directly as ``app.extensions["sqlalchemy"]``. :issue:`698` - The session class can be customized by passing the ``class_`` key in the ``session_options`` parameter. :issue:`327` - ``SignallingSession`` is renamed to ``Session``. - ``Session.get_bind`` more closely matches the base implementation. - Model classes and the ``db`` instance are available without imports in ``flask shell``. :issue:`1089` - The ``CamelCase`` to ``snake_case`` table name converter handles more patterns correctly. If model that was already created in the database changed, either use Alembic to rename the table, or set ``__tablename__`` to keep the old name. :issue:`406` - ``Model`` ``repr`` distinguishes between transient and pending instances. :issue:`967` - A custom model class can implement ``__init_subclass__`` with class parameters. :issue:`1002` - ``db.Table`` is a subclass instead of a function. - The ``engine_options`` parameter is applied as defaults before per-engine configuration. - ``SQLALCHEMY_BINDS`` values can either be an engine URL, or a dict of engine options including URL, for each bind. ``SQLALCHEMY_DATABASE_URI`` and ``SQLALCHEMY_ENGINE_OPTIONS`` correspond to the ``None`` key and take precedence. :issue:`783` - Engines are created when calling ``init_app`` rather than the first time they are accessed. :issue:`698` - ``db.engines`` exposes the map of bind keys to engines for the current app. - ``get_engine``, ``get_tables_for_bind``, and ``get_binds`` are deprecated. - SQLite driver-level URIs that look like ``sqlite:///file:name.db?uri=true`` are supported. :issue:`998, 1045` - SQLite engines do not use ``NullPool`` if ``pool_size`` is 0. - MySQL engines use the "utf8mb4" charset by default. :issue:`875` - MySQL engines do not set ``pool_size`` to 10. - MySQL engines don't set a default for ``pool_recycle`` if not using a queue pool. :issue:`803` - ``Query`` is renamed from ``BaseQuery``. - Added ``Query.one_or_404``. - The query class is applied to ``backref`` in ``relationship``. :issue:`417` - Creating ``Pagination`` objects manually is no longer a public API. They should be created with ``db.paginate`` or ``query.paginate``. :issue:`1088` - ``Pagination.iter_pages`` and ``Query.paginate`` parameters are keyword-only. - ``Pagination`` is iterable, iterating over its items. :issue:`70` - Pagination count query is more efficient. - ``Pagination.iter_pages`` is more efficient. :issue:`622` - ``Pagination.iter_pages`` ``right_current`` parameter is inclusive. - Pagination ``per_page`` cannot be 0. :issue:`1091` - Pagination ``max_per_page`` defaults to 100. :issue:`1091` - Added ``Pagination.first`` and ``last`` properties, which give the number of the first and last item on the page. :issue:`567` - ``SQLALCHEMY_RECORD_QUERIES`` is disabled by default, and is not enabled automatically with ``app.debug`` or ``app.testing``. :issue:`1092` - ``get_debug_queries`` is renamed to ``get_recorded_queries`` to better match the config and functionality. - Recorded query info is a dataclass instead of a tuple. The ``context`` attribute is renamed to ``location``. Finding the location uses a more inclusive check. - ``SQLALCHEMY_TRACK_MODIFICATIONS`` is disabled by default. :pr:`727` - ``SQLALCHEMY_COMMIT_ON_TEARDOWN`` is deprecated. It can cause various design issues that are difficult to debug. Call ``db.session.commit()`` directly instead. :issue:`216` Version 2.5.1 ------------- Released 2021-03-18 - Fix compatibility with Python 2.7. Version 2.5.0 ------------- Released 2021-03-18 - Update to support SQLAlchemy 1.4. - SQLAlchemy ``URL`` objects are immutable. Some internal methods have changed to return a new URL instead of ``None``. :issue:`885` Version 2.4.4 ------------- Released 2020-07-14 - Change base class of meta mixins to ``type``. This fixes an issue caused by a regression in CPython 3.8.4. :issue:`852` Version 2.4.3 ------------- Released 2020-05-26 - Deprecate ``SQLALCHEMY_COMMIT_ON_TEARDOWN`` as it can cause various design issues that are difficult to debug. Call ``db.session.commit()`` directly instead. :issue:`216` Version 2.4.2 ------------- Released 2020-05-25 - Fix bad pagination when records are de-duped. :pr:`812` Version 2.4.1 ------------- Released 2019-09-24 - Fix ``AttributeError`` when using multiple binds with polymorphic models. :pr:`651` Version 2.4.0 ------------- Released 2019-04-24 - Drop support for Python 2.6 and 3.3. :pr:`687` - Address SQLAlchemy 1.3 deprecations. :pr:`684` - Make engine configuration more flexible. Added the ``engine_options`` parameter and ``SQLALCHEMY_ENGINE_OPTIONS`` config. Deprecated the individual engine option config keys ``SQLALCHEMY_NATIVE_UNICODE``, ``SQLALCHEMY_POOL_SIZE``, ``SQLALCHEMY_POOL_TIMEOUT``, ``SQLALCHEMY_POOL_RECYCLE``, and ``SQLALCHEMY_MAX_OVERFLOW``. :pr:`684` - ``get_or_404()`` and ``first_or_404()`` now accept a ``description`` parameter to control the 404 message. :issue:`636` - Use ``time.perf_counter`` for Python 3 on Windows. :issue:`638` - Add an example of Flask's tutorial project, Flaskr, adapted for Flask-SQLAlchemy. :pr:`720` Version 2.3.2 ------------- Released 2017-10-11 - Don't mask the parent table for single-table inheritance models. :pr:`561` Version 2.3.1 ------------- Released 2017-10-05 - If a model has a table name that matches an existing table in the metadata, use that table. Fixes a regression where reflected tables were not picked up by models. :issue:`551` - Raise the correct error when a model has a table name but no primary key. :pr:`556` - Fix ``repr`` on models that don't have an identity because they have not been flushed yet. :issue:`555` - Allow specifying a ``max_per_page`` limit for pagination, to avoid users specifying high values in the request args. :pr:`542` - For ``paginate`` with ``error_out=False``, the minimum value for ``page`` is 1 and ``per_page`` is 0. :issue:`558` Version 2.3.0 ------------- Released 2017-09-28 - Multiple bugs with ``__tablename__`` generation are fixed. Names will be generated for models that define a primary key, but not for single-table inheritance subclasses. Names will not override a ``declared_attr``. ``PrimaryKeyConstraint`` is detected. :pr:`541` - Passing an existing ``declarative_base()`` as ``model_class`` to ``SQLAlchemy.__init__`` will use this as the base class instead of creating one. This allows customizing the metaclass used to construct the base. :issue:`546` - The undocumented ``DeclarativeMeta`` internals that the extension uses for binds and table name generation have been refactored to work as mixins. Documentation is added about how to create a custom metaclass that does not do table name generation. :issue:`546` - Model and metaclass code has been moved to a new ``models`` module. ``_BoundDeclarativeMeta`` is renamed to ``DefaultMeta``; the old name will be removed in 3.0. :issue:`546` - Models have a default ``repr`` that shows the model name and primary key. :pr:`530` - Fixed a bug where using ``init_app`` would cause connectors to always use the ``current_app`` rather than the app they were created for. This caused issues when multiple apps were registered with the extension. :pr:`547` Version 2.2 ----------- Released 2017-02-27, codename Dubnium - Minimum SQLAlchemy version is 0.8 due to use of ``sqlalchemy.inspect``. - Added support for custom ``query_class`` and ``model_class`` as args to the ``SQLAlchemy`` constructor. :pr:`328` - Allow listening to SQLAlchemy events on ``db.session``. :pr:`364` - Allow ``__bind_key__`` on abstract models. :pr:`373` - Allow ``SQLALCHEMY_ECHO`` to be a string. :issue:`409` - Warn when ``SQLALCHEMY_DATABASE_URI`` is not set. :pr:`443` - Don't let pagination generate invalid page numbers. :issue:`460` - Drop support of Flask < 0.10. This means the db session is always tied to the app context and its teardown event. :issue:`461` - Tablename generation logic no longer accesses class properties unless they are ``declared_attr``. :issue:`467` Version 2.1 ----------- Released 2015-10-23, codename Caesium - Table names are automatically generated in more cases, including subclassing mixins and abstract models. - Allow using a custom MetaData object. - Add support for binds parameter to session. Version 2.0 ----------- Released 2014-08-29, codename Bohrium - Changed how the builtin signals are subscribed to skip non-Flask-SQLAlchemy sessions. This will also fix the attribute error about model changes not existing. - Added a way to control how signals for model modifications are tracked. - Made the ``SignallingSession`` a public interface and added a hook for customizing session creation. - If the ``bind`` parameter is given to the signalling session it will no longer cause an error that a parameter is given twice. - Added working table reflection support. - Enabled autoflush by default. - Consider ``SQLALCHEMY_COMMIT_ON_TEARDOWN`` harmful and remove from docs. Version 1.0 ----------- Released 2013-07-20, codename Aurum - Added Python 3.3 support. - Dropped Python 2.5 compatibility. - Various bugfixes. - Changed versioning format to do major releases for each update now. Version 0.16 ------------ - New distribution format (flask_sqlalchemy). - Added support for Flask 0.9 specifics. Version 0.15 ------------ - Added session support for multiple databases. Version 0.14 ------------ - Make relative sqlite paths relative to the application root. Version 0.13 ------------ - Fixed an issue with Flask-SQLAlchemy not selecting the correct binds. Version 0.12 ------------ - Added support for multiple databases. - Expose ``BaseQuery`` as ``db.Query``. - Set default ``query_class`` for ``db.relation``, ``db.relationship``, and ``db.dynamic_loader`` to ``BaseQuery``. - Improved compatibility with Flask 0.7. Version 0.11 ------------ - Fixed a bug introduced in 0.10 with alternative table constructors. Version 0.10 ------------ - Added support for signals. - Table names are now automatically set from the class name unless overridden. - ``Model.query`` now always works for applications directly passed to the ``SQLAlchemy`` constructor. Furthermore the property now raises a ``RuntimeError`` instead of being ``None``. - Added session options to constructor. - Fixed a broken ``__repr__``. - ``db.Table`` is now a factory function that creates table objects. This makes it possible to omit the metadata. Version 0.9 ----------- - Applied changes to pass the Flask extension approval process. Version 0.8 ----------- - Added a few configuration keys for creating connections. - Automatically activate connection recycling for MySQL connections. - Added support for the Flask testing mode. Version 0.7 ----------- - Initial public release flask-sqlalchemy-3.0.3/CODE_OF_CONDUCT.md000066400000000000000000000064361436623544000175740ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at report@palletsprojects.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq flask-sqlalchemy-3.0.3/CONTRIBUTING.rst000066400000000000000000000154671436623544000174420ustar00rootroot00000000000000How to contribute to Flask-SQLAlchemy ===================================== Thank you for considering contributing to Flask-SQLAlchemy! Support questions ----------------- Please don't use the issue tracker for this. The issue tracker is a tool to address bugs and feature requests in Flask-SQLAlchemy itself. Use one of the following resources for questions about using Flask-SQLAlchemy or issues with your own code: - The ``#get-help`` channel on our Discord chat: https://discord.gg/pallets - The mailing list flask@python.org for long term discussion or larger issues. - Ask on `Stack Overflow`_. Search with Google first using: ``site:stackoverflow.com flask-sqlalchemy {search term, exception message, etc.}`` .. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask-sqlalchemy?tab=Frequent Reporting issues ---------------- Flask-SQLAlchemy is a thin wrapper that combines Flask and SQLAlchemy. Make sure your issue is actually with Flask-SQLAlchemy and not SQLAlchemy before submitting it. Check the traceback to see if the error is coming from SQLAlchemy. Check if your issue has already been reported to `SQLAlchemy`_. Include the following information in your post: - Describe what you expected to happen. - If possible, include a `minimal reproducible example`_ to help us identify the issue. This also helps check that the issue is not with your own code. - Describe what actually happened. Include the full traceback if there was an exception. - List your Python, Flask-SQLAlchemy, and SQLAlchemy versions. If possible, check if this issue is already fixed in the latest releases or the latest code in the repository. .. _SQLAlchemy: https://github.com/sqlalchemy/sqlalchemy/issues .. _minimal reproducible example: https://stackoverflow.com/help/minimal-reproducible-example Submitting patches ------------------ If there is not an open issue for what you want to submit, prefer opening one for discussion before working on a PR. You can work on any issue that doesn't have an open PR linked to it or a maintainer assigned to it. These show up in the sidebar. No need to ask if you can work on an issue that interests you. Include the following in your patch: - Use `Black`_ to format your code. This and other tools will run automatically if you install `pre-commit`_ using the instructions below. - All code and docs should be wrapped at 88 characters. - Include tests if your patch adds or changes code. Make sure the test fails without your patch. - Update any relevant docs pages and docstrings. - Add an entry in ``CHANGES.rst``. Use the same style as other entries. Also include ``.. versionchanged::`` inline changelogs in relevant docstrings. .. _Black: https://black.readthedocs.io .. _pre-commit: https://pre-commit.com First time setup ~~~~~~~~~~~~~~~~ - Download and install the `latest version of git`_. - Configure git with your `username`_ and `email`_. .. code-block:: text $ git config --global user.name 'your name' $ git config --global user.email 'your email' - Make sure you have a `GitHub account`_. - Fork Flask-SQLAlchemy to your GitHub account by clicking the `Fork`_ button. - `Clone`_ the main repository locally. .. code-block:: text $ git clone https://github.com/pallets-eco/flask-sqlalchemy $ cd flask-sqlalchemy - Add your fork as a remote to push your work to. Replace ``{username}`` with your username. This names the remote "fork", the default Pallets remote is "origin". .. code-block:: text git remote add fork https://github.com/{username}/flask-sqlalchemy - `Install PDM`_, the tool we use to manage the development environment. - Use PDM to set up the development environment. This automatically creates a virtualenv, installs development tools, and installs the project in editable mode. .. code-block:: text $ pdm sync - Install the pre-commit hooks. .. code-block:: text $ pdm run pre-commit install .. _latest version of git: https://git-scm.com/downloads .. _username: https://docs.github.com/en/github/using-git/setting-your-username-in-git .. _email: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address .. _GitHub account: https://github.com/join .. _Fork: https://github.com/pallets-eco/flask-sqlalchemy/fork .. _Clone: https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#step-2-create-a-local-clone-of-your-fork .. _Install PDM: https://pdm.fming.dev/latest/#installation Start coding ~~~~~~~~~~~~ - Create a branch to identify the issue you would like to work on. If you're submitting a bug or documentation fix, branch off of the latest ".x" branch. .. code-block:: text $ git fetch origin $ git checkout -b your-branch-name origin/3.0.x If you're submitting a feature addition or change, branch off of the "main" branch. .. code-block:: text $ git fetch origin $ git checkout -b your-branch-name origin/main - Using your favorite editor, make your changes, `committing as you go`_. - Include tests that cover any code changes you make. Make sure the test fails without your patch. Run the tests as described below. - Push your commits to your fork on GitHub and `create a pull request`_. Link to the issue being addressed with ``fixes #123`` in the pull request. .. code-block:: text $ git push --set-upstream fork your-branch-name .. _committing as you go: https://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes .. _create a pull request: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request Running the tests ~~~~~~~~~~~~~~~~~ Run the basic test suite with pytest. .. code-block:: text $ pdm run pytest This runs the tests for the current environment, which is usually sufficient. CI will run the full suite when you submit your pull request. You can run the full test suite in parallel with tox if you don't want to wait. .. code-block:: text $ pdm run tox -p Running test coverage ~~~~~~~~~~~~~~~~~~~~~ Generating a report of lines that do not have test coverage can indicate where to start contributing. Collect coverage from the tests and generate a report. .. code-block:: text $ pdm run pytest --cov $ pdm run coverage html Open ``htmlcov/index.html`` in your browser to explore the report. Read more about `coverage `__. Building the docs ~~~~~~~~~~~~~~~~~ Build the docs in the ``docs`` directory using Sphinx. .. code-block:: text $ cd docs $ make html Open ``_build/html/index.html`` in your browser to view the docs. Read more about `Sphinx `__. flask-sqlalchemy-3.0.3/LICENSE.rst000066400000000000000000000027031436623544000166020ustar00rootroot00000000000000Copyright 2010 Pallets Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. flask-sqlalchemy-3.0.3/README.rst000066400000000000000000000042421436623544000164550ustar00rootroot00000000000000Flask-SQLAlchemy ================ Flask-SQLAlchemy is an extension for `Flask`_ that adds support for `SQLAlchemy`_ to your application. It aims to simplify using SQLAlchemy with Flask by providing useful defaults and extra helpers that make it easier to accomplish common tasks. .. _Flask: https://palletsprojects.com/p/flask/ .. _SQLAlchemy: https://www.sqlalchemy.org Installing ---------- Install and update using `pip`_: .. code-block:: text $ pip install -U Flask-SQLAlchemy .. _pip: https://pip.pypa.io/en/stable/getting-started/ A Simple Example ---------------- .. code-block:: python from flask import Flask from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///example.sqlite" db = SQLAlchemy(app) class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String, unique=True, nullable=False) with app.app_context(): db.create_all() db.session.add(User(username="example")) db.session.commit() users = db.session.execute(db.select(User)).scalars() Contributing ------------ For guidance on setting up a development environment and how to make a contribution to Flask-SQLAlchemy, see the `contributing guidelines`_. .. _contributing guidelines: https://github.com/pallets-eco/flask-sqlalchemy/blob/main/CONTRIBUTING.rst Donate ------ The Pallets organization develops and supports Flask-SQLAlchemy and other popular packages. In order to grow the community of contributors and users, and allow the maintainers to devote more time to the projects, `please donate today`_. .. _please donate today: https://palletsprojects.com/donate Links ----- - Documentation: https://flask-sqlalchemy.palletsprojects.com/ - Changes: https://flask-sqlalchemy.palletsprojects.com/changes/ - PyPI Releases: https://pypi.org/project/Flask-SQLAlchemy/ - Source Code: https://github.com/pallets-eco/flask-sqlalchemy/ - Issue Tracker: https://github.com/pallets-eco/flask-sqlalchemy/issues/ - Website: https://palletsprojects.com/ - Twitter: https://twitter.com/PalletsTeam - Chat: https://discord.gg/pallets flask-sqlalchemy-3.0.3/artwork/000077500000000000000000000000001436623544000164555ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/artwork/flask-sqlalchemy.svg000077500000000000000000003750101436623544000224470ustar00rootroot00000000000000 image/svg+xml Flask SQLAlchemy flask-sqlalchemy-3.0.3/docs/000077500000000000000000000000001436623544000157145ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/docs/Makefile000066400000000000000000000012021436623544000173470ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= pdm run sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) flask-sqlalchemy-3.0.3/docs/_static/000077500000000000000000000000001436623544000173425ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/docs/_static/flask-sqlalchemy-logo.png000066400000000000000000000400151436623544000242460ustar00rootroot00000000000000‰PNG  IHDRª”²_sBIT|dˆ pHYs&x&xz_\tEXtSoftwarewww.inkscape.org›î< IDATxœíwœ^EÕÇ¿“lBB:II€@@z/Jï ‚(ˆ UŠD‘"øŠ‚ˆ¢REA@¤÷Š´P"J$´HHï{Þ?~g˜yî>Ïî³›Ýlv>ŸÙ}îÜiwî¹gNŸ`fT›Bí-€­M€NÀ§ÀµÀÕfönÕ¶¥¶Te Õjaàp`' W=U x8ßÌn–¶¥¶D€B蜜 „Âí9^Ö±BóaÀ÷ÍlüÂO³-}ÙSE@ ! nD[}LO÷ÃÍl~¡è öÖõúŸšÙC-3ý¶ôeIe5„°p i›Ÿgf·TÕi‡–jíÍìñf™q[úR¦:€Bø.ðO ½Ýœ`fŸ6ªã†·Ë£ ÌlÖBϸ-})S  †ö®G@: 8ÂÌnwnàS3›SÒA#Þnp °xè\hf§¶øµ¥¥3™¬ßæ!Î}0ÔËk€›½ü„¬~; ú=¦3ü÷H¯· 0˜¬Û·å¶Ü˜Ü „°pƒ¥‡˜ÙÓŽIÿìçp½‹×ßxø+0ø°6Ðè l|×?„ÇG–>´ù>±¶ôeK×IXñ´ÅÀŸ³òZà`[`:ÚÖÿhßÐ×üÚû‡“m¹-7&×§"lp•™B88!è“ÑÖ~¯éfv•ÃKþ%D¯~RýwÔ–Ú’èÌÕý÷tà'!„-K³z3‘¨é?^o»"†– !ô«0N.ôŸÛ ónK_²ÔŽ$+½Ö̦„¢=;dõ–ÎÆ[™Ù ñF¡Gábà3à¿!„ž…{[?ÌújÔ¶ÔèT,ç¿/ñÿ{ë!ÌÙ5«û¤Å‚¾ó®€È[€¡!„sÐ6¿B™1Ûµ-5:à `U3ëBxؼPo0ÄÌÞü¢a§¿ó>þ of#BC€ç*Œ·ÀÌjšùÚÒ— µ#aTB;SH£¸*Ò³ ƒÍlk3`fÏ¿¬0^6mKMJŽ,‡DM‡£-;Oç™Ùé_4aS„1ßG@=èhf“K:áZààB_³>f6£9¢--ý©ð Ø3)o²÷§ø#„и ïmfŸ •ë¤Â!„¯eíŽ,ôÕ ¸¼ù¦ß–¾,©IÆy+°²3ÍÓo\Ÿð¤uøÛ«¼  !| ÀÌæ"Æì‘B‡NkÖ§hK_Š´?¢C_C˜ò(’6*æ×MZ,¿aã~À(/›ŒY03hëñ2mO«V+Ñ–Û2h»Ïç/üð¿ ¨.¾‰DTE€»Âu¥¬~’ Ò]·k[nRÖ8Ìg&ðM/ ÀwñÉ)ÐÇ”6C ‚ FLV,?ë‹ 0¼ XÛrS²þh˯i¡jü^@Z§Ù€4æ«Í»&[ÅòŸ1˜´`#Ú€µ-76§Ð x2žñÀéÈį¶ 9]¯‡ OÊ‘}=\lû‹Ö^ŒEºðú¨;"¾ÐyCô:´öü·\´ðïÜLù›æÏ:°>ŠlTN2³?ù8+ kB¿1³3š0ö"I.ýèTÃ+ ùsÏ=ý7 {ö¿ ²›è˜ýïH]¯Þbš‡H±™È½ø»\Y¹ûŸ!¤ñ‰™M[¸h½TÎgjÄáßȾþmfû‡~ì |‰²F/ à3û‹3xѵï«ys̬’f«ÅR¡ò¤yÅÂõ@2-Þšf r\ö?ÿ=ÞÌf·Þô*§úÜ¥÷N†Òð×ð9ràB8aÕý€Í‹u´Ånfÿð1!`‰¼VoDJÓÍ켦?VÙçé¬|™6®¬LÂeªìj>ò ûiõ>-üžìy’ÿŸ™Omù1ƒÖ„ÂïjÿwA$CŸì?„õ R¢7â âx ¥OH<i!Ÿ·‚æqQ§#¥„VB‚ûF™„€²Ö«l¬ï¿ß¶5³CÇ!‘ÖÞÀWIÑÉÛdfÿòþWCdÀfš°Ÿ›q7ú¡BèÔ»›ûüÖC@Y_ú‰×>@~`"7ñ"0~n -Úb–BѦ£?‰t ò¼2° ¢—Ë%Cròç³æsÀ+f6¿E'ž¥ªBúÔÛA½Ñä£öX`3{+„p&°#°'ðuä>Ýa¥ýÍìVïcM¬X4¬nÿ:„˜›¡—'C´óØ2ù=à]ksåŽøêÀZùÄÜ»LõYÀ‹dÀk-§™8ØÁ"?XËï]ˆÌ»#ì:×ëÌEî,±u)•T]!&ì6h~V·aå;‘ˆíûÀ¦À²­Íµ.ÉaáŸ!›ã±eÞ“¡]èF_÷¾Í9‡…ƨ1…6rÀéáE_3³wCW!L¹ Šø/d´=ØË<ä÷ñ(¢©>'I .C¢³Í=¬_œ—€F~˜Š³@ÿÛÌÞ®gÞíº.z!ãÍ솅XŠ/E !¬ˆ\á·E@\$­O!À¾ÍÌ>X¨ñš Pá ÀIÜñàkˆþ»ÎöØ®CþL`wó?îŠý0®¿~‹˜„<þüÓD¯ç}î€beBV^7˜ÙTï·#ðu<¯‡¶µÕ|s‘î_fv ™B=-Ø18}=÷D»KÌ vn•ÿg"'˘?ÆšYδ-òääÛn$2/we2D\ ÜhM,4+ „6@€Ö׋ÞDŸŒ0éòÀ®H*p•×™ìlfÏxC–| 8ÀXÏëÞ‡°ð¯»,zYµ(Fë•förá+Àv oŠ0f4cœ€€y”9Û² BèŠ>¾"uG@媹Œµs¹¾Z0MDôvÙl‹÷tg„¶Bb­§Ís+ <ØÑÌ>ÎÆŒ¸ñI~} pÂÊ#‹ÙÌ>«0ï^HJ0ØøòT­Y)"]&¡è3ÿ=}”µHTÕÔÜ}81÷EäR%1¢¡]m,I¢ñú`'"9êÛÖí.qØÉÕ¿C’©/nGÉ×ì§%Õ'XÖ'‘t3QˆÊs€³¼|o½ê}l…0èc(øÅ?€¿€º,—é2Ó•¶C³:‰«]ÅÎZ5«_îûÐ>'ÉHg ®w¶ÿŸDcM&GŒ¿¨’c±14])%#£Ñ.3xx±¹Ø%4G£È:ý½¸™‹þÒÌFWž]Ër‹ë£/ö ++/_ñU ü!«3—xÝ¡EuìU^ïݼ^=shlˆ\¶¯AÌ×,Ês­1¿ tmmn» ë}vÏÕ”<íˆWGT³æU̳#2¸#gðÓJmZ £Æä˜õQCtº™B莴RšÙÉ!„¿ £mÐÖôu3{Çûø*"ÞB4ÏO=Oö5³aÙxÝC5Ôó–¤í¦)ibb"öœ°Ìw¢ÏK.×^ ðWDBüŽ9Äÿ]æÓsà ’÷Æ|$ÁYÆûX Iib¶íÛg̬–&$!q Šj}õn3³é%•Ñ—~¥_éa^Þûg#Ì÷ϬÎX`å¬-ûš·û©×›܃>†WíS-¶˜¸Ñ¿ç"¬»2?a Èòp¤~mWG Fm™ÖƤÍôŽvõµ˜HžÃQ¨§vˆ1Ü EÒyR“Ï Èy¦®…÷EÖçH`Í’:‹h(È<\Ø´ Ÿ º¥=R¥Æzo"¯ÕØÏˆVTGP*ðo(„¾Ø“È:æÙ 1P"—ïèåðbË~ÿ£µ¬ïaOdŠùˆÿ¿1{",Üí|GÏøóMG íŸS»Ž¯ÓÞˆ˜—­ÅgˆŒky€¼–gd}­°ÈÕ_ü ê¾àÀ–^g#ŸØah{º7«7œŒ^ôº‘S]a¿™s¼¿«g}´Câªïù˺ÏëVðˆ&îs»Ö_ÈéÀò­ €x7Ö³V9°=ÔÝS²ò©ž ‘\÷?§4>nž‡!Õ±‘sÜÐ?þ³HqÑWx¸(ëxݯ;@쇶‚DzzåŒ÷ã¼ï Y«_EZ±X?„q¾÷;-«;1 ·¡ðD§#Re'„=z´6€5ó;é€v¦Sôå-’Z»¾ükoÿDüI_j÷ð­FÎñYûoµ( "C†r΀Åü>N‹¢¨×³½ÔR·•‘ÅVE[ÿ(Ä AØõ]¯{0ÓœL©“â,´]èc­\ÏÜ×BZµA­ \‹xkH»ÓO‡SJVÍGôé9DF|äÉ|õ“a·1§;HäZû–|øÓ²INB†)ŸWxˆ7q#D?Î@Âý”nÉçÆèç€8a䕲¯<.ÚS°[PŠ•WDzê#ªößHt5•Ò—³gkR ¾£¡þÁn‰ÔP¸ßØ ¸È‘Fî’ô "%Ž !šQ “ÑÞóç¸óhsÛ$k·eK.Â=Ù@'{Y;Ä=^E]Yæp ›×û½?T´uÍi¥C ãôB„ÿL„‰û“"hç^°ËùK¹’„y+åIˆnݱµ©ßOÿ0sþa²>{E´9)ifmöC$Ò…5{¸1fñ(Ò+¨‹œæßa=sŒcÓR‹ÐÜqr•©Óœäìa’?шiZ›D:j°M¡¯.Þv¢Eû"LûâV‡Q*¶˜¢ HjO$äm‡ò%9Ã×zedÙöS¤%IÝm|Ú¾'™pöG’» u_DÑAüÆ(5­6®bn§zýó[êá7(L|çzêB‚ÿZ ›CrÖÈ8%Öù øJ¡Ÿe|!ç“hÔ³6#š³g¡Í¾ø× LzKùyHÓwÈ—")Ȫ~¯2B?aʧHܸ!&éZÄx®€Hµÿf÷xŸ½¼¿nh§Œ÷Ï«b~«{ÝŽ+êFê-‡®-ëG³¢^@ÿY·€Î…þj|ñjѶՋ$ûd£'#fáE–®E‚ëØ/vËúíŒÃ×Db­ª™‚Å9#~ éó1’ÿlgA[úúþ~ÿíkòI¤ašŒv±?: ŽC1¾ŠvÚÈsìWÅü¶ðºÏ´ÔœWxð5ê©{ ¢“Š´Ìá~¿Š8øs¿¾ÕïÏV+Ó_@ѳ i›z"ƒê¼ïÉKBÁaøý(«’|õ SkZï D°b2ËÆ @ªÖ#Ñ 8"LY|ÞÐNwÙNæclüØ×*~ø/¡cEs`3F~*v+¿…éäƒnÙ@ýó6s£ü~ .XGړבå~}}Æ#ƒvCñѬﻑzp'd½u‹÷9©a¯CýÓÐÖ÷M´µ­…ŠÅš$@²Ò¢ ¾aʇÑ|˜QÏ2í ZÿHø?žRšõ}ÿз TdØ×?ðXï1êÈû„l·jà9N÷6“ZÊõ ¤·éfvWm:"]òI$Ó@€›€3Ì T¼nÚY¦tîòý¦™ö™W au1½ŒôÕ÷˜Ù8ï¿)¨Dd¢O–£qô,à‡Ö‹ÙÄBØ‘*àÅ8+úuîþ "¥bŽå£Ì NÜ4²¿·ä}Ÿ‚°ä= _BvÇ,Lg>Im{³UiáB¸©mßn©/z-J¿¢Ÿ5¢m'ŒmT¡ÇÍ´!UÂVOÿ§R*iø™—wE¢«Ï©Þ°e¦¿Ð´6-—C$Ú¦Y^½PmRwÿ¸Ãª Zá>o¿Gïtæî%!„Éh»¤%õŸH¤’ÁímfO-D=píƒäž½*TJ²³|ÃóH`Œ5ÑnòË|k_ž´½ö¼6ÉC·\z‰®îî¶FÄ·òôþñípŠ™ýÅïEw£#ZPEb‰<ý‡þìBö]ƒ°ízh1WEÜë*ÔõXib˜^ËòHkÉ  ‹Qò5ëèÔ艻.Z»~hÝ û3ÑóÿE4é«ÙÁÊÌe}DJ:Ëì!àXKÆòÛ“ÂêoÔ’€º’¡Åô6Z”$à?ÁÌ^iq{"mÖšˆV^½”è]LSG¢™É$÷ä¹…ßyžGýxV¦~½ÿ«`;RÊàuÏ®óÿÑ鯷ÿïCýÎyšëëð¾¯Éûž_EžºMrËöø û#ýjvkðcóOYý3xqнE]QB£YèXô»Ð/@b ËLgSµh !tBÀ»†Ï)K[ ÑÒ*·^d©–RŽ¿; ìÔ cLEôöG G#Æõm„X&Z3E†="íhCŒÓ”2íîvG®.C[ú½‡I€º‘™Ý被;MrpHá1$ЯtâßB'“HäžK’‹£ú’"ûõBÌGgÿ_íïNž—!ù(5&µËÚ6&MCjåO³ã¢~HÂŒã­…Ïøò8 _GŒÑN¤x  äFàoõ!'[úå ЂîÒ>à·‘ª àA3ÛÅËŸDô<™×½xri }Á—¡x;x®Éþ×P>e1çå3q 4•Ô*Éãén‚|÷wBÖqù‡6 þ¯CÒŸ™…¶CÌìÉBŸk!&diumKcÔdzßCC}QïD€úºç"úiÏ„Æ"9ÚóžÇ/Iœ»o¡³=/É¥/[#¬¹5Sw€Ïst?p—y`¬Æð{À !„ÌlBV%§a‡, ás¯;/â×Q_4«OˆüFÿE—ۉ̰-7ûû[Ñ—— Ž¿œ2dBJgù»mW¦Ÿ~HóØžº&„= uÿêåŸã»þ¢8éy)Þé΋¾Œ¸¹ ½ü$Ø/—žETßCjÍAž·É+…^õ~nEôf{ıÇ<—Õä3[LC€·f !  4Îìæ”¼ò ’ÜÄüª5æL¤¾¾ŒRéË«ËLEŒú¬ïJ‹ PóßÛ˜Ù¼ ãÖ·òò♩ÿE_àêÀ fvBám¤½¸q§qAcžžç'ÕL*„i¼ÈýF T‰hÆSíSS’Ó„ýILã*Hl7‰ðTh:½ÇaÀãföF…zùXÝ‘nÿr3»I\& wš‹É†Úõð¹€LE¨†&·i¡›I{ñ°Ma°):õxR´Œo¢EûIjpÔ7Ì싃C{#EBT¯‚ìM' G´î{’'ÆzË"ËözC¦‡>ó¶Ÿxž˜ýϦ܉§ÓªÀ0Í’<ÎVOϽ<÷Cë7€ÒÃ2zÓ°,uÒ·¿„v¾§êÌÂòÈúìR3»)»µ"Ò Ö"­ÕH V< §Èýo™Íñ Mf‹ª™}B‰Ä5ÈÊþ^¤u8mácüúPo6ØÌž!ÜlB¨1ž‚üž.Ïú¿#„ð#DCÅt™=BxYî×"Œ‘à+ÔÕh ¢¼œ²7åCƒ×›BÓ‘eÑ’<4æJGí̦TAЙ„{rþ6&ÍEÛ(1¿UüÈB;¡È5£ƒ´+røû²Gýš?k¨;èÂþ•Q " E’qÛÇA‹£‚°g”§m‡õ9ôpÛ Uk¨;û$ïC‹²©×°ðòf61vnf—ºÊm_/º&„°…™]ëqRÏô1w4³ÞOIrQR/¤ïŽw£häU½ ® mQ=Ðˈ6]IêÁEæ!òe¥Gô|¡i²'^)¹Ë¿óz "×:"rî,´&P—ó+ï ÄÈ)çèç“N7i3ÿÿRNv-*@œà¿s:uéðµÇ²úÛ#‘ÕóY›/‰En-ŒqÚ6 Å»=„°™Bè‹Â>éØ÷®H¤ÇäדHÁÃ*¦Â÷yÑ-þ®qQƒ•‹GæäÛq/R0à(7-§z­T¹ëièŸî¿§"î8@=ÁšOŒ·žÏùc´+MD€7Ù̦ºñˆ\ÈSÔ—I¡ì_GÛ1½V†ØÄÿ?SRºˆD½I>á p«r’oÕ ¿Žn3Iò9À~ÿP¿ÿû ãìH©øä/”údM÷ENê§‘Ï´ÏÍܨÐ5KJFÀð´½ï8rÐvnÀw íîñwÑíhµˆvÎÝäÿRh30»·ɽEøÀyÔ“oxYô2ü¾_çÒ¾æeÏ;`u@â,CúßJ㜛õaÀ©^Þ‹Ò“¯c~ ? ¸ Ï´;)²ÝsÀJ­ X-ðÞ¢Ls9´{œ„ŒÏ/óûqMWËÚ´GØ}¤_ßãM kxa¬oe÷å÷ª=Í­9Ò#Ùï¸ýF4L”‰æ„õ¦þÿyä·¿9²+lâF&åÒÙ”jÄÎ !le2â=¢Lým‘u{£“Iì²ÂC€CCšÒ×bœ† /†9ˆzÑ‘Ïi;˜d™«zwÝçœöß y l^è»#õ‘âû/J@ÍiÐí²ß5UDe@|˜íL'Ž‚ˆùÍ(“LëH„úº¯!ô4³ßTL#’à°Âo÷8_Œ÷RVÌ@LØcî-°Ä'§·×E¶§¢í{¸çHï h_ JžCR–Þèýç€:•¤Ï)ÚÔµW^„[HOýX‹«=Iè~ œQ½örp\koÛÍðξæÏs$â'^r ûÑýgûý3 í®óò½m-"½ò€uÚt&¹[× ‘¾¨<Ÿè·½¬¯_è×1ºÉ<ÜßÑA³œóH¿gã]\Öc½|5RÈ¡S" WûùB<߆”†®ù#eôÞKJöµ1¤)ú"‘v‰@†l‹ )š·åï«k½Šã<èo1àÝ6Ù½­[PsÀ¹<+)x„á1¦*ÎØjcÿýIãu*|³MýÞ^þ¥ÏA[ÕH´3¼/sUJã†ÞÅxp…?ËþA„M@ü¹~?~”½³6½|]‡ùõh´ nV@{ÆŠ>üs(ÜcQÒ¨©ÄH‡ Æíý÷ÓYùPÿ™£í‘5Ö\ OÐiq“ÉðäÛHÞúªo!,gŠ3p¢wÿƒh¦ý¼ÞõA‡ 5:™Ù»ˆyxÑ‹öž :’qIKCÙ‘D«nŽ©QÌ©w¬ôÜ®Í`?åÌÖjˆ´ÊýP—‘ŠôéKVÎ`h¡+RúU­îåúõ „‘âý»ýþšdb)$D6àUŽ[k{ógH2ðíÖÂ*羇Ïygd‘v7Òø½ï÷ãÙ`ÇÚÝGbžþ Œóò<ÄÐõ…69YP6Ðok,ÀõÙ¤îÈÊÇį“Í€ ½ìF¿Þ“Ûê½Bß·C+ŒÛõ9°îã÷¶%I$AŠwýC)>½‘Ï|¥ŒÄù,æLRœDÍÒm(vêAÀí~?žš²aÖ& ôë~ý2Òf .¬û …±bÔZ2z·µõˆlÂSñ(sˆ;üÐÿ$«¥ë×!Ui¼Ãr×øõÃõŒ½¥M|‚‡º~åe³ÀzK®'XˆBÙØ[Sª{X®µ²žù> ¼â¿'"HŸ‰˜Ô9d;^¶¾†"Š÷p@?–ºçŒ )ŒOÁy­â|Za&ýu/?د×"qöF:–rm¿þÚV"<(ë{œZѹP,¦|üˆÅÛ“ÑŽE4Z<‡ êØY <{oJéÖ1diqÉþÑOwÀ\Í× eˈ©5œ‡ÈÚæå‡#©Š!cö¿fÏ<›Ò³Ú“b]¶ØªO.1EË9;m!1HìÛY» h{èK  uev?Êõ¶m`ü?€5ÆòD’¯>ì/ç~_ÜušéÙ’OFáö à€ÖΣOÛw6|‘séb´§øi¡]äÖB;ßdÄ(½™­õ mòC%*®Ã¢OÅ”‡ ÜÀdù#¹§¬Bˆî&O ½Ú:AÚ“˜¢HhGêO§R**»*„ÐÕ¤_>ÞËv@q‘FïÕŸi¡’)ýÑÅ#Oƒ….lŽþ›)mƒÂ#h}ŸDœÿk¦3J£(ñ‰B»¡ˆF}Ëë<Žv‘\Œøt¡ÍVÙïb)µÒ›E HAzO@@Ñ9óÅ:Q£ôC¿þ+’ËÅû±}äTŸ«bý)Õ"ý5»Ï8ªEg/}©E›…ÈÆY‘Òc+Ÿ`1»ŽÈ“ü÷[1œBžwC;ß ²(Öˆ&­õ¶}Háé÷¡t÷Ú§0õuÆ~IDATV[®wN­¸c²ÉÿÀËVòëÍÐömXïôûQï?Ö9j€¾å÷—÷ëù”‰¤\f;SzvÒîO"‰È£ã›ÈæÐ‰¤7d˜¼KsŽÑÈùÔ šñ|³‘Ää9d—‘ÁÃ…v»xùé$ùéz”qodqZ½][]½¸êï²Éß›•¿ü"ûmˆ˜zÿw¼l ’pþÒ¬}Œ#°o•óÈéÕqNE:Žå/ú˺Ó_ØBKÊÌãTJv~ÝãT1­|; 'ËG}:aÊx²É/ í~éåÛ"ÙñˆL‹ïШ+è_-»wØâ ¨ù!s€î^~&I•G4ÞßË.óëCÑv3lÛȯ"Y˜G'ÒjÜ”ÝË9ôË|õ=š™ÈÆÛ‰Ò3]g“H³=,.D;Éî$QU´<Û®Ðîaæe¹p¥¿Ÿ<ØDQ‹uhvoÕÅP¥Zª¼|=Ç,}mïßá÷÷öë«üúŸ~ձѼoT#æ² ¥ׯ¹¬Hé©û"Œ1“f&²¹¬L¦GR â9]-0þS$ןLùwHËiÏÙd†#ˆ\˜v›~>ゥKŸžXëo^þ^ƒój-@õ‰ædå˜ìmÿÚj2@™Cв7 øÀëÆ¯òx¿ŽÄ¾Ñˆw&sù Çd$Ÿ!CtëÊHôÒì$"/VD²ÅSHG–×"‘P‹’ÙÚŠ°áÄ ¼ì@ÓVhwÇ‹¼Î<ïëJŸ{4Ýüj¡]<§áêÅPóí*.¨÷/8bÐÛ²:GzYÑßËo}Føýx¬ýÐB»2ÿE¨YÇ˾‹vÈ— mÍÖ³A§ÈVTŸp¾ýGãévþÀûú—·òZ¤=è¿£»JÔÓï˜z|©:¼ŒÒc§¡¨-¬Ý ‘#+ …À$€¿œ²'ä5X§“«PzhÚ)^¾¥ÒíPì¥I¤Æj)˜±-äÚœ‚v”7õ½ìpÒ¹¢s¸Pfƒ¤­x%Ì›ýy &ö~ÿ¨B» ½ü\du!²3‰t±¯I~ [402ª´shu õ‰_Müw^¶ŒËŽh›Í# lBòˆtëU~¹õˆîmâœ~7 ?e™$Ø6$í€b0ýÝïÿ‰oÖnêz”™ËVÈDpÉ r J™¿a4BÊQfŒëP€4Ðnö}DÚDòj\äB»h㻓Ïo{„d"пI]úôx ³ü’¨y1YùMÀ%þû©¬Îy^öÂ6½HòÕ¨ŽíãÀýM…l*+ªVÉö뇲1ŸF$@G#–Ÿæí†#ìZƒ¶Áf_!uf”vœÍ÷ ?żolßqGºÄG¼Áp¤ýŠ÷ÿj¡Ýݺ‹ê2‹¾‚Dyœ_h%9#ªžcki6ùÑÙËßÂË@˜$ ⼸ýÿ̯B4Ü,JÕ±Oú»8½ž9‰4CýzJ ­Oôò­I63¼ÞØíãñ)eüÓ›aŽC\v~ªóJÏŽ½ è߈>7ðv»¡ùI’­Å^$)KàõòÛ ïóòwÔ îJÛgmÚ‘L)/X5?>ûO^m 7'q1oLrq¸ÅëßC©:6ºàƳ~ŽˆøJg×GUêFYÙ³1§‘º]“•ß=Cô»ÚÑik-¢õ[–Rÿ¬ñÀzU¶=9×uôgˆn'sSÌmQhwƒ—ÍÉ$ámŸ0RCéͳ9n_õóµ6€f›þ}DùÜüÆç&q±ìy e ÜH*Ј)"M·k`üƒ³ò¢à~/ïGòœ4$J[1†Ñ}æ$°_dþQþDóÅÏË=k™6÷$,o"yòõÈ ¥ÚeÆQŠÅÉ.!ZN­ßɾSÈüâ¼]ôš˜B#Dy­ …‡x7{ñ;xÙÑ$"ÿ§Ùý·½,’{’䫹:v,bnºT²ç—;çDÿP¸·:¥ÿƒ½üø¬l<²~ß a¢­}¼§Ÿxý]X†§ëØ´Úa¾×@ý‘Št-$œÀùü}¡M ¾œ¼ëåwÿB6FA;ˆŒ± ¸¹QÏÔÚÀYxˆ‹²ÿ7/ëïÀ·>u½7BýR,Ôç)UÇFk«oúõ(ÈAqì€èJ£À¥úý\à?1pí(å¼£÷6Äœ¬ˆ´ZŸ"+÷[Ø(%Dײ3ÚÌ×ï´ õzû\—AØî‚ #‰†QÐÀ‘”,çø\J²Ãø®÷SKu!Å”=¼QÏÓÚÀYxøm²—>§mÐöùþ;·oüµ—=ëû×m$uìî~­­Ž£‚X„RËþÕ ÷Ú€òr/ߘ¤Þœ‡´S+úÇòŒó Èqpe/ß/{Þ¯7ó.?_ÜVbÿ2õ7!ô<‹˜©_à3’¶¼X¦] •ô _¯ÝýêÌ}WA7 ¸AC $Ž«W 0äy¤ShânÑ·LÝ>×¹ˆ†@Å hûzù“þžþçåï¡ó¶õû;ÚEõïM‚‰ÖÊ ¼QöÂkqÇ/´¥ÄÓ8îÈêÜàe7ùB÷%‰ObµÈ„E º‚K…—GYì¥~½~¨B½eIrßYÀ*^~_6¯—8#696;Ö‰2Ù‡xh?†ü”‘P,ƒÁ\Çhaö]¿þ̯Ëöƒ,ôãI&ü:'ÑÂÓöÊ’Xj2¥>ÿÑšÊhbÈøVÊz9§éeû!ìÔÒR3FˆŽ}§ )Îñ¶+;¾éׇâ-eÆŽнüú%ʸ_SzN@düÖ¡ÔóòdDS¾ä¯Ý)áøó¬‡È˜¾èƒ\Ñû;–dtÒ¨°í$#ðïùu´[(ë΂lÎ 1¯ã ÷#ÃdY^™øÍög{¸¶Ð.î4 h„ Â’¨?Ïcœ/R'ÄE1M¹ÁÈÑI¢¬§É,tHF*ë"íÍ| yy½èè7Õëž ¯0Ï\¨±w.¹˜‚轸ál¯“cÞc¼ìòìw;J /ÑȘ$/ÞÈðE[Úrg¤=#_£îe‘!›é}EíàH´KD?¨½ í"þd“ᡵ²žE.j¡võòkI‘S®ÍîÇX¿õë¡$«ŸH:D s–_?Ž“ …±×Ìú}ÀmåmÖ&a¼èØ—RÃë¨lxm‹Ý) 9 çû#Aú ç!™f£]\Ùño„å;eãÕ‘ Úýqÿ£îV¨ƒ£]€Db¿ÍÖê$1˜^ìŸäkuÒR¨þ€¹ëÇÍ^¶›¿¼~ˆ‰Êym´ÅÏG>9=–ˆmÑ×êd ôT™±kýeÜ…›±•©›G[ÙÌËÎöë hËÛm“ €3½Nn×1é-YÙ<2ñ’»žEF¿6b-sr£Ž ‘JH¤/ÔÐYW|þ1Ôü´óÍ@êÑÛñc’Ê|ôµ,ÄɆ­Œ ,n®…šƒ„å5H$t*i«uþìínõúË#ŒòDÖg‰ÊÖòës²6ݼ,ªxë|”Ù3 ÎæÐ£pïŸK$!ÖAØwª?û%èxù¼Í’̸Q¢µ%Pó¨zlé哘”ü²HÆ­{?d4’GRyÌpÉÞ²ÎÑ:$.×ìI=ö¤”†ÿ‰!ß‹Xõw&’¯B²]˜K2ÄéF©¡ø 2o„‘ë½îë5EŒŽ€ùǼe=ó?ìCËÊû’¢_LRÿ¾Œx¨M<¬Ð.* ê˜U.€Ú™Òí1z{~Ç_^O’Ídt%ŠH€IHpý5yqkŒ x,Ò>½› –ÿ|ÿÒ*æZ“}4ü¬ˆU?ògú-î3„¸ìÖPËÀpf«Â¸½üÿ7 ŠÛvÜößk`þ·{½¢6ê$f[Ο姤H({"mÔ< –Y$ÉÅõ»Tª?p~4å ùG}"ôk‘%ÑDÒ1lå¾'xyÔcG_§Ëð­¸æúÝl®ÑK¡ˆUOñ—>1Ÿ“Yùsäõo/3N<ÍeD·îC¢©§“chN£g:$ÊŠ®5E—è×QD›Ý}WB|ÂDWR7°o”ËíÖÒ ¨ë^\ê×óùçþIuÂo"Ñ×þÈc4ª0×èã¿ï"Ñþ½)ÈK }G9ö[Íöî[ø ¨¹ÅÔDƒ„µ~ç_ò8DäO&aÏ.”JN÷òÍÈ—#Eý¨£yZˆùÆ3&"ìØ•äéj$û… (=+5ê5ˆ“_6ësÚº¥H|ìŸ÷]/‘·¯0ÏhÈ]rœ&²M¨%iÿ~XåsGæ÷7_V@mOiä¿è™zÚj:"fc–¿ Z‘["½–õéÀ~´gøõ Tp„kÄ|’8és¼ìO~}WV¯HÞ‹3v3_€ìQŸôgºGOÐ2 ¯É>Op;ÙzæØÉë}Ž3`Ù½[ óšˆÂUÜUH±«Œf<ö½Õ¯ /?WUNðÖÉBw¥ôœTÃãÃ#{OCrÕÏÉâÉãÇ;€LB!ç“YK!ºlH™ù¬€°ÞPÊ{Drd*²Þž˜Cý~nE1c{„}ÿZ¸1mG’— $ùp Y”Í&93.ƒ“eæãòÿ¸Âýõö~'›Ã(DC×X’¥¬ËK¡î ª´ kuÀk v¥t?ÔË·!a¡üD¾¹$&çÄYßKéìo8mSŠÁ:ƨ/FPÙ™Ri R> *Ô{´ÐïãÙ½Ó© ŒQê6žç›³¶+ƉѻkIaV&1vV¾7`Fb¼ó)píÖ~{J±ÿõÅvˆ¶7„,.FÒšÙ¯Æ×ÿ·$ÅÈÑK% Ç(º¤"bÖ¨çm_‘#dÁ£$×—²¶!Lõí/¡­ïLÙ司(wܵ/)Ü?$›ëùú¨”÷(ó¼](õáŠQõ6¤Tâ‘ç§bý÷&ÑÔ'î½Ya¼©¤àÆÈËÐîÏ·ý=èàVº&.TGJ·¢ ÷¯÷EÙ a¸½<Æ©Š|¾—G»€-Hæpû90ædÄ ”ºÈäùÿ(µÚ7’ÙawÌæº126iHVû‰?×Î…u8‘ºî!·ùü§4Ðg“NXqÀж´/få1~×L´SŒ+3æpD*Õ ±Úá$D0‡ þZèX¿•=ü=…{«ûïxXÅšÙ—ÛÃì=¯¥÷ 9᧤°;›“0ô$דr€ÚžR¦ÍÙÜèBÙûÈx$Ž×‰ÚŽG6G )ÇVhÛl—Õí„DSï5ˆõåçid”Ad\ý+JƒÙ“Ýá”f¢-þ“˜ Ëžˆ!ü¼p2…v¯´>­ p ¬1 u-eB‘#ˤ!ä†Â½èÑT.j°6Bü= õ·FŒJ%EÀxd—¹u·Ûù¤SêŠØ¥êØSÈälJÅ«Íc}þGâ’Ž*ÇìíÎ3…þ^¢îNöBs¨¥z-Ÿá犙Ùzï%2…¶Cô%(„ú …û÷"À=ø(|Î!„>è…ÝÌž ! DB÷ÓÌì£zÆì…8ù]‘ó\÷FLù-„]÷(”FõOVާ°´G~ ²܈±ÞDŒÏ“È»`lö íч7Yò+×A!ŠšfÅãæ¼Î2à !¬Œ>†æLQ°ºi­Ž›«F.t2uʶB²À¢ òy¤¿žƒh¼&ÅÚG€´ÂÎg!ì=Ë_æÌôE’ûôã(æTS·ë"všŠ€c’\lVÍæØaÅÁˆ”¸ŽÄΡB _Dåî8Ÿ¡Pôe£œ<š;+*ckZ3êAÙCå‘¢W'©ÿºû |ˆRŸû˜'!¿ŸÃh¢ßy6ngê:ÅuGÆ/ <ƒQmžˆŒ›—£ž ÑÒS( >lØ j{ ²U$3=YóÏñµºù‹Åx³•ä±Ãª|†{ÑŽtMuçà$ݽõ„:!æ¤7’Onëå·"æärd@ñ¡—÷Cñ÷Á_.½ŠÌæF"uåkVa{lÂ|bÐvC:ñU‘z5¦¹ˆ ™€s’©ŽEáŠfU1Æ1È"ªZ›×ü™^7³Y½^Há°«çY7ñøÌñ!„u‘˜n`¾™­S¯/)XCi?ïûï^ÿŸÛ……ö €o›ÙmÀ’QýC‹¾üµ¸+1é˜Éøe^‡,ÑsN{5D—Å;år Ì{sí‚ÌÜZ ‰ézÆ9ÎÌ.+ciT€ÂH¥ùRa.pN~$Ú+¥7Ð>‹°ÇèåoŠðv%Y>uË~÷AÜp_ô4”f"†oÂÎS³ßùÿùÞWÄÐTúÝ‘;õ=ôÑ€¬ÅöÄcëûü¿‡âµÖR`Šéj3;B;Ê;…<6Ò¤!„ÕHÆäyzÑò+!ߨúÒÏÌ삼 „p2ŠésDc¯îc݇Ի íUès>r©¹Ù1Ì-7ðÒ¨Ñ ë àÊ0Æ#&%æñföŸ&λ#Ö@¡žßñÿ`fñ£©b¬")É(Ëô׈î¾Ò tq§™íõw$°j‘@L“÷Š—:¥Á¹/-€ Bø#’ç\R~lfø½=v¨ÚAXòä?ÚžcÔ@¦én3›—Ñ슞;#Ìvµ™Í¨0¯ÈDçÍÌþÇßsHÛÎóÍlúB¬G;DšôG˜l( ¸8_®Ê®j‘ øÇfö¿ÂúHU¼C#§t®OY&}¨*µ6GÚÌÜmGÄDîq²ÈYÉïwA®Ê õë¾$¡öñ8–A§_ ­×D$—üÒ¿GÔøÖ@Û‹ÊGWžKiˆÌjsYÑF¬ÇM…þæ¡ÝàY„E¯$ÅB(æ¹þüÇãæ„ˆ.¿‚ò²èúò»” ïÙ˜¼TaÔ˜|Kº.h«¼©YsL3™«…0Ë ¤åŠœûÛˆ ž‰^øÞT—Ž0³¿—™S;d{°*zá m—Ó¹^£¶÷˜—£¤Ø1`ë jâÖ;¢ç ¹£<Žgx?‘D¿¡.YßüïGùÍf6§©ÏKÙÖŸ§Â:H²MáÖH$ ½ÎÌ> !¬„h¶NÔM÷"ñÖÞÈ:x®AØz¢Y—E´ñtäH÷qs?OSRá0$X/¦Ï‘ˤØxà¨ÍÚöFrçí³µFÃMCëø4r…V‰1jJZj5¦ÂÖ(êÞ”rØó#ñ1ÒÓ*§AMÐâšØ"ýmÈ,p„e*Y݆ÝÎóÔÅúFòÊ­AæD`âÂb̆ÒR¨1¹xæDÄõVËDLAF%ÃýÿÛÀè…apZ;…–GÛþšž×C¦õ)9f!ÝüEf6ªÅ'Y&}i5&í…ôÖ»!9gcÓÇHNY”]¾kf›iªMJ!„®H”ó*$ \“†…ú .ÿ ´?‹ÄRŸ¶Ä|«M_:@ÍSa´Íí€tÏRaE}ibÆ>ÌòGHU9ÍïO/fk@ÅB膤 =÷Ër/@©Kµi Ú5^ðÿ/™Ù´&ôÓbéK ¨ÅäJƒõÑV¸žçu‘μ¥Ó´³ø§qãËz®©Ü´ªd胇äǯà$™MZȾ[<µj)„°2ì(—ÐxÍLK¤“Éòx’ÁÌøæäÂujÔ…Ln»*Ú‚{{^ÉcîC²pŠ*Ðry>Ic5Ãó,¤ùÊs¬3 qòcÐáe_hЖ¶ôÿã_x ×½IEND®B`‚flask-sqlalchemy-3.0.3/docs/_static/flask-sqlalchemy-title.png000066400000000000000000000255451436623544000244420ustar00rootroot00000000000000‰PNG  IHDR®b¨7ábKGDÿÿÿ ½§“ pHYsa(™atIMEà'û09} IDATxÚíw¸Õõ÷?‹"‚Š(¢"*6Tì 5¶¼, hŒÆØˆhBlI4¨`I,(v£ˆ ˆŠT%ˆ"Ø+¶¨ Ø©J[ï{Ÿã03gι.\Ö÷yÎwêž™½÷w­µWUÅáp8Ž•µü8‡Ã‰Ëáp8'.‡Ãáp8œ¸‡ÃáÄåp8‡—Ãáp8N\‡Ãápâr8‡Ã‰Ëáp8'.‡Ãáp8q9‡ÃáÄåp8‡—Ãáp8œ¸‡Ãápâr8‡Ã‰Ëáp8N\‡Ãáp8q9‡ÃáÄåp8'.‡Ãáp8œ¸‡Ãápâr8‡—Ãáp8N\‡Ãáp¬ŒÄ%ë¯*CDš‹È!"RÛ»¦£†õí¦"²Ú ÐŽuDä9Ê¿Šײ ¬“€——Eäô>¨‰Èµö¼}½k:j@¿î "wŠÈ$àc`³jlËþ"ò40ø ðÿB+/꬀ý`àÀV@ÛÜTvCàL ÐÛÕÑ»¦c%î×»µþ|¥ý{ðúò€nÀYÀÀÀ,`$ð;ÿR5Œ¸D¤PøøøX,Ù¿‹­CÖ6Í­ðo-»îj‘ß@3`.ð*ð‘ª.‰Ý³·Màí€&±&-ªƒû_ÀaÀF š¯x×t¬„}zkà_À4à|U}GD.UÕE˱-;W“ÃUuºˆ4ú¨ê{þÅjq‰H+ ?Ð P`žýÙßџدVäß(yÕêG&b5"Üø0rÏ €?¹%á§6Àÿ 4J;¤†Nlíÿ£TµŸ¿ó][—QÕ7lû€ß¿QÕYË©-…ùk5àDUý8²ûàŸªú¢µF\&™Œ6»ADkªH¿æŸÄ¶ÿ,Ì8o~ {ï÷y·[IJDš[[û71mü3Â:Æ‹ªº0áœÚöÜ·SkxözfhbV‡ÙÀì¤g®ævndÒxTUÿ¶ îÓè \¬ª#b»w.\ޤu8Ðø›ª¾@®k«ê(Ÿök qzúØäÇ àS#‰’Ø5Ù­išÛ¯H*n2PÕA"2¸Ø•`vŒbIMzéª:ZD:ÿ$ØÚ[ÄY'löN6Æïm‚ì\%"ã!ª:1r‰Ã'°/«`»û{ëkM#ïqð=ðp³ª~˜óº¿zÄ„6~~´ëÖ±{5‘ú«Ãà; ¯ªN©†oµ!pp‚ª~dBÃ+"2@UçVð>GÛ»ªê‚„Cj¹//\ ì”òŒ-fGM".UU`ˆ¼HXÈl;d²ªþ6GÇ^ÛÔóùÅ,6/åž“EdàE`§ØîÅ5íÅ›z–ˆ¬»"—ˆìü›°>y™ª¾^„àöN‘+€k'€³S€ë ¦¤*¶é#ýÖÀ3v/¯ ¾µ¥ ÐÜ´¼ëEäG`@¡˜F¹£ #€ª:3'¹ïe‚_G`O`J5|²¿ýTõ#ëk‹Ed0p"0°‚÷Yx"…´ ĵ<×§çdój„5vG Ö¸ “ëë"òAq-È99ÏŽ‘LÒÎGU³ÌÖ£€©À­ª:¾š¾ÙZÀVª!¹'"ƒTµRãiÁñŠ âZQÆ®W Bž8®o+pŸ¿F®ós‘c'±´i°&×–^ëvÓ¨­‘.)XÓL‡Ÿ:±æˆÈ¶ÒpV$SaV{VÇM…N\ÞΚ£q‰Èo€µTõû{?à=›äJÕª—¨êUÝ×È pa)Ä%"§€3Ì5ýàU}³Ä¶ü¤ªçR=bY’Ž{˜/"m‹i"Ò8¡½ ¨Æ y3žÜb›.!˜eÿžrÊ༠E1Sá¢*¨B;^zÃ3Jk '¬‘eákB kÒ8ªNÏ×£€'Uu޶7TÕs€ÍLûŠ¿7º"RÕ &ÅL…ek\§÷Áô"`’ˆÜnfçrˆ´qšÆ%"[‰ÈU"òˆÜdc¢ckc9ÏLçw›å`¸¥Â*õZ[‰Èù"r¦Å &#–ÿ¹ÖB?ò^u¹TDn‘‹Ed“*G‹È36¯ŒvŒ "R[DúÂ4úˆÈ0ÙtEÐd.'ä¥{Ô5®W¢F´;oVê•§ÓooÚUUFâ¨î©@^%xÕ]œrÈŠgå_Hòºp![Fu¡·=¦åžoÿ¿ˆPi! —UUë2G:•$.ÙQDÆÖ5TÕ³Tõ8UÝ–'7Æ*H¤W–©pnì^m,½]?BŸ I7.óšó,ø¼Tü8UU ÙU6&di©H@U½GDŽ‘-Uõ­Øî„Š”1Wq‰ÈÀ«ªúýÿsU}Çž÷9WD65shô]L‘~"²¡ª.«TH¹‰Ëúâ5„Ôa§ž!Öæ¡&É‘ÕUõ¾Sá†y4.9„`J=EU£!:ÃDäIà:ihë¶¥|] Þ§wûÆÇ¿Û¾–f¨ˆœ«ªÏ%\§ p½Å?¾‘Î_ Ü9¶'Ák÷$U}×6¿mšÞ0Y¨ªw$Ü£ :Œ\¿ˆœû$p¿ˆ,VÕ[³L}„ØÊÍ ëâý ãUD®†Gç‹›\¡ªOFÞËdÙ¸KDÚGÖÚSs渃¥³Â?Räœæ„’( Œ.vع[ÙÇŠÞï†R®a×Y‡PhŠIbñgXBˆû9±Äë®iêíG ׌þæ¯û¹^K‚KwôÜûbÇl¼k¦ª/ y"§oÛ=¦âã^%p?ÔÉñ,»×&l¿8¸Ä÷²¯IyOÚ¿mP4ÊyþÖÖÉ£Û#ÄnQ©ŸI¤¶¯hK û¦Œ³cÛ›f{pÆ;©EHVÐ#óÝ•I\ï˜ øgàR{qwÛC>f*m!>éá ×M%œßØLKÿ‹‘Ôâ‚ùØ4çµW3rˆž¿4='åú÷W€¸š™éNsþ~ºäxž öïAH[DÎIç“0ÛE¶ Áa`ðÛ×¹Ø9¶íðe@\õ‰)‚Îð"ç>'†È4²HkgàNûç4/Jn ûÆëV¡ ¯¤ IF¤µŠœ¿ðla¢"¤;¶*ë¼µ­o­Ù¶S’ %u»×i%Ë˶F›GpSÔlÒ¿’ƒ¸fÆymLð\+²­7Ð+ãœ×€¶Fn]rý`‡ˆÀú*!D¥Ø¹uÀwˆlÛ…BmûŒónömkKgi£OLÌê;åW)¿ë*@\·ä<÷`ÓB¢çN7{þaO«OÚøxÎë?9g¶I‰{›yb³C¿!mÆU•¸ì¸&,LNhÿb{®—I‘ûåÔ¸ŽK“rí>Šœ¿‰MÇÑP-2믤Hu7-ƒ ÿ¥”÷ûX‘ó†­Ê9w×p3õþ¿YÊqÝ€KRö \U…6ŒMÓ\€ EÎm ü8 ²m°™žK#¡È±çÓ wnL9ö##ÙßD¶mgëEu2îÑ¡˜6M(ß2hÙ6Âèt#¿³Ò&b󈶫!~2í~Ól¬oÙ¶ P/㜠ìœ#í·²íMìÜZEžoHÂwߤÈ<>,aû±&<¬UäöºU'q ¨qÝšS:ú0vÞ¢xç¶42‹71Çõ72ͪpΨ ä1ÓÄx¿H§H"®û3¤Ÿ‰1ÂzÓ:zÓ2&Õÿ¦ì;•PK)íÜí-[ä¸O=3öÈÐ’G§H^/.ƒ ÿE v‚äûp‘óF$ 8“þYΤա01ØZàÈ"ïÿõ$³iÆ€U ®ÕK%.3Û½•Úm}!ò÷¿²4n`àÊ JÑjfîeßëiÓò/µõèF÷ùo†VY×4¿uc¦ý!1³ã iËÀÀ‘1óîõíy/bÞÈ„Çûío•qÞFM"ZúD³–Ë2qÚþµ#ïÜ[¤o<lœ°}oÓ2·)¢Q§ åzë½cƒCM }Ìì¨cX:íM%\»ó´ó8–®6ÁÌ™Ñ5½·€øÂ|—ýýL{(`~ÊšáO¦Ý²´ÚW`~M`ÛÛ¦/ÌD»ƒª^£ª?–á¬0˜–â†z°‡•c»Üö#d¦ØWUß‘ÍDdóLºÏ‚Ëöq"rdŠ“Ð¢„ó~ ÄNµ­°ÓÀOf2Œâ Ї¤9,©Ï×sAãq¯Â´÷ÿ”õËø>µEõ>U›å8¦ 0-ðÕȶîÀ‘¿ûÛ[Ú¯øxXÛ¼'GçhK3[{ÕB#TBØvBF[§DœÍâ8xHU£ó^Ó£ý£±ã6–3UõáÈ®WüiØMUÿ'"ílyæ4U=†PopR¬¡apåý+¡üÍ¡ªú;Ó^ï̸çpóô,ô›±@3Ù*ãœk“ú•¥ëÜlEI}vY•*íœaÓ[‘c/¯€ÆõŸœ×çÖIf™ ·eʱ÷Æ®ÿqŽëß;çÃ$³QlÍä!ûà«¢qÙ;‰š@ßϲ/—ø¾JÓ¬Ì8¡Ð~“ˆGØÚ˜˜d8´©A6À³$ä¦I¦S3µ=šrNà‚ k+ÍcÛFF%Ê”óž$dó¨Vç Ó FE¾ÉËi ì‘sÚeX j›ÖPŽÖ>>Ã1`BÊöNIë‰F mÖg&4{öÙ}÷Š»c–yÑúì˜øš¶iü“âZxLSù]ÊÚѤ¸¶f&¹ZÍx,p^‚‰vpvÚ»+bú«o–ƒ­cÛ{2ÌsÊz4aûmiÎdvÝîy¾cì}¿5¡&¬ÝK›Ë²4öŠK‰Üøp…/[+Ç}¿³ÅÙžæp°½ª~rx<6ËzÜ=·ð¢ˆü;I±’G©êáåºÜ›TÖÕ$ÄéoŸ˜¤ZŒº$Š[µà+!"ÒÁÌ ÿ!ÄB]JH•ô7U=Øê?u6‘ë’òú™V¸À´Ç(æ릴ï!BM·æìO¢Ò¯Å•4È៦q-¬PØG^ü™_þþÞLßGY–‰N"ÒNDÖ‘ÍEd's3îHˆÚ$á»,¶ÅôÓ—“Æu6ÉY=Ö6g©¸ûx?Ó$ž³Éu°»IýqëNÖX>ÁL‘ñЀ9Ö·»¤œWä:‚'˜sLRqÜÍDä.s]?MUX_;XDƙԑªzUÊ=? ¹ú|îN(ÊzÐ++HÙÂ’b´î =¿®õóè{›D(%³cÊœ¬„X¶“SöoBÈÿK›þªb‚+Í«¯R¦Â\×PÕiª:$GæëÕË ®§Y:ëôF¶èúªˆ\™‘•¡\ÒêgšL!ç܇Àžªúeù³MÊþa¦½>lBÁxBµáÙffx3:ªê©À #¼$ŒˆÝc0UD:¦´¯?éÃå`µX¥Üƒì™Š!-ilž°Jõ‰&¡Ìš“Ìù¥ ¡ÌKOÓˆ¯6S[wB×¶öÓÆÒ` «ˆ4*cl–J\šÕ`)Á ©È¥ªŽ´e‰ÈûpJ|d1íjæõ´~¹}ʾ)ÄÕx$aûgfƽIU0súAFXûìûfÖÂ_6OéõdoKxWsl=/-»3!62©’Á· sc”¼“â)ÿn‚E†ÝÓ–lùcÏ„gljmê@¬8Tõ[¹’àwóò"®œã{õ’Ïôºô–жö&E,"ÊÇ—‰õ,pO~]—lm3Ï=]áïõ€ˆœ$"Uõµ„C.5a¤½­•ü5)h2r½KDäaéœÜ<HJ5t·­›½–p½‡Dä"²«ªþ·ŠßýˆPU@B>ÃbH˽·hyð§Âz–%þÂ~¥¼ƒæñ\–œw°IÇ×”¨q•šÖ©NJPùbI̲¯ª·UEû³`Û¦ª:3åܯl|%¡!Ép׋íFqLá,ïçßÌÔß½¡ó]‚#Y 8ã¤åœe”TÞ¥WÆ÷CzâºIÄ¥ªïŠÈô”±Žªþ$"#€cH®~>—¥×›!¤úzr¹—5øz+^‰Ìµª0Q52)ç(›x7"x4•D\³ÌpBüL½„6nlj6”°\Îóï’òÌMi\ž«T6‰ú˜v·w’Ê/"{›ß%e°Æqªu¼¸»3¿^P/à)à©•ROªð¸ˆQN5æ:š¹µÐ?6$¬[}œ“¸§¼Ÿež_ÒLÒjŽ5"¬×”“š«Á¬Ç-ó÷ úWIå\L’NK6Ç5­ óDZ[v$¬•‘A\ëdh\qÇŠµL³!¥?4!$*˜ôÌ*´šÑž½3¾ßÛçfålÜœà}˜„Ù¥—áŸff|>eÿU„ šóZ­ÌBÇ¡f9X®¦Â¨³"c¶Ä^WDz‹È ö&TðÝ9´òš ëh{›Äû¿Œ¶nFðúS$)h±ï2'AjêX!-6þlï“Eä„„÷ÙÇHûÀœ¤…I¶S¹î’4˜ô8–`7ézÓMƒjicÊÅ!„èý® ½bp.SV©ˆ\ŸcïdàvkC/àfU=¦Ôð®ˆì‘ðŽç—N\†ÓBZ¡G&éýª8O¤ ¡­Î[iøŽtïâ ×f·ô4­þy‚»ø eVA˜N›Ÿ7>.•¸lÝYÓ –£qY¿ùxËÒh%íÿÑHü/ »w%æå-"[˜6ûAµW%ÇvÎ  ¡ˆ\Fžý¿Øö¿'¬ %¤8y¥â²°DUÿBp¹7ƒÀbî,ó™ß0²¸˜_gµ®fN•Æ…À©–ì¶ðNÿaÞe8™Œ¶wð߇à•fFíüÃÌ:Iï~²ó”¹C—Jk‚t_²¿;óKptYVZÕµ„P‹3Ú^°^5Ø& ^6”ƒ¤;b NLûå }üœbÂÆÓqU´Ì¤MÊ-Ó4¤ˆ½¤âú–à ›„FÀÛª:¼ Ï’E\m‹×B–Ÿ86$d¼ „çÌ£qA¢>?cÿ­f‰jéÓBðÙ¶6a½µ×21Á-gÔÊ1)2÷%؆ë<”îvQÕL⼑à®ZŽ©0ÚÑ¿RÕãLb¸° ›„ÎV謁MpÂxÐ¼ŽÆÇö7XÙùJj]s€“øe ¥ÁÌÚ»LM£‘™ ¸€_jF%ÝÁkñ¼Œc%x3Ž,¥ ‚á´‚¶jýå2ò­m•;Iç!ÓM qeŸ¤˜L 8žûíg3Ž9˜”ò_Z%õs†¹ÓÞÕ²ÀR<0Í<ßÕVš¸Zr}¦}‡52L˜õìE1B* SMÊ®rW³"ÅT&´Â2FVaئÚp&q™%æ­Œ¸¬ÅGŽ‘ÍÇ |mß !ޝª~TˆKŠ þFZ»GžésB¤z÷„õ‹º•šTõKU=ÖÌ%tþ¦$~Á‡±€âã …Ql <$"•^§ìÆ/™§·°wߢÌkmgšc¡dI§,â2Üì)"»g¼óŽ÷XI†<ÑÉ&üLÒL€ý¼šûöi„¬ íIö´+hªŒH¦Ç˜vRÜ’a¼8&#µ*cg¿v8Šã à*Ó0Ë!.͸oýŒsÛeh1õãD`®ÜÍ3&é e:•ù}fh?ÅÞùêf.Œ£M†€]˜«~Ì ïEî{ÁM?mÜŽÖ°m­ï_ѾÒû½XeM†° ½¢› aiOÁ)—aŠWQKD†‹ÈvE4°®… ª}I|»öLÂZL¼3îL(LWÄÔø}°jŪú i?#EäA9Ò&Ñ<×:ØÌ_oÛ$t¡„„–ØÄ|eH;î=Bnʾöm¶ÎhË7èÃÍéà"à U}¼„wSÌí{q†ËoÚ5WvRÕg qlig}iÏæš¶¥iMUÁ0Râ–,ÓÆä˦QqÙ:Zôµ_$n"”Ô¨—òÞ¶‘ûDäþXEá¬o4ƒt¯AÁö¹‰Ë0;­¦–}Ó!À([óŠ?CK¹HDÞ‘'ÍÅ=ŠÍÒ™óv“˜¥£€ÏUÒ°Fq3bZÒô"ûLãÿŽX .ÞQÕ\µ÷òLxMZF¦ÂcÈ(KÕ¬[äïx'ëlúpÙ&‡Ô15¶í§Œãçi¹K*$8,Tbý¥pQtñVUÇ«êŽ6Ùo Œ‘"r¡ˆ”VDï,‚§ÑñFTÁ’ŸæœÜflÜ÷g™[LX8˜°žy¹à÷‘nV ¶¿ˆ¼DØí¢ª_Ùš][k_)H“b ˜Nzu^ÜX Å$R·¸­Ãøe=ë·ä‹9+öŽçS2Ìrw‡T¨‚y÷Gg´íNB`ü"²‹½‡º"r”™ìkßük~í2^'ƒ¸f!®öÄU/e _EîV/¬7ð°ˆt‘Ö"²µˆÜNˆÿšj÷"³ D±9Á%>Q™(2ÞÓˆë=3–£q%.C‚)?í¼OpÂêS˜D¤!cJî1™‡¸’J•7`Ù¡eÂõ›”q¬j¶q©£•ˆ/"-LmÛßÁÚÐÆÌSm3>Ì–ÎÌñ|F[¶dio¦4a¡KçY¬cçÝñòØ%Jÿ[ÒêŒJy®·UõB#±Þ„x«m"òŠˆ$cœ¦i\#³È]U§2CleæÙƒUu7U½›\7¡pæ¶Äõ­iˆIÏRϨ¤¾ú©Íaåôñ:ä‹w}X˜%à«ê9…¬'V³am½¤ ¢XFôi,«ðc22ûV1Û5 ÷›ìQ$~üœÌfÚ‘à »¿Ii/"²ãÇÏ·ûÌ'¸å6ˆåó‹?ÿ$äv#¬GÍŒû °~FÛïNhËG¤dj&¬;}œpÎB‚·d§2ßû#Uù¦¦‘ÔŽe;—X޶2®»Á™¦K®±9!¥Ï9U¸Æ–÷ó´ýgS¤‚M€Ý åqnÀò B,.M8þ@"9Íöz…ÇÛˤç›Ì·LÙ¿-•ÌÔÕ6eßÅÉ©W$/éK~ÀZ æIDATffnC,edßz„TQµö œ_$Oéî)ûzRF¡ÛH¡ÔX:oævÀçíNXÓ—”R/gÉæŸöŽ®"R^%¶ïtଜÏÕ‰”⥱ãújÜÕ+ùÝe °³Yº¶Uô÷µI7O\!ÛT`]ÂÒ g ¿6ù $–è”P×çÔófÛ¹‹"Ûfe<ׂ_ÝÈõ;óK™’èqÛ`em{_'žI,1e,í°Œ¶|fí¸X'!Açô”ó¾·ï2Š”ÚD)%2¾6e'ŠÿËÙqŸþP¡Éµ‰M#ˆ«Ìq^3› _¬ª e‚ÏE îÝ@»Fð^ü7!0óqë'“ÍDÙ&!‘pï„ÚrÿW‚"²}°_…Þk+kï6E‡WYºÈçjf²Ü7ãÜmí™÷L©0<Žeqbçu$ÄîK|ƒi¦Yçþà@µYBqØÆEêÎ*MÔIØ?°Ø½Î9.²zJèm‹œpz‚p5Ž”dÁvÌ!„a$ôÁÿ’^ ¹‘}ï9Ÿï‰¬±jÊÆmYm-‰¸º“I¯œö{¿ ¨¶}Äy9ïõVÂd×t’écøí#ÅŸñK› W‹]»½iA s¶o±Ù“ÏΨHú~ ïöC`ÃØ5Î/BÀjæ§Æ%hL‡™Iï#‰?ŠÈÕÎy=Ù2Z9!ûÁXûF‡ SªTw¶5šI„ µË¼_k[Ÿ{Écÿœ–[ŠóífÚ^Ë"“ɹüº âù·àÆ Ç¶µ…íHÉäóÙ:ÛûÙ%g6úç͹¥ð^^"RCªHÝ­‰I±­ë¼ ´ÏÙæ?Ú÷oÛ>8m¬%\c8k›°12úî‹Ô4»”lè6n.ÌqMlŒ LÒtm Þœ³=c]#U&&‘Pÿ*áÜclþëi_aÄ¿ZŽjç£Ó´²Ø±{&iã¦Å_O¤ŽZ9?‰¯›wËx“r6ÛîOfBûØÑ_!ñèdUý}ÖX^3ëb#ˆ‘ŸØ}êX‡›e’Þ‚Ø5v$$w]×Ú¿ÈÈp¦I7[§‚‹ñ=fz›CÈþ~ªþ/ÃvÜ{°¦Ió #ï`!ïÖ,Bf†KR²Fc™±vÖµg,¼ëŸ­Ýùa¦²c×¹Þ:zm{g‹"ïìgàeU=£Ìï±!xxO3],0­l†iÅ3lðlb¿¦Fîým}j™Á¼;»²,råÍ3òkçЪä5‘óL;z5¢ÏËñ[Hé.âWwE£? ä$œé‹ŒØo`s[¿+õÙ:ØÄÕÛֿį\ab¬gý³žig·˜YóàS[£És¿Î„ü}§$ìkgæò~fMrPéHˆúè›0îß3óØ39ÚÒÔLµ™ ô{B–›¯³ò‹ZØÉŸLC<1e½ïûNUõÝØþf„`Û½Ì1aJÊZÛ#†r0-OgK¸F•RcYš ZÀl N]Vߦ¶=ïÚ‘ßÓ?´¢”ÕÕoj™ö5·‚×lgšF£u)/ÃÆ1ªú•Ý{o#ˆIÕ7á(*ÜL3‰ö§2ûnw³$ÔK )Èl¡Bô›"r!g’ ´ÒžoBTô}45Á§¬Xb´=ëâv3ûÜ®ÓÄÚù5¡"ðø”ó×1Ólk³d|Aȱ^‚óÈ<{o÷™Çæù¦¶´ûI†#ÛàßiïÜÞë~F†…z|­Œ¬سÝQd>ºS6Ë—»]y§«\×?1-ï]‹‹ºÌž£°V_P8ê˜ý.‹îè_ŠÈæ<òCL[l÷ŠÞs† ø¯™o5÷<ËÝØ¹h¤ªV9eÝ E\G‰ðÚ¶n±¶ ®w ùá¦,Ç6t2ÓÇ™ª:qz÷ë¼( bCû-Ž‘Ù<àÍ1IöiàiË.q0p¹ˆ´$”¢¹¿`k¯0>2©všˆ´-3¸Ã±²É- Q§EÖj7%øœ™fæu˱ª ý ñ‡ªê¢Œú]Ḋ›VTÛÌV±Ðgôúu nëÏ“¬Z¯Ã±*ɽž‰¿SÕmÛÞ¶­!)Àj€¤_CšÞ÷±ßi¦I'.ÇÊ4@¶ ,úJ¨ÍµÁÃSYÆâXÞHñN[›°îÐÍ4¥Ûò$ôÌѮɘSåwt8V•1Ù•àmy¸ª~cÛ~OÈ7ù!‡ÖÈæÄ~³#ÿoDðÔnnÿ6#¬ÕÍ&¤Ž›áÄåXHSB,Õñ„€÷1À=‘ÁÒ†G¶7!Hõ^àÎx‰úÈõ¶3òÛžÛwW^'€Øuv#dYŸ|—7I¨ÃQÆdoBîÌ®…Z}¶ÎU(ùAU ¯ŠÈ¡Àañ'.ÇÊ2H. ¸÷îg„5$ãØÆ„x»^„@ðÒ²©[Ù‰nvìw„d¶OåpæÅ6”íºÁd‚-Ç*0/!dUé™Çc´ ÷YƒàB¿›—ce(/âlÎ+eɲŸŸFˆë»à¤1?娄dŸûO2ˆ¼KˆS[;¶a1z¬ª^."/•Q%ÚáXÙÆân„`ꮺŒ DDN!äVìïÄåXËY¦iÍ,óü“^7Bú¢Ó\å-KÊ>„Üo±Aõø%áôº„Åä+TõY+’yªá_ÊQÃÇa!ñòñV±zYÞ«!%Xç¸3•—cUxµé~N!„ƒÜ<žÓ4Øš€øóØõFJ(MápÔ´ñÓ訪çdÓ€Þ*öï6æâikÌôÓ€a…¬V§«­ª^´Ô=œ¸«ð ܸu?Bäm¥:hX¯/óæˆs8Vò13šPñ⛈0·;!]W'#¢ù„Œ!_Ù¿…ß„4bñßb !ÅÚM„Œ,BHμ¿ª~ïÄåp,=ë] ë[_Üêß"¤*ú!åœ Éq?WÕ3ý-:VqÒxFUwµ€ã» 5ÆJ½\NîÌÈõ;’%÷2—úvªz~â±N\ǯOBÖð-ù¥:õwFd?™Ù£!~ì") def show_user(username): user = User.query.filter_by(username=username).one_or_404() return render_template("show_user.html", user=user) You can add a custom message to the 404 error: .. code-block:: python user = User.query.filter_by(username=username).one_or_404( description=f"No user named '{username}'." ) Pagination ---------- If you have a lot of results, you may only want to show a certain number at a time, allowing the user to click next and previous links to see pages of data. Call :meth:`~.Query.paginate` on a query to get a :class:`.Pagination` object. See :doc:`/pagination` for more information about the pagination object. During a request, this will take ``page`` and ``per_page`` arguments from the query string ``request.args``. Pass ``max_per_page`` to prevent users from requesting too many results on a single page. If not given, the default values will be page 1 with 20 items per page. .. code-block:: python page = User.query.order_by(User.join_date).paginate() return render_template("user/list.html", page=page) flask-sqlalchemy-3.0.3/docs/license.rst000066400000000000000000000001071436623544000200660ustar00rootroot00000000000000BSD-3-Clause License ==================== .. include:: ../LICENSE.rst flask-sqlalchemy-3.0.3/docs/make.bat000066400000000000000000000014051436623544000173210ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=pdm run sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) if "%1" == "" goto help %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd flask-sqlalchemy-3.0.3/docs/models.rst000066400000000000000000000066371436623544000177450ustar00rootroot00000000000000Models and Tables ================= Use the ``db.Model`` class to define models, or the ``db.Table`` class to create tables. Both handle Flask-SQLAlchemy's bind keys to associate with a specific engine. Defining Models --------------- See SQLAlchemy's `declarative documentation`_ for full information about defining model classes declaratively. .. _declarative documentation: https://docs.sqlalchemy.org/orm/declarative_tables.html Subclass ``db.Model`` to create a model class. This is a SQLAlchemy declarative base class, it will take ``Column`` attributes and create a table. Unlike plain SQLAlchemy, Flask-SQLAlchemy's model will automatically generate a table name if ``__tablename__`` is not set and a primary key column is defined. .. code-block:: python import sqlalchemy as sa class User(db.Model): id = sa.Column(sa.Integer, primary_key=True) type = sa.Column(sa.String) For convenience, the extension object provides access to names in the ``sqlalchemy`` and ``sqlalchemy.orm`` modules. So you can use ``db.Column`` instead of importing and using ``sqlalchemy.Column``, although the two are equivalent. Defining a model does not create it in the database. Use :meth:`~.SQLAlchemy.create_all` to create the models and tables after defining them. If you define models in submodules, you must import them so that SQLAlchemy knows about them before calling ``create_all``. .. code-block:: python with app.app_context(): db.create_all() Defining Tables --------------- See SQLAlchemy's `table documentation`_ for full information about defining table objects. .. _table documentation: https://docs.sqlalchemy.org/core/metadata.html Create instances of ``db.Table`` to define tables. The class takes a table name, then any columns and other table parts such as columns and constraints. Unlike plain SQLAlchemy, the ``metadata`` argument is not required. A metadata will be chosen based on the ``bind_key`` argument, or the default will be used. A common reason to create a table directly is when defining many to many relationships. The association table doesn't need its own model class, as it will be accessed through the relevant relationship attributes on the related models. .. code-block:: python import sqlalchemy as sa user_book_m2m = db.Table( "user_book", sa.Column("user_id", sa.ForeignKey(User.id), primary_key=True), sa.Column("book_id", sa.ForeignKey(Book.id), primary_key=True), ) Reflecting Tables ----------------- If you are connecting to a database that already has tables, SQLAlchemy can detect that schema and create tables with columns automatically. This is called reflection. Those tables can also be assigned to model classes with the ``__table__`` attribute instead of defining the full model. Call the :meth:`~.SQLAlchemy.reflect` method on the extension. It will reflect all the tables for each bind key. Each metadata's ``tables`` attribute will contain the detected table objects. .. code-block:: python with app.app_context(): db.reflect() class User: __table__ = db.metadatas["auth"].tables["user"] In most cases, it will be more maintainable to define the model classes yourself. You only need to define the models and columns you will actually use, even if you're connecting to a broader schema. IDEs will know the available attributes, and migration tools like Alembic can detect changes and generate schema migrations. flask-sqlalchemy-3.0.3/docs/pagination.rst000066400000000000000000000051371436623544000206050ustar00rootroot00000000000000Paging Query Results ==================== If you have a lot of results, you may only want to show a certain number at a time, allowing the user to click next and previous links to see pages of data. This is sometimes called *pagination*, and uses the verb *paginate*. Call :meth:`.SQLAlchemy.paginate` on a select statement to get a :class:`.Pagination` object. During a request, this will take ``page`` and ``per_page`` arguments from the query string ``request.args``. Pass ``max_per_page`` to prevent users from requesting too many results on a single page. If not given, the default values will be page 1 with 20 items per page. .. code-block:: python page = db.paginate(db.select(User).order_by(User.join_date)) return render_template("user/list.html", page=page) Showing the Items ----------------- The :class:`.Pagination` object's :attr:`.Pagination.items` attribute is the list of items for the current page. The object can also be iterated over directly. .. code-block:: jinja
    {% for user in page %}
  • {{ user.username }} {% endfor %}
Page Selection Widget --------------------- The :class:`.Pagination` object has attributes that can be used to create a page selection widget by iterating over page numbers and checking the current page. :meth:`~.Pagination.iter_pages` will produce up to three groups of numbers, separated by ``None``. It defaults to showing 2 page numbers at either edge, 2 numbers before the current, the current, and 4 numbers after the current. For example, if there are 20 pages and the current page is 7, the following values are yielded. .. code-block:: python users.iter_pages() [1, 2, None, 5, 6, 7, 8, 9, 10, 11, None, 19, 20] You can use the :attr:`~.Pagination.total` attribute to show the total number of results, and :attr:`~.Pagination.first` and :attr:`~.Pagination.last` to show the range of items on the current page. The following Jinja macro renders a simple pagination widget. .. code-block:: jinja {% macro render_pagination(pagination, endpoint) %}
{{ pagination.first }} - {{ pagination.last }} of {{ pagination.total }}
{% endmacro %} flask-sqlalchemy-3.0.3/docs/queries.rst000066400000000000000000000060541436623544000201300ustar00rootroot00000000000000Modifying and Querying Data =========================== Insert, Update, Delete ---------------------- See SQLAlchemy's `ORM tutorial`_ and other SQLAlchemy documentation for more information about modifying data with the ORM. .. _ORM tutorial: https://docs.sqlalchemy.org/tutorial/orm_data_manipulation.html To insert data, pass the model object to ``db.session.add()``: .. code-block:: python user = User() db.session.add(user) db.session.commit() To update data, modify attributes on the model objects: .. code-block:: python user.verified = True db.session.commit() To delete data, pass the model object to ``db.session.delete()``: .. code-block:: python db.session.delete(user) db.session.commit() After modifying data, you must call ``db.session.commit()`` to commit the changes to the database. Otherwise, they will be discarded at the end of the request. Select ------ See SQLAlchemy's `Querying Guide`_ and other SQLAlchemy documentation for more information about querying data with the ORM. .. _Querying Guide: https://docs.sqlalchemy.org/orm/queryguide.html Queries are executed through ``db.session.execute()``. They can be constructed using :func:`~sqlalchemy.sql.expression.select`. Executing a select returns a :class:`~sqlalchemy.engine.Result` object that has many methods for working with the returned rows. .. code-block:: python user = db.session.execute(db.select(User).filter_by(username=username)).scalar_one() users = db.session.execute(db.select(User).order_by(User.username)).scalars() Queries for Views ----------------- If you write a Flask view function it's often useful to return a ``404 Not Found`` error for missing entries. Flask-SQLAlchemy provides some extra query methods. - :meth:`.SQLAlchemy.get_or_404` will raise a 404 if the row with the given id doesn't exist, otherwise it will return the instance. - :meth:`.SQLAlchemy.first_or_404` will raise a 404 if the query does not return any results, otherwise it will return the first result. - :meth:`.SQLAlchemy.one_or_404` will raise a 404 if the query does not return exactly one result, otherwise it will return the result. .. code-block:: python @app.route("/user-by-id/") def user_by_id(id): user = db.get_or_404(User, id) return render_template("show_user.html", user=user) @app.route("/user-by-username/") def user_by_username(username): user = db.one_or_404(db.select(User).filter_by(username=username)) return render_template("show_user.html", user=user) You can add a custom message to the 404 error: .. code-block:: python user = db.one_or_404( db.select(User).filter_by(username=username), description=f"No user named '{username}'." ) Legacy Query Interface ---------------------- You may see uses of ``Model.query`` or ``session.query`` to build queries. That query interface is considered legacy in SQLAlchemy. Prefer using the ``session.execute(select(...))`` instead. See :doc:`legacy-query` for documentation. flask-sqlalchemy-3.0.3/docs/quickstart.rst000066400000000000000000000165171436623544000206520ustar00rootroot00000000000000.. _quickstart: Quick Start =========== .. currentmodule:: flask_sqlalchemy Flask-SQLAlchemy simplifies using SQLAlchemy by automatically handling creating, using, and cleaning up the SQLAlchemy objects you'd normally work with. While it adds a few useful features, it still works like SQLAlchemy. This page will walk you through the basic use of Flask-SQLAlchemy. For full capabilities and customization, see the rest of these docs, including the API docs for the :class:`SQLAlchemy` object. Check the SQLAlchemy Documentation ---------------------------------- Flask-SQLAlchemy is a wrapper around SQLAlchemy. You should follow the `SQLAlchemy Tutorial`_ to learn about how to use it, and consult its documentation for detailed information about its features. These docs show how to set up Flask-SQLAlchemy itself, not how to use SQLAlchemy. Flask-SQLAlchemy sets up the engine, declarative model class, and scoped session automatically, so you can skip those parts of the SQLAlchemy tutorial. .. _SQLAlchemy Tutorial: https://docs.sqlalchemy.org/tutorial/index.html Installation ------------ Flask-SQLAlchemy is available on `PyPI`_ and can be installed with various Python tools. For example, to install or update the latest version using pip: .. code-block:: text $ pip install -U Flask-SQLAlchemy .. _PyPI: https://pypi.org/project/Flask-SQLAlchemy/ Configure the Extension ----------------------- The only required Flask app config is the :data:`.SQLALCHEMY_DATABASE_URI` key. That is a connection string that tells SQLAlchemy what database to connect to. Create your Flask application object, load any config, and then initialize the :class:`SQLAlchemy` extension class with the application by calling :meth:`db.init_app <.SQLAlchemy.init_app>`. This example connects to a SQLite database, which is stored in the app's instance folder. .. code-block:: python from flask import Flask from flask_sqlalchemy import SQLAlchemy # create the extension db = SQLAlchemy() # create the app app = Flask(__name__) # configure the SQLite database, relative to the app instance folder app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///project.db" # initialize the app with the extension db.init_app(app) The ``db`` object gives you access to the :attr:`db.Model <.SQLAlchemy.Model>` class to define models, and the :attr:`db.session <.SQLAlchemy.session>` to execute queries. See :doc:`config` for an explanation of connections strings and what other configuration keys are used. The :class:`SQLAlchemy` object also takes some arguments to customize the objects it manages. Define Models ------------- Subclass ``db.Model`` to define a model class. The ``db`` object makes the names in ``sqlalchemy`` and ``sqlalchemy.orm`` available for convenience, such as ``db.Column``. The model will generate a table name by converting the ``CamelCase`` class name to ``snake_case``. .. code-block:: python class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String, unique=True, nullable=False) email = db.Column(db.String) The table name ``"user"`` will automatically be assigned to the model's table. See :doc:`models` for more information about defining and creating models and tables. Create the Tables ----------------- After all models and tables are defined, call :meth:`.SQLAlchemy.create_all` to create the table schema in the database. This requires an application context. Since you're not in a request at this point, create one manually. .. code-block:: python with app.app_context(): db.create_all() If you define models in other modules, you must import them before calling ``create_all``, otherwise SQLAlchemy will not know about them. ``create_all`` does not update tables if they are already in the database. If you change a model's columns, use a migration library like `Alembic`_ with `Flask-Alembic`_ or `Flask-Migrate`_ to generate migrations that update the database schema. .. _Alembic: https://alembic.sqlalchemy.org/ .. _Flask-Alembic: https://flask-alembic.readthedocs.io/ .. _Flask-Migrate: https://flask-migrate.readthedocs.io/ Query the Data -------------- Within a Flask view or CLI command, you can use ``db.session`` to execute queries and modify model data. SQLAlchemy automatically defines an ``__init__`` method for each model that assigns any keyword arguments to corresponding database columns and other attributes. ``db.session.add(obj)`` adds an object to the session, to be inserted. Modifying an object's attributes updates the object. ``db.session.delete(obj)`` deletes an object. Remember to call ``db.session.commit()`` after modifying, adding, or deleting any data. ``db.session.execute(db.select(...))`` constructs a query to select data from the database. Building queries is the main feature of SQLAlchemy, so you'll want to read its `tutorial on select`_ to learn all about it. You'll usually use the ``Result.scalars()`` method to get a list of results, or the ``Result.scalar()`` method to get a single result. .. _tutorial on select: https://docs.sqlalchemy.org/tutorial/data_select.html .. code-block:: python @app.route("/users") def user_list(): users = db.session.execute(db.select(User).order_by(User.username)).scalars() return render_template("user/list.html", users=users) @app.route("/users/create", methods=["GET", "POST"]) def user_create(): if request.method == "POST": user = User( username=request.form["username"], email=request.form["email"], ) db.session.add(user) db.session.commit() return redirect(url_for("user_detail", id=user.id)) return render_template("user/create.html") @app.route("/user/") def user_detail(id): user = db.get_or_404(User, id) return render_template("user/detail.html", user=user) @app.route("/user//delete", methods=["GET", "POST"]) def user_delete(id): user = db.get_or_404(User, id) if request.method == "POST": db.session.delete(user) db.session.commit() return redirect(url_for("user_list")) return render_template("user/delete.html", user=user) You may see uses of ``Model.query`` to build queries. This is an older interface for queries that is considered legacy in SQLAlchemy. Prefer using ``db.session.execute(db.select(...))`` instead. See :doc:`queries` for more information about queries. What to Remember ---------------- For the most part, you should use SQLAlchemy as usual. The :class:`SQLAlchemy` extension instance creates, configures, and gives access to the following things: - :attr:`.SQLAlchemy.Model` declarative model base class. It sets the table name automatically instead of needing ``__tablename__``. - :attr:`.SQLAlchemy.session` is a session that is scoped to the current Flask application context. It is cleaned up after every request. - :attr:`.SQLAlchemy.metadata` and :attr:`.SQLAlchemy.metadatas` gives access to each metadata defined in the config. - :attr:`.SQLAlchemy.engine` and :attr:`.SQLAlchemy.engines` gives access to each engine defined in the config. - :meth:`.SQLAlchemy.create_all` creates all tables. - You must be in an active Flask application context to execute queries and to access the session and engine. flask-sqlalchemy-3.0.3/docs/record-queries.rst000066400000000000000000000021761436623544000214050ustar00rootroot00000000000000Recording Query Information =========================== .. warning:: This feature is intended for debugging only. Flask-SQLAlchemy can record some information about every query that executes during a request. This information can then be retrieved to aid in debugging performance. For example, it can reveal that a relationship performed too many individual selects, or reveal a query that took a long time. To enable this feature, set :data:`.SQLALCHEMY_RECORD_QUERIES` to ``True`` in the Flask app config. Use :func:`.get_recorded_queries` to get a list of query info objects. Each object has the following attributes: ``statement`` The string of SQL generated by SQLAlchemy with parameter placeholders. ``parameters`` The parameters sent with the SQL statement. ``start_time`` / ``end_time`` Timing info about when the query started execution and when the results where returned. Accuracy and value depends on the operating system. ``duration`` The time the query took in seconds. ``location`` A string description of where in your application code the query was executed. This may be unknown in certain cases. flask-sqlalchemy-3.0.3/docs/track-modifications.rst000066400000000000000000000017021436623544000224000ustar00rootroot00000000000000Tracking Modifications ====================== .. warning:: Tracking changes adds significant overhead. In most cases, you'll be better served by using `SQLAlchemy events`_ directly. .. _SQLAlchemy events: https://docs.sqlalchemy.org/core/event.html Flask-SQLAlchemy can set up its session to track inserts, updates, and deletes for models, then send a Blinker signal with a list of these changes either before or during calls to ``session.flush()`` and ``session.commit()``. To enable this feature, set :data:`.SQLALCHEMY_TRACK_MODIFICATIONS` in the Flask app config. Then add a listener to :data:`.models_committed` (emitted after the commit) or :data:`.before_models_committed` (emitted before the commit). .. code-block:: python from flask_sqlalchemy.track_modifications import models_committed def get_modifications(sender: Flask, changes: list[tuple[t.Any, str]]) -> None: ... models_committed.connect(get_modifications) flask-sqlalchemy-3.0.3/examples/000077500000000000000000000000001436623544000166025ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/examples/flaskr/000077500000000000000000000000001436623544000200645ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/examples/flaskr/.gitignore000066400000000000000000000001301436623544000220460ustar00rootroot00000000000000venv/ *.pyc instance/ .pytest_cache/ .coverage htmlcov/ dist/ build/ *.egg-info/ .idea/ flask-sqlalchemy-3.0.3/examples/flaskr/LICENSE.rst000066400000000000000000000027031436623544000217020ustar00rootroot00000000000000Copyright 2010 Pallets Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. flask-sqlalchemy-3.0.3/examples/flaskr/MANIFEST.in000066400000000000000000000001141436623544000216160ustar00rootroot00000000000000graft flaskr/static graft flaskr/templates graft tests global-exclude *.pyc flask-sqlalchemy-3.0.3/examples/flaskr/README.rst000066400000000000000000000031221436623544000215510ustar00rootroot00000000000000Flaskr ====== The basic blog app built in the Flask `tutorial`_, modified to use Flask-SQLAlchemy instead of plain SQL. .. _tutorial: https://flask.palletsprojects.com/tutorial/ Install ------- **Be sure to use the same version of the code as the version of the docs you're reading.** You probably want the latest tagged version, but the default Git version is the master branch. .. code-block:: text # clone the repository $ git clone https://github.com/pallets/flask-sqlalchemy $ cd flask-sqlalchemy/examples/flaskr # checkout the correct version $ git checkout correct-version-tag Create a virtualenv and activate it: .. code-block:: text $ python3 -m venv venv $ . venv/bin/activate Or on Windows cmd: .. code-block:: text $ py -3 -m venv venv $ venv\Scripts\activate.bat Install Flaskr: .. code-block:: text $ pip install -e . Or if you are using the master branch, install Flask-SQLAlchemy from source before installing Flaskr: .. code-block:: text $ pip install -e ../.. $ pip install -e . Run --- .. code-block:: text $ export FLASK_APP=flaskr $ export FLASK_ENV=development $ flask init-db $ flask run Or on Windows cmd: .. code-block:: text > set FLASK_APP=flaskr > set FLASK_ENV=development > flask init-db > flask run Open http://127.0.0.1:5000 in a browser. Test ---- .. code-block:: text $ pip install -e '.[test]' $ pytest Run with coverage report: .. code-block:: text $ coverage run -m pytest $ coverage report $ coverage html # open htmlcov/index.html in a browser flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/000077500000000000000000000000001436623544000213465ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/__init__.py000066400000000000000000000031651436623544000234640ustar00rootroot00000000000000import os import click from flask import Flask from flask.cli import with_appcontext from flask_sqlalchemy import SQLAlchemy __version__ = (1, 0, 0, "dev") db = SQLAlchemy() def create_app(test_config=None): """Create and configure an instance of the Flask application.""" app = Flask(__name__, instance_relative_config=True) # some deploy systems set the database url in the environ db_url = os.environ.get("DATABASE_URL") if db_url is None: # default to a sqlite database in the instance folder db_url = "sqlite:///flaskr.sqlite" app.config.from_mapping( # default secret that should be overridden in environ or config SECRET_KEY=os.environ.get("SECRET_KEY", "dev"), SQLALCHEMY_DATABASE_URI=db_url, ) if test_config is None: # load the instance config, if it exists, when not testing app.config.from_pyfile("config.py", silent=True) else: # load the test config if passed in app.config.update(test_config) # initialize Flask-SQLAlchemy and the init-db command db.init_app(app) app.cli.add_command(init_db_command) # apply the blueprints to the app from flaskr import auth, blog app.register_blueprint(auth.bp) app.register_blueprint(blog.bp) # make "index" point at "/", which is handled by "blog.index" app.add_url_rule("/", endpoint="index") return app def init_db(): db.drop_all() db.create_all() @click.command("init-db") @with_appcontext def init_db_command(): """Clear existing data and create new tables.""" init_db() click.echo("Initialized the database.") flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/auth/000077500000000000000000000000001436623544000223075ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/auth/__init__.py000066400000000000000000000000441436623544000244160ustar00rootroot00000000000000from .views import bp # noqa: F401 flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/auth/models.py000066400000000000000000000013261436623544000241460ustar00rootroot00000000000000from werkzeug.security import check_password_hash from werkzeug.security import generate_password_hash from flaskr import db class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String, unique=True, nullable=False) password_hash = db.Column(db.String, nullable=False) posts = db.relationship("Post", back_populates="author") def set_password(self, value): """Store the password as a hash for security.""" self.password_hash = generate_password_hash(value) # allow password = "..." to set a password password = property(fset=set_password) def check_password(self, value): return check_password_hash(self.password_hash, value) flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/auth/views.py000066400000000000000000000055721436623544000240270ustar00rootroot00000000000000import functools from flask import Blueprint from flask import flash from flask import g from flask import redirect from flask import render_template from flask import request from flask import session from flask import url_for from flaskr import db from flaskr.auth.models import User bp = Blueprint("auth", __name__, url_prefix="/auth") def login_required(view): """View decorator that redirects anonymous users to the login page.""" @functools.wraps(view) def wrapped_view(**kwargs): if g.user is None: return redirect(url_for("auth.login")) return view(**kwargs) return wrapped_view @bp.before_app_request def load_logged_in_user(): """If a user id is stored in the session, load the user object from the database into ``g.user``.""" user_id = session.get("user_id") if user_id is not None: g.user = db.session.get(User, user_id) else: g.user = None @bp.route("/register", methods=("GET", "POST")) def register(): """Register a new user. Validates that the username is not already taken. Hashes the password for security. """ if request.method == "POST": username = request.form["username"] password = request.form["password"] error = None if not username: error = "Username is required." elif not password: error = "Password is required." elif db.session.execute( db.select(db.select(User).filter_by(username=username).exists()) ).scalar(): error = f"User {username} is already registered." if error is None: # the name is available, create the user and go to the login page db.session.add(User(username=username, password=password)) db.session.commit() return redirect(url_for("auth.login")) flash(error) return render_template("auth/register.html") @bp.route("/login", methods=("GET", "POST")) def login(): """Log in a registered user by adding the user id to the session.""" if request.method == "POST": username = request.form["username"] password = request.form["password"] error = None select = db.select(User).filter_by(username=username) user = db.session.execute(select).scalar() if user is None: error = "Incorrect username." elif not user.check_password(password): error = "Incorrect password." if error is None: # store the user id in a new session and return to the index session.clear() session["user_id"] = user.id return redirect(url_for("index")) flash(error) return render_template("auth/login.html") @bp.route("/logout") def logout(): """Clear the current session, including the stored user id.""" session.clear() return redirect(url_for("index")) flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/blog/000077500000000000000000000000001436623544000222715ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/blog/__init__.py000066400000000000000000000000441436623544000244000ustar00rootroot00000000000000from .views import bp # noqa: F401 flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/blog/models.py000066400000000000000000000015541436623544000241330ustar00rootroot00000000000000from datetime import datetime from datetime import timezone from flask import url_for from flaskr import db from flaskr.auth.models import User def now_utc(): return datetime.now(timezone.utc) class Post(db.Model): id = db.Column(db.Integer, primary_key=True) author_id = db.Column(db.ForeignKey(User.id), nullable=False) created = db.Column(db.DateTime, nullable=False, default=now_utc) title = db.Column(db.String, nullable=False) body = db.Column(db.String, nullable=False) # User object backed by author_id # lazy="joined" means the user is returned with the post in one query author = db.relationship(User, lazy="joined", back_populates="posts") @property def update_url(self): return url_for("blog.update", id=self.id) @property def delete_url(self): return url_for("blog.delete", id=self.id) flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/blog/views.py000066400000000000000000000053051436623544000240030ustar00rootroot00000000000000from flask import Blueprint from flask import flash from flask import g from flask import redirect from flask import render_template from flask import request from flask import url_for from werkzeug.exceptions import abort from flaskr import db from flaskr.auth.views import login_required from flaskr.blog.models import Post bp = Blueprint("blog", __name__) @bp.route("/") def index(): """Show all the posts, most recent first.""" select = db.select(Post).order_by(Post.created.desc()) posts = db.session.execute(select).scalars() return render_template("blog/index.html", posts=posts) def get_post(id, check_author=True): """Get a post and its author by id. Checks that the id exists and optionally that the current user is the author. :param id: id of post to get :param check_author: require the current user to be the author :return: the post with author information :raise 404: if a post with the given id doesn't exist :raise 403: if the current user isn't the author """ post = db.get_or_404(Post, id, description=f"Post id {id} doesn't exist.") if check_author and post.author != g.user: abort(403) return post @bp.route("/create", methods=("GET", "POST")) @login_required def create(): """Create a new post for the current user.""" if request.method == "POST": title = request.form["title"] body = request.form["body"] error = None if not title: error = "Title is required." if error is not None: flash(error) else: db.session.add(Post(title=title, body=body, author=g.user)) db.session.commit() return redirect(url_for("blog.index")) return render_template("blog/create.html") @bp.route("//update", methods=("GET", "POST")) @login_required def update(id): """Update a post if the current user is the author.""" post = get_post(id) if request.method == "POST": title = request.form["title"] body = request.form["body"] error = None if not title: error = "Title is required." if error is not None: flash(error) else: post.title = title post.body = body db.session.commit() return redirect(url_for("blog.index")) return render_template("blog/update.html", post=post) @bp.route("//delete", methods=("POST",)) @login_required def delete(id): """Delete a post. Ensures that the post exists and that the logged in user is the author of the post. """ post = get_post(id) db.session.delete(post) db.session.commit() return redirect(url_for("blog.index")) flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/static/000077500000000000000000000000001436623544000226355ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/static/style.css000066400000000000000000000032401436623544000245060ustar00rootroot00000000000000html { font-family: sans-serif; background: #eee; padding: 1rem; } body { max-width: 960px; margin: 0 auto; background: white; } h1, h2, h3, h4, h5, h6 { font-family: serif; color: #377ba8; margin: 1rem 0; } a { color: #377ba8; } hr { border: none; border-top: 1px solid lightgray; } nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; } nav h1 { flex: auto; margin: 0; } nav h1 a { text-decoration: none; padding: 0.25rem 0.5rem; } nav ul { display: flex; list-style: none; margin: 0; padding: 0; } nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; } .content { padding: 0 1rem 1rem; } .content > header { border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; } .content > header h1 { flex: auto; margin: 1rem 0 0.25rem 0; } .flash { margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; } .post > header { display: flex; align-items: flex-end; font-size: 0.85em; } .post > header > div:first-of-type { flex: auto; } .post > header h1 { font-size: 1.5em; margin-bottom: 0; } .post .about { color: slategray; font-style: italic; } .post .body { white-space: pre-line; } .content:last-child { margin-bottom: 0; } .content form { margin: 1em 0; display: flex; flex-direction: column; } .content label { font-weight: bold; margin-bottom: 0.5em; } .content input, .content textarea { margin-bottom: 1em; } .content textarea { min-height: 12em; resize: vertical; } input.danger { color: #cc2f2e; } input[type=submit] { align-self: start; min-width: 10em; } flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/templates/000077500000000000000000000000001436623544000233445ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/templates/auth/000077500000000000000000000000001436623544000243055ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/templates/auth/login.html000066400000000000000000000006501436623544000263040ustar00rootroot00000000000000{% extends 'base.html' %} {% block header %}

{% block title %}Log In{% endblock %}

{% endblock %} {% block content %}
{% endblock %} flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/templates/auth/register.html000066400000000000000000000006541436623544000270240ustar00rootroot00000000000000{% extends 'base.html' %} {% block header %}

{% block title %}Register{% endblock %}

{% endblock %} {% block content %}
{% endblock %} flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/templates/base.html000066400000000000000000000013601436623544000251440ustar00rootroot00000000000000 {% block title %}{% endblock %} - Flaskr
{% block header %}{% endblock %}
{% for message in get_flashed_messages() %}
{{ message }}
{% endfor %} {% block content %}{% endblock %}
flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/templates/blog/000077500000000000000000000000001436623544000242675ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/templates/blog/create.html000066400000000000000000000006771436623544000264320ustar00rootroot00000000000000{% extends 'base.html' %} {% block header %}

{% block title %}New Post{% endblock %}

{% endblock %} {% block content %}
{% endblock %} flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/templates/blog/index.html000066400000000000000000000014011436623544000262600ustar00rootroot00000000000000{% extends 'base.html' %} {% block header %}

{% block title %}Posts{% endblock %}

{% if g.user %} New {% endif %} {% endblock %} {% block content %} {% for post in posts %}

{{ post['title'] }}

by {{ post.author.username }} on {{ post.created.strftime('%Y-%m-%d') }}
{% if g.user['id'] == post['author_id'] %} Edit {% endif %}

{{ post['body'] }}

{% if not loop.last %}
{% endif %} {% endfor %} {% endblock %} flask-sqlalchemy-3.0.3/examples/flaskr/flaskr/templates/blog/update.html000066400000000000000000000012341436623544000264370ustar00rootroot00000000000000{% extends 'base.html' %} {% block header %}

{% block title %}Edit "{{ post['title'] }}"{% endblock %}

{% endblock %} {% block content %}

{% endblock %} flask-sqlalchemy-3.0.3/examples/flaskr/setup.cfg000066400000000000000000000011501436623544000217020ustar00rootroot00000000000000[metadata] name = flaskr version = attr: flaskr.__version__ url = https://flask-sqlalchemy.palletsprojects.com/ author = Pallets author_email = contact@palletsprojects.com license = BSD-3-Clause license_file = LICENSE.rst description = A basic blog app built with Flask-SQLAlchemy. long_description = file: README.rst [options] packages = find: include_package_data = true python_requires = >= 3.7 install_requires = Flask>=2.2 Flask-SQLAlchemy>=3 SQLAlchemy>=1.4.18 [options.extras_require] test = pytest coverage [tool:pytest] testpaths = tests [coverage:run] branch = True source = flaskr flask-sqlalchemy-3.0.3/examples/flaskr/setup.py000066400000000000000000000000461436623544000215760ustar00rootroot00000000000000from setuptools import setup setup() flask-sqlalchemy-3.0.3/examples/flaskr/tests/000077500000000000000000000000001436623544000212265ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/examples/flaskr/tests/conftest.py000066400000000000000000000034451436623544000234330ustar00rootroot00000000000000from datetime import datetime import pytest from werkzeug.security import generate_password_hash from flaskr import create_app from flaskr import db from flaskr import init_db from flaskr.auth.models import User from flaskr.blog.models import Post _user1_pass = generate_password_hash("test") _user2_pass = generate_password_hash("other") @pytest.fixture def app(): """Create and configure a new app instance for each test.""" # create the app with common test config app = create_app({"TESTING": True, "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:"}) # create the database and load test data # set _password to pre-generated hashes, since hashing for each test is slow with app.app_context(): init_db() user = User(username="test", password_hash=_user1_pass) db.session.add_all( ( user, User(username="other", password_hash=_user2_pass), Post( title="test title", body="test\nbody", author=user, created=datetime(2018, 1, 1), ), ) ) db.session.commit() yield app @pytest.fixture def client(app): """A test client for the app.""" return app.test_client() @pytest.fixture def runner(app): """A test runner for the app's Click commands.""" return app.test_cli_runner() class AuthActions: def __init__(self, client): self._client = client def login(self, username="test", password="test"): return self._client.post( "/auth/login", data={"username": username, "password": password} ) def logout(self): return self._client.get("/auth/logout") @pytest.fixture def auth(client): return AuthActions(client) flask-sqlalchemy-3.0.3/examples/flaskr/tests/test_auth.py000066400000000000000000000043051436623544000236020ustar00rootroot00000000000000import pytest from flask import g from flask import session from flaskr import db from flaskr.auth.models import User def test_register(client, app): # test that viewing the page renders without template errors assert client.get("/auth/register").status_code == 200 # test that successful registration redirects to the login page response = client.post("/auth/register", data={"username": "a", "password": "a"}) assert response.headers["Location"] == "/auth/login" # test that the user was inserted into the database with app.app_context(): select = db.select(User).filter_by(username="a") user = db.session.execute(select).scalar() assert user is not None def test_user_password(app): user = User(username="a", password="a") assert user.password_hash != "a" assert user.check_password("a") @pytest.mark.parametrize( ("username", "password", "message"), ( ("", "", "Username is required."), ("a", "", "Password is required."), ("test", "test", "already registered"), ), ) def test_register_validate_input(client, username, password, message): response = client.post( "/auth/register", data={"username": username, "password": password} ) assert message in response.text def test_login(client, auth): # test that viewing the page renders without template errors assert client.get("/auth/login").status_code == 200 # test that successful login redirects to the index page response = auth.login() assert response.headers["Location"] == "/" # login request set the user_id in the session # check that the user is loaded from the session with client: client.get("/") assert session["user_id"] == 1 assert g.user.username == "test" @pytest.mark.parametrize( ("username", "password", "message"), (("a", "test", "Incorrect username."), ("test", "a", "Incorrect password.")), ) def test_login_validate_input(auth, username, password, message): response = auth.login(username, password) assert message in response.text def test_logout(client, auth): auth.login() with client: auth.logout() assert "user_id" not in session flask-sqlalchemy-3.0.3/examples/flaskr/tests/test_blog.py000066400000000000000000000044711436623544000235700ustar00rootroot00000000000000import pytest from flaskr import db from flaskr.auth.models import User from flaskr.blog.models import Post def test_index(client, auth): text = client.get("/").text assert "Log In" in text assert "Register" in text auth.login() text = client.get("/").text assert "test title" in text assert "by test on 2018-01-01" in text assert "test\nbody" in text assert 'href="/1/update"' in text @pytest.mark.parametrize("path", ("/create", "/1/update", "/1/delete")) def test_login_required(client, path): response = client.post(path) assert response.headers["Location"] == "/auth/login" def test_author_required(app, client, auth): # change the post author to another user with app.app_context(): db.session.get(Post, 1).author = db.session.get(User, 2) db.session.commit() auth.login() # current user can't modify other user's post assert client.post("/1/update").status_code == 403 assert client.post("/1/delete").status_code == 403 # current user doesn't see edit link assert 'href="/1/update"' not in client.get("/").text @pytest.mark.parametrize("path", ("/2/update", "/2/delete")) def test_exists_required(client, auth, path): auth.login() assert client.post(path).status_code == 404 def test_create(client, auth, app): auth.login() assert client.get("/create").status_code == 200 client.post("/create", data={"title": "created", "body": ""}) with app.app_context(): select = db.select(db.func.count(Post.id)) post_count = db.session.execute(select).scalar() assert post_count == 2 def test_update(client, auth, app): auth.login() assert client.get("/1/update").status_code == 200 client.post("/1/update", data={"title": "updated", "body": ""}) with app.app_context(): assert db.session.get(Post, 1).title == "updated" @pytest.mark.parametrize("path", ("/create", "/1/update")) def test_create_update_validate(client, auth, path): auth.login() response = client.post(path, data={"title": "", "body": ""}) assert "Title is required." in response.text def test_delete(client, auth, app): auth.login() response = client.post("/1/delete") assert response.headers["Location"] == "/" with app.app_context(): assert db.session.get(Post, 1) is None flask-sqlalchemy-3.0.3/examples/flaskr/tests/test_init.py000066400000000000000000000013411436623544000236010ustar00rootroot00000000000000from flaskr import create_app def test_config(): """Test create_app without passing test config.""" assert not create_app().testing assert create_app({"TESTING": True}).testing def test_db_url_environ(monkeypatch): """Test DATABASE_URL environment variable.""" monkeypatch.setenv("DATABASE_URL", "sqlite:///environ") app = create_app() assert app.config["SQLALCHEMY_DATABASE_URI"] == "sqlite:///environ" def test_init_db_command(runner, monkeypatch): called = False def fake_init_db(): nonlocal called called = True monkeypatch.setattr("flaskr.init_db", fake_init_db) result = runner.invoke(args=["init-db"]) assert "Initialized" in result.output assert called flask-sqlalchemy-3.0.3/examples/todo/000077500000000000000000000000001436623544000175475ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/examples/todo/app.py000066400000000000000000000035121436623544000207020ustar00rootroot00000000000000from datetime import datetime from datetime import timezone from flask import flash from flask import Flask from flask import redirect from flask import render_template from flask import request from flask import url_for from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) app.secret_key = "Achee6phIexoh8dagiQuew0ephuga4Ih" app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///todo.sqlite" db = SQLAlchemy(app) def now_utc(): return datetime.now(timezone.utc) class Todo(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String, nullable=False) text = db.Column(db.String, nullable=False) done = db.Column(db.Boolean, nullable=False, default=False) pub_date = db.Column(db.DateTime, nullable=False, default=now_utc) with app.app_context(): db.create_all() @app.route("/") def show_all(): select = db.select(Todo).order_by(Todo.pub_date.desc()) todos = db.session.execute(select).scalars() return render_template("show_all.html", todos=todos) @app.route("/new", methods=["GET", "POST"]) def new(): if request.method == "POST": if not request.form["title"]: flash("Title is required", "error") elif not request.form["text"]: flash("Text is required", "error") else: todo = Todo(title=request.form["title"], text=request.form["text"]) db.session.add(todo) db.session.commit() flash("Todo item was successfully created") return redirect(url_for("show_all")) return render_template("new.html") @app.route("/update", methods=["POST"]) def update_done(): for todo in db.session.execute(db.select(Todo)).scalars(): todo.done = f"done.{todo.id}" in request.form flash("Updated status") db.session.commit() return redirect(url_for("show_all")) flask-sqlalchemy-3.0.3/examples/todo/templates/000077500000000000000000000000001436623544000215455ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/examples/todo/templates/layout.html000066400000000000000000000004131436623544000237460ustar00rootroot00000000000000 Todo

Todo

{%- for category, message in get_flashed_messages(with_categories=true) %}

{{ "Error: " if category == 'error' }}{{ message }} {%- endfor %} {% block body %}{% endblock %} flask-sqlalchemy-3.0.3/examples/todo/templates/new.html000066400000000000000000000007011436623544000232220ustar00rootroot00000000000000{% extends "layout.html" %} {% block body %}

New Item



Back to list

{% endblock %} flask-sqlalchemy-3.0.3/examples/todo/templates/show_all.html000066400000000000000000000013741436623544000242500ustar00rootroot00000000000000{% extends "layout.html" %} {% block body %}

All Items

{%- for todo in todos %} {%- endfor %}
# Title Date Done?
{{ todo.id }} {{ todo.title }} {{ todo.pub_date.strftime("%Y-%m-%d %H:%M") }}
{{ todo.text }}
New
{% endblock %} flask-sqlalchemy-3.0.3/pdm.lock000066400000000000000000004227411436623544000164300ustar00rootroot00000000000000# This file is @generated by PDM. # It is not intended for manual editing. [[package]] name = "alabaster" version = "0.7.13" requires_python = ">=3.6" summary = "A configurable sidebar-enabled Sphinx theme" [[package]] name = "attrs" version = "22.2.0" requires_python = ">=3.6" summary = "Classes Without Boilerplate" [[package]] name = "babel" version = "2.11.0" requires_python = ">=3.6" summary = "Internationalization utilities" dependencies = [ "pytz>=2015.7", ] [[package]] name = "blinker" version = "1.5" requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" summary = "Fast, simple object-to-object and broadcast signaling" [[package]] name = "cachetools" version = "5.3.0" requires_python = "~=3.7" summary = "Extensible memoizing collections and decorators" [[package]] name = "certifi" version = "2022.12.7" requires_python = ">=3.6" summary = "Python package for providing Mozilla's CA Bundle." [[package]] name = "cfgv" version = "3.3.1" requires_python = ">=3.6.1" summary = "Validate configuration and produce human readable error messages." [[package]] name = "chardet" version = "5.1.0" requires_python = ">=3.7" summary = "Universal encoding detector for Python 3" [[package]] name = "charset-normalizer" version = "3.0.1" summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." [[package]] name = "click" version = "8.1.3" requires_python = ">=3.7" summary = "Composable command line interface toolkit" dependencies = [ "colorama; platform_system == \"Windows\"", "importlib-metadata; python_version < \"3.8\"", ] [[package]] name = "colorama" version = "0.4.6" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" summary = "Cross-platform colored terminal text." [[package]] name = "coverage" version = "7.1.0" requires_python = ">=3.7" summary = "Code coverage measurement for Python" [[package]] name = "coverage" version = "7.1.0" extras = ["toml"] requires_python = ">=3.7" summary = "Code coverage measurement for Python" dependencies = [ "coverage==7.1.0", "tomli; python_full_version <= \"3.11.0a6\"", ] [[package]] name = "distlib" version = "0.3.6" summary = "Distribution utilities" [[package]] name = "docutils" version = "0.19" requires_python = ">=3.7" summary = "Docutils -- Python Documentation Utilities" [[package]] name = "exceptiongroup" version = "1.1.0" requires_python = ">=3.7" summary = "Backport of PEP 654 (exception groups)" [[package]] name = "filelock" version = "3.9.0" requires_python = ">=3.7" summary = "A platform independent file lock." [[package]] name = "flask" version = "2.2.2" requires_python = ">=3.7" summary = "A simple framework for building complex web applications." dependencies = [ "Jinja2>=3.0", "Werkzeug>=2.2.2", "click>=8.0", "importlib-metadata>=3.6.0; python_version < \"3.10\"", "itsdangerous>=2.0", ] [[package]] name = "greenlet" version = "2.0.1" requires_python = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" summary = "Lightweight in-process concurrent programming" [[package]] name = "identify" version = "2.5.15" requires_python = ">=3.7" summary = "File identification library for Python" [[package]] name = "idna" version = "3.4" requires_python = ">=3.5" summary = "Internationalized Domain Names in Applications (IDNA)" [[package]] name = "imagesize" version = "1.4.1" requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" summary = "Getting image size from png/jpeg/jpeg2000/gif file" [[package]] name = "importlib-metadata" version = "6.0.0" requires_python = ">=3.7" summary = "Read metadata from Python packages" dependencies = [ "typing-extensions>=3.6.4; python_version < \"3.8\"", "zipp>=0.5", ] [[package]] name = "iniconfig" version = "2.0.0" requires_python = ">=3.7" summary = "brain-dead simple config-ini parsing" [[package]] name = "itsdangerous" version = "2.1.2" requires_python = ">=3.7" summary = "Safely pass data to untrusted environments and back." [[package]] name = "jinja2" version = "3.1.2" requires_python = ">=3.7" summary = "A very fast and expressive template engine." dependencies = [ "MarkupSafe>=2.0", ] [[package]] name = "markupsafe" version = "2.1.2" requires_python = ">=3.7" summary = "Safely add untrusted strings to HTML/XML markup." [[package]] name = "mypy" version = "0.991" requires_python = ">=3.7" summary = "Optional static typing for Python" dependencies = [ "mypy-extensions>=0.4.3", "tomli>=1.1.0; python_version < \"3.11\"", "typed-ast<2,>=1.4.0; python_version < \"3.8\"", "typing-extensions>=3.10", ] [[package]] name = "mypy-extensions" version = "0.4.3" summary = "Experimental type system extensions for programs checked with the mypy typechecker." [[package]] name = "nodeenv" version = "1.7.0" requires_python = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" summary = "Node.js virtual environment builder" dependencies = [ "setuptools", ] [[package]] name = "packaging" version = "23.0" requires_python = ">=3.7" summary = "Core utilities for Python packages" [[package]] name = "pallets-sphinx-themes" version = "2.0.3" requires_python = ">=3.6" summary = "Sphinx themes for Pallets and related projects." dependencies = [ "Sphinx", "importlib-metadata; python_version < \"3.8\"", "packaging", ] [[package]] name = "platformdirs" version = "2.6.2" requires_python = ">=3.7" summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." dependencies = [ "typing-extensions>=4.4; python_version < \"3.8\"", ] [[package]] name = "pluggy" version = "1.0.0" requires_python = ">=3.6" summary = "plugin and hook calling mechanisms for python" dependencies = [ "importlib-metadata>=0.12; python_version < \"3.8\"", ] [[package]] name = "pre-commit" version = "2.21.0" requires_python = ">=3.7" summary = "A framework for managing and maintaining multi-language pre-commit hooks." dependencies = [ "cfgv>=2.0.0", "identify>=1.0.0", "importlib-metadata; python_version < \"3.8\"", "nodeenv>=0.11.1", "pyyaml>=5.1", "virtualenv>=20.10.0", ] [[package]] name = "pygments" version = "2.14.0" requires_python = ">=3.6" summary = "Pygments is a syntax highlighting package written in Python." [[package]] name = "pyproject-api" version = "1.5.0" requires_python = ">=3.7" summary = "API to interact with the python pyproject.toml based projects" dependencies = [ "packaging>=21.3", "tomli>=2.0.1; python_version < \"3.11\"", ] [[package]] name = "pytest" version = "7.2.1" requires_python = ">=3.7" summary = "pytest: simple powerful testing with Python" dependencies = [ "attrs>=19.2.0", "colorama; sys_platform == \"win32\"", "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", "importlib-metadata>=0.12; python_version < \"3.8\"", "iniconfig", "packaging", "pluggy<2.0,>=0.12", "tomli>=1.0.0; python_version < \"3.11\"", ] [[package]] name = "pytest-cov" version = "4.0.0" requires_python = ">=3.6" summary = "Pytest plugin for measuring coverage." dependencies = [ "coverage[toml]>=5.2.1", "pytest>=4.6", ] [[package]] name = "pytz" version = "2022.7.1" summary = "World timezone definitions, modern and historical" [[package]] name = "pyyaml" version = "6.0" requires_python = ">=3.6" summary = "YAML parser and emitter for Python" [[package]] name = "requests" version = "2.28.2" requires_python = ">=3.7, <4" summary = "Python HTTP for Humans." dependencies = [ "certifi>=2017.4.17", "charset-normalizer<4,>=2", "idna<4,>=2.5", "urllib3<1.27,>=1.21.1", ] [[package]] name = "setuptools" version = "66.1.1" requires_python = ">=3.7" summary = "Easily download, build, install, upgrade, and uninstall Python packages" [[package]] name = "snowballstemmer" version = "2.2.0" summary = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." [[package]] name = "sphinx" version = "5.3.0" requires_python = ">=3.6" summary = "Python documentation generator" dependencies = [ "Jinja2>=3.0", "Pygments>=2.12", "alabaster<0.8,>=0.7", "babel>=2.9", "colorama>=0.4.5; sys_platform == \"win32\"", "docutils<0.20,>=0.14", "imagesize>=1.3", "importlib-metadata>=4.8; python_version < \"3.10\"", "packaging>=21.0", "requests>=2.5.0", "snowballstemmer>=2.0", "sphinxcontrib-applehelp", "sphinxcontrib-devhelp", "sphinxcontrib-htmlhelp>=2.0.0", "sphinxcontrib-jsmath", "sphinxcontrib-qthelp", "sphinxcontrib-serializinghtml>=1.1.5", ] [[package]] name = "sphinx-issues" version = "3.0.1" requires_python = ">=3.6" summary = "A Sphinx extension for linking to your project's issue tracker" dependencies = [ "sphinx", ] [[package]] name = "sphinxcontrib-applehelp" version = "1.0.2" requires_python = ">=3.5" summary = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" [[package]] name = "sphinxcontrib-devhelp" version = "1.0.2" requires_python = ">=3.5" summary = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." [[package]] name = "sphinxcontrib-htmlhelp" version = "2.0.0" requires_python = ">=3.6" summary = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" requires_python = ">=3.5" summary = "A sphinx extension which renders display math in HTML via JavaScript" [[package]] name = "sphinxcontrib-log-cabinet" version = "1.0.1" summary = "Organize changelog directives in Sphinx docs." dependencies = [ "Sphinx", ] [[package]] name = "sphinxcontrib-qthelp" version = "1.0.3" requires_python = ">=3.5" summary = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." [[package]] name = "sphinxcontrib-serializinghtml" version = "1.1.5" requires_python = ">=3.5" summary = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." [[package]] name = "sqlalchemy" version = "2.0.0" requires_python = ">=3.7" summary = "Database Abstraction Library" dependencies = [ "greenlet!=0.4.17; platform_machine == \"aarch64\" or (platform_machine == \"ppc64le\" or (platform_machine == \"x86_64\" or (platform_machine == \"amd64\" or (platform_machine == \"AMD64\" or (platform_machine == \"win32\" or platform_machine == \"WIN32\")))))", "importlib-metadata; python_version < \"3.8\"", "typing-extensions>=4.2.0", ] [[package]] name = "tomli" version = "2.0.1" requires_python = ">=3.7" summary = "A lil' TOML parser" [[package]] name = "tox" version = "4.4.2" requires_python = ">=3.7" summary = "tox is a generic virtualenv management and test command line tool" dependencies = [ "cachetools>=5.2.1", "chardet>=5.1", "colorama>=0.4.6", "filelock>=3.9", "importlib-metadata>=6; python_version < \"3.8\"", "packaging>=23", "platformdirs>=2.6.2", "pluggy>=1", "pyproject-api>=1.5", "tomli>=2.0.1; python_version < \"3.11\"", "typing-extensions>=4.4; python_version < \"3.8\"", "virtualenv>=20.17.1", ] [[package]] name = "tox-pdm" version = "0.6.1" requires_python = ">=3.7" summary = "A plugin for tox that utilizes PDM as the package manager and installer" dependencies = [ "tomli; python_version < \"3.11\"", "tox>=3.18.0", ] [[package]] name = "typed-ast" version = "1.5.4" requires_python = ">=3.6" summary = "a fork of Python 2 and 3 ast modules with type comment support" [[package]] name = "typing-extensions" version = "4.4.0" requires_python = ">=3.7" summary = "Backported and Experimental Type Hints for Python 3.7+" [[package]] name = "urllib3" version = "1.26.14" requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" summary = "HTTP library with thread-safe connection pooling, file post, and more." [[package]] name = "virtualenv" version = "20.17.1" requires_python = ">=3.6" summary = "Virtual Python Environment builder" dependencies = [ "distlib<1,>=0.3.6", "filelock<4,>=3.4.1", "importlib-metadata>=4.8.3; python_version < \"3.8\"", "platformdirs<3,>=2.4", ] [[package]] name = "werkzeug" version = "2.2.2" requires_python = ">=3.7" summary = "The comprehensive WSGI web application library." dependencies = [ "MarkupSafe>=2.1.1", ] [[package]] name = "zipp" version = "3.11.0" requires_python = ">=3.7" summary = "Backport of pathlib-compatible object wrapper for zip files" [metadata] lock_version = "4.1" content_hash = "sha256:2decebce9f60e5e1669dbe8b3b6ff96ad20f63b899810677caf6306c0a868e3e" [metadata.files] "alabaster 0.7.13" = [ {url = "https://files.pythonhosted.org/packages/64/88/c7083fc61120ab661c5d0b82cb77079fc1429d3f913a456c1c82cf4658f7/alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, {url = "https://files.pythonhosted.org/packages/94/71/a8ee96d1fd95ca04a0d2e2d9c4081dac4c2d2b12f7ddb899c8cb9bfd1532/alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, ] "attrs 22.2.0" = [ {url = "https://files.pythonhosted.org/packages/21/31/3f468da74c7de4fcf9b25591e682856389b3400b4b62f201e65f15ea3e07/attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, {url = "https://files.pythonhosted.org/packages/fb/6e/6f83bf616d2becdf333a1640f1d463fef3150e2e926b7010cb0f81c95e88/attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, ] "babel 2.11.0" = [ {url = "https://files.pythonhosted.org/packages/92/f7/86301a69926e11cd52f73396d169554d09b20b1723a040c2dcc1559ef588/Babel-2.11.0-py3-none-any.whl", hash = "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe"}, {url = "https://files.pythonhosted.org/packages/ff/80/45b42203ecc32c8de281f52e3ec81cb5e4ef16127e9e8543089d8b1649fb/Babel-2.11.0.tar.gz", hash = "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6"}, ] "blinker 1.5" = [ {url = "https://files.pythonhosted.org/packages/2b/12/82786486cefb68685bb1c151730f510b0f4e5d621d77f245bc0daf9a6c64/blinker-1.5.tar.gz", hash = "sha256:923e5e2f69c155f2cc42dafbbd70e16e3fde24d2d4aa2ab72fbe386238892462"}, {url = "https://files.pythonhosted.org/packages/30/41/caa5da2dbe6d26029dfe11d31dfa8132b4d6d30b6d6b61a24824075a5f06/blinker-1.5-py2.py3-none-any.whl", hash = "sha256:1eb563df6fdbc39eeddc177d953203f99f097e9bf0e2b8f9f3cf18b6ca425e36"}, ] "cachetools 5.3.0" = [ {url = "https://files.pythonhosted.org/packages/4d/91/5837e9f9e77342bb4f3ffac19ba216eef2cd9b77d67456af420e7bafe51d/cachetools-5.3.0.tar.gz", hash = "sha256:13dfddc7b8df938c21a940dfa6557ce6e94a2f1cdfa58eb90c805721d58f2c14"}, {url = "https://files.pythonhosted.org/packages/db/14/2b48a834d349eee94677e8702ea2ef98b7c674b090153ea8d3f6a788584e/cachetools-5.3.0-py3-none-any.whl", hash = "sha256:429e1a1e845c008ea6c85aa35d4b98b65d6a9763eeef3e37e92728a12d1de9d4"}, ] "certifi 2022.12.7" = [ {url = "https://files.pythonhosted.org/packages/37/f7/2b1b0ec44fdc30a3d31dfebe52226be9ddc40cd6c0f34ffc8923ba423b69/certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, {url = "https://files.pythonhosted.org/packages/71/4c/3db2b8021bd6f2f0ceb0e088d6b2d49147671f25832fb17970e9b583d742/certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, ] "cfgv 3.3.1" = [ {url = "https://files.pythonhosted.org/packages/6d/82/0a0ebd35bae9981dea55c06f8e6aaf44a49171ad798795c72c6f64cba4c2/cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {url = "https://files.pythonhosted.org/packages/c4/bf/d0d622b660d414a47dc7f0d303791a627663f554345b21250e39e7acb48b/cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] "chardet 5.1.0" = [ {url = "https://files.pythonhosted.org/packages/41/32/cdc91dcf83849c7385bf8e2a5693d87376536ed000807fa07f5eab33430d/chardet-5.1.0.tar.gz", hash = "sha256:0d62712b956bc154f85fb0a266e2a3c5913c2967e00348701b32411d6def31e5"}, {url = "https://files.pythonhosted.org/packages/74/8f/8fc49109009e8d2169d94d72e6b1f4cd45c13d147ba7d6170fb41f22b08f/chardet-5.1.0-py3-none-any.whl", hash = "sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9"}, ] "charset-normalizer 3.0.1" = [ {url = "https://files.pythonhosted.org/packages/00/35/830c29e5dab61932224c7a6c89427090164a3e425cf03486ce7a3ce60623/charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"}, {url = "https://files.pythonhosted.org/packages/01/ff/9ee4a44e8c32fe96dfc12daa42f29294608a55eadc88f327939327fb20fb/charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"}, {url = "https://files.pythonhosted.org/packages/02/49/78b4c1bc8b1b0e0fc66fb31ce30d8302f10a1412ba75de72c57532f0beb0/charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"}, {url = "https://files.pythonhosted.org/packages/03/5e/e81488c74e86eef85cf085417ed945da2dcca87ed22d76202680c16bd3c3/charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"}, {url = "https://files.pythonhosted.org/packages/0b/8b/3cf0eff3c8b6734cd4336c23a3141846d579931a31e6476c8091961f1e25/charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"}, {url = "https://files.pythonhosted.org/packages/0e/d3/c5fa421dc69bb77c581ed561f1ec6656109c97731ad1128aa93d8bad3053/charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"}, {url = "https://files.pythonhosted.org/packages/0e/fd/0d099502582af039ef8a8c954d69d7dadbe5f425cb1b24d175eb0034ea9e/charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"}, {url = "https://files.pythonhosted.org/packages/0f/45/f462f534dd2853ebbc186ed859661db454665b1dc9ae6c690d982153cda9/charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"}, {url = "https://files.pythonhosted.org/packages/12/e5/aa09a1c39c3e444dd223d63e2c816c18ed78d035cff954143b2a539bdc9e/charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"}, {url = "https://files.pythonhosted.org/packages/16/bd/671f11f920dfb46de848e9176d84ddb25b3bbdffac6751cbbf691c0b5b17/charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"}, {url = "https://files.pythonhosted.org/packages/17/67/4b25c0358a2e812312b551e734d58855d58f47d0e0e9d1573930003910cb/charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"}, {url = "https://files.pythonhosted.org/packages/17/da/fdf8ffc33716c82cae06008159a55a581fa515e8dd02e3395dcad42ff83d/charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"}, {url = "https://files.pythonhosted.org/packages/20/a2/16b2cbf5f73bdd10624b94647b85c008ba25059792a5c7b4fdb8358bceeb/charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"}, {url = "https://files.pythonhosted.org/packages/25/19/298089cef2eb82fd3810d982aa239d4226594f99e1fe78494cb9b47b03c9/charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"}, {url = "https://files.pythonhosted.org/packages/25/b5/f477e419b06e49f3bae446cbdc1fd71d2599be8b12b4d45c641c5a4495b1/charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"}, {url = "https://files.pythonhosted.org/packages/27/b1/8dfcfa5d9978b845466cd41973b3d714eba3926fcb50f6fcddd45cfb75a2/charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"}, {url = "https://files.pythonhosted.org/packages/2d/02/0f875eb6a1cf347bd3a6098f458f79796aafa3b51090fd7b2784736dc67d/charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"}, {url = "https://files.pythonhosted.org/packages/2e/7b/5053a4a46fac017fd2aea3dc9abdd9983fd4cef153b6eb6aedcb0d7cb6e3/charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"}, {url = "https://files.pythonhosted.org/packages/31/06/f6330ee70c041a032ee1a5d32785d69748cfa41f64b6d327cc08cae51de9/charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"}, {url = "https://files.pythonhosted.org/packages/31/af/67b7653a35dbd56f6bb9ff54652a551eae8420d1d0545f0042c5bdb15fb0/charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"}, {url = "https://files.pythonhosted.org/packages/35/86/d85885ed7ac236a297b0b8beab5f0703fc0516f803ddf7b1910f255b83f3/charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"}, {url = "https://files.pythonhosted.org/packages/37/00/ca188e0a2b3cd3184cdd2521b8765cf579327d128caa8aedc3dc7614020a/charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"}, {url = "https://files.pythonhosted.org/packages/37/60/7a01f3a129d1af1f26ab2c56aae89a72dbf33fd46a467c1aa994ec62b90b/charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"}, {url = "https://files.pythonhosted.org/packages/3a/91/a233f06d33dc3ac90a9991d238fbc68c59615d9f71be1801e14ac4e42d7f/charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"}, {url = "https://files.pythonhosted.org/packages/46/69/9f42514a9f58c602ab89a2af89081a475dccd959f9bc01ba7e61372d31bd/charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"}, {url = "https://files.pythonhosted.org/packages/55/2b/35619e03725bfa4af4a902e1996c9ee8052d6bce005ff79c9bd988820cb4/charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"}, {url = "https://files.pythonhosted.org/packages/56/5d/275fb120957dfe5a2262d04f28bc742fd4bcc2bd270d19bb8757e09737ef/charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"}, {url = "https://files.pythonhosted.org/packages/5a/d8/9e76846e70e729de85ecc6af21edc584a2adfef202dc5f5ae00a02622e3d/charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"}, {url = "https://files.pythonhosted.org/packages/5b/e7/5527effca09d873e07e128d3daac7c531203b5105cb4e2956c2b7a8cc41c/charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"}, {url = "https://files.pythonhosted.org/packages/67/c6/cf4e8a8f41201284bdf200f764b29a87f6f7d22fe3c9eddab602af489acc/charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"}, {url = "https://files.pythonhosted.org/packages/68/2b/02e9d6a98ddb73fa238d559a9edcc30b247b8dc4ee848b6184c936e99dc0/charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"}, {url = "https://files.pythonhosted.org/packages/6a/ab/3a00ecbddabe25132c20c1bd45e6f90c537b5f7a0b5bcaba094c4922928c/charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"}, {url = "https://files.pythonhosted.org/packages/6e/a3/997ff79260f76210b1d73463b9081ae7edbf16ff3d611b67f5e72c685cab/charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"}, {url = "https://files.pythonhosted.org/packages/6e/d7/1d4035fcbf7d0f2e89588a142628355d8d1cd652a227acefb9ec85908cd4/charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"}, {url = "https://files.pythonhosted.org/packages/71/67/79be03bf7ab4198d994c2e8da869ca354487bfa25656b95cf289cf6338a2/charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"}, {url = "https://files.pythonhosted.org/packages/80/54/183163f9910936e57a60ee618f4f5cc91c2f8333ee2d4ebc6c50f6c8684d/charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"}, {url = "https://files.pythonhosted.org/packages/82/49/ab81421d5aa25bc8535896a017c93204cb4051f2a4e72b1ad8f3b594e072/charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"}, {url = "https://files.pythonhosted.org/packages/84/0e/5965dd90991e4f2588718b865115a78c8b040193ac3676f757b7fb6af9d0/charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"}, {url = "https://files.pythonhosted.org/packages/84/ff/78a4942ef1ea4d1c464cc9a132122b36c5390c5cf6301ed0f9e3e6e24bd9/charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"}, {url = "https://files.pythonhosted.org/packages/86/eb/31c9025b4ed7eddd930c5f2ac269efb953de33140608c7539675d74a2081/charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"}, {url = "https://files.pythonhosted.org/packages/87/5d/0ebaee2249a04fd20bb4baeb9ea2c29dee17317175d9d67b4f5f34cf048d/charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"}, {url = "https://files.pythonhosted.org/packages/89/87/c237a299a658b35d19fd531eeb8247480627fc2fb4b7a471334b48850f45/charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"}, {url = "https://files.pythonhosted.org/packages/8f/e2/73ea48d2608f71a879588b607e093d550b8eaa177eb31bbdf1c01e515818/charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"}, {url = "https://files.pythonhosted.org/packages/90/2c/bb5e4f7e2e9871793b5c0fb5c6c4056458a148a05143143320f2d4a410a9/charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"}, {url = "https://files.pythonhosted.org/packages/90/59/941e2e5ae6828a688c6437ad16e026eb3606d0cfdd13ea5c9090980f3ffd/charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"}, {url = "https://files.pythonhosted.org/packages/92/00/b8dc8dd725297b05f1ab4929c9d7e879f31746131534221c5c8948bc7563/charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"}, {url = "https://files.pythonhosted.org/packages/93/d1/569445a704414e150f198737c245ab96b40d28d5b68045a62c414a5157de/charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"}, {url = "https://files.pythonhosted.org/packages/96/d7/1675d9089a1f4677df5eb29c3f8b064aa1e70c1251a0a8a127803158942d/charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"}, {url = "https://files.pythonhosted.org/packages/97/f9/366db2d2cf69d641159d6448b813ac9b1b5f9807a46fde6c50b36c1387f8/charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"}, {url = "https://files.pythonhosted.org/packages/98/e4/d4685870fda1cc7c5e29899ec329500460418e54f4f5df76ee520e30689a/charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"}, {url = "https://files.pythonhosted.org/packages/98/f4/5ca33ee1e0b3412cbd13eae230321a9fe819acf1a99ad6482420fb97cc6b/charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"}, {url = "https://files.pythonhosted.org/packages/99/24/eb846dc9a797da58e6e5b3b5a71d3ff17264de3f424fb29aaa5d27173b55/charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"}, {url = "https://files.pythonhosted.org/packages/9a/bf/c9fa15ccf216a69aaaa735c961d7fac2a2801a1b01023fe05d194bf076b4/charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"}, {url = "https://files.pythonhosted.org/packages/9c/42/c1ebc736c57459aab28bfb8aa28c6a047796f2ea46050a3b129b4920dbe4/charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"}, {url = "https://files.pythonhosted.org/packages/9c/94/1725fc3e0dbe8918a4ec6dd317ec1ef388e701bdfb5053e1f34f5c6d5a8e/charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"}, {url = "https://files.pythonhosted.org/packages/9f/5a/9dc8932d1e5f8eeaa502e3c3fce91c86be20c04eb3ec202d2b7d74b567e5/charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"}, {url = "https://files.pythonhosted.org/packages/a0/98/7b0d3a853af59e092cdd77c7e1c67ca92fd6acc126285240dbb552b4162f/charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"}, {url = "https://files.pythonhosted.org/packages/a2/93/0b1aa4dbc0ae2aa2e1b2e6d037ab8984dc09912d6b26d63ced14da07e3a7/charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"}, {url = "https://files.pythonhosted.org/packages/a2/a7/adc963ad8f8fddadd6be088e636972705ec9d1d92d1b45e6119eb02b7e9e/charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"}, {url = "https://files.pythonhosted.org/packages/a3/09/a837b27b122e710dfad15b0b5df04cd0623c8d8d3382e4298f50798fb84a/charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"}, {url = "https://files.pythonhosted.org/packages/a3/4b/f565c852163312a0991c30598f403fd06796a12e408d7839cc46ca8d7f4a/charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"}, {url = "https://files.pythonhosted.org/packages/aa/a4/2d6255d4db5d4558a92458fd8dacddfdda2fb4ad9c0a87db6f6034aded34/charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"}, {url = "https://files.pythonhosted.org/packages/af/63/2c00ff4e657fb9bb76306ffbc7878fd52067e39716f5e8b0dd5582caf1fa/charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"}, {url = "https://files.pythonhosted.org/packages/b2/4c/9a4f30042bfee22d34d80daf75f51817cdd23180d718e0160aab235c4faf/charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"}, {url = "https://files.pythonhosted.org/packages/b5/1a/932d86fde86bb0d2992c74552c9a422883fe0890132bbc9a5e2211f03318/charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"}, {url = "https://files.pythonhosted.org/packages/b6/c2/da108d835354b49aa5c738906e9b6a197b071bc5d77d223f6cd98119172a/charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"}, {url = "https://files.pythonhosted.org/packages/c0/4d/6b82099e3f25a9ed87431e2f51156c14f3a9ce8fad73880a3856cd95f1d5/charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"}, {url = "https://files.pythonhosted.org/packages/c1/06/b7b1d3d186e0f288500b8a1161ede6b38a0abbf878c2033d667e815e6bd7/charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"}, {url = "https://files.pythonhosted.org/packages/c1/b2/d81606aebeb7e9a33dc877ff3a206c9946f5bb374c99d22d4a28825aa270/charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"}, {url = "https://files.pythonhosted.org/packages/c4/d4/94f1ea460cce04483d2460efba6fd4d66e6f60ad6fc6075dba13e3501e48/charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"}, {url = "https://files.pythonhosted.org/packages/c8/a2/8f873138c99423de3b402daf8ccd7a538632c83d0c129444a6a18ef34e03/charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"}, {url = "https://files.pythonhosted.org/packages/c9/dd/80a5e8c080b7e1cc2b0ca35f0d6aeedafd7bbd06d25031ac20868b1366d6/charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"}, {url = "https://files.pythonhosted.org/packages/d2/7f/3c8a6db3eda16ce79a01552ec85ac8fd0ea6265976eb4db250a60b7416ab/charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"}, {url = "https://files.pythonhosted.org/packages/d3/5b/4031145fcfb9ceaf49dad2fbf9a44e062eb2c08aff36f71d8aafbecf4567/charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"}, {url = "https://files.pythonhosted.org/packages/d9/7a/60d45c9453212b30eebbf8b5cddbdef330eebddfcf335bce7920c43fb72e/charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"}, {url = "https://files.pythonhosted.org/packages/dc/ff/2c7655d83b1d6d6a0e132d50d54131fcb8da763b417ccc6c4a506aa0e08c/charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"}, {url = "https://files.pythonhosted.org/packages/df/2f/4806e155191f75e720aca98a969581c6b2676f0379dd315c34c388bbf8b5/charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"}, {url = "https://files.pythonhosted.org/packages/df/c5/dd3a17a615775d0ffc3e12b0e47833d8b7e0a4871431dad87a3f92382a19/charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"}, {url = "https://files.pythonhosted.org/packages/e1/7f/64b51f144fa9e74da63fa690d9563eae627f4df6cc6ae5185a781e1912e5/charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"}, {url = "https://files.pythonhosted.org/packages/e3/96/8cdbce165c96cce5f2c9c7748f7ed8e0cf0c5d03e213bbc90b7c3e918bf5/charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"}, {url = "https://files.pythonhosted.org/packages/e7/0d/5eaceb5abfc000cca204af9f50e9839462dc0bb1c4e0f4b14ed370e3febd/charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"}, {url = "https://files.pythonhosted.org/packages/e8/80/141f6af05332cbb811ab469f64deb1e1d4cc9e8b0c003aa8a38d689ce84a/charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"}, {url = "https://files.pythonhosted.org/packages/f0/78/30d853a3073c866b47abede6d86b5532aa99ac67a95e86077d20be1ce481/charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"}, {url = "https://files.pythonhosted.org/packages/f1/14/ed5990189a6a25ae9f8d63e74cd0336189f9ad7e51f066ba2f6cb73e8126/charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"}, {url = "https://files.pythonhosted.org/packages/f1/ff/9a1c65d8c44958f45ae40cd558ab63bd499a35198a2014e13c0887c07ed1/charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"}, {url = "https://files.pythonhosted.org/packages/f5/84/cac681144a28114bd9e40d3cdbfd961c14ecc2b56f1baec2094afd6744c7/charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"}, {url = "https://files.pythonhosted.org/packages/f5/ec/a9bed59079bd0267d34ada58a4048c96a59b3621e7f586ea85840d41831d/charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"}, {url = "https://files.pythonhosted.org/packages/fc/64/443267b7824283b3e0e33cee4240c079939a970c2c9a5a3164fc988d690b/charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"}, ] "click 8.1.3" = [ {url = "https://files.pythonhosted.org/packages/59/87/84326af34517fca8c58418d148f2403df25303e02736832403587318e9e8/click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, {url = "https://files.pythonhosted.org/packages/c2/f1/df59e28c642d583f7dacffb1e0965d0e00b218e0186d7858ac5233dce840/click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, ] "colorama 0.4.6" = [ {url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] "coverage 7.1.0" = [ {url = "https://files.pythonhosted.org/packages/0e/4a/2c9a63f52f819aaad02e99d1bc818e6bb21856a285b7a3d559eff2a3840e/coverage-7.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7ee5c9bb51695f80878faaa5598040dd6c9e172ddcf490382e8aedb8ec3fec8d"}, {url = "https://files.pythonhosted.org/packages/11/88/53d271bf64f64da832c696b552ab2bb4aa59128fa6422de60f50b5a74bba/coverage-7.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb726cb861c3117a553f940372a495fe1078249ff5f8a5478c0576c7be12050"}, {url = "https://files.pythonhosted.org/packages/13/4a/d452bd3a9e749151afd107c34b66f69ae1f8bbd48bfc2ed2a6b89748c4d8/coverage-7.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5b15ed7644ae4bee0ecf74fee95808dcc34ba6ace87e8dfbf5cb0dc20eab45a"}, {url = "https://files.pythonhosted.org/packages/16/f6/7ad07d231c09a5dad2813457c9f102780e1049f8019fbe78c4a9a024d7f0/coverage-7.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:6a43c7823cd7427b4ed763aa7fb63901ca8288591323b58c9cd6ec31ad910f3c"}, {url = "https://files.pythonhosted.org/packages/18/a0/bfa6c6ab7a5f0aeb69dd169d956ead54133f5bca68a5945c4569ea2c40b3/coverage-7.1.0.tar.gz", hash = "sha256:10188fe543560ec4874f974b5305cd1a8bdcfa885ee00ea3a03733464c4ca265"}, {url = "https://files.pythonhosted.org/packages/18/dc/93a22c132c893d461c6e904a0bfe4e1ddcdbcb558f0e2c9756369556051d/coverage-7.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9817733f0d3ea91bea80de0f79ef971ae94f81ca52f9b66500c6a2fea8e4b4f8"}, {url = "https://files.pythonhosted.org/packages/1c/2b/5d1387301c36f3bcb040aa5d51372475d85642711fb2237b5545eb564fa5/coverage-7.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9d58885215094ab4a86a6aef044e42994a2bd76a446dc59b352622655ba6621b"}, {url = "https://files.pythonhosted.org/packages/1d/5f/f5b1e50e7806758d3a189e565eea2b54199658f27fff2da36d915f042e16/coverage-7.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ffeeb38ee4a80a30a6877c5c4c359e5498eec095878f1581453202bfacc8fbc2"}, {url = "https://files.pythonhosted.org/packages/1e/b5/dc65a49335dd78e1def7e0ec84bd144ba442b74e29a7dd236c551bd8b6bf/coverage-7.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2bf1d5f2084c3932b56b962a683074a3692bce7cabd3aa023c987a2a8e7612f6"}, {url = "https://files.pythonhosted.org/packages/26/c7/9272d6094a8aea80d244b105429ad5953795344415f10538c089184daf27/coverage-7.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8ae125d1134bf236acba8b83e74c603d1b30e207266121e76484562bc816344c"}, {url = "https://files.pythonhosted.org/packages/2c/40/2b3f87a28d592f5eed431490cc019ac74859d537e0d33ab7ccd976a4c860/coverage-7.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b643cb30821e7570c0aaf54feaf0bfb630b79059f85741843e9dc23f33aaca2c"}, {url = "https://files.pythonhosted.org/packages/37/84/6d932952986f5e44a38229ab6ef34ecb438bf29d0c3eb23da1f7582fd44d/coverage-7.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4e4881fa9e9667afcc742f0c244d9364d197490fbc91d12ac3b5de0bf2df146"}, {url = "https://files.pythonhosted.org/packages/3e/f1/385b7fc2c7902d70f807b985010f955ffa5da4f74d5f32cd5317dd749f74/coverage-7.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a726d742816cb3a8973c8c9a97539c734b3a309345236cd533c4883dda05b8d"}, {url = "https://files.pythonhosted.org/packages/40/7a/849efa68d38db7ed6e4794de122dc9558d420b84715d2604f9bec5dbbda5/coverage-7.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc7c85a150501286f8b56bd8ed3aa4093f4b88fb68c0843d21ff9656f0009d6a"}, {url = "https://files.pythonhosted.org/packages/45/a0/d3986ff9a585e8053893c05606be3a812ff7407869d1006c8abbba5e6179/coverage-7.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3164d31078fa9efe406e198aecd2a02d32a62fecbdef74f76dad6a46c7e48311"}, {url = "https://files.pythonhosted.org/packages/45/cf/45b4083971818aaf96dfb8a49d3a793fc012a154e20ab0a85d162029860c/coverage-7.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b4198d85a3755d27e64c52f8c95d6333119e49fd001ae5798dac872c95e0f8"}, {url = "https://files.pythonhosted.org/packages/47/39/c7bf56840efb9c89ff578da1092023d071bb0e12f63b017741402c3bac47/coverage-7.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:8361be1c2c073919500b6601220a6f2f98ea0b6d2fec5014c1d9cfa23dd07038"}, {url = "https://files.pythonhosted.org/packages/47/c8/de630d1b872cc65f1878536e00fc0a1b610508f018ad90f957b171de06a6/coverage-7.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2617759031dae1bf183c16cef8fcfb3de7617f394c813fa5e8e46e9b82d4222"}, {url = "https://files.pythonhosted.org/packages/4e/a0/a8002c51ce13ad68db9d30c71282b3166186ab022d978c3f707b18dce6bd/coverage-7.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c7c0d0827e853315c9bbd43c1162c006dd808dbbe297db7ae66cd17b07830f0"}, {url = "https://files.pythonhosted.org/packages/52/f9/155bfe715ae87f52b3cc15aca476dc8db91c8daca7419d6e5eee931cc21f/coverage-7.1.0-pp37.pp38.pp39-none-any.whl", hash = "sha256:755e89e32376c850f826c425ece2c35a4fc266c081490eb0a841e7c1cb0d3bda"}, {url = "https://files.pythonhosted.org/packages/60/ff/32baaa70fda28652105fca9f534830a31dec6dd713bd65f88149bb2a4aac/coverage-7.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2cba5c6db29ce991029b5e4ac51eb36774458f0a3b8d3137241b32d1bb91f06"}, {url = "https://files.pythonhosted.org/packages/6c/97/5c6ceef429c0b38d1f5db700697f7f40c379ba498a3778f7365a64d8b03d/coverage-7.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:33d1ae9d4079e05ac4cc1ef9e20c648f5afabf1a92adfaf2ccf509c50b85717f"}, {url = "https://files.pythonhosted.org/packages/6d/79/ba99924e6926760b8a5855dd473c2205de0a9115883262f6ef7f23a36f96/coverage-7.1.0-cp38-cp38-win32.whl", hash = "sha256:04481245ef966fbd24ae9b9e537ce899ae584d521dfbe78f89cad003c38ca2ab"}, {url = "https://files.pythonhosted.org/packages/70/bf/001c2d03855f137ab5d6c67992ea724e231f75756e3360d247bd84cfe231/coverage-7.1.0-cp311-cp311-win32.whl", hash = "sha256:ded59300d6330be27bc6cf0b74b89ada58069ced87c48eaf9344e5e84b0072f7"}, {url = "https://files.pythonhosted.org/packages/75/2d/912410554689b492c2c04663a5cd65ed372cfb80eaedfcb9c5b693c197cb/coverage-7.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:51b236e764840a6df0661b67e50697aaa0e7d4124ca95e5058fa3d7cbc240b7c"}, {url = "https://files.pythonhosted.org/packages/76/cb/36a1cf1c75caa970533dd6e3edd92a98f1997686c3c4acbceaa92ff6a06e/coverage-7.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da9b41d4539eefd408c46725fb76ecba3a50a3367cafb7dea5f250d0653c1040"}, {url = "https://files.pythonhosted.org/packages/76/f1/1754166ef29b4fc4db8f0cc03007bfafea9c6fd7e4453ad04118a6264903/coverage-7.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d47dd659a4ee952e90dc56c97d78132573dc5c7b09d61b416a9deef4ebe01a0c"}, {url = "https://files.pythonhosted.org/packages/78/ce/04337e09985687238b4b57403786a4a87814fe6035013f65359134c77c6c/coverage-7.1.0-cp37-cp37m-win32.whl", hash = "sha256:3b155caf3760408d1cb903b21e6a97ad4e2bdad43cbc265e3ce0afb8e0057e73"}, {url = "https://files.pythonhosted.org/packages/8f/42/9e3920032dbe70ec83bf60672d28ff764fb7ad49bac060411a68a54b8758/coverage-7.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d12d076582507ea460ea2a89a8c85cb558f83406c8a41dd641d7be9a32e1274f"}, {url = "https://files.pythonhosted.org/packages/92/5d/9a24dc820aa16eccda21ccdef823510bca3997901230f610ef5153eb915e/coverage-7.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c45948f613d5d18c9ec5eaa203ce06a653334cf1bd47c783a12d0dd4fd9c851"}, {url = "https://files.pythonhosted.org/packages/95/36/0779051758526614eddd6ddfdb53764c6f2c3d58e89c80a04bef021c88d7/coverage-7.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e5cdbb5cafcedea04924568d990e20ce7f1945a1dd54b560f879ee2d57226912"}, {url = "https://files.pythonhosted.org/packages/97/76/1a39d67eed8dd260f1fc94423309eb3eb809150554062c8938403c891deb/coverage-7.1.0-cp310-cp310-win32.whl", hash = "sha256:4b14d5e09c656de5038a3f9bfe5228f53439282abcab87317c9f7f1acb280352"}, {url = "https://files.pythonhosted.org/packages/9a/90/ebe8683c01e647d15b128ce0b20aca7215317cbf2e36df7722a759e88b74/coverage-7.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:218fe982371ac7387304153ecd51205f14e9d731b34fb0568181abaf7b443ba0"}, {url = "https://files.pythonhosted.org/packages/9d/1f/0332d1a22abe7462e6bf12906c95dc1b89ad288611a891f9189fb2e62678/coverage-7.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3baf5f126f30781b5e93dbefcc8271cb2491647f8283f20ac54d12161dff080e"}, {url = "https://files.pythonhosted.org/packages/ad/bb/a5c7cd34be5d589f6bdc6b81b052e3ac5a56a8cba5d75d9c17a6ab36f564/coverage-7.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef382417db92ba23dfb5864a3fc9be27ea4894e86620d342a116b243ade5d35d"}, {url = "https://files.pythonhosted.org/packages/b0/60/39dbed89206fe0572561169e33ed3f0e76041adfec1a5b577371fef20d97/coverage-7.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b946bbcd5a8231383450b195cfb58cb01cbe7f8949f5758566b881df4b33baf"}, {url = "https://files.pythonhosted.org/packages/b6/28/dc2b4d89a5a043ae6010bd02c6b93574d6031218f466a5e02686c4ee2187/coverage-7.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:33ff26d0f6cc3ca8de13d14fde1ff8efe1456b53e3f0273e63cc8b3c84a063d8"}, {url = "https://files.pythonhosted.org/packages/b6/d7/215ea44cbed1a15663d0a88620bedd13d6d1d9287c55c0af1a0f07170e2b/coverage-7.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:29571503c37f2ef2138a306d23e7270687c0efb9cab4bd8038d609b5c2393a3a"}, {url = "https://files.pythonhosted.org/packages/be/bf/217ad144ffb569b73d83e18eb794fedd9926cf636a9df2629de191e7c3ae/coverage-7.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:98b85dd86514d889a2e3dd22ab3c18c9d0019e696478391d86708b805f4ea0fa"}, {url = "https://files.pythonhosted.org/packages/c2/d0/60a9ac8ff523b0e3a4ff759fdebad023a5accc49d4e95c304ccaa282939c/coverage-7.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec8e767f13be637d056f7e07e61d089e555f719b387a7070154ad80a0ff31801"}, {url = "https://files.pythonhosted.org/packages/c3/f4/447fae940c944263e9d58c9351021b2eff6b7b4452488b7eff9c27913c4a/coverage-7.1.0-cp39-cp39-win32.whl", hash = "sha256:d248cd4a92065a4d4543b8331660121b31c4148dd00a691bfb7a5cdc7483cfa4"}, {url = "https://files.pythonhosted.org/packages/cb/8f/2f5c5f2dd93d90d03e246aa84ba5e756929cd2fa15890af97ba0c4f84ddf/coverage-7.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38da2db80cc505a611938d8624801158e409928b136c8916cd2e203970dde4dc"}, {url = "https://files.pythonhosted.org/packages/cc/c8/3700779abfa359ef9af9ab2c76cfd86f2b3e8446c32c4e136823684698d0/coverage-7.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7ed681b0f8e8bcbbffa58ba26fcf5dbc8f79e7997595bf071ed5430d8c08d6f3"}, {url = "https://files.pythonhosted.org/packages/cc/eb/3c2096bfcca48d5966a38c3fe8e144599cb8cb0fb46accae29072a6121f0/coverage-7.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ccb092c9ede70b2517a57382a601619d20981f56f440eae7e4d7eaafd1d1d09"}, {url = "https://files.pythonhosted.org/packages/d1/40/d7c2594c6960e144202b95cf1e756a60a6847f15624cd9004d53f4fb8f46/coverage-7.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c31b75ae466c053a98bf26843563b3b3517b8f37da4d47b1c582fdc703112bc3"}, {url = "https://files.pythonhosted.org/packages/d6/b2/f29709f5cf448cca85f5a1ca586ecec3c48d68e9fac23b6dd185efaa5cfc/coverage-7.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2a60d6513781e87047c3e630b33b4d1e89f39836dac6e069ffee28c4786715f5"}, {url = "https://files.pythonhosted.org/packages/d7/77/7e63d1b143df33195b3c468953aa87613324483adebb240d28486b4f3ac5/coverage-7.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db61a79c07331e88b9a9974815c075fbd812bc9dbc4dc44b366b5368a2936063"}, {url = "https://files.pythonhosted.org/packages/ea/a5/2fc1027e4530b9bda6dd541e8b63ea16623e58e306d2df3f2aa672eb7f90/coverage-7.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a5a5879a939cb84959d86869132b00176197ca561c664fc21478c1eee60d75"}, {url = "https://files.pythonhosted.org/packages/ec/36/d7e3235268624b7b8b8da9ce31f586e562bfaeaaf736b44f742dc0e82c92/coverage-7.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beeb129cacea34490ffd4d6153af70509aa3cda20fdda2ea1a2be870dfec8d52"}, {url = "https://files.pythonhosted.org/packages/ef/92/edd214c7099c76a003238a342daf78621a04a9a8f37ce5dc61f3e4e91410/coverage-7.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:63ffd21aa133ff48c4dff7adcc46b7ec8b565491bfc371212122dd999812ea1c"}, {url = "https://files.pythonhosted.org/packages/f8/f6/c978a4f888393779725b64a1b1de5137a30b00d8a017be3074c225827d1b/coverage-7.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32df215215f3af2c1617a55dbdfb403b772d463d54d219985ac7cd3bf124cada"}, ] "distlib 0.3.6" = [ {url = "https://files.pythonhosted.org/packages/58/07/815476ae605bcc5f95c87a62b95e74a1bce0878bc7a3119bc2bf4178f175/distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, {url = "https://files.pythonhosted.org/packages/76/cb/6bbd2b10170ed991cf64e8c8b85e01f2fb38f95d1bc77617569e0b0b26ac/distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, ] "docutils 0.19" = [ {url = "https://files.pythonhosted.org/packages/6b/5c/330ea8d383eb2ce973df34d1239b3b21e91cd8c865d21ff82902d952f91f/docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, {url = "https://files.pythonhosted.org/packages/93/69/e391bd51bc08ed9141ecd899a0ddb61ab6465309f1eb470905c0c8868081/docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, ] "exceptiongroup 1.1.0" = [ {url = "https://files.pythonhosted.org/packages/15/ab/dd27fb742b19a9d020338deb9ab9a28796524081bca880ac33c172c9a8f6/exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, {url = "https://files.pythonhosted.org/packages/e8/14/9c6a7e5f12294ccd6975a45e02899ed25468cd7c2c86f3d9725f387f9f5f/exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, ] "filelock 3.9.0" = [ {url = "https://files.pythonhosted.org/packages/0b/dc/eac02350f06c6ed78a655ceb04047df01b02c6b7ea3fc02d4df24ca87d24/filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, {url = "https://files.pythonhosted.org/packages/14/4c/b201d0292ca4e0950f0741212935eac9996f69cd66b92a3587e594999163/filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, ] "flask 2.2.2" = [ {url = "https://files.pythonhosted.org/packages/0f/43/15f4f9ab225b0b25352412e8daa3d0e3d135fcf5e127070c74c3632c8b4c/Flask-2.2.2-py3-none-any.whl", hash = "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"}, {url = "https://files.pythonhosted.org/packages/69/b6/53cfa30eed5aa7343daff36622843688ba8c6fe9829bb2b92e193ab1163f/Flask-2.2.2.tar.gz", hash = "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b"}, ] "greenlet 2.0.1" = [ {url = "https://files.pythonhosted.org/packages/00/0b/5a6cbd757eeb53c4adb3a5037b3e0474a548838d699034982984c9409440/greenlet-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e112e03d37987d7b90c1e98ba5e1b59e1645226d78d73282f45b326f7bddcb9"}, {url = "https://files.pythonhosted.org/packages/01/ee/90c95aa12243d93b7b88c4dda580c728ce384a6becb793a57438d01e7ac6/greenlet-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0459d94f73265744fee4c2d5ec44c6f34aa8a31017e6e9de770f7bcf29710be9"}, {url = "https://files.pythonhosted.org/packages/02/a0/3977da8e4d21f37e93b803078cb27e026714161977d8997ba9169913b0b1/greenlet-2.0.1-cp27-cp27m-win_amd64.whl", hash = "sha256:3001d00eba6bbf084ae60ec7f4bb8ed375748f53aeaefaf2a37d9f0370558524"}, {url = "https://files.pythonhosted.org/packages/0a/77/837fb1e3218d1fc539268ae7e558d53d9eca5cbbf102abf6f5752380fbeb/greenlet-2.0.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:cd4ccc364cf75d1422e66e247e52a93da6a9b73cefa8cad696f3cbbb75af179d"}, {url = "https://files.pythonhosted.org/packages/0c/29/d6dcf7061d3cab9471ae0576066a40aedcbbb9f9741b52a5f35252ec6f17/greenlet-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b0ff9878333823226d270417f24f4d06f235cb3e54d1103b71ea537a6a86ce"}, {url = "https://files.pythonhosted.org/packages/13/76/59877e19553c9b0a9b8ade7a4548e39755e678c69d161ce4fb5b20f55338/greenlet-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0109af1138afbfb8ae647e31a2b1ab030f58b21dd8528c27beaeb0093b7938a9"}, {url = "https://files.pythonhosted.org/packages/14/9a/6779fca2a4961efe49e12ed4352334cedb67750dd741f5eb13aec1895844/greenlet-2.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c0757db9bd08470ff8277791795e70d0bf035a011a528ee9a5ce9454b6cba2"}, {url = "https://files.pythonhosted.org/packages/17/ef/264b22b4e88cb90400543bc653eea1aa1b93bdc1bf584429d43b87062b81/greenlet-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:41b825d65f31e394b523c84db84f9383a2f7eefc13d987f308f4663794d2687e"}, {url = "https://files.pythonhosted.org/packages/1a/ed/72998fb3609f6c4b0817df32e2b98a88bb8510613d12d495bbab8534ebd0/greenlet-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b493db84d124805865adc587532ebad30efa68f79ad68f11b336e0a51ec86c2"}, {url = "https://files.pythonhosted.org/packages/1c/e2/4ebd0108dfb738c9e00a2f010a53329b3fdac4e6c327cfbb0dee5f33682e/greenlet-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c8b1c43e75c42a6cafcc71defa9e01ead39ae80bd733a2608b297412beede68"}, {url = "https://files.pythonhosted.org/packages/26/ac/a20b569931f6299095bf1a0be53c9b705d9093318a7bc89c0151db5d8aab/greenlet-2.0.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d566b82e92ff2e09dd6342df7e0eb4ff6275a3f08db284888dcd98134dbd4243"}, {url = "https://files.pythonhosted.org/packages/30/0f/da31725f79c0ec26bebe5f3eee3ef8434466910b6880e48045a47bcedc3b/greenlet-2.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cce1e90dd302f45716a7715517c6aa0468af0bf38e814ad4eab58e88fc09f7f7"}, {url = "https://files.pythonhosted.org/packages/37/d4/016401b6f9c282e681e0660f43a84ef6b9ff75c3339a740fb938f0e51a4a/greenlet-2.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dca09dedf1bd8684767bc736cc20c97c29bc0c04c413e3276e0962cd7aeb148"}, {url = "https://files.pythonhosted.org/packages/3b/13/e9ca546921df9887f46c25c72fd4a1e0f7293821a803bb25f986c9f96f53/greenlet-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:13ebf93c343dd8bd010cd98e617cb4c1c1f352a0cf2524c82d3814154116aa82"}, {url = "https://files.pythonhosted.org/packages/3b/c4/01247dcd15d3f9919760bc8c0846f97020e5bacc35b7899cf5cb02313a16/greenlet-2.0.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:0722c9be0797f544a3ed212569ca3fe3d9d1a1b13942d10dd6f0e8601e484d26"}, {url = "https://files.pythonhosted.org/packages/40/37/c371ad2720b00869937a159a93f8711338eabb9af618db6f5d4836fe2e0a/greenlet-2.0.1-cp27-cp27m-win32.whl", hash = "sha256:1407fe45246632d0ffb7a3f4a520ba4e6051fc2cbd61ba1f806900c27f47706a"}, {url = "https://files.pythonhosted.org/packages/44/8b/f1938147df360b4fcfe3efd4dd06d12d7509bddac391b1ceb419c8ebca53/greenlet-2.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9f2c221eecb7ead00b8e3ddb913c67f75cba078fd1d326053225a3f59d850d72"}, {url = "https://files.pythonhosted.org/packages/48/dc/ffd65707d554f34862e42b62a0d1b2d826ba417bbb17d5f832cd92ccdcda/greenlet-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:ea688d11707d30e212e0110a1aac7f7f3f542a259235d396f88be68b649e47d1"}, {url = "https://files.pythonhosted.org/packages/4a/6b/f099c2aa8928bcb7bcd14c12ebd4fd63df4ed4ea825012bf7546339d9b4b/greenlet-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e9744c657d896c7b580455e739899e492a4a452e2dd4d2b3e459f6b244a638d"}, {url = "https://files.pythonhosted.org/packages/56/fd/756f7f78ba8f307ac90a7883f426aa564eac4e38d6cc53c11cf72879648b/greenlet-2.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:811e1d37d60b47cb8126e0a929b58c046251f28117cb16fcd371eed61f66b764"}, {url = "https://files.pythonhosted.org/packages/57/bd/28bafd6620cb81cb031abb8c9468f253e8cf4b81345e761e90c213e141cb/greenlet-2.0.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:9ed358312e63bf683b9ef22c8e442ef6c5c02973f0c2a939ec1d7b50c974015c"}, {url = "https://files.pythonhosted.org/packages/61/38/791c5ca51a9773a5a999feec5abbef4dd4067cddd504125e6266690a0ad8/greenlet-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974a39bdb8c90a85982cdb78a103a32e0b1be986d411303064b28a80611f6e51"}, {url = "https://files.pythonhosted.org/packages/62/4b/fd139b1a6afba8be9f8c1e06acf0ac35e3cc1ad70795edce8f9587eb54d6/greenlet-2.0.1-cp35-cp35m-win32.whl", hash = "sha256:81b0ea3715bf6a848d6f7149d25bf018fd24554a4be01fcbbe3fdc78e890b955"}, {url = "https://files.pythonhosted.org/packages/63/62/26fe837a910eadb48973458641f2d084967dbfdf000a7fefbb89bef8a753/greenlet-2.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be35822f35f99dcc48152c9839d0171a06186f2d71ef76dc57fa556cc9bf6b45"}, {url = "https://files.pythonhosted.org/packages/64/0b/a067f0c78e85abb8356231095735dc0b975efdc8ecc3934b788240bc2eaf/greenlet-2.0.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:cb242fc2cda5a307a7698c93173d3627a2a90d00507bccf5bc228851e8304963"}, {url = "https://files.pythonhosted.org/packages/65/64/2a21081db4a2952a6122c6efaca67c9331dac4b1cd30d862e096f023da59/greenlet-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:097e3dae69321e9100202fc62977f687454cd0ea147d0fd5a766e57450c569fd"}, {url = "https://files.pythonhosted.org/packages/72/07/51644335b344e1295fd7af13eb9a00b16237aea78c96fe5b5b2df1803a71/greenlet-2.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:356e4519d4dfa766d50ecc498544b44c0249b6de66426041d7f8b751de4d6b48"}, {url = "https://files.pythonhosted.org/packages/72/af/209920ea4a19a74b7638886b180e3144670fd14aec0b3a2a0da90587a67b/greenlet-2.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be9e0fb2ada7e5124f5282d6381903183ecc73ea019568d6d63d33f25b2a9000"}, {url = "https://files.pythonhosted.org/packages/75/ef/db6e0c16df3972f90614bd5ceb969d918a8c53ce4675f75cc641b2ad20ab/greenlet-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b23d2a46d53210b498e5b701a1913697671988f4bf8e10f935433f6e7c332fb6"}, {url = "https://files.pythonhosted.org/packages/78/96/86b6f5d21aa23fcd28987521ff1d71424e891c79b1a1af8ef991c9082b65/greenlet-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:505138d4fa69462447a562a7c2ef723c6025ba12ac04478bc1ce2fcc279a2db5"}, {url = "https://files.pythonhosted.org/packages/79/88/843935bbb56e51642fb92a6a91f07a359b95bbf72bb0b21515b62ab212d8/greenlet-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:662e8f7cad915ba75d8017b3e601afc01ef20deeeabf281bd00369de196d7726"}, {url = "https://files.pythonhosted.org/packages/7d/2e/316dd548499c8c1d0adb516bff06ec093a68024942f113ac6c794da09c7b/greenlet-2.0.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:b1992ba9d4780d9af9726bbcef6a1db12d9ab1ccc35e5773685a24b7fb2758eb"}, {url = "https://files.pythonhosted.org/packages/7d/7e/da3fd28de12c39804a8797a7dff089de0dd9a7d267b5de42fe029a308469/greenlet-2.0.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:f6327b6907b4cb72f650a5b7b1be23a2aab395017aa6f1adb13069d66360eb3f"}, {url = "https://files.pythonhosted.org/packages/80/2c/7080f662dfe235e574826c144a48859e9a1588f1ba2bb65e297cc094f66b/greenlet-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a20d33124935d27b80e6fdacbd34205732660e0a1d35d8b10b3328179a2b51a1"}, {url = "https://files.pythonhosted.org/packages/82/99/ec66a82aa3b6da6df577bdb8448a856f703f163ac920918ed785ca1a07ea/greenlet-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:72b00a8e7c25dcea5946692a2485b1a0c0661ed93ecfedfa9b6687bd89a24ef5"}, {url = "https://files.pythonhosted.org/packages/88/27/24bb86495d9ef39122229b2ac5c2c338f14334f69305e2ce51855b234c71/greenlet-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6ee1aa7ab36475035eb48c01efae87d37936a8173fc4d7b10bb02c2d75dd8f6"}, {url = "https://files.pythonhosted.org/packages/8c/c5/ccdd89c89eb4bdf54f891deaafa57201f1d8fe8960e30b15869199470593/greenlet-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:3d75b8d013086b08e801fbbb896f7d5c9e6ccd44f13a9241d2bf7c0df9eda928"}, {url = "https://files.pythonhosted.org/packages/8d/34/bcd0848d4f748831058aabf76749f5979bc3c656770be511a11cbd58da4f/greenlet-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fb412b7db83fe56847df9c47b6fe3f13911b06339c2aa02dcc09dce8bbf582cd"}, {url = "https://files.pythonhosted.org/packages/90/bf/2f12be708384ab7ba9755819ce31afdd281a0d511407223c34bc9c3c0fad/greenlet-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d38ffd0e81ba8ef347d2be0772e899c289b59ff150ebbbbe05dc61b1246eb4e0"}, {url = "https://files.pythonhosted.org/packages/93/45/d8990ab748feec26be644761862e22a138b4f4a287a665ecad0219c9ba71/greenlet-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bf633a50cc93ed17e494015897361010fc08700d92676c87931d3ea464123ce"}, {url = "https://files.pythonhosted.org/packages/95/a4/d89fd972a69454f0cfd67192d3fdbf82d9b9a9271c30c76ea4e0e57ad745/greenlet-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:afe07421c969e259e9403c3bb658968702bc3b78ec0b6fde3ae1e73440529c23"}, {url = "https://files.pythonhosted.org/packages/9a/b5/d5bc7914e8a2cee35d54eb2f8a70536c4cd1cbb66b0892ac9cff2777c955/greenlet-2.0.1-cp38-cp38-win32.whl", hash = "sha256:88c8d517e78acdf7df8a2134a3c4b964415b575d2840a2746ddb1cc6175f8608"}, {url = "https://files.pythonhosted.org/packages/a4/12/fb767fec85b9cefe7110428191cd6a74d22a1dcf563f955444f11855a1b7/greenlet-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56961cfca7da2fdd178f95ca407fa330c64f33289e1804b592a77d5593d9bd94"}, {url = "https://files.pythonhosted.org/packages/ab/63/7051c4702001be50f6fff239ea208bf0eae2148e206395fa2865c2da721a/greenlet-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:659f167f419a4609bc0516fb18ea69ed39dbb25594934bd2dd4d0401660e8a1e"}, {url = "https://files.pythonhosted.org/packages/b5/c5/827149685292c53e5da546351e4f40f5a8513f200d920ae80423963dadef/greenlet-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d37990425b4687ade27810e3b1a1c37825d242ebc275066cfee8cb6b8829ccd"}, {url = "https://files.pythonhosted.org/packages/b7/52/51b365ec7123a10a76f5d0cc7fc812f14e22b7823d49b816e1f55785d648/greenlet-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6a08799e9e88052221adca55741bf106ec7ea0710bca635c208b751f0d5b617"}, {url = "https://files.pythonhosted.org/packages/b8/0a/199f646c0a525a5d1ea396efa0db82caaef895a51d9cfb83b5447efac285/greenlet-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:b5e83e4de81dcc9425598d9469a624826a0b1211380ac444c7c791d4a2137c19"}, {url = "https://files.pythonhosted.org/packages/bc/29/71f9b473bde6164d75eec241fde77d31ad04b58ae286068ff805199e14ac/greenlet-2.0.1-cp35-cp35m-win_amd64.whl", hash = "sha256:38255a3f1e8942573b067510f9611fc9e38196077b0c8eb7a8c795e105f9ce77"}, {url = "https://files.pythonhosted.org/packages/bd/c0/5b88c82604aa11272b27da3fbc9328438ff68bb25e4c1591e065308826d2/greenlet-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:6f61d71bbc9b4a3de768371b210d906726535d6ca43506737682caa754b956cd"}, {url = "https://files.pythonhosted.org/packages/bf/b3/5805253348519f06b23a1682cb03fa4c9311eabaf4df40f397a45105e391/greenlet-2.0.1-cp39-cp39-win32.whl", hash = "sha256:db38f80540083ea33bdab614a9d28bcec4b54daa5aff1668d7827a9fc769ae0a"}, {url = "https://files.pythonhosted.org/packages/c4/b8/cf300eb2b06084ed642855379ccd2ac50627954ee0d9ba2f4c1bd73027bb/greenlet-2.0.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4f09b0010e55bec3239278f642a8a506b91034f03a4fb28289a7d448a67f1515"}, {url = "https://files.pythonhosted.org/packages/c8/61/512afbd6cd0472b5361bcb4999bc3db472eadd377e64218596b9d061c6c5/greenlet-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5a8e05057fab2a365c81abc696cb753da7549d20266e8511eb6c9d9f72fe3e92"}, {url = "https://files.pythonhosted.org/packages/cb/ea/ae12dab020482e09ee71e0428b5bde522ffdade92ad04065ed7376a024b4/greenlet-2.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13ba6e8e326e2116c954074c994da14954982ba2795aebb881c07ac5d093a58a"}, {url = "https://files.pythonhosted.org/packages/dc/59/e1df96dc26b207171c89bd486caeeb891c9aa1d5c0be9a859b0eead5239f/greenlet-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:4aeaebcd91d9fee9aa768c1b39cb12214b30bf36d2b7370505a9f2165fedd8d9"}, {url = "https://files.pythonhosted.org/packages/dc/b7/3c347dfcbd9b1976890b71e88c9e8972378af4f724b7595b3adf3e861a4f/greenlet-2.0.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:04957dc96669be041e0c260964cfef4c77287f07c40452e61abe19d647505581"}, {url = "https://files.pythonhosted.org/packages/dd/5e/7309beef044bb4ceb0f052dc3797678a5f4eb6d07789787b2e4a15803b7a/greenlet-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c140e7eb5ce47249668056edf3b7e9900c6a2e22fb0eaf0513f18a1b2c14e1da"}, {url = "https://files.pythonhosted.org/packages/e1/1f/b5b8099362fd1c4a45a1763981a2c8885344d19952970bebefa2b73edc0d/greenlet-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d21681f09e297a5adaa73060737e3aa1279a13ecdcfcc6ef66c292cb25125b2d"}, {url = "https://files.pythonhosted.org/packages/e9/f3/d020c777e22c284199b6771c71907c923d9c2cf84bca8612077333d3ded6/greenlet-2.0.1-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:2d0bac0385d2b43a7bd1d651621a4e0f1380abc63d6fb1012213a401cbd5bf8f"}, {url = "https://files.pythonhosted.org/packages/fd/6a/f07b0028baff9bca61ecfcd9ee021e7e33369da8094f00eff409f2ff32be/greenlet-2.0.1.tar.gz", hash = "sha256:42e602564460da0e8ee67cb6d7236363ee5e131aa15943b6670e44e5c2ed0f67"}, {url = "https://files.pythonhosted.org/packages/ff/ab/31c5327752c3381a9d3b2599245f4c2c21e9f3c73039fa4f34725562a7dd/greenlet-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5067920de254f1a2dee8d3d9d7e4e03718e8fd2d2d9db962c8c9fa781ae82a39"}, ] "identify 2.5.15" = [ {url = "https://files.pythonhosted.org/packages/07/8b/18e5c44e1bbb3e1f690f62a4cd3b0f095c60f87a420d7fde510c4e8c1c26/identify-2.5.15-py2.py3-none-any.whl", hash = "sha256:1f4b36c5f50f3f950864b2a047308743f064eaa6f6645da5e5c780d1c7125487"}, {url = "https://files.pythonhosted.org/packages/82/a9/3cf658d585698e8f14b09011018f4f3fd9d56b0eecefb79de89ec5cb6a92/identify-2.5.15.tar.gz", hash = "sha256:c22aa206f47cc40486ecf585d27ad5f40adbfc494a3fa41dc3ed0499a23b123f"}, ] "idna 3.4" = [ {url = "https://files.pythonhosted.org/packages/8b/e1/43beb3d38dba6cb420cefa297822eac205a277ab43e5ba5d5c46faf96438/idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, {url = "https://files.pythonhosted.org/packages/fc/34/3030de6f1370931b9dbb4dad48f6ab1015ab1d32447850b9fc94e60097be/idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, ] "imagesize 1.4.1" = [ {url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, {url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, ] "importlib-metadata 6.0.0" = [ {url = "https://files.pythonhosted.org/packages/26/a7/9da7d5b23fc98ab3d424ac2c65613d63c1f401efb84ad50f2fa27b2caab4/importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, {url = "https://files.pythonhosted.org/packages/90/07/6397ad02d31bddf1841c9ad3ec30a693a3ff208e09c2ef45c9a8a5f85156/importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, ] "iniconfig 2.0.0" = [ {url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, {url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, ] "itsdangerous 2.1.2" = [ {url = "https://files.pythonhosted.org/packages/68/5f/447e04e828f47465eeab35b5d408b7ebaaaee207f48b7136c5a7267a30ae/itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, {url = "https://files.pythonhosted.org/packages/7f/a1/d3fb83e7a61fa0c0d3d08ad0a94ddbeff3731c05212617dff3a94e097f08/itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, ] "jinja2 3.1.2" = [ {url = "https://files.pythonhosted.org/packages/7a/ff/75c28576a1d900e87eb6335b063fab47a8ef3c8b4d88524c4bf78f670cce/Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, {url = "https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, ] "markupsafe 2.1.2" = [ {url = "https://files.pythonhosted.org/packages/02/2c/18d55e5df6a9ea33709d6c33e08cb2e07d39e20ad05d8c6fbf9c9bcafd54/MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, {url = "https://files.pythonhosted.org/packages/04/cf/9464c3c41b7cdb8df660cda75676697e7fb49ce1be7691a1162fc88da078/MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, {url = "https://files.pythonhosted.org/packages/06/3b/d026c21cd1dbee89f41127e93113dcf5fa85c6660d108847760b59b3a66d/MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, {url = "https://files.pythonhosted.org/packages/0a/88/78cb3d95afebd183d8b04442685ab4c70cfc1138b850ba20e2a07aff2f53/MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, {url = "https://files.pythonhosted.org/packages/0d/15/82b108c697bec4c26c00aed6975b778cf0eac6cbb77598862b10550b7fcc/MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, {url = "https://files.pythonhosted.org/packages/19/00/3b8eb0093c885576a1ce7f2263e7b8c01e55b9977433f8246f57cd81b0be/MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, {url = "https://files.pythonhosted.org/packages/1f/20/76f6337f1e7238a626ab34405ddd634636011b2ff947dcbd8995f16a7776/MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, {url = "https://files.pythonhosted.org/packages/22/88/9c0cae2f5ada778182f2842b377dd273d6be689953345c10b165478831eb/MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, {url = "https://files.pythonhosted.org/packages/29/d2/243e6b860d97c18d848fc2bee2f39d102755a2b04a5ce4d018d839711b46/MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, {url = "https://files.pythonhosted.org/packages/30/3e/0a69a24adb38df83e2f6989c38d68627a5f27181c82ecaa1fd03d1236dca/MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, {url = "https://files.pythonhosted.org/packages/34/19/64b0abc021b22766e86efee32b0e2af684c4b731ce8ac1d519c791800c13/MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, {url = "https://files.pythonhosted.org/packages/37/b2/6f4d5cac75ba6fe9f17671304fe339ea45a73c5609b5f5e652aa79c915c8/MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, {url = "https://files.pythonhosted.org/packages/39/8d/5c5ce72deb8567ab48a18fbd99dc0af3dd651b6691b8570947e54a28e0f3/MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, {url = "https://files.pythonhosted.org/packages/3d/66/2f636ba803fd6eb4cee7b3106ae02538d1e84a7fb7f4f8775c6528a87d31/MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, {url = "https://files.pythonhosted.org/packages/41/54/6e88795c64ab5dcda31b06406c062c2740d1a64db18219d4e21fc90928c1/MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, {url = "https://files.pythonhosted.org/packages/46/0c/10ee33673c5e36fa3809cf136971f81d951ca38516188ee11a965d9b2fe9/MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, {url = "https://files.pythonhosted.org/packages/48/cc/d027612e17b56088cfccd2c8e083518995fcb25a7b4f17fc146362a0499d/MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, {url = "https://files.pythonhosted.org/packages/4b/34/dc837e5ad9e14634aac4342eb8b12a9be20a4f74f50cc0d765f7aa2fc1e3/MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, {url = "https://files.pythonhosted.org/packages/50/41/1442b693a40eb76d835ca2016e86a01479f17d7fd8288f9830f6790e366a/MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, {url = "https://files.pythonhosted.org/packages/52/36/b35c577c884ea352fc0c1eaed9ca4946ffc22cc9c3527a70408bfa9e9496/MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, {url = "https://files.pythonhosted.org/packages/56/0d/c9818629672a3368b773fa94597d79da77bdacc3186f097bb85023f785f6/MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, {url = "https://files.pythonhosted.org/packages/5a/94/d056bf5dbadf7f4b193ee2a132b3d49ffa1602371e3847518b2982045425/MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, {url = "https://files.pythonhosted.org/packages/5e/f6/8eb8a5692c1986b6e863877b0b8a83628aff14e5fbfaf11d9522b532bd9d/MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, {url = "https://files.pythonhosted.org/packages/66/21/dadb671aade8eb67ef96e0d8f90b1bd5e8cfb6ad9d8c7fa2c870ec0c257b/MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, {url = "https://files.pythonhosted.org/packages/76/b5/05ce70a3e31ecebcd3628cd180dc4761293aa496db85170fdc085ed2d79a/MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, {url = "https://files.pythonhosted.org/packages/77/26/af46880038c6eac3832e751298f1965f3a550f38d1e9ddaabd674860076b/MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, {url = "https://files.pythonhosted.org/packages/78/e6/91c9a20a943ea231c59024e181c4c5480097daf132428f2272670974637f/MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, {url = "https://files.pythonhosted.org/packages/79/e2/b818bf277fa6b01244943498cb2127372c01dde5eff7682837cc72740618/MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, {url = "https://files.pythonhosted.org/packages/7b/0f/0e99c2f342933c43af69849a6ba63f2eef54e14c6d0e10a26470fb6b40a9/MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, {url = "https://files.pythonhosted.org/packages/7c/e6/454df09f18e0ea34d189b447a9e1a9f66c2aa332b77fd5577ebc7ca14d42/MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, {url = "https://files.pythonhosted.org/packages/80/64/ccb65aadd71e7685caa69a885885a673e8748525a243fb26acea37201b44/MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, {url = "https://files.pythonhosted.org/packages/82/70/b3978786c7b576c25d84b009d2a20a11b5300d252fc3ce984e37b932e97c/MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, {url = "https://files.pythonhosted.org/packages/82/e3/4efcd74f10a7999783955aec36386f71082e6d7dafcc06b77b9df72b325a/MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, {url = "https://files.pythonhosted.org/packages/87/a1/d0f05a09c6c1aef89d1eea0ab0ff1ea897d4117d27f1571034a7e3ff515b/MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, {url = "https://files.pythonhosted.org/packages/93/ca/1c3ae0c6a5712d4ba98610cada03781ea0448436b17d1dcd4759115b15a1/MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, {url = "https://files.pythonhosted.org/packages/93/fa/d72f68f84f8537ee8aa3e0764d1eb11e5e025a5ca90c16e94a40f894c2fc/MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, {url = "https://files.pythonhosted.org/packages/95/7e/68018b70268fb4a2a605e2be44ab7b4dd7ce7808adae6c5ef32e34f4b55a/MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, {url = "https://files.pythonhosted.org/packages/95/88/8c8cce021ac1b1eedde349c6a41f6c256da60babf95e572071361ff3f66b/MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, {url = "https://files.pythonhosted.org/packages/96/92/a873b4a7fa20c2e30bffe883bb560330f3b6ce03aaf278f75f96d161935b/MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, {url = "https://files.pythonhosted.org/packages/9d/80/8320f182d06a9b289b1a9f266f593feb91d3781c7e104bbe09e0c4c11439/MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, {url = "https://files.pythonhosted.org/packages/be/18/988e1913a40cc8eb725b1e073eacc130f7803a061577bdc0b9343eb3c696/MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, {url = "https://files.pythonhosted.org/packages/c3/e5/42842a44bfd9ba2955c562b1139334a2f64cedb687e8969777fd07de42a9/MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, {url = "https://files.pythonhosted.org/packages/c7/0e/22d0c8e6ee84414e251bd1bc555b2705af6b3fb99f0ba1ead2a0f51d423b/MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, {url = "https://files.pythonhosted.org/packages/cf/c1/d7596976a868fe3487212a382cc121358a53dc8e8d85ff2ee2c3d3b40f04/MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, {url = "https://files.pythonhosted.org/packages/d1/10/ff89b23d4a24051c4e4f689b79ee06f230d7e9431445e24f5dd9d9a89730/MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, {url = "https://files.pythonhosted.org/packages/e3/a9/e366665c7eae59c9c9d34b747cd5a3994847719a2304e0c8dec8b604dd98/MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, {url = "https://files.pythonhosted.org/packages/e6/ff/d2378ca3cb3ac4a37af767b820b0f0bf3f5e9193a6acce0eefc379425c1c/MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, {url = "https://files.pythonhosted.org/packages/e9/c6/2da36728c1310f141395176556500aeedfdea8c2b02a3b72ba61b69280e8/MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, {url = "https://files.pythonhosted.org/packages/ea/60/2400ba59cf2465fa136487ee7299f52121a9d04b2cf8539ad43ad10e70e8/MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, {url = "https://files.pythonhosted.org/packages/f9/aa/ebcd114deab08f892b1d70badda4436dbad1747f9e5b72cffb3de4c7129d/MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, ] "mypy 0.991" = [ {url = "https://files.pythonhosted.org/packages/0e/5c/fbe112ca73d4c6a9e65336f48099c60800514d8949b4129c093a84a28dc8/mypy-0.991.tar.gz", hash = "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06"}, {url = "https://files.pythonhosted.org/packages/14/05/5a4206e269268f4aecb1096bf2375a231c959987ccf3e31313221b8bc153/mypy-0.991-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05"}, {url = "https://files.pythonhosted.org/packages/28/9c/e1805f2fea93a92671f33b00dd577119f37e4a8b859d6f6ea62d3e9129fa/mypy-0.991-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f"}, {url = "https://files.pythonhosted.org/packages/33/20/c4c15c9e9b7929ef44e35e83c0bcc254c8bf5998bbef0954ae658288e8c6/mypy-0.991-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a"}, {url = "https://files.pythonhosted.org/packages/39/05/7a7d58afc7d00e819e553ad2485a29141e14575e3b0c43b9da6f869ede4c/mypy-0.991-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb"}, {url = "https://files.pythonhosted.org/packages/44/d0/81d47bffc80d0cff84174aab266adc3401e735e13c5613418e825c146986/mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"}, {url = "https://files.pythonhosted.org/packages/49/83/34d682a10604845d77a0e7dbde1d0e70f3784d0f67b0df11d2eaf7bb8360/mypy-0.991-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135"}, {url = "https://files.pythonhosted.org/packages/4b/98/125e5d14222de8e92f44314f8df21a9c351b531b37c551526acd67486a7d/mypy-0.991-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad"}, {url = "https://files.pythonhosted.org/packages/5d/c8/fc9b7cd600330e8c9dbd52b499a76eeaf4b48969a605fb50415a9d361d5b/mypy-0.991-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70"}, {url = "https://files.pythonhosted.org/packages/6b/22/5e19d1a6f8e029296e7b2fa462d8753fb4365126684c2f840dcb1447e6e8/mypy-0.991-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5"}, {url = "https://files.pythonhosted.org/packages/80/23/76e56e004acca691b4da4086a8c38bd67b7ae73536848dcab76cfed5c188/mypy-0.991-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305"}, {url = "https://files.pythonhosted.org/packages/87/ec/62fd00fa5d8ead3ecafed3eb99ee805911f41b11536c5940df1bcb2c845d/mypy-0.991-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6"}, {url = "https://files.pythonhosted.org/packages/89/76/7159258fdbf26a5ceef100b80a82d2f79b9066725a5daeb6383a8f773910/mypy-0.991-cp311-cp311-win_amd64.whl", hash = "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297"}, {url = "https://files.pythonhosted.org/packages/90/a5/3a2c0c02e99a845318cc25556097d96eb8eb85fe53619ac8ff37b44acc46/mypy-0.991-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd"}, {url = "https://files.pythonhosted.org/packages/91/27/716b1cfce990cb58dc92f6601852141bc25e1524c06b3f3a39b0de6d9210/mypy-0.991-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3"}, {url = "https://files.pythonhosted.org/packages/97/e3/1da0f08c60f555c04b93eff4016611fa1858ea53111dbdc757a37c234042/mypy-0.991-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711"}, {url = "https://files.pythonhosted.org/packages/9b/b1/0d5f1549c2894fd9af744e886156870d98ea0b1784952989f10e51eb0030/mypy-0.991-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813"}, {url = "https://files.pythonhosted.org/packages/ac/a6/e4d6dca539c637735d0d93f1eee3ac35cedfd9c047da7386b3a59e93f35b/mypy-0.991-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476"}, {url = "https://files.pythonhosted.org/packages/af/9a/ee3b76f36e90ecb5e44dd2827bf5992d02c127192366a4c7864cfeab95b6/mypy-0.991-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf"}, {url = "https://files.pythonhosted.org/packages/b1/30/24a92552a7c3df25db5a2e56ae359b4aa9bba6aebc8f0e25523a94e5c1e7/mypy-0.991-cp37-cp37m-win_amd64.whl", hash = "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"}, {url = "https://files.pythonhosted.org/packages/b8/ab/aa2e02fce8ee8885fe98ee2a0549290e9de5caa28febc0cf243bfab020e7/mypy-0.991-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372"}, {url = "https://files.pythonhosted.org/packages/bc/b2/6e71e47b259992dcd99d257ce452c0de3f711be713d048fe8f0fda9a9996/mypy-0.991-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d"}, {url = "https://files.pythonhosted.org/packages/ca/0d/da98f81e7c13a60111dc10a16cbf1b48dc8500df90a1fc959878a5981f49/mypy-0.991-cp39-cp39-win_amd64.whl", hash = "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461"}, {url = "https://files.pythonhosted.org/packages/d7/f4/dcab9f3c5ed410caca1b9374dbb2b2caa778d225e32f174e266e20291edf/mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"}, {url = "https://files.pythonhosted.org/packages/df/bb/3cf400e05e30939a0fc58b34e0662d8abe8e206464665065b56cf2ca9a62/mypy-0.991-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33"}, {url = "https://files.pythonhosted.org/packages/e3/84/188ddeaebfc8b5bbdcc3c7f05c09b61758540b2df84aad0146263d66960a/mypy-0.991-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93"}, {url = "https://files.pythonhosted.org/packages/e7/a1/c503a15ad69ff133a76c159b8287f0eadc1f521d9796bf81f935886c98f6/mypy-0.991-py3-none-any.whl", hash = "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb"}, {url = "https://files.pythonhosted.org/packages/e9/7e/cc2de45afb46fee694bf285f91df3e227a3b0c671f775524814549c26556/mypy-0.991-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648"}, {url = "https://files.pythonhosted.org/packages/f3/1d/cc67a674f1cd7f1c10619487a4245185f6f8f14cbd685b60709318e9ac27/mypy-0.991-cp310-cp310-win_amd64.whl", hash = "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c"}, {url = "https://files.pythonhosted.org/packages/f7/3a/19c01d59d24f1f36fabdeb61a286b4fc5e0456bf6211f5159ad5ebb5f735/mypy-0.991-cp38-cp38-win_amd64.whl", hash = "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243"}, ] "mypy-extensions 0.4.3" = [ {url = "https://files.pythonhosted.org/packages/5c/eb/975c7c080f3223a5cdaff09612f3a5221e4ba534f7039db34c35d95fa6a5/mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {url = "https://files.pythonhosted.org/packages/63/60/0582ce2eaced55f65a4406fc97beba256de4b7a95a0034c6576458c6519f/mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] "nodeenv 1.7.0" = [ {url = "https://files.pythonhosted.org/packages/96/a8/d3b5baead78adadacb99e7281b3e842126da825cf53df61688cfc8b8ff91/nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, {url = "https://files.pythonhosted.org/packages/f3/9d/a28ecbd1721cd6c0ea65da6bfb2771d31c5d7e32d916a8f643b062530af3/nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, ] "packaging 23.0" = [ {url = "https://files.pythonhosted.org/packages/47/d5/aca8ff6f49aa5565df1c826e7bf5e85a6df852ee063600c1efa5b932968c/packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, {url = "https://files.pythonhosted.org/packages/ed/35/a31aed2993e398f6b09a790a181a7927eb14610ee8bbf02dc14d31677f1c/packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, ] "pallets-sphinx-themes 2.0.3" = [ {url = "https://files.pythonhosted.org/packages/38/46/660bea149d51bd4aa85acb0ab2680e00de95f6164c24706e49f1de1f8242/Pallets-Sphinx-Themes-2.0.3.tar.gz", hash = "sha256:54f991c3897f363c757dcba01adb44791609cb02cc7eaac3a5fcdd3d3d9dfeb3"}, {url = "https://files.pythonhosted.org/packages/f4/da/86beb3d63f0f37d3e9fb1ea3a984288a72b1861d6e48f274b2dcf9b1f6dc/Pallets_Sphinx_Themes-2.0.3-py3-none-any.whl", hash = "sha256:0e94b3f024f171a3a30f8910950d9ec7fff95a2c24b7d30e66573d816036ea69"}, ] "platformdirs 2.6.2" = [ {url = "https://files.pythonhosted.org/packages/c1/c7/9be9d651b93efce682b45142a6267034fc4215972780748618c02e236361/platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, {url = "https://files.pythonhosted.org/packages/cf/4d/198b7e6c6c2b152f4f9f4cdf975d3590e33e63f1920f2d89af7f0390e6db/platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, ] "pluggy 1.0.0" = [ {url = "https://files.pythonhosted.org/packages/9e/01/f38e2ff29715251cf25532b9082a1589ab7e4f571ced434f98d0139336dc/pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {url = "https://files.pythonhosted.org/packages/a1/16/db2d7de3474b6e37cbb9c008965ee63835bba517e22cdb8c35b5116b5ce1/pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] "pre-commit 2.21.0" = [ {url = "https://files.pythonhosted.org/packages/6b/00/1637ae945c6e10838ef5c41965f1c864e59301811bb203e979f335608e7c/pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, {url = "https://files.pythonhosted.org/packages/a6/6b/6cfe3a8b351b54f4b6c6d2ad4286804e3367f628dce379c603d3b96635f4/pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, ] "pygments 2.14.0" = [ {url = "https://files.pythonhosted.org/packages/0b/42/d9d95cc461f098f204cd20c85642ae40fbff81f74c300341b8d0e0df14e0/Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, {url = "https://files.pythonhosted.org/packages/da/6a/c427c06913204e24de28de5300d3f0e809933f376e0b7df95194b2bb3f71/Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, ] "pyproject-api 1.5.0" = [ {url = "https://files.pythonhosted.org/packages/25/8d/261289ccd17044fb1fdf99da1ece3fda631b61b53b3c49f02e21f3e8af00/pyproject_api-1.5.0-py3-none-any.whl", hash = "sha256:4c111277dfb96bcd562c6245428f27250b794bfe3e210b8714c4f893952f2c17"}, {url = "https://files.pythonhosted.org/packages/86/92/a63e1fd25adfde23f21d87676a4dc39fb969d2979c60aac9d7b3425d6223/pyproject_api-1.5.0.tar.gz", hash = "sha256:0962df21f3e633b8ddb9567c011e6c1b3dcdfc31b7860c0ede7e24c5a1200fbe"}, ] "pytest 7.2.1" = [ {url = "https://files.pythonhosted.org/packages/cc/02/8f59bf194c9a1ceac6330850715e9ec11e21e2408a30a596c65d54cf4d2a/pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, {url = "https://files.pythonhosted.org/packages/e5/6c/f3a15217ac72912c28c5d7a7a8e87ff6d6475c9530595ae9f0f8dedd8dd8/pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, ] "pytest-cov 4.0.0" = [ {url = "https://files.pythonhosted.org/packages/ea/70/da97fd5f6270c7d2ce07559a19e5bf36a76f0af21500256f005a69d9beba/pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, {url = "https://files.pythonhosted.org/packages/fe/1f/9ec0ddd33bd2b37d6ec50bb39155bca4fe7085fa78b3b434c05459a860e3/pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, ] "pytz 2022.7.1" = [ {url = "https://files.pythonhosted.org/packages/03/3e/dc5c793b62c60d0ca0b7e58f1fdd84d5aaa9f8df23e7589b39cc9ce20a03/pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, {url = "https://files.pythonhosted.org/packages/2e/09/fbd3c46dce130958ee8e0090f910f1fe39e502cc5ba0aadca1e8a2b932e5/pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, ] "pyyaml 6.0" = [ {url = "https://files.pythonhosted.org/packages/02/25/6ba9f6bb50a3d4fbe22c1a02554dc670682a07c8701d1716d19ddea2c940/PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {url = "https://files.pythonhosted.org/packages/08/f4/ffa743f860f34a5e8c60abaaa686f82c9ac7a2b50e5a1c3b1eb564d59159/PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {url = "https://files.pythonhosted.org/packages/0f/93/5f81d1925ce3b531f5ff215376445ec220887cd1c9a8bde23759554dbdfd/PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {url = "https://files.pythonhosted.org/packages/12/fc/a4d5a7554e0067677823f7265cb3ae22aed8a238560b5133b58cda252dad/PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, {url = "https://files.pythonhosted.org/packages/21/67/b42191239c5650c9e419c4a08a7a022bbf1abf55b0391c380a72c3af5462/PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, {url = "https://files.pythonhosted.org/packages/2e/b3/13dfd4eeb5e4b2d686b6d1822b40702e991bf3a4194ca5cbcce8d43749db/PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, {url = "https://files.pythonhosted.org/packages/36/2b/61d51a2c4f25ef062ae3f74576b01638bebad5e045f747ff12643df63844/PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, {url = "https://files.pythonhosted.org/packages/44/e5/4fea13230bcebf24b28c0efd774a2dd65a0937a2d39e94a4503438b078ed/PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {url = "https://files.pythonhosted.org/packages/4d/7d/c2ab8da648cd2b937de11fb35649b127adab4851cbeaf5fd9b60a2dab0f7/PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, {url = "https://files.pythonhosted.org/packages/55/e3/507a92589994a5b3c3d7f2a7a066339d6ff61c5c839bae56f7eff03d9c7b/PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {url = "https://files.pythonhosted.org/packages/56/8f/e8b49ad21d26111493dc2d5cae4d7efbd0e2e065440665f5023515f87f64/PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, {url = "https://files.pythonhosted.org/packages/59/00/30e33fcd2a4562cd40c49c7740881009240c5cbbc0e41ca79ca4bba7c24b/PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, {url = "https://files.pythonhosted.org/packages/5e/f4/7b4bb01873be78fc9fde307f38f62e380b7111862c165372cf094ca2b093/PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, {url = "https://files.pythonhosted.org/packages/63/6b/f5dc7942bac17192f4ef00b2d0cdd1ae45eea453d05c1944c0573debe945/PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, {url = "https://files.pythonhosted.org/packages/67/d4/b95266228a25ef5bd70984c08b4efce2c035a4baa5ccafa827b266e3dc36/PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, {url = "https://files.pythonhosted.org/packages/68/3f/c027422e49433239267c62323fbc6320d6ac8d7d50cf0cb2a376260dad5f/PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, {url = "https://files.pythonhosted.org/packages/6c/3d/524c642f3db37e7e7ab8d13a3f8b0c72d04a619abc19100097d987378fc6/PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, {url = "https://files.pythonhosted.org/packages/74/68/3c13deaa496c14a030c431b7b828d6b343f79eb241b4848c7918091a64a2/PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, {url = "https://files.pythonhosted.org/packages/77/da/e845437ffe0dffae4e7562faf23a4f264d886431c5d2a2816c853288dc8e/PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, {url = "https://files.pythonhosted.org/packages/7f/d9/6a0d14ac8d3b5605dc925d177c1d21ee9f0b7b39287799db1e50d197b2f4/PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, {url = "https://files.pythonhosted.org/packages/81/59/561f7e46916b78f3c4cab8d0c307c81656f11e32c846c0c97fda0019ed76/PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, {url = "https://files.pythonhosted.org/packages/89/26/0bfd7b756b34c68f8fd158b7bc762b6b1705fc1b3cebf4cdbb53fd9ea75b/PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, {url = "https://files.pythonhosted.org/packages/91/49/d46d7b15cddfa98533e89f3832f391aedf7e31f37b4d4df3a7a7855a7073/PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {url = "https://files.pythonhosted.org/packages/9d/f6/7e91fbb58c9ee528759aea5892e062cccb426720c5830ddcce92eba00ff1/PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, {url = "https://files.pythonhosted.org/packages/a4/ba/e508fc780e3c94c12753a54fe8f74de535741a10d33b29a576a9bec03500/PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, {url = "https://files.pythonhosted.org/packages/a4/e6/4d7a01bc0730c8f958a62d6a4c4f3df23b6139ad68c132b168970d84f192/PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, {url = "https://files.pythonhosted.org/packages/a8/32/1bbe38477fb23f1d83041fefeabf93ef1cd6f0efcf44c221519507315d92/PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {url = "https://files.pythonhosted.org/packages/a8/5b/c4d674846ea4b07ee239fbf6010bcc427c4e4552ba5655b446e36b9a40a7/PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, {url = "https://files.pythonhosted.org/packages/b3/85/79b9e5b4e8d3c0ac657f4e8617713cca8408f6cdc65d2ee6554217cedff1/PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, {url = "https://files.pythonhosted.org/packages/b7/09/2f6f4851bbca08642fef087bade095edc3c47f28d1e7bff6b20de5262a77/PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, {url = "https://files.pythonhosted.org/packages/cb/5f/05dd91f5046e2256e35d885f3b8f0f280148568f08e1bf20421887523e9a/PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, {url = "https://files.pythonhosted.org/packages/d1/c0/4fe04181b0210ee2647cfbb89ecd10a36eef89f10d8aca6a192c201bbe58/PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, {url = "https://files.pythonhosted.org/packages/d7/42/7ad4b6d67a16229496d4f6e74201bdbebcf4bc1e87d5a70c9297d4961bd2/PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, {url = "https://files.pythonhosted.org/packages/db/4e/74bc723f2d22677387ab90cd9139e62874d14211be7172ed8c9f9a7c81a9/PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, {url = "https://files.pythonhosted.org/packages/df/75/ee0565bbf65133e5b6ffa154db43544af96ea4c42439e6b58c1e0eb44b4e/PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, {url = "https://files.pythonhosted.org/packages/eb/5f/6e6fe6904e1a9c67bc2ca5629a69e7a5a0b17f079da838bab98a1e548b25/PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, {url = "https://files.pythonhosted.org/packages/ef/ad/b443cce94539e57e1a745a845f95c100ad7b97593d7e104051e43f730ecd/PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, {url = "https://files.pythonhosted.org/packages/f5/6f/b8b4515346af7c33d3b07cd8ca8ea0700ca72e8d7a750b2b87ac0268ca4e/PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, {url = "https://files.pythonhosted.org/packages/f8/54/799b059314b13e1063473f76e908f44106014d18f54b16c83a16edccd5ec/PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, {url = "https://files.pythonhosted.org/packages/fc/48/531ecd926fe0a374346dd811bf1eda59a95583595bb80eadad511f3269b8/PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, ] "requests 2.28.2" = [ {url = "https://files.pythonhosted.org/packages/9d/ee/391076f5937f0a8cdf5e53b701ffc91753e87b07d66bae4a09aa671897bf/requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, {url = "https://files.pythonhosted.org/packages/d2/f4/274d1dbe96b41cf4e0efb70cbced278ffd61b5c7bb70338b62af94ccb25b/requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, ] "setuptools 66.1.1" = [ {url = "https://files.pythonhosted.org/packages/3c/7b/00030a938499c8f8345be02ab5b7d748d359ea59c2f020b7b0a21b82f832/setuptools-66.1.1.tar.gz", hash = "sha256:ac4008d396bc9cd983ea483cb7139c0240a07bbc74ffb6232fceffedc6cf03a8"}, {url = "https://files.pythonhosted.org/packages/c2/8b/abb577ca6ab2c71814d535b1ed1464c5f4aaefe1a31bbeb85013eb9b2401/setuptools-66.1.1-py3-none-any.whl", hash = "sha256:6f590d76b713d5de4e49fe4fbca24474469f53c83632d5d0fd056f7ff7e8112b"}, ] "snowballstemmer 2.2.0" = [ {url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, {url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, ] "sphinx 5.3.0" = [ {url = "https://files.pythonhosted.org/packages/67/a7/01dd6fd9653c056258d65032aa09a615b5d7b07dd840845a9f41a8860fbc/sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, {url = "https://files.pythonhosted.org/packages/af/b2/02a43597980903483fe5eb081ee8e0ba2bb62ea43a70499484343795f3bf/Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, ] "sphinx-issues 3.0.1" = [ {url = "https://files.pythonhosted.org/packages/00/35/a50591c1242d3f3927bcfd0e967c2858fef8fdb50f25b742015b6c841e03/sphinx-issues-3.0.1.tar.gz", hash = "sha256:b7c1dc1f4808563c454d11c1112796f8c176cdecfee95f0fd2302ef98e21e3d6"}, {url = "https://files.pythonhosted.org/packages/ff/95/75ad650ed20237f3756d7fcd6ce2e7c7c65e833641a980e125f4d4687cc5/sphinx_issues-3.0.1-py3-none-any.whl", hash = "sha256:8b25dc0301159375468f563b3699af7a63720fd84caf81c1442036fcd418b20c"}, ] "sphinxcontrib-applehelp 1.0.2" = [ {url = "https://files.pythonhosted.org/packages/9f/01/ad9d4ebbceddbed9979ab4a89ddb78c9760e74e6757b1880f1b2760e8295/sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, {url = "https://files.pythonhosted.org/packages/dc/47/86022665a9433d89a66f5911b558ddff69861766807ba685de2e324bd6ed/sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, ] "sphinxcontrib-devhelp 1.0.2" = [ {url = "https://files.pythonhosted.org/packages/98/33/dc28393f16385f722c893cb55539c641c9aaec8d1bc1c15b69ce0ac2dbb3/sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, {url = "https://files.pythonhosted.org/packages/c5/09/5de5ed43a521387f18bdf5f5af31d099605c992fd25372b2b9b825ce48ee/sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, ] "sphinxcontrib-htmlhelp 2.0.0" = [ {url = "https://files.pythonhosted.org/packages/63/40/c854ef09500e25f6432dcbad0f37df87fd7046d376272292d8654cc71c95/sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, {url = "https://files.pythonhosted.org/packages/eb/85/93464ac9bd43d248e7c74573d58a791d48c475230bcf000df2b2700b9027/sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, ] "sphinxcontrib-jsmath 1.0.1" = [ {url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, ] "sphinxcontrib-log-cabinet 1.0.1" = [ {url = "https://files.pythonhosted.org/packages/27/e7/dbfc155c1b4c429a9a8149032a56bfb7bab4efabc656abb24ab4619c715d/sphinxcontrib_log_cabinet-1.0.1-py2.py3-none-any.whl", hash = "sha256:3decc888e8e453d1912cd95d50efb0794a4670a214efa65e71a7de277dcfe2cd"}, {url = "https://files.pythonhosted.org/packages/75/26/0687391e10c605a4d0c7ebe118c57c51ecc687128bcdae5803d9b96def81/sphinxcontrib-log-cabinet-1.0.1.tar.gz", hash = "sha256:103b2e62df4e57abb943bea05ee9c2beb7da922222c8b77314ffd6ab9901c558"}, ] "sphinxcontrib-qthelp 1.0.3" = [ {url = "https://files.pythonhosted.org/packages/2b/14/05f9206cf4e9cfca1afb5fd224c7cd434dcc3a433d6d9e4e0264d29c6cdb/sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, {url = "https://files.pythonhosted.org/packages/b1/8e/c4846e59f38a5f2b4a0e3b27af38f2fcf904d4bfd82095bf92de0b114ebd/sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, ] "sphinxcontrib-serializinghtml 1.1.5" = [ {url = "https://files.pythonhosted.org/packages/b5/72/835d6fadb9e5d02304cf39b18f93d227cd93abd3c41ebf58e6853eeb1455/sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {url = "https://files.pythonhosted.org/packages/c6/77/5464ec50dd0f1c1037e3c93249b040c8fc8078fdda97530eeb02424b6eea/sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] "sqlalchemy 2.0.0" = [ {url = "https://files.pythonhosted.org/packages/00/c9/4fab29c5efaf72df0ad9b46f783197f35b66ddaad62db26c69befb504830/SQLAlchemy-2.0.0-py3-none-any.whl", hash = "sha256:192210daec1062e93fcc732de0c602c4b58097257c56874baa6e491849e82ceb"}, {url = "https://files.pythonhosted.org/packages/0b/1a/ebb65586a6b8fc76142eb426849e0fd6b7d6ffc9e1015330d3b828bf3eb7/SQLAlchemy-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:13929b9806b002e3018a2f4d6666466298f43043c53b037a27520d8e8dad238d"}, {url = "https://files.pythonhosted.org/packages/0b/39/a9f9371ae3823bda5cab13fbc805391f7154adf117d387efadf6f68ac398/SQLAlchemy-2.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7a2709d68ec901add77aa378253568905ba8112ae82ae8b3d3e85fd56b06f44d"}, {url = "https://files.pythonhosted.org/packages/0d/b3/7cb5fc3b48f31b3b23a0d369ec2d02a5914e02419b93e1147116d71264c9/SQLAlchemy-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0794ed9ed2cc3c42475998baf3ead135ce3849e72993fd61c82722a1def8a5"}, {url = "https://files.pythonhosted.org/packages/11/23/de68f34b73efa197b0e7a943aa93f2a6504a0492a72ac6dc699dc984d1d3/SQLAlchemy-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0fb3b58ba21898b94255e86da5e3bfc15cf99e039babcaccaa2ce10b6322929e"}, {url = "https://files.pythonhosted.org/packages/14/f1/2a848edaef2361742dfffa977061a861547e78ec0e1db1fc4c22fe7bdef4/SQLAlchemy-2.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3758f3e12dd7a1448d8d2c5d4d36dc32a504a0ff6dded23b06d955c73f1b71b4"}, {url = "https://files.pythonhosted.org/packages/18/09/64ef2927615654cd98b05315a443f516f851342afeac5ae817cfb3c0920a/SQLAlchemy-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b17bc162c317973d87613eac869cc50c1fef7a8b9d657b7d7f764ab5d9fee72"}, {url = "https://files.pythonhosted.org/packages/1d/1d/f0adb3dc2bf734b966e212bda7ccbe521df1b9c320c97ba03b9c514182bb/SQLAlchemy-2.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc81c03d4bccc82c81e4e21da5cea2071eca2fcddb248b462b151911c4b47b8"}, {url = "https://files.pythonhosted.org/packages/22/37/81a9e5be2aabcf7eeb63634d2afa14339e9abf4edcaee927d33bc372c377/SQLAlchemy-2.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6e4a17bbcb882fcff597d6ffdf113144383ea346bcae97079b96faaf7d460fb"}, {url = "https://files.pythonhosted.org/packages/29/9c/cbe31b50fe0a21c32d182d1e2ddeb7787a1dbb9693aac6062dd915107ebe/SQLAlchemy-2.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:05923707b416b7034c0b14e59e14614cb1432647188ba46bcfd911998cdea48d"}, {url = "https://files.pythonhosted.org/packages/2f/70/a9689b3eb20f704fcf9608fa71d3a36445e7704fb0844da79b57387a293d/SQLAlchemy-2.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:2051899b520a4332da0fe7098d155e0981044aed91567623c7aff4bd4addddc8"}, {url = "https://files.pythonhosted.org/packages/31/6a/520a6469d61c6b21a29f7feeb9f7023c4ee983c22a7142772dd0f6a14ae4/SQLAlchemy-2.0.0-cp38-cp38-win32.whl", hash = "sha256:28f8371e07c66f7bd8d665c0532e68986e1616f0505bef05a9bcb384889f94f2"}, {url = "https://files.pythonhosted.org/packages/52/e4/75f3dec7bcadd23f095b02de99bd4ea3798ac1153ac4a5f05681abf0349c/SQLAlchemy-2.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7809546951b4a5ad1f0b5d5c87b42218af8c0574f50e89d141dfff531c069389"}, {url = "https://files.pythonhosted.org/packages/54/55/a475df74f583b4ceeefd5a121fd28045af54efe204863de1e3b154385674/SQLAlchemy-2.0.0.tar.gz", hash = "sha256:92388d03220eda6d744277a4d2cbcbb557509c7f7582215f61f8a04ec264be59"}, {url = "https://files.pythonhosted.org/packages/5b/38/d6887548d1d2626cc756430bacb4016249442bdd4898d1f181f7e2fef35f/SQLAlchemy-2.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:54fa0308430ea13239557b6b38a41988ab9d0356420879b2e8b976f58c8b8229"}, {url = "https://files.pythonhosted.org/packages/64/7a/9ccf1c89c5b657c592e4e7d8ef15dcbb8b76c96edf989f0d01cebb66a041/SQLAlchemy-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:419228c073060face5e35388ddf00229f1be3664c91143f6e6897d67254589f7"}, {url = "https://files.pythonhosted.org/packages/71/b8/daebf72e5165b2820c1cbe8d80e2a512cce42900aa91ef28d6af81be9d57/SQLAlchemy-2.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:492dfab60c3df7105c97474a08408f15a506966340643eeaf40f59daa08a516e"}, {url = "https://files.pythonhosted.org/packages/72/d3/3dfceccf991b45ebc32ff088f003948037e4caa69ac0ae8c808f7fb26299/SQLAlchemy-2.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a0b4047d7d9405005637fbfd70122746c78f2dada934067bfdd439bc934cb5fb"}, {url = "https://files.pythonhosted.org/packages/73/51/d86ad02768ee26932ab6017d93f273b05c158dbde6b1ce2c21fb1ff54cbd/SQLAlchemy-2.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6c75b77de2fd99bd19a609c00e870325574000c441f7bdb0cd33d15961ed93bc"}, {url = "https://files.pythonhosted.org/packages/73/a5/1ada12a89370fa34c4244527afd5a5671357cf6b45e613d2dd447042b27f/SQLAlchemy-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b850d709cddfe0fa03f0ce7d58389947813053a3cfd5c7cc2fa5a49b77b7f7b5"}, {url = "https://files.pythonhosted.org/packages/74/55/2c1dfa43c7e421ed8cbcd3ded244ae42c9bfd33b0f806218f36d1df0ff4d/SQLAlchemy-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77666361fdd70868a414762d0eead141183caf1e0cb6735484c0cad6d41ac869"}, {url = "https://files.pythonhosted.org/packages/75/a3/e382030a0f785c7310f613cd9867d594aff6f0653dc1103e9e43218cef08/SQLAlchemy-2.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d1b1004e00023b37cc2385da670db28cb3dd96b9f01aafc3f9c437c030bf73f8"}, {url = "https://files.pythonhosted.org/packages/79/b6/b390905ee39990d8ad6060e9fe5c8c1ed1034a2a05ac3f0022fc1cd6c6b5/SQLAlchemy-2.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:20ef6ed15ecc17036523157e1f9900f0fa9163c29ce793d441b0bdd337057354"}, {url = "https://files.pythonhosted.org/packages/7e/28/112b6197eb97c4476916463de1022ff4a0e8c8ffdfd861cf60c4af35419c/SQLAlchemy-2.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e052ae0c2a887472a74405e3afa5aa5c75cddc8a98a49bbf4a84a09dbc1cb896"}, {url = "https://files.pythonhosted.org/packages/8b/35/cb5510a44a73a1da5c588e28d8b3e040b07ed56e69fc46a3ce1b77414530/SQLAlchemy-2.0.0-cp37-cp37m-win32.whl", hash = "sha256:c1cae76f84755527c269ceb49e3a79ff370101bfd93d3f9d298bd7951e1b5e41"}, {url = "https://files.pythonhosted.org/packages/8f/b9/20574d47d02cdfac66284d9d1a23f3af0d6ef946dcd20c38cf6f7749129a/SQLAlchemy-2.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:83db15a39539c6acb92075215aa68b9757085717d222ef678b0040cdf192adbb"}, {url = "https://files.pythonhosted.org/packages/8f/e9/6822c15d63ce8ede75e9ff6b1254f7fbfab5fa644daed24d7b2ca4089310/SQLAlchemy-2.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7c6934dfa9ab53853b1d31723ea2b8ea494de73ad3f36ea42f5859b74cb3afc3"}, {url = "https://files.pythonhosted.org/packages/91/f6/6aeceeaab65341ca1f87ddde45930980a759bdc2e54ce770ae88d77333b9/SQLAlchemy-2.0.0-cp310-cp310-win32.whl", hash = "sha256:86bc43f80b3fdae55f2dc6a3b0a9fe6f5c69001763e4095998e467b068a037d2"}, {url = "https://files.pythonhosted.org/packages/92/ea/808db76eb040e558a5b000143214a21fc5f3a09e8e03453cfe9b1b3b19cf/SQLAlchemy-2.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:606f55614af6777261e54cb5d541a5c555539c5abc5e0b40d299c9a3bd06fae5"}, {url = "https://files.pythonhosted.org/packages/97/13/55592db022211a56d34afc5f9d67b00a279626df0f15fa13bfc02c77b1b3/SQLAlchemy-2.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a0f6d402a12ce2dc9243553ae8088459e94540b2afc4b4c3fc3a0272b9aa2827"}, {url = "https://files.pythonhosted.org/packages/9d/d9/f903a20e6e66202516777f281ff07fc0400125bc174246423edb8d959f5c/SQLAlchemy-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c658c985830d4d80598387b2eca5944507acc9d52af8ec867d4c9fa0d4e27fd7"}, {url = "https://files.pythonhosted.org/packages/a2/76/2db3a6ace4a2ff61528910d6b34dbc35c1c36829f788d1d0b44404339986/SQLAlchemy-2.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b4428bf59a5f12549f92f4274c8b2667313f105e36a7822c47727ea5572e0f7"}, {url = "https://files.pythonhosted.org/packages/a8/49/b5db3773f3643e5dc0b7da7e854c4015df195a5eb640a6aaa8f1ef9609ee/SQLAlchemy-2.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:176ddfce8d720f90ffccfecfe66f41b1af8906bb74acc536068d067bdb0fd080"}, {url = "https://files.pythonhosted.org/packages/ad/c1/844e33521eb0fca42a897bcf03580053152ea6510efa8febe04fab7570b5/SQLAlchemy-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0bc643a0228179bfcbc8df81c8d197b843e48d97073f41f90ada8f6aad1614d"}, {url = "https://files.pythonhosted.org/packages/ae/73/4b76cacc21143ec3678747d2738d073bfd4ec4d399081fead8fa888c4dcd/SQLAlchemy-2.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33a05cc9533a580f94a69852c8dea26d7dec0bc8182bb8d68180a5103c0b0add"}, {url = "https://files.pythonhosted.org/packages/cd/27/3090eaaf13d4a989198126561e0c13e290ebaa32c357ec22a43d0f5d4541/SQLAlchemy-2.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:228851937becdbaeefdc937a3b43e9711b0a094eccc745f00b993ecd860a913b"}, {url = "https://files.pythonhosted.org/packages/cf/d7/7849486c094a368cc7b158abdb9e07f842e7479640f6c4391402745be653/SQLAlchemy-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:6e6cb16419328100fc92ee676bcb09846034586461aeb96c89a072feb48c9a6d"}, {url = "https://files.pythonhosted.org/packages/de/1c/fb2db5e661464534d1d938953e83a36a072c2c3af7b986592bcae847beab/SQLAlchemy-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:92b828f195bb967f85bda508bed5b4fe24b4ef0cac9ac2d9e403584ba504a304"}, {url = "https://files.pythonhosted.org/packages/f1/ab/db1936ee46c2de59696939addfa6e59f65cc3e0de7363eab968c596c61a6/SQLAlchemy-2.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:807d4f83dcf0b7fd60b7af5f677e3d20151083c3454304813e450f6f6e4b4a5c"}, {url = "https://files.pythonhosted.org/packages/f9/2c/2cbf3c3849e9eaa15a4ea08320aae1378ca839da94642f63039855d95dbb/SQLAlchemy-2.0.0-cp39-cp39-win32.whl", hash = "sha256:47348dad936e0899e2910853df1af736a84b3bddbd5dfe6471a5a39e00b32f06"}, {url = "https://files.pythonhosted.org/packages/fe/1e/ffdb3553745ed7302554eea0e9c500c79454cf80f9786c868fa246178e42/SQLAlchemy-2.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7b2231470060cd55b870806fb654f2ba66f7fc822f56fe594fa1fbd95e646da5"}, ] "tomli 2.0.1" = [ {url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] "tox 4.4.2" = [ {url = "https://files.pythonhosted.org/packages/72/68/6c1b1536e0dbfaf1351d4fa0f922305ff2cf2a90570bda7c378caa0eaf1c/tox-4.4.2-py3-none-any.whl", hash = "sha256:258895ba5de919490c03ef97467c4c8c42954537dfd96ae72cc2fb63dac67cf0"}, {url = "https://files.pythonhosted.org/packages/85/30/a2004e7e6fb71ce3fc3427262e23709b7ccd103dabc9f9b4bab92cb49e48/tox-4.4.2.tar.gz", hash = "sha256:3d8a8dd8a5afdc0d37af3e2b4959e427fe22530d0aa599baf0120e144b3defa3"}, ] "tox-pdm 0.6.1" = [ {url = "https://files.pythonhosted.org/packages/57/42/c437d69c3b884c58027999e524c91716ac355048284be771d810d80ceed1/tox-pdm-0.6.1.tar.gz", hash = "sha256:952ea67f2ec891f11eb00749f63fc0f980384435ca782c448d154390f9f42f5e"}, {url = "https://files.pythonhosted.org/packages/dd/2e/9c694f9775e3bd6e9eadf6c38fdc9f2e55d2ecbc7a4bba30eecf2a4b7f91/tox_pdm-0.6.1-py3-none-any.whl", hash = "sha256:9e3cf83b7b55c3e33aaee0e65cf341739581ff4604a4178f0ef7dbab73a0bb35"}, ] "typed-ast 1.5.4" = [ {url = "https://files.pythonhosted.org/packages/04/93/482d12fd3334b53ec4087e658ab161ab23affcf8b052166b4cf972ca673b/typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, {url = "https://files.pythonhosted.org/packages/07/d2/d55702e8deba2c80282fea0df53130790d8f398648be589750954c2dcce4/typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, {url = "https://files.pythonhosted.org/packages/0b/e7/8ec06fc870254889198f933a595f139b7871b24bab1116d6128440731ea9/typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, {url = "https://files.pythonhosted.org/packages/0f/59/430b86961d63278fcbced5ba72655ee93aa35e8e908bad4ff138480eb25d/typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, {url = "https://files.pythonhosted.org/packages/1a/f6/dd891624aaf98b918d7012b9d01753d0192c4eb18cf33ce616c0e08f62ba/typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, {url = "https://files.pythonhosted.org/packages/2f/87/25abe9558ed6cbd83ad5bfdccf7210a7eefaaf0232f86de99f65992e91fd/typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, {url = "https://files.pythonhosted.org/packages/2f/d5/02059fe6ca70b11bb831007962323160372ca83843e0bf296e8b6d833198/typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, {url = "https://files.pythonhosted.org/packages/34/2d/17fc1845dd5210345904b054c9fa90f451d64df56de0470f429bc8d63d39/typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, {url = "https://files.pythonhosted.org/packages/38/54/48f7d5b1f954f3a4d8f76e1a11c8497ae899b900cd5a67f826fa3937f701/typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, {url = "https://files.pythonhosted.org/packages/40/1a/5731a1a3908f60032aead10c2ffc9af12ee708bc9a156ed14a5065a9873a/typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, {url = "https://files.pythonhosted.org/packages/48/6c/d96a545d337589dc5d7ecc0f8991122800ffec8dc10a24090619883b515e/typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, {url = "https://files.pythonhosted.org/packages/4e/c1/cddc664ed3dd7d6bb62c80286c4e088b10556efc9a8db2049b425f8f23f7/typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, {url = "https://files.pythonhosted.org/packages/5c/e3/f539e658614ebf5a521c8ba7cbbb98afc5f5e90ddb0332ea22c164612dad/typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, {url = "https://files.pythonhosted.org/packages/70/2c/6d18e111d2c5422bb9e561bbf36885e430407859b2adef9b3fb575f189d5/typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, {url = "https://files.pythonhosted.org/packages/78/18/3ecf5043f227ebd4a43af57e18e6a38f9fe0b81dbfbb8d62eec669d7b69e/typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, {url = "https://files.pythonhosted.org/packages/96/35/612258bab9e1867b28e3137910df35576b7b0fbb9b6f3013cc23435a79ed/typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, {url = "https://files.pythonhosted.org/packages/9b/d5/5540eb496c6817eaee8120fb759c7adb36f91ef647c6bb2877f09acc0569/typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, {url = "https://files.pythonhosted.org/packages/c4/90/dacf9226b34961277f357c17c33b7cae3f05a5f5b8a1d23bd630d7a97a36/typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, {url = "https://files.pythonhosted.org/packages/ca/da/fbc14befbf19d69d05b4b8b019edbc6554d958037a821c6d5585767fe0ff/typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, {url = "https://files.pythonhosted.org/packages/cd/f3/188eede730be3f6ddb9a788cd6b7289207c5fceebbf8ae190f9716dd8c05/typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, {url = "https://files.pythonhosted.org/packages/d8/4e/db9505b53c44d7bc324a3d2e09bdf82b0943d6e08b183ae382860f482a87/typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, {url = "https://files.pythonhosted.org/packages/dd/87/09764c19a60a192b935579c93a07e781f6a52def10b723c8c5748e69a863/typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, {url = "https://files.pythonhosted.org/packages/e3/7c/7407838e9c540031439f2948bce2763cdd6882ebb72cc0a25b763c10529e/typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, {url = "https://files.pythonhosted.org/packages/f9/57/89ac0020d5ffc762487376d0c78e5d02af795657f18c411155b73de3c765/typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, ] "typing-extensions 4.4.0" = [ {url = "https://files.pythonhosted.org/packages/0b/8e/f1a0a5a76cfef77e1eb6004cb49e5f8d72634da638420b9ea492ce8305e8/typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, {url = "https://files.pythonhosted.org/packages/e3/a7/8f4e456ef0adac43f452efc2d0e4b242ab831297f1bac60ac815d37eb9cf/typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] "urllib3 1.26.14" = [ {url = "https://files.pythonhosted.org/packages/c5/52/fe421fb7364aa738b3506a2d99e4f3a56e079c0a798e9f4fa5e14c60922f/urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, {url = "https://files.pythonhosted.org/packages/fe/ca/466766e20b767ddb9b951202542310cba37ea5f2d792dae7589f1741af58/urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, ] "virtualenv 20.17.1" = [ {url = "https://files.pythonhosted.org/packages/18/a2/7931d40ecb02b5236a34ac53770f2f6931e3082b7a7dafe915d892d749d6/virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"}, {url = "https://files.pythonhosted.org/packages/7b/19/65f13cff26c8cc11fdfcb0499cd8f13388dd7b35a79a376755f152b42d86/virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"}, ] "werkzeug 2.2.2" = [ {url = "https://files.pythonhosted.org/packages/c8/27/be6ddbcf60115305205de79c29004a0c6bc53cec814f733467b1bb89386d/Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"}, {url = "https://files.pythonhosted.org/packages/f8/c1/1c8e539f040acd80f844c69a5ef8e2fccdf8b442dabb969e497b55d544e1/Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f"}, ] "zipp 3.11.0" = [ {url = "https://files.pythonhosted.org/packages/8e/b3/8b16a007184714f71157b1a71bbe632c5d66dd43bc8152b3c799b13881e1/zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, {url = "https://files.pythonhosted.org/packages/d8/20/256eb3f3f437c575fb1a2efdce5e801a5ce3162ea8117da96c43e6ee97d8/zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, ] flask-sqlalchemy-3.0.3/pyproject.toml000066400000000000000000000046571436623544000177140ustar00rootroot00000000000000[project] name = "Flask-SQLAlchemy" description = "Add SQLAlchemy support to your Flask application." readme = "README.rst" license = { text = "BSD-3-Clause" } authors = [{name = "Armin Ronacher", email = "armin.ronacher@active-4.com"},] maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"},] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", ] dynamic = ["version"] requires-python = ">=3.7" dependencies = ["Flask>=2.2", "SQLAlchemy>=1.4.18"] [project.urls] Donate = "https://palletsprojects.com/donate" Documentation = "https://flask-sqlalchemy.palletsprojects.com/" Changes = "https://flask-sqlalchemy.palletsprojects.com/changes/" "Source Code" = "https://github.com/pallets-eco/flask-sqlalchemy/" "Issue Tracker" = "https://github.com/pallets-eco/flask-sqlalchemy/issues/" Twitter = "https://twitter.com/PalletsTeam" Chat = "https://discord.gg/pallets" [project.optional-dependencies] [build-system] requires = ["pdm-pep517>=1.0.0"] build-backend = "pdm.pep517.api" [tool.pdm] version = { source = "file", path = "src/flask_sqlalchemy/__init__.py" } [tool.pdm.dev-dependencies] tests = [ "pytest", "blinker", ] coverage = [ "pytest-cov", "coverage[toml]", ] mypy = [ "mypy", "pytest", "sqlalchemy", ] docs = [ "sphinx", "pallets-sphinx-themes", "sphinx-issues", "sphinxcontrib-log-cabinet", ] pre-commit = [ "pre-commit", ] tox = [ "tox", "tox-pdm", ] [tool.pdm.build] source-includes = [ "docs/", "examples/", "tests/", "CHANGES.rst", "pdm.lock", "tox.ini", ] excludes = [ "docs/_build", ] [tool.pytest.ini_options] testpaths = ["tests"] filterwarnings = ["error"] [tool.coverage.run] branch = true source = ["flask_sqlalchemy", "tests"] [tool.coverage.paths] source = ["src", "*/site-packages"] [tool.mypy] python_version = "3.7" files = ["src/flask_sqlalchemy", "tests"] show_error_codes = true pretty = true strict = true # db.Model attribute doesn't recognize subclassing disable_error_code = ["name-defined"] # db.Model is Any disallow_subclassing_any = false [[tool.mypy.overrides]] module = [ "cryptography.*", "importlib_metadata.*", ] ignore_missing_imports = true flask-sqlalchemy-3.0.3/src/000077500000000000000000000000001436623544000155535ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/src/flask_sqlalchemy/000077500000000000000000000000001436623544000210755ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/src/flask_sqlalchemy/__init__.py000066400000000000000000000023421436623544000232070ustar00rootroot00000000000000from __future__ import annotations import typing as t from .extension import SQLAlchemy __version__ = "3.0.3" __all__ = [ "SQLAlchemy", ] _deprecated_map = { "Model": ".model.Model", "DefaultMeta": ".model.DefaultMeta", "Pagination": ".pagination.Pagination", "BaseQuery": ".query.Query", "get_debug_queries": ".record_queries.get_recorded_queries", "SignallingSession": ".session.Session", "before_models_committed": ".track_modifications.before_models_committed", "models_committed": ".track_modifications.models_committed", } def __getattr__(name: str) -> t.Any: import importlib import warnings if name in _deprecated_map: path = _deprecated_map[name] import_path, _, new_name = path.rpartition(".") action = "moved and renamed" if new_name == name: action = "moved" warnings.warn( f"'{name}' has been {action} to '{path[1:]}'. The top-level import is" " deprecated and will be removed in Flask-SQLAlchemy 3.1.", DeprecationWarning, stacklevel=2, ) mod = importlib.import_module(import_path, __name__) return getattr(mod, new_name) raise AttributeError(name) flask-sqlalchemy-3.0.3/src/flask_sqlalchemy/cli.py000066400000000000000000000007441436623544000222230ustar00rootroot00000000000000from __future__ import annotations import typing as t from flask import current_app def add_models_to_shell() -> dict[str, t.Any]: """Registered with :meth:`~flask.Flask.shell_context_processor` if ``add_models_to_shell`` is enabled. Adds the ``db`` instance and all model classes to ``flask shell``. """ db = current_app.extensions["sqlalchemy"] out = {m.class_.__name__: m.class_ for m in db.Model._sa_registry.mappers} out["db"] = db return out flask-sqlalchemy-3.0.3/src/flask_sqlalchemy/extension.py000066400000000000000000001125141436623544000234670ustar00rootroot00000000000000from __future__ import annotations import os import typing as t from weakref import WeakKeyDictionary import sqlalchemy as sa import sqlalchemy.event import sqlalchemy.exc import sqlalchemy.orm from flask import abort from flask import current_app from flask import Flask from flask import has_app_context from .model import _QueryProperty from .model import DefaultMeta from .model import Model from .pagination import Pagination from .pagination import SelectPagination from .query import Query from .session import _app_ctx_id from .session import Session from .table import _Table class SQLAlchemy: """Integrates SQLAlchemy with Flask. This handles setting up one or more engines, associating tables and models with specific engines, and cleaning up connections and sessions after each request. Only the engine configuration is specific to each application, other things like the model, table, metadata, and session are shared for all applications using that extension instance. Call :meth:`init_app` to configure the extension on an application. After creating the extension, create model classes by subclassing :attr:`Model`, and table classes with :attr:`Table`. These can be accessed before :meth:`init_app` is called, making it possible to define the models separately from the application. Accessing :attr:`session` and :attr:`engine` requires an active Flask application context. This includes methods like :meth:`create_all` which use the engine. This class also provides access to names in SQLAlchemy's ``sqlalchemy`` and ``sqlalchemy.orm`` modules. For example, you can use ``db.Column`` and ``db.relationship`` instead of importing ``sqlalchemy.Column`` and ``sqlalchemy.orm.relationship``. This can be convenient when defining models. :param app: Call :meth:`init_app` on this Flask application now. :param metadata: Use this as the default :class:`sqlalchemy.schema.MetaData`. Useful for setting a naming convention. :param session_options: Arguments used by :attr:`session` to create each session instance. A ``scopefunc`` key will be passed to the scoped session, not the session instance. See :class:`sqlalchemy.orm.sessionmaker` for a list of arguments. :param query_class: Use this as the default query class for models and dynamic relationships. The query interface is considered legacy in SQLAlchemy. :param model_class: Use this as the model base class when creating the declarative model class :attr:`Model`. Can also be a fully created declarative model class for further customization. :param engine_options: Default arguments used when creating every engine. These are lower precedence than application config. See :func:`sqlalchemy.create_engine` for a list of arguments. :param add_models_to_shell: Add the ``db`` instance and all model classes to ``flask shell``. .. versionchanged:: 3.0 An active Flask application context is always required to access ``session`` and ``engine``. .. versionchanged:: 3.0 Separate ``metadata`` are used for each bind key. .. versionchanged:: 3.0 The ``engine_options`` parameter is applied as defaults before per-engine configuration. .. versionchanged:: 3.0 The session class can be customized in ``session_options``. .. versionchanged:: 3.0 Added the ``add_models_to_shell`` parameter. .. versionchanged:: 3.0 Engines are created when calling ``init_app`` rather than the first time they are accessed. .. versionchanged:: 3.0 All parameters except ``app`` are keyword-only. .. versionchanged:: 3.0 The extension instance is stored directly as ``app.extensions["sqlalchemy"]``. .. versionchanged:: 3.0 Setup methods are renamed with a leading underscore. They are considered internal interfaces which may change at any time. .. versionchanged:: 3.0 Removed the ``use_native_unicode`` parameter and config. .. versionchanged:: 3.0 The ``COMMIT_ON_TEARDOWN`` configuration is deprecated and will be removed in Flask-SQLAlchemy 3.1. Call ``db.session.commit()`` directly instead. .. versionchanged:: 2.4 Added the ``engine_options`` parameter. .. versionchanged:: 2.1 Added the ``metadata``, ``query_class``, and ``model_class`` parameters. .. versionchanged:: 2.1 Use the same query class across ``session``, ``Model.query`` and ``Query``. .. versionchanged:: 0.16 ``scopefunc`` is accepted in ``session_options``. .. versionchanged:: 0.10 Added the ``session_options`` parameter. """ def __init__( self, app: Flask | None = None, *, metadata: sa.MetaData | None = None, session_options: dict[str, t.Any] | None = None, query_class: type[Query] = Query, model_class: type[Model] | sa.orm.DeclarativeMeta = Model, engine_options: dict[str, t.Any] | None = None, add_models_to_shell: bool = True, ): if session_options is None: session_options = {} self.Query = query_class """The default query class used by ``Model.query`` and ``lazy="dynamic"`` relationships. .. warning:: The query interface is considered legacy in SQLAlchemy. Customize this by passing the ``query_class`` parameter to the extension. """ self.session = self._make_scoped_session(session_options) """A :class:`sqlalchemy.orm.scoping.scoped_session` that creates instances of :class:`.Session` scoped to the current Flask application context. The session will be removed, returning the engine connection to the pool, when the application context exits. Customize this by passing ``session_options`` to the extension. This requires that a Flask application context is active. .. versionchanged:: 3.0 The session is scoped to the current app context. """ self.metadatas: dict[str | None, sa.MetaData] = {} """Map of bind keys to :class:`sqlalchemy.schema.MetaData` instances. The ``None`` key refers to the default metadata, and is available as :attr:`metadata`. Customize the default metadata by passing the ``metadata`` parameter to the extension. This can be used to set a naming convention. When metadata for another bind key is created, it copies the default's naming convention. .. versionadded:: 3.0 """ if metadata is not None: metadata.info["bind_key"] = None self.metadatas[None] = metadata self.Table = self._make_table_class() """A :class:`sqlalchemy.schema.Table` class that chooses a metadata automatically. Unlike the base ``Table``, the ``metadata`` argument is not required. If it is not given, it is selected based on the ``bind_key`` argument. :param bind_key: Used to select a different metadata. :param args: Arguments passed to the base class. These are typically the table's name, columns, and constraints. :param kwargs: Arguments passed to the base class. .. versionchanged:: 3.0 This is a subclass of SQLAlchemy's ``Table`` rather than a function. """ self.Model = self._make_declarative_base(model_class) """A SQLAlchemy declarative model class. Subclass this to define database models. If a model does not set ``__tablename__``, it will be generated by converting the class name from ``CamelCase`` to ``snake_case``. It will not be generated if the model looks like it uses single-table inheritance. If a model or parent class sets ``__bind_key__``, it will use that metadata and database engine. Otherwise, it will use the default :attr:`metadata` and :attr:`engine`. This is ignored if the model sets ``metadata`` or ``__table__``. Customize this by subclassing :class:`.Model` and passing the ``model_class`` parameter to the extension. A fully created declarative model class can be passed as well, to use a custom metaclass. """ if engine_options is None: engine_options = {} self._engine_options = engine_options self._app_engines: WeakKeyDictionary[Flask, dict[str | None, sa.engine.Engine]] self._app_engines = WeakKeyDictionary() self._add_models_to_shell = add_models_to_shell if app is not None: self.init_app(app) def __repr__(self) -> str: if not has_app_context(): return f"<{type(self).__name__}>" message = f"{type(self).__name__} {self.engine.url}" if len(self.engines) > 1: message = f"{message} +{len(self.engines) - 1}" return f"<{message}>" def init_app(self, app: Flask) -> None: """Initialize a Flask application for use with this extension instance. This must be called before accessing the database engine or session with the app. This sets default configuration values, then configures the extension on the application and creates the engines for each bind key. Therefore, this must be called after the application has been configured. Changes to application config after this call will not be reflected. The following keys from ``app.config`` are used: - :data:`.SQLALCHEMY_DATABASE_URI` - :data:`.SQLALCHEMY_ENGINE_OPTIONS` - :data:`.SQLALCHEMY_ECHO` - :data:`.SQLALCHEMY_BINDS` - :data:`.SQLALCHEMY_RECORD_QUERIES` - :data:`.SQLALCHEMY_TRACK_MODIFICATIONS` :param app: The Flask application to initialize. """ if "sqlalchemy" in app.extensions: raise RuntimeError( "A 'SQLAlchemy' instance has already been registered on this Flask app." " Import and use that instance instead." ) app.extensions["sqlalchemy"] = self if self._add_models_to_shell: from .cli import add_models_to_shell app.shell_context_processor(add_models_to_shell) if app.config.get("SQLALCHEMY_COMMIT_ON_TEARDOWN", False): import warnings warnings.warn( "'SQLALCHEMY_COMMIT_ON_TEARDOWN' is deprecated and will be removed in" " Flask-SQAlchemy 3.1. Call 'db.session.commit()'` directly instead.", DeprecationWarning, ) app.teardown_appcontext(self._teardown_commit) else: app.teardown_appcontext(self._teardown_session) basic_uri: str | sa.engine.URL | None = app.config.setdefault( "SQLALCHEMY_DATABASE_URI", None ) basic_engine_options = self._engine_options.copy() basic_engine_options.update( app.config.setdefault("SQLALCHEMY_ENGINE_OPTIONS", {}) ) echo: bool = app.config.setdefault("SQLALCHEMY_ECHO", False) config_binds: dict[ str | None, str | sa.engine.URL | dict[str, t.Any] ] = app.config.setdefault("SQLALCHEMY_BINDS", {}) engine_options: dict[str | None, dict[str, t.Any]] = {} # Build the engine config for each bind key. for key, value in config_binds.items(): engine_options[key] = self._engine_options.copy() if isinstance(value, (str, sa.engine.URL)): engine_options[key]["url"] = value else: engine_options[key].update(value) # Build the engine config for the default bind key. if basic_uri is not None: basic_engine_options["url"] = basic_uri if "url" in basic_engine_options: engine_options.setdefault(None, {}).update(basic_engine_options) if not engine_options: raise RuntimeError( "Either 'SQLALCHEMY_DATABASE_URI' or 'SQLALCHEMY_BINDS' must be set." ) engines = self._app_engines.setdefault(app, {}) # Dispose existing engines in case init_app is called again. if engines: for engine in engines.values(): engine.dispose() engines.clear() # Create the metadata and engine for each bind key. for key, options in engine_options.items(): self._make_metadata(key) options.setdefault("echo", echo) options.setdefault("echo_pool", echo) self._apply_driver_defaults(options, app) engines[key] = self._make_engine(key, options, app) if app.config.setdefault("SQLALCHEMY_RECORD_QUERIES", False): from . import record_queries for engine in engines.values(): record_queries._listen(engine) if app.config.setdefault("SQLALCHEMY_TRACK_MODIFICATIONS", False): from . import track_modifications track_modifications._listen(self.session) def _make_scoped_session( self, options: dict[str, t.Any] ) -> sa.orm.scoped_session[Session]: """Create a :class:`sqlalchemy.orm.scoping.scoped_session` around the factory from :meth:`_make_session_factory`. The result is available as :attr:`session`. The scope function can be customized using the ``scopefunc`` key in the ``session_options`` parameter to the extension. By default it uses the current thread or greenlet id. This method is used for internal setup. Its signature may change at any time. :meta private: :param options: The ``session_options`` parameter from ``__init__``. Keyword arguments passed to the session factory. A ``scopefunc`` key is popped. .. versionchanged:: 3.0 The session is scoped to the current app context. .. versionchanged:: 3.0 Renamed from ``create_scoped_session``, this method is internal. """ scope = options.pop("scopefunc", _app_ctx_id) factory = self._make_session_factory(options) return sa.orm.scoped_session(factory, scope) def _make_session_factory( self, options: dict[str, t.Any] ) -> sa.orm.sessionmaker[Session]: """Create the SQLAlchemy :class:`sqlalchemy.orm.sessionmaker` used by :meth:`_make_scoped_session`. To customize, pass the ``session_options`` parameter to :class:`SQLAlchemy`. To customize the session class, subclass :class:`.Session` and pass it as the ``class_`` key. This method is used for internal setup. Its signature may change at any time. :meta private: :param options: The ``session_options`` parameter from ``__init__``. Keyword arguments passed to the session factory. .. versionchanged:: 3.0 The session class can be customized. .. versionchanged:: 3.0 Renamed from ``create_session``, this method is internal. """ options.setdefault("class_", Session) options.setdefault("query_cls", self.Query) return sa.orm.sessionmaker(db=self, **options) def _teardown_commit(self, exc: BaseException | None) -> None: """Commit the session at the end of the request if there was not an unhandled exception during the request. :meta private: .. deprecated:: 3.0 Will be removed in 3.1. Use ``db.session.commit()`` directly instead. """ if exc is None: self.session.commit() self.session.remove() def _teardown_session(self, exc: BaseException | None) -> None: """Remove the current session at the end of the request. :meta private: .. versionadded:: 3.0 """ self.session.remove() def _make_metadata(self, bind_key: str | None) -> sa.MetaData: """Get or create a :class:`sqlalchemy.schema.MetaData` for the given bind key. This method is used for internal setup. Its signature may change at any time. :meta private: :param bind_key: The name of the metadata being created. .. versionadded:: 3.0 """ if bind_key in self.metadatas: return self.metadatas[bind_key] if bind_key is not None: # Copy the naming convention from the default metadata. naming_convention = self._make_metadata(None).naming_convention else: naming_convention = None # Set the bind key in info to be used by session.get_bind. metadata = sa.MetaData( naming_convention=naming_convention, info={"bind_key": bind_key} ) self.metadatas[bind_key] = metadata return metadata def _make_table_class(self) -> type[_Table]: """Create a SQLAlchemy :class:`sqlalchemy.schema.Table` class that chooses a metadata automatically based on the ``bind_key``. The result is available as :attr:`Table`. This method is used for internal setup. Its signature may change at any time. :meta private: .. versionadded:: 3.0 """ class Table(_Table): def __new__( cls, *args: t.Any, bind_key: str | None = None, **kwargs: t.Any ) -> Table: # If a metadata arg is passed, go directly to the base Table. Also do # this for no args so the correct error is shown. if not args or (len(args) >= 2 and isinstance(args[1], sa.MetaData)): return super().__new__(cls, *args, **kwargs) if ( bind_key is None and "info" in kwargs and "bind_key" in kwargs["info"] ): import warnings warnings.warn( "'table.info['bind_key'] is deprecated and will not be used in" " Flask-SQLAlchemy 3.1. Pass the 'bind_key' parameter instead.", DeprecationWarning, stacklevel=2, ) bind_key = kwargs["info"].get("bind_key") metadata = self._make_metadata(bind_key) return super().__new__(cls, *[args[0], metadata, *args[1:]], **kwargs) return Table def _make_declarative_base( self, model: type[Model] | sa.orm.DeclarativeMeta ) -> type[t.Any]: """Create a SQLAlchemy declarative model class. The result is available as :attr:`Model`. To customize, subclass :class:`.Model` and pass it as ``model_class`` to :class:`SQLAlchemy`. To customize at the metaclass level, pass an already created declarative model class as ``model_class``. This method is used for internal setup. Its signature may change at any time. :meta private: :param model: A model base class, or an already created declarative model class. .. versionchanged:: 3.0 Renamed with a leading underscore, this method is internal. .. versionchanged:: 2.3 ``model`` can be an already created declarative model class. """ if not isinstance(model, sa.orm.DeclarativeMeta): metadata = self._make_metadata(None) model = sa.orm.declarative_base( metadata=metadata, cls=model, name="Model", metaclass=DefaultMeta ) if None not in self.metadatas: # Use the model's metadata as the default metadata. model.metadata.info["bind_key"] = None # type: ignore[union-attr] self.metadatas[None] = model.metadata # type: ignore[union-attr] else: # Use the passed in default metadata as the model's metadata. model.metadata = self.metadatas[None] # type: ignore[union-attr] model.query_class = self.Query model.query = _QueryProperty() model.__fsa__ = self return model def _apply_driver_defaults(self, options: dict[str, t.Any], app: Flask) -> None: """Apply driver-specific configuration to an engine. SQLite in-memory databases use ``StaticPool`` and disable ``check_same_thread``. File paths are relative to the app's :attr:`~flask.Flask.instance_path`, which is created if it doesn't exist. MySQL sets ``charset="utf8mb4"``, and ``pool_timeout`` defaults to 2 hours. This method is used for internal setup. Its signature may change at any time. :meta private: :param options: Arguments passed to the engine. :param app: The application that the engine configuration belongs to. .. versionchanged:: 3.0 SQLite paths are relative to ``app.instance_path``. It does not use ``NullPool`` if ``pool_size`` is 0. Driver-level URIs are supported. .. versionchanged:: 3.0 MySQL sets ``charset="utf8mb4". It does not set ``pool_size`` to 10. It does not set ``pool_recycle`` if not using a queue pool. .. versionchanged:: 3.0 Renamed from ``apply_driver_hacks``, this method is internal. It does not return anything. .. versionchanged:: 2.5 Returns ``(sa_url, options)``. """ url = sa.engine.make_url(options["url"]) if url.drivername in {"sqlite", "sqlite+pysqlite"}: if url.database is None or url.database in {"", ":memory:"}: options["poolclass"] = sa.pool.StaticPool if "connect_args" not in options: options["connect_args"] = {} options["connect_args"]["check_same_thread"] = False else: # the url might look like sqlite:///file:path?uri=true is_uri = url.query.get("uri", False) if is_uri: db_str = url.database[5:] else: db_str = url.database if not os.path.isabs(db_str): os.makedirs(app.instance_path, exist_ok=True) db_str = os.path.join(app.instance_path, db_str) if is_uri: db_str = f"file:{db_str}" options["url"] = url.set(database=db_str) elif url.drivername.startswith("mysql"): # set queue defaults only when using queue pool if ( "pool_class" not in options or options["pool_class"] is sa.pool.QueuePool ): options.setdefault("pool_recycle", 7200) if "charset" not in url.query: options["url"] = url.update_query_dict({"charset": "utf8mb4"}) def _make_engine( self, bind_key: str | None, options: dict[str, t.Any], app: Flask ) -> sa.engine.Engine: """Create the :class:`sqlalchemy.engine.Engine` for the given bind key and app. To customize, use :data:`.SQLALCHEMY_ENGINE_OPTIONS` or :data:`.SQLALCHEMY_BINDS` config. Pass ``engine_options`` to :class:`SQLAlchemy` to set defaults for all engines. This method is used for internal setup. Its signature may change at any time. :meta private: :param bind_key: The name of the engine being created. :param options: Arguments passed to the engine. :param app: The application that the engine configuration belongs to. .. versionchanged:: 3.0 Renamed from ``create_engine``, this method is internal. """ return sa.engine_from_config(options, prefix="") @property def metadata(self) -> sa.MetaData: """The default metadata used by :attr:`Model` and :attr:`Table` if no bind key is set. """ return self.metadatas[None] @property def engines(self) -> t.Mapping[str | None, sa.engine.Engine]: """Map of bind keys to :class:`sqlalchemy.engine.Engine` instances for current application. The ``None`` key refers to the default engine, and is available as :attr:`engine`. To customize, set the :data:`.SQLALCHEMY_BINDS` config, and set defaults by passing the ``engine_options`` parameter to the extension. This requires that a Flask application context is active. .. versionadded:: 3.0 """ app = current_app._get_current_object() # type: ignore[attr-defined] if app not in self._app_engines: raise RuntimeError( "The current Flask app is not registered with this 'SQLAlchemy'" " instance. Did you forget to call 'init_app', or did you create" " multiple 'SQLAlchemy' instances?" ) return self._app_engines[app] @property def engine(self) -> sa.engine.Engine: """The default :class:`~sqlalchemy.engine.Engine` for the current application, used by :attr:`session` if the :attr:`Model` or :attr:`Table` being queried does not set a bind key. To customize, set the :data:`.SQLALCHEMY_ENGINE_OPTIONS` config, and set defaults by passing the ``engine_options`` parameter to the extension. This requires that a Flask application context is active. """ return self.engines[None] def get_engine(self, bind_key: str | None = None) -> sa.engine.Engine: """Get the engine for the given bind key for the current application. This requires that a Flask application context is active. :param bind_key: The name of the engine. .. deprecated:: 3.0 Will be removed in Flask-SQLAlchemy 3.1. Use ``engines[key]`` instead. .. versionchanged:: 3.0 Renamed the ``bind`` parameter to ``bind_key``. Removed the ``app`` parameter. """ import warnings warnings.warn( "'get_engine' is deprecated and will be removed in Flask-SQLAlchemy 3.1." " Use 'engine' or 'engines[key]' instead.", DeprecationWarning, stacklevel=2, ) return self.engines[bind_key] def get_tables_for_bind(self, bind_key: str | None = None) -> list[sa.Table]: """Get all tables in the metadata for the given bind key. :param bind_key: The bind key to get. .. deprecated:: 3.0 Will be removed in Flask-SQLAlchemy 3.1. Use ``metadata.tables`` instead. .. versionchanged:: 3.0 Renamed the ``bind`` parameter to ``bind_key``. """ import warnings warnings.warn( "'get_tables_for_bind' is deprecated and will be removed in" " Flask-SQLAlchemy 3.1. Use 'metadata.tables' instead.", DeprecationWarning, stacklevel=2, ) return list(self.metadatas[bind_key].tables.values()) def get_binds(self) -> dict[sa.Table, sa.engine.Engine]: """Map all tables to their engine based on their bind key, which can be used to create a session with ``Session(binds=db.get_binds(app))``. This requires that a Flask application context is active. .. deprecated:: 3.0 Will be removed in Flask-SQLAlchemy 3.1. ``db.session`` supports multiple binds directly. .. versionchanged:: 3.0 Removed the ``app`` parameter. """ import warnings warnings.warn( "'get_binds' is deprecated and will be removed in Flask-SQLAlchemy 3.1." " 'db.session' supports multiple binds directly.", DeprecationWarning, stacklevel=2, ) return { table: engine for bind_key, engine in self.engines.items() for table in self.metadatas[bind_key].tables.values() } def get_or_404( self, entity: type[t.Any], ident: t.Any, *, description: str | None = None ) -> t.Any: """Like :meth:`session.get() ` but aborts with a ``404 Not Found`` error instead of returning ``None``. :param entity: The model class to query. :param ident: The primary key to query. :param description: A custom message to show on the error page. .. versionadded:: 3.0 """ value = self.session.get(entity, ident) if value is None: abort(404, description=description) return value def first_or_404( self, statement: sa.sql.Select[t.Any], *, description: str | None = None ) -> t.Any: """Like :meth:`Result.scalar() `, but aborts with a ``404 Not Found`` error instead of returning ``None``. :param statement: The ``select`` statement to execute. :param description: A custom message to show on the error page. .. versionadded:: 3.0 """ value = self.session.execute(statement).scalar() if value is None: abort(404, description=description) return value def one_or_404( self, statement: sa.sql.Select[t.Any], *, description: str | None = None ) -> t.Any: """Like :meth:`Result.scalar_one() `, but aborts with a ``404 Not Found`` error instead of raising ``NoResultFound`` or ``MultipleResultsFound``. :param statement: The ``select`` statement to execute. :param description: A custom message to show on the error page. .. versionadded:: 3.0 """ try: return self.session.execute(statement).scalar_one() except (sa.exc.NoResultFound, sa.exc.MultipleResultsFound): abort(404, description=description) def paginate( self, select: sa.sql.Select[t.Any], *, page: int | None = None, per_page: int | None = None, max_per_page: int | None = None, error_out: bool = True, count: bool = True, ) -> Pagination: """Apply an offset and limit to a select statment based on the current page and number of items per page, returning a :class:`.Pagination` object. The statement should select a model class, like ``select(User)``. This applies ``unique()`` and ``scalars()`` modifiers to the result, so compound selects will not return the expected results. :param select: The ``select`` statement to paginate. :param page: The current page, used to calculate the offset. Defaults to the ``page`` query arg during a request, or 1 otherwise. :param per_page: The maximum number of items on a page, used to calculate the offset and limit. Defaults to the ``per_page`` query arg during a request, or 20 otherwise. :param max_per_page: The maximum allowed value for ``per_page``, to limit a user-provided value. Use ``None`` for no limit. Defaults to 100. :param error_out: Abort with a ``404 Not Found`` error if no items are returned and ``page`` is not 1, or if ``page`` or ``per_page`` is less than 1, or if either are not ints. :param count: Calculate the total number of values by issuing an extra count query. For very complex queries this may be inaccurate or slow, so it can be disabled and set manually if necessary. .. versionchanged:: 3.0 The ``count`` query is more efficient. .. versionadded:: 3.0 """ return SelectPagination( select=select, session=self.session(), page=page, per_page=per_page, max_per_page=max_per_page, error_out=error_out, count=count, ) def _call_for_binds( self, bind_key: str | None | list[str | None], op_name: str ) -> None: """Call a method on each metadata. :meta private: :param bind_key: A bind key or list of keys. Defaults to all binds. :param op_name: The name of the method to call. .. versionchanged:: 3.0 Renamed from ``_execute_for_all_tables``. """ if bind_key == "__all__": keys: list[str | None] = list(self.metadatas) elif bind_key is None or isinstance(bind_key, str): keys = [bind_key] else: keys = bind_key for key in keys: try: engine = self.engines[key] except KeyError: message = f"Bind key '{key}' is not in 'SQLALCHEMY_BINDS' config." if key is None: message = f"'SQLALCHEMY_DATABASE_URI' config is not set. {message}" raise sa.exc.UnboundExecutionError(message) from None metadata = self.metadatas[key] getattr(metadata, op_name)(bind=engine) def create_all(self, bind_key: str | None | list[str | None] = "__all__") -> None: """Create tables that do not exist in the database by calling ``metadata.create_all()`` for all or some bind keys. This does not update existing tables, use a migration library for that. This requires that a Flask application context is active. :param bind_key: A bind key or list of keys to create the tables for. Defaults to all binds. .. versionchanged:: 3.0 Renamed the ``bind`` parameter to ``bind_key``. Removed the ``app`` parameter. .. versionchanged:: 0.12 Added the ``bind`` and ``app`` parameters. """ self._call_for_binds(bind_key, "create_all") def drop_all(self, bind_key: str | None | list[str | None] = "__all__") -> None: """Drop tables by calling ``metadata.drop_all()`` for all or some bind keys. This requires that a Flask application context is active. :param bind_key: A bind key or list of keys to drop the tables from. Defaults to all binds. .. versionchanged:: 3.0 Renamed the ``bind`` parameter to ``bind_key``. Removed the ``app`` parameter. .. versionchanged:: 0.12 Added the ``bind`` and ``app`` parameters. """ self._call_for_binds(bind_key, "drop_all") def reflect(self, bind_key: str | None | list[str | None] = "__all__") -> None: """Load table definitions from the database by calling ``metadata.reflect()`` for all or some bind keys. This requires that a Flask application context is active. :param bind_key: A bind key or list of keys to reflect the tables from. Defaults to all binds. .. versionchanged:: 3.0 Renamed the ``bind`` parameter to ``bind_key``. Removed the ``app`` parameter. .. versionchanged:: 0.12 Added the ``bind`` and ``app`` parameters. """ self._call_for_binds(bind_key, "reflect") def _set_rel_query(self, kwargs: dict[str, t.Any]) -> None: """Apply the extension's :attr:`Query` class as the default for relationships and backrefs. :meta private: """ kwargs.setdefault("query_class", self.Query) if "backref" in kwargs: backref = kwargs["backref"] if isinstance(backref, str): backref = (backref, {}) backref[1].setdefault("query_class", self.Query) def relationship( self, *args: t.Any, **kwargs: t.Any ) -> sa.orm.RelationshipProperty[t.Any]: """A :func:`sqlalchemy.orm.relationship` that applies this extension's :attr:`Query` class for dynamic relationships and backrefs. .. versionchanged:: 3.0 The :attr:`Query` class is set on ``backref``. """ self._set_rel_query(kwargs) return sa.orm.relationship(*args, **kwargs) def dynamic_loader( self, argument: t.Any, **kwargs: t.Any ) -> sa.orm.RelationshipProperty[t.Any]: """A :func:`sqlalchemy.orm.dynamic_loader` that applies this extension's :attr:`Query` class for relationships and backrefs. .. versionchanged:: 3.0 The :attr:`Query` class is set on ``backref``. """ self._set_rel_query(kwargs) return sa.orm.dynamic_loader(argument, **kwargs) def _relation( self, *args: t.Any, **kwargs: t.Any ) -> sa.orm.RelationshipProperty[t.Any]: """A :func:`sqlalchemy.orm.relationship` that applies this extension's :attr:`Query` class for dynamic relationships and backrefs. SQLAlchemy 2.0 removes this name, use ``relationship`` instead. :meta private: .. versionchanged:: 3.0 The :attr:`Query` class is set on ``backref``. """ # Deprecated, removed in SQLAlchemy 2.0. Accessed through ``__getattr__``. self._set_rel_query(kwargs) f = sa.orm.relation # type: ignore[attr-defined] return f(*args, **kwargs) # type: ignore[no-any-return] def __getattr__(self, name: str) -> t.Any: if name == "db": import warnings warnings.warn( "The 'db' attribute is deprecated and will be removed in" " Flask-SQLAlchemy 3.1. The extension is registered directly as" " 'app.extensions[\"sqlalchemy\"]'.", DeprecationWarning, stacklevel=2, ) return self if name == "relation": return self._relation if name == "event": return sa.event if name.startswith("_"): raise AttributeError(name) for mod in (sa, sa.orm): if hasattr(mod, name): return getattr(mod, name) raise AttributeError(name) flask-sqlalchemy-3.0.3/src/flask_sqlalchemy/model.py000066400000000000000000000156761436623544000225660ustar00rootroot00000000000000from __future__ import annotations import re import typing as t import sqlalchemy as sa import sqlalchemy.orm from .query import Query if t.TYPE_CHECKING: from .extension import SQLAlchemy class _QueryProperty: """A class property that creates a query object for a model. :meta private: """ @t.overload def __get__(self, obj: None, cls: type[Model]) -> Query: ... @t.overload def __get__(self, obj: Model, cls: type[Model]) -> Query: ... def __get__(self, obj: Model | None, cls: type[Model]) -> Query: return cls.query_class( cls, session=cls.__fsa__.session() # type: ignore[arg-type] ) class Model: """The base class of the :attr:`.SQLAlchemy.Model` declarative model class. To define models, subclass :attr:`db.Model <.SQLAlchemy.Model>`, not this. To customize ``db.Model``, subclass this and pass it as ``model_class`` to :class:`.SQLAlchemy`. To customize ``db.Model`` at the metaclass level, pass an already created declarative model class as ``model_class``. """ __fsa__: t.ClassVar[SQLAlchemy] """Internal reference to the extension object. :meta private: """ query_class: t.ClassVar[type[Query]] = Query """Query class used by :attr:`query`. Defaults to :attr:`.SQLAlchemy.Query`, which defaults to :class:`.Query`. """ query: t.ClassVar[Query] = _QueryProperty() # type: ignore[assignment] """A SQLAlchemy query for a model. Equivalent to ``db.session.query(Model)``. Can be customized per-model by overriding :attr:`query_class`. .. warning:: The query interface is considered legacy in SQLAlchemy. Prefer using ``session.execute(select())`` instead. """ def __repr__(self) -> str: state = sa.inspect(self) assert state is not None if state.transient: pk = f"(transient {id(self)})" elif state.pending: pk = f"(pending {id(self)})" else: pk = ", ".join(map(str, state.identity)) return f"<{type(self).__name__} {pk}>" class BindMetaMixin(type): """Metaclass mixin that sets a model's ``metadata`` based on its ``__bind_key__``. If the model sets ``metadata`` or ``__table__`` directly, ``__bind_key__`` is ignored. If the ``metadata`` is the same as the parent model, it will not be set directly on the child model. """ __fsa__: SQLAlchemy metadata: sa.MetaData def __init__( cls, name: str, bases: tuple[type, ...], d: dict[str, t.Any], **kwargs: t.Any ) -> None: if not ("metadata" in cls.__dict__ or "__table__" in cls.__dict__): bind_key = getattr(cls, "__bind_key__", None) parent_metadata = getattr(cls, "metadata", None) metadata = cls.__fsa__._make_metadata(bind_key) if metadata is not parent_metadata: cls.metadata = metadata super().__init__(name, bases, d, **kwargs) class NameMetaMixin(type): """Metaclass mixin that sets a model's ``__tablename__`` by converting the ``CamelCase`` class name to ``snake_case``. A name is set for non-abstract models that do not otherwise define ``__tablename__``. If a model does not define a primary key, it will not generate a name or ``__table__``, for single-table inheritance. """ metadata: sa.MetaData __tablename__: str __table__: sa.Table def __init__( cls, name: str, bases: tuple[type, ...], d: dict[str, t.Any], **kwargs: t.Any ) -> None: if should_set_tablename(cls): cls.__tablename__ = camel_to_snake_case(cls.__name__) super().__init__(name, bases, d, **kwargs) # __table_cls__ has run. If no table was created, use the parent table. if ( "__tablename__" not in cls.__dict__ and "__table__" in cls.__dict__ and cls.__dict__["__table__"] is None ): del cls.__table__ def __table_cls__(cls, *args: t.Any, **kwargs: t.Any) -> sa.Table | None: """This is called by SQLAlchemy during mapper setup. It determines the final table object that the model will use. If no primary key is found, that indicates single-table inheritance, so no table will be created and ``__tablename__`` will be unset. """ schema = kwargs.get("schema") if schema is None: key = args[0] else: key = f"{schema}.{args[0]}" # Check if a table with this name already exists. Allows reflected tables to be # applied to models by name. if key in cls.metadata.tables: return sa.Table(*args, **kwargs) # If a primary key is found, create a table for joined-table inheritance. for arg in args: if (isinstance(arg, sa.Column) and arg.primary_key) or isinstance( arg, sa.PrimaryKeyConstraint ): return sa.Table(*args, **kwargs) # If no base classes define a table, return one that's missing a primary key # so SQLAlchemy shows the correct error. for base in cls.__mro__[1:-1]: if "__table__" in base.__dict__: break else: return sa.Table(*args, **kwargs) # Single-table inheritance, use the parent table name. __init__ will unset # __table__ based on this. if "__tablename__" in cls.__dict__: del cls.__tablename__ return None def should_set_tablename(cls: type) -> bool: """Determine whether ``__tablename__`` should be generated for a model. - If no class in the MRO sets a name, one should be generated. - If a declared attr is found, it should be used instead. - If a name is found, it should be used if the class is a mixin, otherwise one should be generated. - Abstract models should not have one generated. Later, ``__table_cls__`` will determine if the model looks like single or joined-table inheritance. If no primary key is found, the name will be unset. """ if cls.__dict__.get("__abstract__", False) or not any( isinstance(b, sa.orm.DeclarativeMeta) for b in cls.__mro__[1:] ): return False for base in cls.__mro__: if "__tablename__" not in base.__dict__: continue if isinstance(base.__dict__["__tablename__"], sa.orm.declared_attr): return False return not ( base is cls or base.__dict__.get("__abstract__", False) or not isinstance(base, sa.orm.DeclarativeMeta) ) return True def camel_to_snake_case(name: str) -> str: """Convert a ``CamelCase`` name to ``snake_case``.""" name = re.sub(r"((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))", r"_\1", name) return name.lower().lstrip("_") class DefaultMeta(BindMetaMixin, NameMetaMixin, sa.orm.DeclarativeMeta): """SQLAlchemy declarative metaclass that provides ``__bind_key__`` and ``__tablename__`` support. """ flask-sqlalchemy-3.0.3/src/flask_sqlalchemy/pagination.py000066400000000000000000000253111436623544000236020ustar00rootroot00000000000000from __future__ import annotations import typing as t from math import ceil import sqlalchemy as sa import sqlalchemy.orm from flask import abort from flask import request class Pagination: """Apply an offset and limit to the query based on the current page and number of items per page. Don't create pagination objects manually. They are created by :meth:`.SQLAlchemy.paginate` and :meth:`.Query.paginate`. This is a base class, a subclass must implement :meth:`_query_items` and :meth:`_query_count`. Those methods will use arguments passed as ``kwargs`` to perform the queries. :param page: The current page, used to calculate the offset. Defaults to the ``page`` query arg during a request, or 1 otherwise. :param per_page: The maximum number of items on a page, used to calculate the offset and limit. Defaults to the ``per_page`` query arg during a request, or 20 otherwise. :param max_per_page: The maximum allowed value for ``per_page``, to limit a user-provided value. Use ``None`` for no limit. Defaults to 100. :param error_out: Abort with a ``404 Not Found`` error if no items are returned and ``page`` is not 1, or if ``page`` or ``per_page`` is less than 1, or if either are not ints. :param count: Calculate the total number of values by issuing an extra count query. For very complex queries this may be inaccurate or slow, so it can be disabled and set manually if necessary. :param kwargs: Information about the query to paginate. Different subclasses will require different arguments. .. versionchanged:: 3.0 Iterating over a pagination object iterates over its items. .. versionchanged:: 3.0 Creating instances manually is not a public API. """ def __init__( self, page: int | None = None, per_page: int | None = None, max_per_page: int | None = 100, error_out: bool = True, count: bool = True, **kwargs: t.Any, ) -> None: self._query_args = kwargs page, per_page = self._prepare_page_args( page=page, per_page=per_page, max_per_page=max_per_page, error_out=error_out, ) self.page: int = page """The current page.""" self.per_page: int = per_page """The maximum number of items on a page.""" items = self._query_items() if not items and page != 1 and error_out: abort(404) self.items: list[t.Any] = items """The items on the current page. Iterating over the pagination object is equivalent to iterating over the items. """ if count: total = self._query_count() else: total = None self.total: int | None = total """The total number of items across all pages.""" @staticmethod def _prepare_page_args( *, page: int | None = None, per_page: int | None = None, max_per_page: int | None = None, error_out: bool = True, ) -> tuple[int, int]: if request: if page is None: try: page = int(request.args.get("page", 1)) except (TypeError, ValueError): if error_out: abort(404) page = 1 if per_page is None: try: per_page = int(request.args.get("per_page", 20)) except (TypeError, ValueError): if error_out: abort(404) per_page = 20 else: if page is None: page = 1 if per_page is None: per_page = 20 if max_per_page is not None: per_page = min(per_page, max_per_page) if page < 1: if error_out: abort(404) else: page = 1 if per_page < 1: if error_out: abort(404) else: per_page = 20 return page, per_page @property def _query_offset(self) -> int: """The index of the first item to query, passed to ``offset()``. :meta private: .. versionadded:: 3.0 """ return (self.page - 1) * self.per_page def _query_items(self) -> list[t.Any]: """Execute the query to get the items on the current page. Uses init arguments stored in :attr:`_query_args`. :meta private: .. versionadded:: 3.0 """ raise NotImplementedError def _query_count(self) -> int: """Execute the query to get the total number of items. Uses init arguments stored in :attr:`_query_args`. :meta private: .. versionadded:: 3.0 """ raise NotImplementedError @property def first(self) -> int: """The number of the first item on the page, starting from 1, or 0 if there are no items. .. versionadded:: 3.0 """ if len(self.items) == 0: return 0 return (self.page - 1) * self.per_page + 1 @property def last(self) -> int: """The number of the last item on the page, starting from 1, inclusive, or 0 if there are no items. .. versionadded:: 3.0 """ first = self.first return max(first, first + len(self.items) - 1) @property def pages(self) -> int: """The total number of pages.""" if self.total == 0 or self.total is None: return 0 return ceil(self.total / self.per_page) @property def has_prev(self) -> bool: """``True`` if this is not the first page.""" return self.page > 1 @property def prev_num(self) -> int | None: """The previous page number, or ``None`` if this is the first page.""" if not self.has_prev: return None return self.page - 1 def prev(self, *, error_out: bool = False) -> Pagination: """Query the :class:`Pagination` object for the previous page. :param error_out: Abort with a ``404 Not Found`` error if no items are returned and ``page`` is not 1, or if ``page`` or ``per_page`` is less than 1, or if either are not ints. """ p = type(self)( page=self.page - 1, per_page=self.per_page, error_out=error_out, count=False, **self._query_args, ) p.total = self.total return p @property def has_next(self) -> bool: """``True`` if this is not the last page.""" return self.page < self.pages @property def next_num(self) -> int | None: """The next page number, or ``None`` if this is the last page.""" if not self.has_next: return None return self.page + 1 def next(self, *, error_out: bool = False) -> Pagination: """Query the :class:`Pagination` object for the next page. :param error_out: Abort with a ``404 Not Found`` error if no items are returned and ``page`` is not 1, or if ``page`` or ``per_page`` is less than 1, or if either are not ints. """ p = type(self)( page=self.page + 1, per_page=self.per_page, error_out=error_out, count=False, **self._query_args, ) p.total = self.total return p def iter_pages( self, *, left_edge: int = 2, left_current: int = 2, right_current: int = 4, right_edge: int = 2, ) -> t.Iterator[int | None]: """Yield page numbers for a pagination widget. Skipped pages between the edges and middle are represented by a ``None``. For example, if there are 20 pages and the current page is 7, the following values are yielded. .. code-block:: python 1, 2, None, 5, 6, 7, 8, 9, 10, 11, None, 19, 20 :param left_edge: How many pages to show from the first page. :param left_current: How many pages to show left of the current page. :param right_current: How many pages to show right of the current page. :param right_edge: How many pages to show from the last page. .. versionchanged:: 3.0 Improved efficiency of calculating what to yield. .. versionchanged:: 3.0 ``right_current`` boundary is inclusive. .. versionchanged:: 3.0 All parameters are keyword-only. """ pages_end = self.pages + 1 if pages_end == 1: return left_end = min(1 + left_edge, pages_end) yield from range(1, left_end) if left_end == pages_end: return mid_start = max(left_end, self.page - left_current) mid_end = min(self.page + right_current + 1, pages_end) if mid_start - left_end > 0: yield None yield from range(mid_start, mid_end) if mid_end == pages_end: return right_start = max(mid_end, pages_end - right_edge) if right_start - mid_end > 0: yield None yield from range(right_start, pages_end) def __iter__(self) -> t.Iterator[t.Any]: yield from self.items class SelectPagination(Pagination): """Returned by :meth:`.SQLAlchemy.paginate`. Takes ``select`` and ``session`` arguments in addition to the :class:`Pagination` arguments. .. versionadded:: 3.0 """ def _query_items(self) -> list[t.Any]: select = self._query_args["select"] select = select.limit(self.per_page).offset(self._query_offset) session = self._query_args["session"] return list(session.execute(select).unique().scalars()) def _query_count(self) -> int: select = self._query_args["select"] sub = select.options(sa.orm.lazyload("*")).order_by(None).subquery() session = self._query_args["session"] out = session.execute(sa.select(sa.func.count()).select_from(sub)).scalar() return out # type: ignore[no-any-return] class QueryPagination(Pagination): """Returned by :meth:`.Query.paginate`. Takes a ``query`` argument in addition to the :class:`Pagination` arguments. .. versionadded:: 3.0 """ def _query_items(self) -> list[t.Any]: query = self._query_args["query"] out = query.limit(self.per_page).offset(self._query_offset).all() return out # type: ignore[no-any-return] def _query_count(self) -> int: # Query.count automatically disables eager loads out = self._query_args["query"].order_by(None).count() return out # type: ignore[no-any-return] flask-sqlalchemy-3.0.3/src/flask_sqlalchemy/py.typed000066400000000000000000000000001436623544000225620ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/src/flask_sqlalchemy/query.py000066400000000000000000000072501436623544000226200ustar00rootroot00000000000000from __future__ import annotations import typing as t import sqlalchemy as sa import sqlalchemy.exc import sqlalchemy.orm from flask import abort from .pagination import Pagination from .pagination import QueryPagination class Query(sa.orm.Query): # type: ignore[type-arg] """SQLAlchemy :class:`~sqlalchemy.orm.query.Query` subclass with some extra methods useful for querying in a web application. This is the default query class for :attr:`.Model.query`. .. versionchanged:: 3.0 Renamed to ``Query`` from ``BaseQuery``. """ def get_or_404(self, ident: t.Any, description: str | None = None) -> t.Any: """Like :meth:`~sqlalchemy.orm.Query.get` but aborts with a ``404 Not Found`` error instead of returning ``None``. :param ident: The primary key to query. :param description: A custom message to show on the error page. """ rv = self.get(ident) if rv is None: abort(404, description=description) return rv def first_or_404(self, description: str | None = None) -> t.Any: """Like :meth:`~sqlalchemy.orm.Query.first` but aborts with a ``404 Not Found`` error instead of returning ``None``. :param description: A custom message to show on the error page. """ rv = self.first() if rv is None: abort(404, description=description) return rv def one_or_404(self, description: str | None = None) -> t.Any: """Like :meth:`~sqlalchemy.orm.Query.one` but aborts with a ``404 Not Found`` error instead of raising ``NoResultFound`` or ``MultipleResultsFound``. :param description: A custom message to show on the error page. .. versionadded:: 3.0 """ try: return self.one() except (sa.exc.NoResultFound, sa.exc.MultipleResultsFound): abort(404, description=description) def paginate( self, *, page: int | None = None, per_page: int | None = None, max_per_page: int | None = None, error_out: bool = True, count: bool = True, ) -> Pagination: """Apply an offset and limit to the query based on the current page and number of items per page, returning a :class:`.Pagination` object. :param page: The current page, used to calculate the offset. Defaults to the ``page`` query arg during a request, or 1 otherwise. :param per_page: The maximum number of items on a page, used to calculate the offset and limit. Defaults to the ``per_page`` query arg during a request, or 20 otherwise. :param max_per_page: The maximum allowed value for ``per_page``, to limit a user-provided value. Use ``None`` for no limit. Defaults to 100. :param error_out: Abort with a ``404 Not Found`` error if no items are returned and ``page`` is not 1, or if ``page`` or ``per_page`` is less than 1, or if either are not ints. :param count: Calculate the total number of values by issuing an extra count query. For very complex queries this may be inaccurate or slow, so it can be disabled and set manually if necessary. .. versionchanged:: 3.0 All parameters are keyword-only. .. versionchanged:: 3.0 The ``count`` query is more efficient. .. versionchanged:: 3.0 ``max_per_page`` defaults to 100. """ return QueryPagination( query=self, page=page, per_page=per_page, max_per_page=max_per_page, error_out=error_out, count=count, ) flask-sqlalchemy-3.0.3/src/flask_sqlalchemy/record_queries.py000066400000000000000000000103061436623544000244620ustar00rootroot00000000000000from __future__ import annotations import dataclasses import inspect import typing as t from time import perf_counter import sqlalchemy as sa import sqlalchemy.event from flask import current_app from flask import g from flask import has_app_context def get_recorded_queries() -> list[_QueryInfo]: """Get the list of recorded query information for the current session. Queries are recorded if the config :data:`.SQLALCHEMY_RECORD_QUERIES` is enabled. Each query info object has the following attributes: ``statement`` The string of SQL generated by SQLAlchemy with parameter placeholders. ``parameters`` The parameters sent with the SQL statement. ``start_time`` / ``end_time`` Timing info about when the query started execution and when the results where returned. Accuracy and value depends on the operating system. ``duration`` The time the query took in seconds. ``location`` A string description of where in your application code the query was executed. This may not be possible to calculate, and the format is not stable. .. versionchanged:: 3.0 Renamed from ``get_debug_queries``. .. versionchanged:: 3.0 The info object is a dataclass instead of a tuple. .. versionchanged:: 3.0 The info object attribute ``context`` is renamed to ``location``. .. versionchanged:: 3.0 Not enabled automatically in debug or testing mode. """ return g.get("_sqlalchemy_queries", []) # type: ignore[no-any-return] @dataclasses.dataclass class _QueryInfo: """Information about an executed query. Returned by :func:`get_recorded_queries`. .. versionchanged:: 3.0 Renamed from ``_DebugQueryTuple``. .. versionchanged:: 3.0 Changed to a dataclass instead of a tuple. .. versionchanged:: 3.0 ``context`` is renamed to ``location``. """ statement: str | None parameters: t.Any start_time: float end_time: float location: str @property def duration(self) -> float: return self.end_time - self.start_time @property def context(self) -> str: import warnings warnings.warn( "'context' is renamed to 'location'. The old name is deprecated and will be" " removed in Flask-SQLAlchemy 3.1.", DeprecationWarning, stacklevel=2, ) return self.location def __getitem__(self, key: int) -> object: import warnings name = ("statement", "parameters", "start_time", "end_time", "location")[key] warnings.warn( "Query info is a dataclass, not a tuple. Lookup by index is deprecated and" f" will be removed in Flask-SQLAlchemy 3.1. Use 'info.{name}' instead.", DeprecationWarning, stacklevel=2, ) return getattr(self, name) def _listen(engine: sa.engine.Engine) -> None: sa.event.listen(engine, "before_cursor_execute", _record_start, named=True) sa.event.listen(engine, "after_cursor_execute", _record_end, named=True) def _record_start(context: sa.engine.ExecutionContext, **kwargs: t.Any) -> None: if not has_app_context(): return context._fsa_start_time = perf_counter() # type: ignore[attr-defined] def _record_end(context: sa.engine.ExecutionContext, **kwargs: t.Any) -> None: if not has_app_context(): return if "_sqlalchemy_queries" not in g: g._sqlalchemy_queries = [] import_top = current_app.import_name.partition(".")[0] import_dot = f"{import_top}." frame = inspect.currentframe() while frame: name = frame.f_globals.get("__name__") if name and (name == import_top or name.startswith(import_dot)): code = frame.f_code location = f"{code.co_filename}:{frame.f_lineno} ({code.co_name})" break frame = frame.f_back else: location = "" g._sqlalchemy_queries.append( _QueryInfo( statement=context.statement, parameters=context.parameters, start_time=context._fsa_start_time, # type: ignore[attr-defined] end_time=perf_counter(), location=location, ) ) flask-sqlalchemy-3.0.3/src/flask_sqlalchemy/session.py000066400000000000000000000061411436623544000231340ustar00rootroot00000000000000from __future__ import annotations import typing as t import sqlalchemy as sa import sqlalchemy.exc import sqlalchemy.orm from flask.globals import app_ctx if t.TYPE_CHECKING: from .extension import SQLAlchemy class Session(sa.orm.Session): """A SQLAlchemy :class:`~sqlalchemy.orm.Session` class that chooses what engine to use based on the bind key associated with the metadata associated with the thing being queried. To customize ``db.session``, subclass this and pass it as the ``class_`` key in the ``session_options`` to :class:`.SQLAlchemy`. .. versionchanged:: 3.0 Renamed from ``SignallingSession``. """ def __init__(self, db: SQLAlchemy, **kwargs: t.Any) -> None: super().__init__(**kwargs) self._db = db self._model_changes: dict[object, tuple[t.Any, str]] = {} def get_bind( self, mapper: t.Any | None = None, clause: t.Any | None = None, bind: sa.engine.Engine | sa.engine.Connection | None = None, **kwargs: t.Any, ) -> sa.engine.Engine | sa.engine.Connection: """Select an engine based on the ``bind_key`` of the metadata associated with the model or table being queried. If no bind key is set, uses the default bind. .. versionchanged:: 3.0.3 Fix finding the bind for a joined inheritance model. .. versionchanged:: 3.0 The implementation more closely matches the base SQLAlchemy implementation. .. versionchanged:: 2.1 Support joining an external transaction. """ if bind is not None: return bind engines = self._db.engines if mapper is not None: try: mapper = sa.inspect(mapper) except sa.exc.NoInspectionAvailable as e: if isinstance(mapper, type): raise sa.orm.exc.UnmappedClassError(mapper) from e raise engine = _clause_to_engine(mapper.local_table, engines) if engine is not None: return engine if clause is not None: engine = _clause_to_engine(clause, engines) if engine is not None: return engine if None in engines: return engines[None] return super().get_bind(mapper=mapper, clause=clause, bind=bind, **kwargs) def _clause_to_engine( clause: t.Any | None, engines: t.Mapping[str | None, sa.engine.Engine] ) -> sa.engine.Engine | None: """If the clause is a table, return the engine associated with the table's metadata's bind key. """ if isinstance(clause, sa.Table) and "bind_key" in clause.metadata.info: key = clause.metadata.info["bind_key"] if key not in engines: raise sa.exc.UnboundExecutionError( f"Bind key '{key}' is not in 'SQLALCHEMY_BINDS' config." ) return engines[key] return None def _app_ctx_id() -> int: """Get the id of the current Flask application context for the session scope.""" return id(app_ctx._get_current_object()) # type: ignore[attr-defined] flask-sqlalchemy-3.0.3/src/flask_sqlalchemy/table.py000066400000000000000000000015331436623544000225400ustar00rootroot00000000000000from __future__ import annotations import typing as t import sqlalchemy as sa import sqlalchemy.sql.schema as sa_sql_schema class _Table(sa.Table): @t.overload def __init__( self, name: str, *args: sa_sql_schema.SchemaItem, bind_key: str | None = None, **kwargs: t.Any, ) -> None: ... @t.overload def __init__( self, name: str, metadata: sa.MetaData, *args: sa_sql_schema.SchemaItem, **kwargs: t.Any, ) -> None: ... @t.overload def __init__( self, name: str, *args: sa_sql_schema.SchemaItem, **kwargs: t.Any ) -> None: ... def __init__( self, name: str, *args: sa_sql_schema.SchemaItem, **kwargs: t.Any ) -> None: super().__init__(name, *args, **kwargs) # type: ignore[arg-type] flask-sqlalchemy-3.0.3/src/flask_sqlalchemy/track_modifications.py000066400000000000000000000052551436623544000254720ustar00rootroot00000000000000from __future__ import annotations import typing as t import sqlalchemy as sa import sqlalchemy.event import sqlalchemy.orm from flask import current_app from flask import has_app_context from flask.signals import Namespace # type: ignore[attr-defined] if t.TYPE_CHECKING: from .session import Session _signals = Namespace() models_committed = _signals.signal("models-committed") """This Blinker signal is sent after the session is committed if there were changed models in the session. The sender is the application that emitted the changes. The receiver is passed the ``changes`` argument with a list of tuples in the form ``(instance, operation)``. The operations are ``"insert"``, ``"update"``, and ``"delete"``. """ before_models_committed = _signals.signal("before-models-committed") """This signal works exactly like :data:`models_committed` but is emitted before the commit takes place. """ def _listen(session: sa.orm.scoped_session[Session]) -> None: sa.event.listen(session, "before_flush", _record_ops, named=True) sa.event.listen(session, "before_commit", _record_ops, named=True) sa.event.listen(session, "before_commit", _before_commit) sa.event.listen(session, "after_commit", _after_commit) sa.event.listen(session, "after_rollback", _after_rollback) def _record_ops(session: Session, **kwargs: t.Any) -> None: if not has_app_context(): return if not current_app.config["SQLALCHEMY_TRACK_MODIFICATIONS"]: return for targets, operation in ( (session.new, "insert"), (session.dirty, "update"), (session.deleted, "delete"), ): for target in targets: state = sa.inspect(target) key = state.identity_key if state.has_identity else id(target) session._model_changes[key] = (target, operation) def _before_commit(session: Session) -> None: if not has_app_context(): return app = current_app._get_current_object() # type: ignore[attr-defined] if not app.config["SQLALCHEMY_TRACK_MODIFICATIONS"]: return if session._model_changes: changes = list(session._model_changes.values()) before_models_committed.send(app, changes=changes) def _after_commit(session: Session) -> None: if not has_app_context(): return app = current_app._get_current_object() # type: ignore[attr-defined] if not app.config["SQLALCHEMY_TRACK_MODIFICATIONS"]: return if session._model_changes: changes = list(session._model_changes.values()) models_committed.send(app, changes=changes) session._model_changes.clear() def _after_rollback(session: Session) -> None: session._model_changes.clear() flask-sqlalchemy-3.0.3/tests/000077500000000000000000000000001436623544000161265ustar00rootroot00000000000000flask-sqlalchemy-3.0.3/tests/conftest.py000066400000000000000000000017761436623544000203400ustar00rootroot00000000000000from __future__ import annotations import typing as t from pathlib import Path import pytest import sqlalchemy as sa from flask import Flask from flask.ctx import AppContext from flask_sqlalchemy import SQLAlchemy @pytest.fixture def app(request: pytest.FixtureRequest, tmp_path: Path) -> Flask: app = Flask(request.module.__name__, instance_path=str(tmp_path / "instance")) app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite://" app.config["SQLALCHEMY_RECORD_QUERIES"] = False return app @pytest.fixture def app_ctx(app: Flask) -> t.Generator[AppContext, None, None]: with app.app_context() as ctx: yield ctx @pytest.fixture def db(app: Flask) -> SQLAlchemy: return SQLAlchemy(app) @pytest.fixture def Todo(app: Flask, db: SQLAlchemy) -> t.Any: class Todo(db.Model): id = sa.Column(sa.Integer, primary_key=True) title = sa.Column(sa.String) with app.app_context(): db.create_all() yield Todo with app.app_context(): db.drop_all() flask-sqlalchemy-3.0.3/tests/test_cli.py000066400000000000000000000005551436623544000203130ustar00rootroot00000000000000from __future__ import annotations import typing as t import pytest from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy.cli import add_models_to_shell @pytest.mark.usefixtures("app_ctx") def test_shell_context(db: SQLAlchemy, Todo: t.Any) -> None: context = add_models_to_shell() assert context["db"] is db assert context["Todo"] is Todo flask-sqlalchemy-3.0.3/tests/test_engine.py000066400000000000000000000075171436623544000210160ustar00rootroot00000000000000from __future__ import annotations import os.path import unittest.mock import pytest import sqlalchemy as sa import sqlalchemy.pool from flask import Flask from flask_sqlalchemy import SQLAlchemy def test_default_engine(app: Flask, db: SQLAlchemy) -> None: with app.app_context(): assert db.engine is db.engines[None] with pytest.raises(RuntimeError): assert db.engine @pytest.mark.usefixtures("app_ctx") def test_engine_per_bind(app: Flask) -> None: app.config["SQLALCHEMY_BINDS"] = {"a": "sqlite://"} db = SQLAlchemy(app) assert db.engines["a"] is not db.engine @pytest.mark.usefixtures("app_ctx") def test_config_engine_options(app: Flask) -> None: app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {"echo": True} db = SQLAlchemy(app) assert db.engine.echo @pytest.mark.usefixtures("app_ctx") def test_init_engine_options(app: Flask) -> None: app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {"echo": False} app.config["SQLALCHEMY_BINDS"] = {"a": "sqlite://"} db = SQLAlchemy(app, engine_options={"echo": True}) # init is default assert db.engines["a"].echo # config overrides init assert not db.engine.echo @pytest.mark.usefixtures("app_ctx") def test_config_echo(app: Flask) -> None: app.config["SQLALCHEMY_ECHO"] = True db = SQLAlchemy(app) assert db.engine.echo assert db.engine.pool.echo @pytest.mark.usefixtures("app_ctx") @pytest.mark.parametrize( "value", [ "sqlite://", sa.engine.URL.create("sqlite"), {"url": "sqlite://"}, {"url": sa.engine.URL.create("sqlite")}, ], ) def test_url_type(app: Flask, value: str | sa.engine.URL) -> None: app.config["SQLALCHEMY_BINDS"] = {"a": value} db = SQLAlchemy(app) assert str(db.engines["a"].url) == "sqlite://" def test_no_binds_error(app: Flask) -> None: del app.config["SQLALCHEMY_DATABASE_URI"] with pytest.raises(RuntimeError) as info: SQLAlchemy(app) e = "Either 'SQLALCHEMY_DATABASE_URI' or 'SQLALCHEMY_BINDS' must be set." assert str(info.value) == e @pytest.mark.usefixtures("app_ctx") def test_no_default_url(app: Flask) -> None: del app.config["SQLALCHEMY_DATABASE_URI"] app.config["SQLALCHEMY_BINDS"] = {"a": "sqlite://"} db = SQLAlchemy(app, engine_options={"echo": True}) assert None not in db.engines assert "a" in db.engines @pytest.mark.usefixtures("app_ctx") def test_sqlite_relative_path(app: Flask) -> None: app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///test.db" db = SQLAlchemy(app) db.create_all() assert not isinstance(db.engine.pool, sa.pool.StaticPool) db_path = db.engine.url.database assert db_path.startswith(app.instance_path) # type: ignore[union-attr] assert os.path.exists(db_path) # type: ignore[arg-type] @pytest.mark.usefixtures("app_ctx") def test_sqlite_driver_level_uri(app: Flask) -> None: app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///file:test.db?uri=true" db = SQLAlchemy(app) db.create_all() db_path = db.engine.url.database assert db_path is not None assert db_path.startswith(f"file:{app.instance_path}") assert os.path.exists(db_path[5:]) @unittest.mock.patch.object(SQLAlchemy, "_make_engine", autospec=True) def test_sqlite_memory_defaults(make_engine: unittest.mock.Mock, app: Flask) -> None: SQLAlchemy(app) options = make_engine.call_args[0][2] assert options["poolclass"] is sa.pool.StaticPool assert options["connect_args"]["check_same_thread"] is False @unittest.mock.patch.object(SQLAlchemy, "_make_engine", autospec=True) def test_mysql_defaults(make_engine: unittest.mock.Mock, app: Flask) -> None: app.config["SQLALCHEMY_DATABASE_URI"] = "mysql:///test" SQLAlchemy(app) options = make_engine.call_args[0][2] assert options["pool_recycle"] == 7200 assert options["url"].query["charset"] == "utf8mb4" flask-sqlalchemy-3.0.3/tests/test_legacy_query.py000066400000000000000000000072051436623544000222340ustar00rootroot00000000000000from __future__ import annotations import typing as t import warnings import pytest import sqlalchemy as sa import sqlalchemy.exc from flask import Flask from werkzeug.exceptions import NotFound from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy.query import Query @pytest.fixture(autouse=True) def ignore_query_warning() -> t.Generator[None, None, None]: if hasattr(sa.exc, "LegacyAPIWarning"): with warnings.catch_warnings(): exc = sa.exc.LegacyAPIWarning warnings.simplefilter("ignore", exc) yield else: yield @pytest.mark.usefixtures("app_ctx") def test_get_or_404(db: SQLAlchemy, Todo: t.Any) -> None: item = Todo() db.session.add(item) db.session.commit() assert Todo.query.get_or_404(1) is item with pytest.raises(NotFound): Todo.query.get_or_404(2) @pytest.mark.usefixtures("app_ctx") def test_first_or_404(db: SQLAlchemy, Todo: t.Any) -> None: db.session.add(Todo(title="a")) db.session.commit() assert Todo.query.filter_by(title="a").first_or_404().title == "a" with pytest.raises(NotFound): Todo.query.filter_by(title="b").first_or_404() @pytest.mark.usefixtures("app_ctx") def test_one_or_404(db: SQLAlchemy, Todo: t.Any) -> None: db.session.add(Todo(title="a")) db.session.add(Todo(title="b")) db.session.add(Todo(title="b")) db.session.commit() assert Todo.query.filter_by(title="a").one_or_404().title == "a" with pytest.raises(NotFound): # MultipleResultsFound Todo.query.filter_by(title="b").one_or_404() with pytest.raises(NotFound): # NoResultFound Todo.query.filter_by(title="c").one_or_404() @pytest.mark.usefixtures("app_ctx") def test_paginate(db: SQLAlchemy, Todo: t.Any) -> None: db.session.add_all(Todo() for _ in range(150)) db.session.commit() p = Todo.query.paginate() assert p.total == 150 assert len(p.items) == 20 p2 = p.next() assert p2.page == 2 assert p2.total == 150 @pytest.mark.usefixtures("app_ctx") def test_default_query_class(db: SQLAlchemy) -> None: class Parent(db.Model): id = sa.Column(sa.Integer, primary_key=True) children1 = db.relationship("Child", backref="parent1", lazy="dynamic") class Child(db.Model): id = sa.Column(sa.Integer, primary_key=True) parent_id = sa.Column(sa.ForeignKey(Parent.id)) # type: ignore[var-annotated] parent2 = db.relationship( Parent, backref=db.backref("children2", lazy="dynamic", viewonly=True), viewonly=True, ) p = Parent() assert type(Parent.query) is Query assert isinstance(p.children1, Query) assert isinstance(p.children2, Query) assert isinstance(db.session.query(Child), Query) @pytest.mark.usefixtures("app_ctx") def test_custom_query_class(app: Flask) -> None: class CustomQuery(Query): pass db = SQLAlchemy(app, query_class=CustomQuery) class Parent(db.Model): id = sa.Column(sa.Integer, primary_key=True) children1 = db.relationship("Child", backref="parent1", lazy="dynamic") class Child(db.Model): id = sa.Column(sa.Integer, primary_key=True) parent_id = sa.Column(sa.ForeignKey(Parent.id)) # type: ignore[var-annotated] parent2 = db.relationship( Parent, backref=db.backref("children2", lazy="dynamic", viewonly=True), viewonly=True, ) p = Parent() assert type(Parent.query) is CustomQuery assert isinstance(p.children1, CustomQuery) assert isinstance(p.children2, CustomQuery) assert isinstance(db.session.query(Child), CustomQuery) flask-sqlalchemy-3.0.3/tests/test_metadata.py000066400000000000000000000076541436623544000213330ustar00rootroot00000000000000from __future__ import annotations import pytest import sqlalchemy as sa import sqlalchemy.exc import sqlalchemy.orm from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy.model import DefaultMeta from flask_sqlalchemy.model import Model def test_default_metadata(db: SQLAlchemy) -> None: assert db.metadata is db.metadatas[None] assert db.metadata.info["bind_key"] is None assert db.Model.metadata is db.metadata def test_custom_metadata() -> None: metadata = sa.MetaData() db = SQLAlchemy(metadata=metadata) assert db.metadata is metadata assert db.metadata.info["bind_key"] is None assert db.Model.metadata is db.metadata def test_metadata_from_custom_model() -> None: base = sa.orm.declarative_base(cls=Model, metaclass=DefaultMeta) metadata = base.metadata db = SQLAlchemy(model_class=base) assert db.Model.metadata is metadata assert db.Model.metadata is db.metadata def test_custom_metadata_overrides_custom_model() -> None: base = sa.orm.declarative_base(cls=Model, metaclass=DefaultMeta) metadata = sa.MetaData() db = SQLAlchemy(model_class=base, metadata=metadata) assert db.Model.metadata is metadata assert db.Model.metadata is db.metadata def test_metadata_per_bind(app: Flask) -> None: app.config["SQLALCHEMY_BINDS"] = {"a": "sqlite://"} db = SQLAlchemy(app) assert db.metadatas["a"] is not db.metadata assert db.metadatas["a"].info["bind_key"] == "a" def test_copy_naming_convention(app: Flask) -> None: app.config["SQLALCHEMY_BINDS"] = {"a": "sqlite://"} db = SQLAlchemy( app, metadata=sa.MetaData(naming_convention={"pk": "spk_%(table_name)s"}) ) assert db.metadata.naming_convention["pk"] == "spk_%(table_name)s" assert db.metadatas["a"].naming_convention == db.metadata.naming_convention @pytest.mark.usefixtures("app_ctx") def test_create_drop_all(app: Flask) -> None: app.config["SQLALCHEMY_BINDS"] = {"a": "sqlite://"} db = SQLAlchemy(app) class User(db.Model): id = sa.Column(sa.Integer, primary_key=True) class Post(db.Model): __bind_key__ = "a" id = sa.Column(sa.Integer, primary_key=True) with pytest.raises(sa.exc.OperationalError): db.session.execute(sa.select(User)).scalars() with pytest.raises(sa.exc.OperationalError): db.session.execute(sa.select(Post)).scalars() db.create_all() db.session.execute(sa.select(User)).scalars() db.session.execute(sa.select(Post)).scalars() db.drop_all() with pytest.raises(sa.exc.OperationalError): db.session.execute(sa.select(User)).scalars() with pytest.raises(sa.exc.OperationalError): db.session.execute(sa.select(Post)).scalars() @pytest.mark.usefixtures("app_ctx") @pytest.mark.parametrize("bind_key", ["a", ["a"]]) def test_create_key_spec(app: Flask, bind_key: str | list[str | None]) -> None: app.config["SQLALCHEMY_BINDS"] = {"a": "sqlite://"} db = SQLAlchemy(app) class User(db.Model): id = sa.Column(sa.Integer, primary_key=True) class Post(db.Model): __bind_key__ = "a" id = sa.Column(sa.Integer, primary_key=True) db.create_all(bind_key=bind_key) db.session.execute(sa.select(Post)).scalars() with pytest.raises(sa.exc.OperationalError): db.session.execute(sa.select(User)).scalars() @pytest.mark.usefixtures("app_ctx") def test_reflect(app: Flask) -> None: app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///user.db" app.config["SQLALCHEMY_BINDS"] = {"post": "sqlite:///post.db"} db = SQLAlchemy(app) db.Table("user", sa.Column("id", sa.Integer, primary_key=True)) db.Table("post", sa.Column("id", sa.Integer, primary_key=True), bind_key="post") db.create_all() del app.extensions["sqlalchemy"] db = SQLAlchemy(app) assert not db.metadata.tables db.reflect() assert "user" in db.metadata.tables assert "post" in db.metadatas["post"].tables flask-sqlalchemy-3.0.3/tests/test_model.py000066400000000000000000000031021436623544000206330ustar00rootroot00000000000000from __future__ import annotations import typing as t import pytest import sqlalchemy as sa import sqlalchemy.orm from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy.model import DefaultMeta from flask_sqlalchemy.model import Model def test_default_model_class(db: SQLAlchemy) -> None: assert db.Model.query_class is db.Query assert db.Model.metadata is db.metadata assert issubclass(db.Model, Model) assert isinstance(db.Model, DefaultMeta) def test_custom_model_class(app: Flask) -> None: class CustomModel(Model): pass db = SQLAlchemy(app, model_class=CustomModel) assert issubclass(db.Model, CustomModel) assert isinstance(db.Model, DefaultMeta) @pytest.mark.usefixtures("app_ctx") @pytest.mark.parametrize("base", [Model, object]) def test_custom_declarative_class(app: Flask, base: t.Any) -> None: class CustomMeta(DefaultMeta): pass CustomModel = sa.orm.declarative_base(cls=base, name="Model", metaclass=CustomMeta) db = SQLAlchemy(app, model_class=CustomModel) assert db.Model is CustomModel assert db.Model.query_class is db.Query assert "query" in db.Model.__dict__ @pytest.mark.usefixtures("app_ctx") def test_model_repr(db: SQLAlchemy) -> None: class User(db.Model): id = sa.Column(sa.Integer, primary_key=True) db.create_all() user = User() assert repr(user) == f"" db.session.add(user) assert repr(user) == f"" db.session.flush() assert repr(user) == f"" flask-sqlalchemy-3.0.3/tests/test_model_bind.py000066400000000000000000000050231436623544000216330ustar00rootroot00000000000000from __future__ import annotations import sqlalchemy as sa from flask_sqlalchemy import SQLAlchemy def test_bind_key_default(db: SQLAlchemy) -> None: class User(db.Model): id = sa.Column(sa.Integer, primary_key=True) assert User.metadata is db.metadata def test_metadata_per_bind(db: SQLAlchemy) -> None: class User(db.Model): __bind_key__ = "other" id = sa.Column(sa.Integer, primary_key=True) assert User.metadata is db.metadatas["other"] def test_multiple_binds_same_table_name(db: SQLAlchemy) -> None: class UserA(db.Model): __tablename__ = "user" id = sa.Column(sa.Integer, primary_key=True) class UserB(db.Model): __bind_key__ = "other" __tablename__ = "user" id = sa.Column(sa.Integer, primary_key=True) assert UserA.metadata is db.metadata assert UserB.metadata is db.metadatas["other"] assert UserA.__table__.metadata is not UserB.__table__.metadata def test_inherit_parent(db: SQLAlchemy) -> None: class User(db.Model): __bind_key__ = "auth" id = sa.Column(sa.Integer, primary_key=True) type = sa.Column(sa.String) __mapper_args__ = {"polymorphic_on": type, "polymorphic_identity": "user"} class Admin(User): id = sa.Column(sa.Integer, sa.ForeignKey(User.id), primary_key=True) __mapper_args__ = {"polymorphic_identity": "admin"} assert "admin" in db.metadatas["auth"].tables # inherits metadata, doesn't set it directly assert "metadata" not in Admin.__dict__ def test_inherit_abstract_parent(db: SQLAlchemy) -> None: class AbstractUser(db.Model): __abstract__ = True __bind_key__ = "auth" class User(AbstractUser): id = sa.Column(sa.Integer, primary_key=True) assert "user" in db.metadatas["auth"].tables assert "metadata" not in User.__dict__ def test_explicit_metadata(db: SQLAlchemy) -> None: other_metadata = sa.MetaData() class User(db.Model): __bind_key__ = "other" metadata = other_metadata id = sa.Column(sa.Integer, primary_key=True) assert User.__table__.metadata is other_metadata assert "other" not in db.metadatas def test_explicit_table(db: SQLAlchemy) -> None: user_table = db.Table( "user", sa.Column("id", sa.Integer, primary_key=True), bind_key="auth", ) class User(db.Model): __bind_key__ = "other" __table__ = user_table assert User.__table__.metadata is db.metadatas["auth"] assert "other" not in db.metadatas flask-sqlalchemy-3.0.3/tests/test_model_name.py000066400000000000000000000171351436623544000216460ustar00rootroot00000000000000from __future__ import annotations import inspect import typing as t import pytest import sqlalchemy as sa import sqlalchemy.exc import sqlalchemy.orm from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy.model import camel_to_snake_case @pytest.mark.parametrize( ("name", "expect"), [ ("CamelCase", "camel_case"), ("Snake_case", "snake_case"), ("HTMLLayout", "html_layout"), ("LayoutHTML", "layout_html"), ("HTTP2Request", "http2_request"), ("ShoppingCartSession", "shopping_cart_session"), ("ABC", "abc"), ("PreABC", "pre_abc"), ("ABCPost", "abc_post"), ("PreABCPost", "pre_abc_post"), ("HTTP2RequestSession", "http2_request_session"), ("UserST4", "user_st4"), ( "HTTP2ClientType3EncoderParametersSSE", "http2_client_type3_encoder_parameters_sse", ), ( "LONGName4TestingCamelCase2snake_caseXYZ", "long_name4_testing_camel_case2snake_case_xyz", ), ("FooBarSSE2", "foo_bar_sse2"), ("AlarmMessageSS2SignalTransformer", "alarm_message_ss2_signal_transformer"), ("AstV2Node", "ast_v2_node"), ("HTTPResponseCodeXYZ", "http_response_code_xyz"), ("get2HTTPResponse123Code", "get2_http_response123_code"), # ("getHTTPresponseCode", "get_htt_presponse_code"), # ("__test__Method", "test___method"), ], ) def test_camel_to_snake_case(name: str, expect: str) -> None: assert camel_to_snake_case(name) == expect def test_name(db: SQLAlchemy) -> None: class FOOBar(db.Model): id = sa.Column(sa.Integer, primary_key=True) class BazBar(db.Model): id = sa.Column(sa.Integer, primary_key=True) class Ham(db.Model): __tablename__ = "spam" id = sa.Column(sa.Integer, primary_key=True) assert FOOBar.__tablename__ == "foo_bar" assert BazBar.__tablename__ == "baz_bar" assert Ham.__tablename__ == "spam" def test_single_name(db: SQLAlchemy) -> None: """Single table inheritance should not set a new name.""" class Duck(db.Model): id = sa.Column(sa.Integer, primary_key=True) class Mallard(Duck): pass assert "__tablename__" not in Mallard.__dict__ assert Mallard.__tablename__ == "duck" def test_joined_name(db: SQLAlchemy) -> None: """Model has a separate primary key; it should set a new name.""" class Duck(db.Model): id = sa.Column(sa.Integer, primary_key=True) class Donald(Duck): id = sa.Column(sa.Integer, sa.ForeignKey(Duck.id), primary_key=True) assert Donald.__tablename__ == "donald" def test_mixin_id(db: SQLAlchemy) -> None: """Primary key provided by mixin should still allow model to set tablename. """ class Base: id = sa.Column(sa.Integer, primary_key=True) class Duck(Base, db.Model): pass assert not hasattr(Base, "__tablename__") assert Duck.__tablename__ == "duck" def test_mixin_attr(db: SQLAlchemy) -> None: """A declared attr tablename will be used down multiple levels of inheritance. """ class Mixin: @sa.orm.declared_attr # type: ignore[arg-type] def __tablename__(cls) -> str: # noqa: B902 return cls.__name__.upper() # type: ignore[attr-defined,no-any-return] class Bird(Mixin, db.Model): id = sa.Column(sa.Integer, primary_key=True) class Duck(Bird): # object reference id = sa.Column(sa.Integer, sa.ForeignKey(Bird.id), primary_key=True) class Mallard(Duck): # string reference id = sa.Column(sa.Integer, sa.ForeignKey("DUCK.id"), primary_key=True) assert Bird.__tablename__ == "BIRD" assert Duck.__tablename__ == "DUCK" assert Mallard.__tablename__ == "MALLARD" def test_abstract_name(db: SQLAlchemy) -> None: """Abstract model should not set a name. Subclass should set a name.""" class Base(db.Model): __abstract__ = True id = sa.Column(sa.Integer, primary_key=True) class Duck(Base): pass assert "__tablename__" not in Base.__dict__ assert Duck.__tablename__ == "duck" def test_complex_inheritance(db: SQLAlchemy) -> None: """Joined table inheritance, but the new primary key is provided by a mixin, not directly on the class. """ class Duck(db.Model): id = sa.Column(sa.Integer, primary_key=True) class IdMixin: @sa.orm.declared_attr def id(cls): # type: ignore[no-untyped-def] # noqa: B902 return sa.Column(sa.Integer, sa.ForeignKey(Duck.id), primary_key=True) class RubberDuck(IdMixin, Duck): # type: ignore[misc] pass assert RubberDuck.__tablename__ == "rubber_duck" def test_manual_name(db: SQLAlchemy) -> None: """Setting a manual name prevents generation for the immediate model. A name is generated for joined but not single-table inheritance. """ class Duck(db.Model): __tablename__ = "DUCK" id = sa.Column(sa.Integer, primary_key=True) type = sa.Column(sa.String) __mapper_args__ = {"polymorphic_on": type} class Daffy(Duck): id = sa.Column(sa.Integer, sa.ForeignKey(Duck.id), primary_key=True) __mapper_args__ = {"polymorphic_identity": "Tower"} # type: ignore[dict-item] class Donald(Duck): __mapper_args__ = {"polymorphic_identity": "Mouse"} # type: ignore[dict-item] assert Duck.__tablename__ == "DUCK" assert Daffy.__tablename__ == "daffy" assert "__tablename__" not in Donald.__dict__ assert Donald.__tablename__ == "DUCK" def test_primary_constraint(db: SQLAlchemy) -> None: """Primary key will be picked up from table args.""" class Duck(db.Model): id = sa.Column(sa.Integer) __table_args__ = (sa.PrimaryKeyConstraint(id),) assert Duck.__table__ is not None assert Duck.__tablename__ == "duck" def test_no_access_to_class_property(db: SQLAlchemy) -> None: """Ensure the implementation doesn't access class properties or declared attrs while inspecting the unmapped model. """ class class_property: def __init__(self, f: t.Callable[..., t.Any]) -> None: self.f = f def __get__(self, instance: t.Any, owner: type[t.Any]) -> t.Any: return self.f(owner) class Duck(db.Model): id = sa.Column(sa.Integer, primary_key=True) class ns: is_duck = False floats = False class Witch(Duck): @sa.orm.declared_attr # type: ignore[arg-type] def is_duck(self) -> None: # declared attrs will be accessed during mapper configuration, # but make sure they're not accessed before that info = inspect.getouterframes(inspect.currentframe())[2] assert info[3] != "_should_set_tablename" ns.is_duck = True @class_property def floats(self) -> None: ns.floats = True assert ns.is_duck assert not ns.floats def test_metadata_has_table(db: SQLAlchemy) -> None: user = db.Table("user", sa.Column("id", sa.Integer, primary_key=True)) class User(db.Model): pass assert User.__table__ is user def test_correct_error_for_no_primary_key(db: SQLAlchemy) -> None: with pytest.raises(sa.exc.ArgumentError) as info: class User(db.Model): pass assert "could not assemble any primary key" in str(info.value) def test_single_has_parent_table(db: SQLAlchemy) -> None: class Duck(db.Model): id = sa.Column(sa.Integer, primary_key=True) class Call(Duck): pass assert Call.__table__ is Duck.__table__ assert "__table__" not in Call.__dict__ flask-sqlalchemy-3.0.3/tests/test_pagination.py000066400000000000000000000123541436623544000216750ustar00rootroot00000000000000from __future__ import annotations import typing as t import pytest from flask import Flask from werkzeug.exceptions import NotFound from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy.pagination import Pagination class RangePagination(Pagination): def __init__( self, total: int | None = 150, page: int = 1, per_page: int = 10 ) -> None: if total is None: self._data = range(150) else: self._data = range(total) super().__init__(total=total, page=page, per_page=per_page) if total is None: self.total = None def _query_items(self) -> list[t.Any]: first = self._query_offset last = first + self.per_page + 1 return list(self._data[first:last]) def _query_count(self) -> int: return len(self._data) def test_first_page() -> None: p = RangePagination() assert p.page == 1 assert p.per_page == 10 assert p.total == 150 assert p.pages == 15 assert not p.has_prev assert p.prev_num is None assert p.has_next assert p.next_num == 2 def test_last_page() -> None: p = RangePagination(page=15) assert p.page == 15 assert p.has_prev assert p.prev_num == 14 assert not p.has_next assert p.next_num is None def test_item_numbers_first_page() -> None: p = RangePagination() p.items = list(range(10)) assert p.first == 1 assert p.last == 10 def test_item_numbers_last_page() -> None: p = RangePagination(page=15) p.items = list(range(5)) assert p.first == 141 assert p.last == 145 def test_item_numbers_0() -> None: p = RangePagination(total=0) assert p.first == 0 assert p.last == 0 @pytest.mark.parametrize("total", [0, None]) def test_0_pages(total: int | None) -> None: p = RangePagination(total=total) assert p.pages == 0 assert not p.has_prev assert not p.has_next @pytest.mark.parametrize( ("page", "expect"), [ (1, [1, 2, 3, 4, 5, None, 14, 15]), (2, [1, 2, 3, 4, 5, 6, None, 14, 15]), (3, [1, 2, 3, 4, 5, 6, 7, None, 14, 15]), (4, [1, 2, 3, 4, 5, 6, 7, 8, None, 14, 15]), (5, [1, 2, 3, 4, 5, 6, 7, 8, 9, None, 14, 15]), (6, [1, 2, None, 4, 5, 6, 7, 8, 9, 10, None, 14, 15]), (7, [1, 2, None, 5, 6, 7, 8, 9, 10, 11, None, 14, 15]), (8, [1, 2, None, 6, 7, 8, 9, 10, 11, 12, None, 14, 15]), (9, [1, 2, None, 7, 8, 9, 10, 11, 12, 13, 14, 15]), (10, [1, 2, None, 8, 9, 10, 11, 12, 13, 14, 15]), (11, [1, 2, None, 9, 10, 11, 12, 13, 14, 15]), (12, [1, 2, None, 10, 11, 12, 13, 14, 15]), (13, [1, 2, None, 11, 12, 13, 14, 15]), (14, [1, 2, None, 12, 13, 14, 15]), (15, [1, 2, None, 13, 14, 15]), ], ) def test_iter_pages(page: int, expect: list[int | None]) -> None: p = RangePagination(page=page) assert list(p.iter_pages()) == expect def test_iter_0_pages() -> None: p = RangePagination(total=0) assert list(p.iter_pages()) == [] @pytest.mark.parametrize("page", [1, 2, 3, 4]) def test_iter_pages_short(page: int) -> None: p = RangePagination(page=page, total=40) assert list(p.iter_pages()) == [1, 2, 3, 4] class _PaginateCallable: def __init__(self, app: Flask, db: SQLAlchemy, Todo: t.Any) -> None: self.app = app self.db = db self.Todo = Todo def __call__( self, page: int | None = None, per_page: int | None = None, max_per_page: int | None = None, error_out: bool = True, count: bool = True, ) -> Pagination: qs = {"page": page, "per_page": per_page} with self.app.test_request_context(query_string=qs): return self.db.paginate( self.db.select(self.Todo), max_per_page=max_per_page, error_out=error_out, count=count, ) @pytest.fixture def paginate(app: Flask, db: SQLAlchemy, Todo: t.Any) -> _PaginateCallable: with app.app_context(): for i in range(1, 101): db.session.add(Todo(title=f"task {i}")) db.session.commit() return _PaginateCallable(app, db, Todo) def test_paginate(paginate: _PaginateCallable) -> None: p = paginate() assert p.page == 1 assert p.per_page == 20 assert len(p.items) == 20 assert p.total == 100 assert p.pages == 5 def test_paginate_qs(paginate: _PaginateCallable) -> None: p = paginate(page=2, per_page=10) assert p.page == 2 assert p.per_page == 10 def test_paginate_max(paginate: _PaginateCallable) -> None: p = paginate(per_page=100, max_per_page=50) assert p.per_page == 50 def test_no_count(paginate: _PaginateCallable) -> None: p = paginate(count=False) assert p.total is None @pytest.mark.parametrize( ("page", "per_page"), [("abc", None), (None, "abc"), (0, None), (None, -1)] ) def test_error_out(paginate: _PaginateCallable, page: t.Any, per_page: t.Any) -> None: with pytest.raises(NotFound): paginate(page=page, per_page=per_page) @pytest.mark.usefixtures("app_ctx") def test_no_items_404(db: SQLAlchemy, Todo: t.Any) -> None: p = db.paginate(db.select(Todo)) assert len(p.items) == 0 with pytest.raises(NotFound): db.paginate(db.select(Todo), page=2) flask-sqlalchemy-3.0.3/tests/test_record_queries.py000066400000000000000000000016071436623544000225560ustar00rootroot00000000000000from __future__ import annotations import pytest import sqlalchemy as sa from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy.record_queries import get_recorded_queries @pytest.mark.usefixtures("app_ctx") def test_query_info(app: Flask) -> None: app.config["SQLALCHEMY_RECORD_QUERIES"] = True db = SQLAlchemy(app) class Example(db.Model): id = sa.Column(sa.Integer, primary_key=True) db.create_all() db.session.execute(sa.select(Example).filter(Example.id < 5)).scalars() info = get_recorded_queries()[-1] assert info.statement is not None assert "SELECT" in info.statement assert "FROM example" in info.statement assert info.parameters[0][0] == 5 assert info.duration == info.end_time - info.start_time assert "tests/test_record_queries.py:" in info.location assert "(test_query_info)" in info.location flask-sqlalchemy-3.0.3/tests/test_session.py000066400000000000000000000047331436623544000212310ustar00rootroot00000000000000from __future__ import annotations import pytest import sqlalchemy as sa from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy.session import Session def test_scope(app: Flask, db: SQLAlchemy) -> None: with pytest.raises(RuntimeError): db.session() with app.app_context(): first = db.session() second = db.session() assert first is second assert isinstance(first, Session) with app.app_context(): third = db.session() assert first is not third def test_custom_scope(app: Flask) -> None: count = 0 def scope() -> int: nonlocal count count += 1 return count db = SQLAlchemy(app, session_options={"scopefunc": scope}) with app.app_context(): first = db.session() second = db.session() assert first is not second # a new scope is generated on each call first.close() second.close() @pytest.mark.usefixtures("app_ctx") def test_session_class(app: Flask) -> None: class CustomSession(Session): pass db = SQLAlchemy(app, session_options={"class_": CustomSession}) assert isinstance(db.session(), CustomSession) @pytest.mark.usefixtures("app_ctx") def test_session_uses_bind_key(app: Flask) -> None: app.config["SQLALCHEMY_BINDS"] = {"a": "sqlite://"} db = SQLAlchemy(app) class User(db.Model): id = sa.Column(sa.Integer, primary_key=True) class Post(db.Model): __bind_key__ = "a" id = sa.Column(sa.Integer, primary_key=True) assert db.session.get_bind(mapper=User) is db.engine assert db.session.get_bind(mapper=Post) is db.engines["a"] @pytest.mark.usefixtures("app_ctx") def test_get_bind_inheritance(app: Flask) -> None: app.config["SQLALCHEMY_BINDS"] = {"a": "sqlite://"} db = SQLAlchemy(app) class User(db.Model): __bind_key__ = "a" id = sa.Column(sa.Integer, primary_key=True) type = sa.Column(sa.String, nullable=False) __mapper_args__ = {"polymorphic_on": type, "polymorphic_identity": "user"} class Admin(User): id = sa.Column(sa.ForeignKey(User.id), primary_key=True) org = sa.Column(sa.String, nullable=False) __mapper_args__ = {"polymorphic_identity": "admin"} db.create_all() db.session.add(Admin(org="pallets")) db.session.commit() admin = db.session.execute(db.select(Admin)).scalar_one() db.session.expire(admin) assert admin.org == "pallets" flask-sqlalchemy-3.0.3/tests/test_table_bind.py000066400000000000000000000022631436623544000216250ustar00rootroot00000000000000from __future__ import annotations import sqlalchemy as sa from flask_sqlalchemy import SQLAlchemy def test_bind_key_default(db: SQLAlchemy) -> None: user_table = db.Table("user", sa.Column("id", sa.Integer, primary_key=True)) assert user_table.metadata is db.metadata def test_metadata_per_bind(db: SQLAlchemy) -> None: user_table = db.Table( "user", sa.Column("id", sa.Integer, primary_key=True), bind_key="other" ) assert user_table.metadata is db.metadatas["other"] def test_multiple_binds_same_table_name(db: SQLAlchemy) -> None: user1_table = db.Table("user", sa.Column("id", sa.Integer, primary_key=True)) user2_table = db.Table( "user", sa.Column("id", sa.Integer, primary_key=True), bind_key="other" ) assert user1_table.metadata is db.metadata assert user2_table.metadata is db.metadatas["other"] def test_explicit_metadata(db: SQLAlchemy) -> None: other_metadata = sa.MetaData() user_table = db.Table( "user", other_metadata, sa.Column("id", sa.Integer, primary_key=True), bind_key="other", ) assert user_table.metadata is other_metadata assert "other" not in db.metadatas flask-sqlalchemy-3.0.3/tests/test_track_modifications.py000066400000000000000000000036561436623544000235650ustar00rootroot00000000000000from __future__ import annotations import typing as t import pytest import sqlalchemy as sa from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy.track_modifications import before_models_committed from flask_sqlalchemy.track_modifications import models_committed pytest.importorskip("blinker") @pytest.mark.usefixtures("app_ctx") def test_track_modifications(app: Flask) -> None: app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True db = SQLAlchemy(app) class Example(db.Model): id = sa.Column(sa.Integer, primary_key=True) data = sa.Column(sa.String) db.create_all() before: list[tuple[t.Any, str]] = [] after: list[tuple[t.Any, str]] = [] def before_commit(sender: Flask, changes: list[tuple[t.Any, str]]) -> None: nonlocal before before = changes def after_commit(sender: Flask, changes: list[tuple[t.Any, str]]) -> None: nonlocal after after = changes connect_before = before_models_committed.connected_to(before_commit, app) connect_after = models_committed.connected_to(after_commit, app) with connect_before, connect_after: item = Example() db.session.add(item) assert not before assert not after db.session.commit() assert len(before) == 1 assert before[0] == (item, "insert") assert before == after db.session.remove() item = db.session.get(Example, 1) # type: ignore[assignment] item.data = "test" # type: ignore[assignment] db.session.commit() assert len(before) == 1 assert before[0] == (item, "update") assert before == after db.session.remove() item = db.session.get(Example, 1) # type: ignore[assignment] db.session.delete(item) db.session.commit() assert len(before) == 1 assert before[0] == (item, "delete") assert before == after flask-sqlalchemy-3.0.3/tox.ini000066400000000000000000000012131436623544000162740ustar00rootroot00000000000000[tox] envlist = py3{11,10,9,8,7} pypy39 py311-lowest style typing docs skip_missing_interpreters = true [testenv] package = wheel wheel_build_env = .pkg groups = tests deps = lowest: flask==2.2 lowest: sqlalchemy==1.4.18 commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs} [testenv:style] groups = pre-commit skip_install = true commands = pre-commit run --all-files [testenv:typing] package = wheel wheel_build_env = .pkg groups = mypy commands = mypy [testenv:docs] package = wheel wheel_build_env = .pkg groups = docs commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html