pax_global_header00006660000000000000000000000064150441534400014512gustar00rootroot0000000000000052 comment=bcbe6941fc5e82396e7fcb695d4bded5dff13afd pyinvoke-invocations-ea947a4/000077500000000000000000000000001504415344000163305ustar00rootroot00000000000000pyinvoke-invocations-ea947a4/.circleci/000077500000000000000000000000001504415344000201635ustar00rootroot00000000000000pyinvoke-invocations-ea947a4/.circleci/config.yml000066400000000000000000000012571504415344000221600ustar00rootroot00000000000000version: 2.1 orbs: orb: invocations/orb@1.5.0 workflows: main: jobs: - orb/lint: name: Lint - orb/format: name: Style check - orb/coverage: name: Test - orb/test-release: name: Release test - orb/test: name: Test << matrix.version >> # It's not worth testing on other interpreters if the baseline one # failed. Can't run >4 jobs at a time anyhow! requires: ["Test"] matrix: parameters: version: ["3.10", "3.11", "3.12", "3.13"] - orb/docs: name: "Docs" requires: ["Test"] task: "docs --nitpick" pyinvoke-invocations-ea947a4/.codecov.yml000066400000000000000000000000501504415344000205460ustar00rootroot00000000000000comment: false coverage: precision: 0 pyinvoke-invocations-ea947a4/.coveragerc000066400000000000000000000000741504415344000204520ustar00rootroot00000000000000[run] branch = True include = invocations/* tests/* pyinvoke-invocations-ea947a4/.flake8000066400000000000000000000001661504415344000175060ustar00rootroot00000000000000[flake8] exclude = .git,build,dist,.cci_pycache ignore = E124,E125,E128,E261,E301,E302,E303,W503 max-line-length = 79 pyinvoke-invocations-ea947a4/.gitignore000066400000000000000000000000771504415344000203240ustar00rootroot00000000000000build .coverage htmlcov .cache docs/_build coverage.xml _build pyinvoke-invocations-ea947a4/LICENSE000066400000000000000000000024421504415344000173370ustar00rootroot00000000000000Copyright (c) 2020 Jeff Forcier. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 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. 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. pyinvoke-invocations-ea947a4/MANIFEST.in000066400000000000000000000003201504415344000200610ustar00rootroot00000000000000include LICENSE include README.rst include pyproject.toml recursive-include docs * recursive-exclude docs/_build * recursive-include tests * recursive-exclude * *.pyc *.pyo recursive-exclude **/__pycache__ * pyinvoke-invocations-ea947a4/README.rst000066400000000000000000000044371504415344000200270ustar00rootroot00000000000000|version| |python| |license| |ci| |coverage| .. |version| image:: https://img.shields.io/pypi/v/invocations :target: https://pypi.org/project/invocations/ :alt: PyPI - Package Version .. |python| image:: https://img.shields.io/pypi/pyversions/invocations :target: https://pypi.org/project/invocations/ :alt: PyPI - Python Version .. |license| image:: https://img.shields.io/pypi/l/invocations :target: https://github.com/pyinvoke/invocations/blob/main/LICENSE :alt: PyPI - License .. |ci| image:: https://img.shields.io/circleci/build/github/pyinvoke/invocations/main :target: https://app.circleci.com/pipelines/github/pyinvoke/invocations :alt: CircleCI .. |coverage| image:: https://img.shields.io/codecov/c/gh/pyinvoke/invocations :target: https://app.codecov.io/gh/pyinvoke/invocations :alt: Codecov What is this? ============= Invocations is a collection of reusable `Invoke `_ tasks, task collections and helper functions. Originally sourced from the Invoke project's own project-management tasks file, they are now highly configurable and used across a number of projects, with the intent to become a clearinghouse for implementing common best practices. Currently implemented topics include (but are not limited to): - management of Sphinx documentation trees - Python project release lifecycles - dependency vendoring - running test suites (unit, integration, coverage-oriented, etc) - console utilities such as confirmation prompts and more. Roadmap ======= While Invocations has been released with a major version number to signal adherence to semantic versioning, it's somewhat early in development and has not fully achieved its design vision yet. We expect it to gain maturity in tandem with the adoption and development of Invoke post-1.x. It's also highly likely that Invocations will see a few major releases as its API (and those of its sister library, `patchwork `_) matures. For a high level roadmap re: when Invocations will get significant updates, see the maintainer's `roadmap page `_. Development =========== This project uses the same dev methodology as Invoke proper - please see its development page `here `_. pyinvoke-invocations-ea947a4/SECURITY.md000066400000000000000000000003011504415344000201130ustar00rootroot00000000000000## Security contact information To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. pyinvoke-invocations-ea947a4/docs/000077500000000000000000000000001504415344000172605ustar00rootroot00000000000000pyinvoke-invocations-ea947a4/docs/api/000077500000000000000000000000001504415344000200315ustar00rootroot00000000000000pyinvoke-invocations-ea947a4/docs/api/autodoc.rst000066400000000000000000000001111504415344000222120ustar00rootroot00000000000000=========== ``autodoc`` =========== .. automodule:: invocations.autodoc pyinvoke-invocations-ea947a4/docs/api/ci.rst000066400000000000000000000000651504415344000211570ustar00rootroot00000000000000====== ``ci`` ====== .. automodule:: invocations.ci pyinvoke-invocations-ea947a4/docs/api/console.rst000066400000000000000000000001111504415344000222160ustar00rootroot00000000000000=========== ``console`` =========== .. automodule:: invocations.console pyinvoke-invocations-ea947a4/docs/api/docs.rst000066400000000000000000000000751504415344000215150ustar00rootroot00000000000000======== ``docs`` ======== .. automodule:: invocations.docs pyinvoke-invocations-ea947a4/docs/api/environment.rst000066400000000000000000000001311504415344000231220ustar00rootroot00000000000000=============== ``environment`` =============== .. automodule:: invocations.environment pyinvoke-invocations-ea947a4/docs/api/packaging.rst000066400000000000000000000003511504415344000225060ustar00rootroot00000000000000============= ``packaging`` ============= ``packaging.release`` ===================== .. automodule:: invocations.packaging.release ``packaging.vendorize`` ======================= .. automodule:: invocations.packaging.vendorize pyinvoke-invocations-ea947a4/docs/api/pytest.rst000066400000000000000000000001051504415344000221070ustar00rootroot00000000000000========== ``pytest`` ========== .. automodule:: invocations.pytest pyinvoke-invocations-ea947a4/docs/changelog.rst000066400000000000000000000346711504415344000217540ustar00rootroot00000000000000========= Changelog ========= - :release:`4.0.2 <2025-08-04>` - :support`- backported` Add ``pip`` explicitly to our core dependencies so that envs which don't naturally include it (a thing these days!) still install it. We do literally import from pip as well as call out to it in subprocesses, at least for now. - :release:`4.0.1 <2025-08-03>` - :bug:`-` Fix some Python 3.10-specific type hint syntax that snuck in. Apologies from our dayjob, which is on Python 3.11. - :bug:`-` Use ``$EDITOR pyproject.toml`` for version number updates now; overlooked in earlier commits. - :release:`4.0.0 <2025-08-03>` - :support:`-` Add a ``__version__`` member to the package root module. - :support:`-` Drop support for Python<3.9 and switch to using pyproject.toml. - :support:`-` Update ``packaging.release.build`` to use ``pypa/build`` instead of ``setup.py``. .. warning:: Backwards compatibility note: the ``--directory`` argument to tasks in this module is now ~= ``python -m build --outdir``, meaning it controls what is usually named ``dist/``. Previously, this option controlled a parent directory, *inside of which* was created ``dist/`` and ``build/``. There's no longer any explicit ``build/`` (that would be up to your project's build backend) and so this is now simply the dist/output dir. - :support:`-` Mute SyntaxWarnings from older ``semantic_version`` versions (temporary measure until we can upgrade to modern versions). - :release:`3.3.0 <2023-05-12>` - :feature:`-` Add mypy type-checking variant of the recently added import test, in ``packaging.release.test_install``. This helps prove packages exposing ``py.typed`` in their source tree are including it in their distributions correctly. - :release:`3.2.0 <2023-05-11>` - :feature:`-` Minor enhancements to the ``checks`` module: - ``blacken`` now has a ``format`` alias (and will likely reverse the real name and the alias in 4.0) - Added ``lint`` task which currently just runs ``flake8``, will likely learn how to be configurable later. - Added ``all_`` default task for the collection, which runs both ``blacken`` (in regular, not diff-only mode - idea is to be useful for devs, not CI, which already does both independently) and ``lint`` in series. - :release:`3.1.0 <2023-05-02>` - :feature:`-` Updated ``packaging.release.test_install`` to attempt imports of freshly test-installed packages, to catch import-time errors on top of install-time ones. This can be opted out of by giving the ``skip_import`` kwarg (aka the ``--skip-import`` flag on the CLI). - :release:`3.0.2 <2023-04-28>` - :support:`- backported` Unpin ``tabulate`` in our install requirements, it's had many more releases since we instituted a defensive pin vs some bugs in its later 0.7 line! - :release:`3.0.1 <2023-01-06>` - :bug:`-` We neglected to remove references to ``six`` in a few spots - including some that utilized Invoke's old vendor of same; this causes issues when trying to use development and upcoming versions of Invoke. Six is now truly gone! - :release:`3.0.0 <2022-12-31>` - :support:`-` Various fixes and doc updates re: the `~invocations.autodoc` module's compatibility with modern Sphinx versions. - :support:`-` The ``dual_wheels``, ``alt_python``, and ``check_desc`` arguments/config options for the ``invocations.packaging.release`` module have been removed. .. warning:: This is a backwards-incompatible change. .. note:: If you were using ``check_desc``, note that the release tasks have been using ``twine check`` for a few releases now, as a default part of execution, and will continue doing so; ``check_desc`` only impacted the use of the older ``setup.py check`` command. - :support:`-` The ``invocations.travis`` module has been removed. If you relied upon it, we may accept PRs to make the newer ``invocations.ci`` module more generic. .. warning:: This is a backwards-incompatible change. - :support:`-` Drop Python 2 (and 3.5) support. We now support Python 3.6+ only. This naturally includes a number of dependency updates (direct and indirect) as well. .. warning:: This is a backwards-incompatible change. - :release:`2.6.1 <2022-06-26>` - :support:`- backported` Remove upper bounds pinning on many deps; this makes it easier for related projects to test upgrades, run CI, etc. In general, we're moving away from this tactic. - :release:`2.6.0 <2022-03-25>` - :feature:`-` Enhance ``packaging.release.test-install`` so it's more flexible about the primary directory argument (re: a ``dist`` dir, or a parent of one) and errors usefully when you (probably) gave it an incorrect path. - :feature:`-` Update ``packaging.release.publish`` with a new config option, ``rebuild_with_env``, to support a downstream (Fabric) release use-case. - :release:`2.5.0 <2022-03-25>` - :feature:`-` Port ``make-sshable`` from the ``travis`` module to the new ``ci`` one. - :release:`2.4.0 <2022-03-17>` - :feature:`-` Allow supplying additional test runners to ``pytest.coverage``; primarily useful for setting up multiple additive test runs before publishing reports. - :feature:`-` Add a new `invocations.ci` task module for somewhat-more-generic CI support than the now legacy ``invocations.travis`` tasks. - :feature:`-` Add additional CLI flags to the use of ``gpg`` when signing releases, to support headless passphrase entry. It was found that modern GPG versions require ``--batch`` and ``--pinentry-mode=loopback`` for ``--passphrase-fd`` to function correctly. - :release:`2.3.0 <2021-09-24>` - :bug:`- major` Ensure that the venv used for ``packaging.release.test_install`` has its ``pip`` upgraded to match the invoking interpreter's version of same; this avoids common pitfalls where the "inner" pip is a bundled-with-venv, much-older version incapable of modern package installations. - :support:`-` Overhaul testing and release procedures to use CircleCI & modern Invocations. - :bug:`- major` The ``packaging.release.upload`` task wasn't properly exposed externally, even though another task's docstring referenced it. Fixed. - :release:`2.2.0 <2021-09-03>` - :bug:`- major` ``packaging.release.status`` (and its use elsewhere, eg ``prepare``) didn't adequately reload the local project's version module during its second/final recheck; this causes that check to fail when said version was edited as part of a ``prepare`` run. It now force-reloads said version module. - :feature:`-` ``packaging.release.push``, in dry-run mode, now dry-runs its ``git push`` subcommand -- meaning the subcommand itself is what is "dry-ran", instead of truly executing ``git push --dry-run`` -- when a CI environment is detected. - This prevents spurious errors when the git remote (eg Github) bails out on read-only authentication credentials, which is common within CI systems. - It's also just not very useful to dry-run a real git push within CI, since almost certainly the commands to generate git objects to get pushed will themselves not have truly run! - :feature:`-` Added the ``invocations.environment`` module with top-level functions such as `~invocations.environment.in_ci`. - :release:`2.1.0 <2021-08-27>` - :feature:`-` Add ``packaging.release.test_install`` task and call it just prior to the final step in ``packaging.release.upload`` (so one doesn't upload packages which build OK but don't actually install OK). - :feature:`-` Add Codecov support to ``pytest.coverage``. - :support:`-` Rely on Invoke 1.6+ for some of its new features. - :support:`-` ``packaging.release.prepare`` now runs its internal status check twice, once at the start (as before) and again at the end (to prove that the actions taken did in fact satisfy needs). - :feature:`-` ``packaging.release.prepare`` grew a ``dry_run`` flag to match the rest of its friends. - :bug:`- major` ``packaging.release.prepare`` now generates annotated Git tags instead of lightweight ones. This was a perplexing oversight (Git has always intended annotated tags to be used for release purposes) so we're considering it a bugfix instead of a backwards incompatible feature change. - :feature:`-` The ``packaging.release.all_`` task has been expanded to actually do "ALL THE THINGS!!!", given a ``dry_run`` flag, and renamed on the CLI to ``all`` (no trailing underscore). - :feature:`-` Add ``packaging.release.push`` for pushing Git objects as part of a release. - :feature:`-` Added ``twine check`` (which validates packaging metadata's ``long_description``) as a pre-upload step within ``packaging.release.publish``. - This includes some tweaking of ``readme_renderer`` behavior (used internally by twine) so it correctly spots more malformed RST, as Sphinx does. - :bug:`- major` ``packaging.release.publish`` missed a spot when it grew "kwargs beat configuration" behavior - the ``index`` kwarg still got overwritten by the config value, if defined. This has been fixed. - :bug:`- major` Correctly test for ``html`` report type inside of ``pytest.coverage`` when deciding whether to run ``open`` at the end. - :bug:`- major` ``pytest.coverage`` incorrectly concatenated its ``opts`` argument to internal options; this has been fixed. - :release:`2.0.0 <2021-01-24>` - :support:`-` Drop Python 3.4 support. We didn't actually do anything to make the code not work on 3.4, but we've removed some 3.4 related runtime (and development) dependency limitations. Our CI will also no longer test on 3.4. .. warning:: This is technically a backwards incompatible change. - :support:`12` Upgrade our packaging manifest so tests (also docs, requirements files, etc) are included in the distribution archives. Thanks to Tomáš Chvátal for the report. - :support:`21` Only require ``enum34`` under Python 2 to prevent it clashing with the stdlib ``enum`` under Python 3. Credit: Alex Gaynor. - :bug:`- major` ``release.build``'s ``--clean`` flag has been updated: - It now honors configuration like the other flags in this task, specifically ``packaging.clean``. - It now defaults to ``False`` (rationale: most build operations in the wild tend to assume no cleaning by default, so defaulting to the opposite was sometimes surprising). .. warning:: This is a backwards incompatible change. - When ``True``, it applies to both build and dist directories, instead of just build. .. warning:: This is a backwards incompatible change. - :support:`-` Reverse the default value of ``release.build`` and ``release.publish``)'s ``wheel`` argument from ``False`` to ``True``. Included in this change is a new required runtime dependency on the ``wheel`` package. Rationale: at this point in time, most users will be expecting wheels to be available, and not building wheels is likely to be the uncommon case. .. warning:: This is a backwards incompatible change. - :bug:`- major` ``release.build`` and ``release.publish`` had bad kwargs-vs-config logic preventing flags such as ``--wheel`` or ``--python`` from actually working (config defaults always won out, leading to silent ignoring of user input). This has been fixed; config will now only be honored unless the CLI appears to be overriding it. - :support:`-` Replace some old Python 2.6-compatible syntax bits. - :feature:`-` Add a ``warnings`` kwarg/flag to ``pytest.test``, allowing one to call it with ``--no-warnings`` as an inline 'alias' for pytest's own ``--disable-warnings`` flag. - :bug:`- major` Fix minor display bug causing the ``pytest`` task module to append a trailing space to the invocation of pytest itself. - :support:`-` Modify ``release`` task tree to look at ``main`` branches in addition to ``master`` ones, for "are we on a feature release line or a bugfix one?" calculations, etc. - :release:`1.4.0 <2018-06-26>` - :release:`1.3.1 <2018-06-26>` - :release:`1.2.2 <2018-06-26>` - :release:`1.1.1 <2018-06-26>` - :release:`1.0.1 <2018-06-26>` - :bug:`-` Was missing a 'hide output' flag on a subprocess shell call, the result of which was mystery git branch names appearing in the output of ``inv release`` and friends. Fixed now. - :bug:`-` ``checks.blacken`` had a typo regarding its folder selection argument; the CLI/function arg was ``folder`` while the configuration value was ``folders`` (plural). It's been made consistent: the CLI/function argument is now ``folders``. - :feature:`-` Add a ``find_opts`` argument to ``checks.blacken`` for improved control over what files get blackened. - :release:`1.3.0 <2018-06-20>` - :feature:`-` Bump Releases requirement up to 1.6 and leverage its new ability to load Sphinx extensions, in ``packaging.release.prepare`` (which parses Releases changelogs programmatically). Prior to this, projects which needed extensions to build their doctree would throw errors when using the ``packaging.release`` module. - :release:`1.2.1 <2018-06-18>` - :support:`- backported` Remove some apparently non-functional ``setup.py`` logic around conditionally requiring ``enum34``; it was never getting selected and thus breaking a couple modules that relied on it. ``enum34`` is now a hard requirement like the other semi-optional-but-not-really requirements. - :release:`1.2.0 <2018-05-22>` - :feature:`-` Add ``travis.blacken`` which wraps the new ``checks.blacken`` (in diff+check mode, for test output useful for users who cannot themselves simply run black) in addition to performing Travis-oriented Python version checks and pip installation. This is necessary to remove boilerplate around the fact that ``black`` is not even visible to Python versions less than 3.6. - :feature:`-` Break out a generic form of the ``travis.sudo-coverage`` task into ``travis.sudo-run`` which can be used for arbitrary commands run under the ssh/sudo capable user generated by ``travis.make-sudouser``/``travis.make-sshable``. - :feature:`-` Add 'missing' arguments to ``pytest.integration`` so its signature now largely matches ``pytest.test``, which it wraps. - :feature:`-` Add the ``checks`` module, containing ``checks.blacken`` which executes the `black `_ code formatter. Thanks to Chris Rose. - :release:`1.1.0 <2018-05-14>` - :feature:`-` Split out the body of the (sadly incomplete) ``packaging.release.all`` task into the better-named ``packaging.release.prepare``. (``all`` continues to behave as it did, it just now calls ``prepare`` explicitly.) - :release:`1.0.0 <2018-05-08>` - :feature:`-` Pre-history / code primarily for internal consumption pyinvoke-invocations-ea947a4/docs/conf.py000066400000000000000000000040531504415344000205610ustar00rootroot00000000000000from datetime import datetime from os import environ, getcwd from os.path import abspath, join, dirname import sys from invocations.environment import in_ci # Core settings extensions = [ "releases", "sphinx.ext.intersphinx", "sphinx.ext.autodoc", "invocations.autodoc", ] templates_path = ["_templates"] source_suffix = ".rst" master_doc = "index" exclude_patterns = ["_build"] default_role = "obj" project = "Invocations" copyright = f"{datetime.now().year} Jeff Forcier" # Ensure project directory is on PYTHONPATH for version, autodoc access sys.path.insert(0, abspath(join(getcwd(), ".."))) # Enforce use of Alabaster (even on RTD) and configure it html_theme = "alabaster" html_theme_options = { "description": "Common/best-practice Invoke tasks and collections", "github_user": "pyinvoke", "github_repo": "invocations", # TODO: make new UA property? only good for full domains and not RTD.io? # 'analytics_id': 'UA-18486793-X', "travis_button": False, "tidelift_url": "https://tidelift.com/subscription/pkg/pypi-invocations?utm_source=pypi-invocations&utm_medium=referral&utm_campaign=docs", # noqa } html_sidebars = { "**": ["about.html", "navigation.html", "searchbox.html", "donate.html"] } # Other extension configs autodoc_default_options = { "members": True, "special-members": True, } # Without this, as of Sphinx 4-ish? our autodoc plugin goes boom because its # parent class (in sphinx itself!) isn't in our reference tree & the ref fails autodoc_inherit_docstrings = False releases_github_path = "pyinvoke/invocations" # Intersphinx # TODO: this could probably get wrapped up into us or some other shared lib? on_rtd = environ.get("READTHEDOCS") == "True" on_dev = not (on_rtd or in_ci()) # Invoke inv_target = join( dirname(__file__), "..", "..", "invoke", "sites", "docs", "_build" ) if not on_dev: inv_target = "http://docs.pyinvoke.org/en/latest/" # Put them all together, + Python core intersphinx_mapping = { "python": ("http://docs.python.org/", None), "invoke": (inv_target, None), } pyinvoke-invocations-ea947a4/docs/index.rst000066400000000000000000000002751504415344000211250ustar00rootroot00000000000000=========== Invocations =========== .. include:: ../README.rst Contents ======== .. toctree:: :glob: changelog API/task docs ============= .. toctree:: :glob: api/* pyinvoke-invocations-ea947a4/invocations/000077500000000000000000000000001504415344000206645ustar00rootroot00000000000000pyinvoke-invocations-ea947a4/invocations/__init__.py000066400000000000000000000006501504415344000227760ustar00rootroot00000000000000from importlib import metadata __version__ = metadata.version("invocations") # Ignore SyntaxWarnings that come out of old semantic_version versions under # newer Pythons (re: comparing ints with 'is') # TODO: finish the WIP re: upgrading to modern semantic_version (it's # nontrivial unfortunately) then nuke this. from warnings import filterwarnings filterwarnings(action="ignore", category=SyntaxWarning, module=".*") pyinvoke-invocations-ea947a4/invocations/autodoc.py000066400000000000000000000061721504415344000227020ustar00rootroot00000000000000""" Sphinx autodoc hooks for documenting Invoke-level objects such as tasks. Unlike most of the rest of Invocations, this module isn't for reuse in the "import and call functions" sense, but instead acts as a Sphinx extension which allows Sphinx's `autodoc`_ functionality to see and document Invoke tasks and similar Invoke objects. .. note:: This functionality is mostly useful for redistributable/reusable tasks which have been defined as importable members of some Python package or module, as opposed to "local-only" tasks that live in a single project's ``tasks.py``. However, it will work for any tasks that Sphinx autodoc can import, so in a pinch you could for example tweak ``sys.path`` in your Sphinx ``conf.py`` to get it loading up a "local" tasks file for import. To use: - Add ``"invocations.autodoc"`` to your Sphinx ``conf.py``'s ``extensions`` list. - Use Sphinx autodoc's ``automodule`` directive normally, aiming it at your tasks module(s), e.g. ``.. automodule:: myproject.tasks`` in some ``.rst`` document of your choosing. - As noted above, this only works for modules that are importable, like any other Sphinx autodoc use case. - Unless you want to opt-in which module members get documented, use ``:members:`` or add ``"members": True`` to your ``conf.py``'s ``autodoc_default_options``. - By default, only tasks with docstrings will be picked up, unless you also give the ``:undoc-members:`` flag or add ``:undoc-members:`` / add ``"undoc-members": True`` to ``autodoc_default_options``. - Please see the `autodoc`_ docs for details on these settings and more! - Build your docs, and you should see your tasks showing up as documented functions in the result. .. _autodoc: http://www.sphinx-doc.org/en/master/ext/autodoc.html """ import inspect from invoke import Task # For sane mock patching. Meh. from sphinx.ext import autodoc class TaskDocumenter( autodoc.DocstringSignatureMixin, autodoc.ModuleLevelDocumenter ): objtype = "task" directivetype = "function" @classmethod def can_document_member(cls, member, membername, isattr, parent): return isinstance(member, Task) def format_args(self): function = self.object.body # TODO: consider extending (or adding a sibling to) Task.argspec so it # preserves more of the full argspec tuple. # TODO: whether to preserve the initial context argument is an open # question. For now, it will appear, but only pending invoke#170 - # after which point "call tasks as raw functions" may be less common. # TODO: also, it may become moot-ish if we turn this all into emission # of custom domain objects and/or make the CLI arguments the focus return autodoc.stringify_signature(inspect.signature(function)) def document_members(self, all_members=False): # Neuter this so superclass bits don't introspect & spit out autodoc # directives for task attributes. Most of that's not useful. pass def setup(app): app.setup_extension("sphinx.ext.autodoc") app.add_autodocumenter(TaskDocumenter) pyinvoke-invocations-ea947a4/invocations/checks.py000066400000000000000000000046731504415344000225100ustar00rootroot00000000000000""" Tasks for common project sanity-checking such as linting or type checking. .. versionadded:: 1.2 """ from invoke import task @task(name="blacken", aliases=["format"], iterable=["folders"]) def blacken( c, line_length=79, folders=None, check=False, diff=False, find_opts=None ): r""" Run black on the current source tree (all ``.py`` files). :param int line_length: Line length argument. Default: ``79``. :param list folders: List of folders (or, on the CLI, an argument that can be given N times) to search within for ``.py`` files. Default: ``["."]``. Honors the ``blacken.folders`` config option. :param bool check: Whether to run ``black --check``. Default: ``False``. :param bool diff: Whether to run ``black --diff``. Default: ``False``. :param str find_opts: Extra option string appended to the end of the internal ``find`` command. For example, skip a vendor directory with ``"-and -not -path ./vendor\*"``, add ``-mtime N``, or etc. Honors the ``blacken.find_opts`` config option. .. versionadded:: 1.2 .. versionchanged:: 1.4 Added the ``find_opts`` argument. .. versionchanged:: 3.2 Added the ``format`` alias. """ config = c.config.get("blacken", {}) default_folders = ["."] configured_folders = config.get("folders", default_folders) folders = folders or configured_folders default_find_opts = "" configured_find_opts = config.get("find_opts", default_find_opts) find_opts = find_opts or configured_find_opts black_command_line = "black -l {}".format(line_length) if check: black_command_line = "{} --check".format(black_command_line) if diff: black_command_line = "{} --diff".format(black_command_line) if find_opts: find_opts = " {}".format(find_opts) else: find_opts = "" cmd = "find {} -name '*.py'{} | xargs {}".format( " ".join(folders), find_opts, black_command_line ) c.run(cmd, pty=True) @task def lint(c): """ Apply linting. .. versionadded:: 3.2 """ # TODO: configurable and/or switch to ruff c.run("flake8", warn=True, pty=True) @task(default=True) def all_(c): """ Run all common formatters/linters for the project. .. versionadded:: 3.2 """ # TODO: contextmanager config, if we don't already have that c.config.run.echo = True blacken(c) lint(c) pyinvoke-invocations-ea947a4/invocations/ci.py000066400000000000000000000074321504415344000216370ustar00rootroot00000000000000""" Tasks intended for use under continuous integration. Presently, this tends to assume CircleCI, but it is intended to be generic & we'll accept patches to make any Circle-isms configurable. Most of it involves setting up to run a test suite under a special user who is allowed to run ``sudo`` and who also needs a password to do so. This allows testing sudo-related functionality which would otherwise suffer false-positives, since most CI environments allow passwordless sudo for the default user. Thus, the pattern is: - use that default user's sudo privileges to generate the special user (if they don't already exist in the image) - as the default user, execute the test suite runner via ``sudo -u `` - the test suite will then at times run its own ``sudo someprogram`` & be prompted for its password (which the test suite should read from the config data, same as this outer set of tasks does). .. note:: This module defines default values for the ``ci.sudo`` config subtree, but if you're using an execution environment where the default sudoers group isn't ``sudo`` (eg ``wheel``) you'll want to override ``ci.sudo.group`` in your own config files. """ from invoke import task, Collection @task def make_sudouser(c): """ Create a passworded sudo-capable user. Used by other tasks to execute the test suite so sudo tests work. """ user = c.ci.sudo.user password = c.ci.sudo.password groups = c.ci.sudo.groups # "--create-home" because we need a place to put conf files, keys etc # "--groups xxx" for (non-passwordless) sudo access, eg 'sudo' group on # Debian, plus any others, eg shared group membership with regular user for # writing out artifact files (assuming $HOME is g+w, which it is on Circle) c.sudo( "useradd {} --create-home --groups {}".format(user, ",".join(groups)) ) # Password set noninteractively via chpasswd (assumes invoking user itself # is able to passwordless sudo; this is true on CircleCI) c.run("echo {}:{} | sudo chpasswd".format(user, password)) @task def sudo_run(c, command): """ Run some command under CI-oriented sudo subshell/virtualenv. :param str command: Command string to run, e.g. ``inv coverage``, ``inv integration``, etc. (Does not necessarily need to be an Invoke task, but...) """ # NOTE: due to circle sudoers config, circleci user can't do "sudo -u" w/o # password prompt. However, 'sudo su' seems to work just as well... # NOTE: well. provided you do this really asinine PATH preservation to work # around su's path resetting. no, --preserve-environment doesn't work, even # if you have --preserve-environment=PATH on the outer 'sudo' (which does # work for what sudo directly calls) # TODO: may want to rub --pty on the 'su' but so far seems irrelevant c.run( 'sudo su {} -c "export PATH=$PATH && {}"'.format( c.ci.sudo.user, command ) ) @task def make_sshable(c): """ Set up passwordless SSH keypair & authorized_hosts access to localhost. """ user = c.ci.sudo.user home = "~{}".format(user) # Run sudo() as the new sudo user; means less chown'ing, etc. c.config.sudo.user = user c.config.sudo.password = c.ci.sudo.password ssh_dir = "{}/.ssh".format(home) for cmd in ("mkdir {0}", "chmod 0700 {0}"): sudo_run(c, cmd.format(ssh_dir, user)) sudo_run(c, "ssh-keygen -t rsa -f {}/id_rsa -N ''".format(ssh_dir)) sudo_run(c, f"cp {ssh_dir}/id_rsa.pub {ssh_dir}/authorized_keys") ns = Collection(make_sudouser, sudo_run, make_sshable) ns.configure( { "ci": { "sudo": { "user": "invoker", "password": "secret", "groups": ["sudo", "circleci"], } } } ) pyinvoke-invocations-ea947a4/invocations/console.py000066400000000000000000000036261504415344000227070ustar00rootroot00000000000000""" Text console UI helpers and patterns, e.g. 'Y/n' prompts and the like. """ import sys # NOTE: originally cribbed from fab 1's contrib.console.confirm def confirm(question, assume_yes=True): """ Ask user a yes/no question and return their response as a boolean. ``question`` should be a simple, grammatically complete question such as "Do you wish to continue?", and will have a string similar to ``" [Y/n] "`` appended automatically. This function will *not* append a question mark for you. By default, when the user presses Enter without typing anything, "yes" is assumed. This can be changed by specifying ``assume_yes=False``. .. note:: If the user does not supply input that is (case-insensitively) equal to "y", "yes", "n" or "no", they will be re-prompted until they do. :param str question: The question part of the prompt. :param bool assume_yes: Whether to assume the affirmative answer by default. Default value: ``True``. :returns: A `bool`. """ # Set up suffix if assume_yes: suffix = "Y/n" else: suffix = "y/N" # Loop till we get something we like # TODO: maybe don't do this? It can be annoying. Turn into 'q'-for-quit? while True: # TODO: ensure that this is Ctrl-C friendly, ISTR issues with # raw_input/input on some Python versions blocking KeyboardInterrupt. response = input("{} [{}] ".format(question, suffix)) response = response.lower().strip() # Normalize # Default if not response: return assume_yes # Yes if response in ["y", "yes"]: return True # No if response in ["n", "no"]: return False # Didn't get empty, yes or no, so complain and loop err = "I didn't understand you. Please specify '(y)es' or '(n)o'." print(err, file=sys.stderr) pyinvoke-invocations-ea947a4/invocations/docs.py000066400000000000000000000146061504415344000221750ustar00rootroot00000000000000""" Tasks for managing Sphinx documentation trees. """ from os.path import join, isdir from tempfile import mkdtemp from shutil import rmtree import sys from invoke import task, Collection, Context from .watch import make_handler, observe # Underscored func name to avoid shadowing kwargs in build() @task(name="clean") def _clean(c): """ Nuke docs build target directory so next build is clean. """ if isdir(c.sphinx.target): rmtree(c.sphinx.target) # Ditto @task(name="browse") def _browse(c): """ Open build target's index.html in a browser (using 'open'). """ index = join(c.sphinx.target, c.sphinx.target_file) c.run("open {}".format(index)) @task( default=True, help={ "opts": "Extra sphinx-build options/args", "clean": "Remove build tree before building", "browse": "Open docs index in browser after building", "nitpick": "Build with stricter warnings/errors enabled", "source": "Source directory; overrides config setting", "target": "Output directory; overrides config setting", }, ) def build( c, clean=False, browse=False, nitpick=False, opts=None, source=None, target=None, ): """ Build the project's Sphinx docs. """ if clean: _clean(c) if opts is None: opts = "" if nitpick: opts += " -n -W -T" cmd = "sphinx-build{} {} {}".format( (" " + opts) if opts else "", source or c.sphinx.source, target or c.sphinx.target, ) c.run(cmd, pty=True) if browse: _browse(c) @task def doctest(c): """ Run Sphinx' doctest builder. This will act like a test run, displaying test results & exiting nonzero if all tests did not pass. A temporary directory is used for the build target, as the only output is the text file which is automatically printed. """ tmpdir = mkdtemp() try: opts = "-b doctest" target = tmpdir build(c, clean=True, target=target, opts=opts) finally: rmtree(tmpdir) @task def tree(c): """ Display documentation contents with the 'tree' program. """ ignore = ".git|*.pyc|*.swp|dist|*.egg-info|_static|_build|_templates" c.run('tree -Ca -I "{}" {}'.format(ignore, c.sphinx.source)) # Vanilla/default/parameterized collection for normal use ns = Collection(_clean, _browse, build, tree, doctest) ns.configure( { "sphinx": { "source": "docs", # TODO: allow lazy eval so one attr can refer to another? "target": join("docs", "_build"), "target_file": "index.html", } } ) # Multi-site variants, used by various projects (fabric, invoke, paramiko) # Expects a tree like sites/www/ + sites/docs/, # and that you want 'inline' html build dirs, e.g. sites/www/_build/index.html. def _site(name, help_part): _path = join("sites", name) # TODO: turn part of from_module into .clone(), heh. self = sys.modules[__name__] coll = Collection.from_module( self, name=name, config={"sphinx": {"source": _path, "target": join(_path, "_build")}}, ) coll.__doc__ = "Tasks for building {}".format(help_part) coll["build"].__doc__ = "Build {}".format(help_part) return coll # Usage doc/API site (published as e.g. docs.myproject.org) docs = _site("docs", "the API docs subsite.") # Main/about/changelog site (e.g. (www.)?myproject.org) www = _site("www", "the main project website.") @task def sites(c): """ Build both doc sites w/ maxed nitpicking. """ # TODO: This is super lolzy but we haven't actually tackled nontrivial # in-Python task calling yet, so we do this to get a copy of 'our' context, # which has been updated with the per-collection config data of the # docs/www subcollections. docs_c = Context(config=c.config.clone()) www_c = Context(config=c.config.clone()) docs_c.update(**docs.configuration()) www_c.update(**www.configuration()) # Must build both normally first to ensure good intersphinx inventory files # exist =/ circular dependencies ahoy! Do it quietly to avoid pulluting # output; only super-serious errors will bubble up. # TODO: wants a 'temporarily tweak context settings' contextmanager # TODO: also a fucking spinner cuz this confuses me every time I run it # when the docs aren't already prebuilt # TODO: this is still bad because it means the actually displayed build # output "looks like" nothing was built (due to that first pass building # most pages) docs_c["run"].hide = True www_c["run"].hide = True docs["build"](docs_c) www["build"](www_c) docs_c["run"].hide = False www_c["run"].hide = False # Run the actual builds, with nitpick=True (nitpicks + tracebacks) docs["build"](docs_c, nitpick=True) www["build"](www_c, nitpick=True) @task def watch_docs(c): """ Watch both doc trees & rebuild them if files change. This includes e.g. rebuilding the API docs if the source code changes; rebuilding the WWW docs if the README changes; etc. Reuses the configuration values ``packaging.package`` or ``tests.package`` (the former winning over the latter if both defined) when determining which source directory to scan for API doc updates. """ # TODO: break back down into generic single-site version, then create split # tasks as with docs/www above. Probably wants invoke#63. # NOTE: 'www'/'docs' refer to the module level sub-collections. meh. # Readme & WWW triggers WWW www_c = Context(config=c.config.clone()) www_c.update(**www.configuration()) www_handler = make_handler( ctx=www_c, task_=www["build"], regexes=[r"\./README.rst", r"\./sites/www"], ignore_regexes=[r".*/\..*\.swp", r"\./sites/www/_build"], ) # Code and docs trigger API docs_c = Context(config=c.config.clone()) docs_c.update(**docs.configuration()) regexes = [r"\./sites/docs"] package = c.get("packaging", {}).get("package", None) if package is None: package = c.get("tests", {}).get("package", None) if package: regexes.append(r"\./{}/".format(package)) api_handler = make_handler( ctx=docs_c, task_=docs["build"], regexes=regexes, ignore_regexes=[r".*/\..*\.swp", r"\./sites/docs/_build"], ) observe(www_handler, api_handler) pyinvoke-invocations-ea947a4/invocations/environment.py000066400000000000000000000011551504415344000236040ustar00rootroot00000000000000""" Helpers concerning the invoking shell environment. For example, generalized "do we appear to be on CI?" tests, which may be used in multiple other modules. """ import os def in_ci(): """ Return ``True`` if we appear to be running inside a CI environment. Checks for CI system env vars such as ``CIRCLECI`` or ``TRAVIS`` - specifically whether they exist and are non-empty. The actual value is not currently relevant, as long as it's not the empty string. """ for sentinel in ("CIRCLECI", "TRAVIS"): if os.environ.get(sentinel, False): return True return False pyinvoke-invocations-ea947a4/invocations/packaging/000077500000000000000000000000001504415344000226105ustar00rootroot00000000000000pyinvoke-invocations-ea947a4/invocations/packaging/__init__.py000066400000000000000000000004501504415344000247200ustar00rootroot00000000000000# Make the inner modules' tasks/collections readily available. from .vendorize import vendorize from . import release # Most of the time, importers of this module want the 'release' sub-collection. # TODO: update other libs & then remove this so it's a bit cleaner. ns = release # flake8: noqa pyinvoke-invocations-ea947a4/invocations/packaging/release.py000066400000000000000000001024131504415344000246030ustar00rootroot00000000000000""" Python package release tasks. This module assumes: - you're using semantic versioning for your releases - you're using a modern pyproject.toml for packaging metadata """ import getpass import logging import os import re import venv from functools import partial from io import StringIO from pathlib import Path from shutil import rmtree from typing import Union from build._builder import _read_pyproject_toml from invoke.vendor.lexicon import Lexicon from blessings import Terminal from docutils.utils import Reporter from enum import Enum from invoke import Collection, task, Exit from pip import __version__ as pip_version import readme_renderer.rst # transitively required via twine in setup.py from releases.util import parse_changelog from tabulate import tabulate from twine.commands.check import check as twine_check from .semantic_version_monkey import Version from ..console import confirm from ..environment import in_ci from ..util import tmpdir debug = logging.getLogger("invocations.packaging.release").debug # Monkeypatch readme_renderer.rst so it acts more like Sphinx re: docutils # warning levels - otherwise it overlooks (and misrenders) stuff like bad # header formats etc! # (The defaults in readme_renderer are halt_level=WARNING and # report_level=SEVERE) # NOTE: this only works because we directly call twine via Python and not via # subprocess. for key in ("halt_level", "report_level"): readme_renderer.rst.SETTINGS[key] = Reporter.INFO_LEVEL # TODO: this could be a good module to test out a more class-centric method of # organizing tasks. E.g.: # - 'Checks'/readonly things like 'should_changelog' live in a base class # - one subclass defines dry-run actions for the 'verbs', and is used for # sanity checking or dry-running # - another subclass defines actual, mutating actions for the 'verbs', and is # used for actual release management # - are those classes simply arbitrary tasky classes used *by* # actual task functions exposing them; or are they the collections themselves # (as per #347)? # - if the latter, how should one "switch" between the subclasses when dry # running vs real running? # - what's the CLI "API" look like for that? # - Different subcollections, e.g. `inv release.dry-run(.all/changelog/etc)` # vs `inv release.all`? # - Dry-run flag (which feels more natural/obvious/expected)? How # would/should that flag affect collection/task loading/selection? # - especially given task load concerns are typically part of core, but # this dry-run-or-not behavior clearly doesn't want to be in core? # # State junk # # Blessings Terminal object for ANSI colorization. # NOTE: mildly uncomfortable with the instance living at module level, but also # pretty sure it's unlikely to change meaningfully over time, between # threads/etc - and it'd be otherwise a PITA to cart around/re-instantiate. t = Terminal() check = "\u2714" ex = "\u2718" # Types of releases/branches Release = Enum("Release", "BUGFIX FEATURE UNDEFINED") # Actions to take for various components - done as enums whose values are # useful one-line status outputs. class Changelog(Enum): OKAY = t.green(check + " no unreleased issues") NEEDS_RELEASE = t.red(ex + " needs :release: entry") class VersionFile(Enum): OKAY = t.green(check + " version up to date") NEEDS_BUMP = t.red(ex + " needs version bump") class Tag(Enum): OKAY = t.green(check + " all set") NEEDS_CUTTING = t.red(ex + " needs cutting") # Bits for testing branch names to determine release type BUGFIX_RE = re.compile(r"^\d+\.\d+$") BUGFIX_RELEASE_RE = re.compile(r"^\d+\.\d+\.\d+$") # TODO: allow tweaking this if folks use different branch methodology: # - same concept, different name, e.g. s/main/dev/ # - different concept entirely, e.g. no main-ish, only feature branches FEATURE_RE = re.compile(r"^(main|master)$") class UndefinedReleaseType(Exception): pass def _converge(c): """ Examine world state, returning data on what needs updating for release. :param c: Invoke ``Context`` object or subclass. :returns: Two dicts (technically, dict subclasses, which allow attribute access), ``actions`` and ``state`` (in that order.) ``actions`` maps release component names to variables (usually class constants) determining what action should be taken for that component: - ``changelog``: members of `.Changelog` such as ``NEEDS_RELEASE`` or ``OKAY``. - ``version``: members of `.VersionFile`. ``state`` contains the data used to calculate the actions, in case the caller wants to do further analysis: - ``branch``: the name of the checked-out Git branch. - ``changelog``: the parsed project changelog, a `dict` of releases. - ``release_type``: what type of release the branch appears to be (will be a member of `.Release` such as ``Release.BUGFIX``.) - ``latest_line_release``: the latest changelog release found for current release type/line. - ``latest_overall_release``: the absolute most recent release entry. Useful for determining next minor/feature release. - ``current_version``: the version string as found in the package's packaging metadata. """ # # Data/state gathering # # Get data about current repo context: what branch are we on & what kind of # release does it appear to represent? branch, release_type = _release_line(c) # Short-circuit if type is undefined; we can't do useful work for that. if release_type is Release.UNDEFINED: raise UndefinedReleaseType( "You don't seem to be on a release-related branch; " "why are you trying to cut a release?" ) # Parse our changelog so we can tell what's released and what's not. # TODO: below needs to go in something doc-y somewhere; having it in a # non-user-facing subroutine docstring isn't visible enough. """ .. note:: Requires that one sets the ``packaging.changelog_file`` configuration option; it should be a relative or absolute path to your ``changelog.rst`` (or whatever it's named in your project). """ # TODO: allow skipping changelog if not using Releases since we have no # other good way of detecting whether a changelog needs/got an update. # TODO: chdir to sphinx.source, import conf.py, look at # releases_changelog_name - that way it will honor that setting and we can # ditch this explicit one instead. (and the docstring above) changelog = parse_changelog( c.packaging.changelog_file, load_extensions=True ) # Get latest appropriate changelog release and any unreleased issues, for # current line line_release, issues = _release_and_issues(changelog, branch, release_type) # Also get latest overall release, sometimes that matters (usually only # when latest *appropriate* release doesn't exist yet) overall_release = _versions_from_changelog(changelog)[-1] # Obtain the project's defined (not installed) version number pyproject = Path.cwd() / "pyproject.toml" current_version = _read_pyproject_toml(pyproject)["project"]["version"] # Grab all git tags tags = _get_tags(c) state = Lexicon( { "branch": branch, "release_type": release_type, "changelog": changelog, "latest_line_release": Version(line_release) if line_release else None, "latest_overall_release": overall_release, # already a Version "unreleased_issues": issues, "current_version": Version(current_version), "tags": tags, } ) # Version number determinations: # - latest actually-released version # - the next version after that for current branch # - which of the two is the actual version we're looking to converge on, # depends on current changelog state. latest_version, next_version = _latest_and_next_version(state) state.latest_version = latest_version state.next_version = next_version state.expected_version = latest_version if state.unreleased_issues: state.expected_version = next_version # # Logic determination / convergence # actions = Lexicon() # Changelog: needs new release entry if there are any unreleased issues for # current branch's line. # TODO: annotate with number of released issues [of each type?] - so not # just "up to date!" but "all set (will release 3 features & 5 bugs)" actions.changelog = Changelog.OKAY if release_type in (Release.BUGFIX, Release.FEATURE) and issues: actions.changelog = Changelog.NEEDS_RELEASE # Version file: simply whether version file equals the target version. # TODO: corner case of 'version file is >1 release in the future', but # that's still wrong, just would be a different 'bad' status output. actions.version = VersionFile.OKAY if state.current_version != state.expected_version: actions.version = VersionFile.NEEDS_BUMP # Git tag: similar to version file, except the check is existence of tag # instead of comparison to file contents. We even reuse the # 'expected_version' variable wholesale. actions.tag = Tag.OKAY if state.expected_version not in state.tags: actions.tag = Tag.NEEDS_CUTTING actions.all_okay = ( actions.changelog == Changelog.OKAY and actions.version == VersionFile.OKAY and actions.tag == Tag.OKAY ) # # Return # return actions, state @task def status(c): """ Print current release (version, changelog, tag, etc) status. Doubles as a subroutine, returning the return values from its inner call to ``_converge`` (an ``(actions, state)`` two-tuple of Lexicons). """ actions, state = _converge(c) table = [] # NOTE: explicit 'sensible' sort (in rough order of how things are usually # modified, and/or which depend on one another, e.g. tags are near the end) for component in "changelog version tag".split(): table.append((component.capitalize(), actions[component].value)) print(tabulate(table)) return actions, state # TODO: thought we had automatic trailing underscore stripping but...no? @task(name="all", default=True) def all_(c, dry_run=False): """ Catchall version-bump/tag/changelog/PyPI upload task. :param bool dry_run: Handed to all subtasks which themselves have a ``dry_run`` flag. .. versionchanged:: 2.1 Expanded functionality to run ``publish`` and ``push`` as well as ``prepare``. .. versionchanged:: 2.1 Added the ``dry_run`` flag. """ prepare(c, dry_run=dry_run) publish(c, dry_run=dry_run) push(c, dry_run=dry_run) @task def prepare(c, dry_run=False): """ Edit changelog & version, git commit, and git tag, to set up for release. :param bool dry_run: Whether to take any actual actions or just say what might occur. Will also non-fatally exit if not on some form of release branch. Default: ``False``. :returns: ``True`` if short-circuited due to all-ok, ``None`` otherwise. .. versionchanged:: 2.1 Added the ``dry_run`` parameter. .. versionchanged:: 2.1 Generate annotated git tags instead of lightweight ones. """ # Print dry-run/status/actions-to-take data & grab programmatic result # TODO: maybe expand the enum-based stuff to have values that split up # textual description, command string, etc. See the TODO up by their # definition too, re: just making them non-enum classes period. # TODO: otherwise, we at least want derived eg changelog/version/etc paths # transmitted from status() into here... try: actions, state = status(c) except UndefinedReleaseType: if not dry_run: raise raise Exit( code=0, message="Can't dry-run release tasks, not on a release branch; skipping.", # noqa ) # Short-circuit if nothing to do if actions.all_okay: return True # If work to do and not dry-running, make sure user confirms to move ahead if not dry_run: if not confirm("Take the above actions?"): raise Exit("Aborting.") # TODO: factor out what it means to edit a file: # - $EDITOR or explicit expansion of it in case no shell involved # - pty=True and hide=False, because otherwise things can be bad # - what else? # Changelog! (pty for non shite editing, eg vim sure won't like non-pty) if actions.changelog == Changelog.NEEDS_RELEASE: # TODO: identify top of list and inject a ready-made line? Requires vim # assumption...GREAT opportunity for class/method based tasks! cmd = "$EDITOR {.packaging.changelog_file}".format(c) c.run(cmd, pty=True, hide=False, dry=dry_run) # Version file! if actions.version == VersionFile.NEEDS_BUMP: cmd = "$EDITOR pyproject.toml" c.run(cmd, pty=True, hide=False, dry=dry_run) if actions.tag == Tag.NEEDS_CUTTING: # Commit, if necessary, so the tag includes everything. # NOTE: this strips out untracked files. effort. cmd = 'git status --porcelain | egrep -v "^\\?"' if c.run(cmd, hide=True, warn=True).ok: c.run( 'git commit -am "Cut {}"'.format(state.expected_version), hide=False, dry=dry_run, echo=True, ) # Tag! c.run( 'git tag -a {} -m ""'.format(state.expected_version), hide=False, dry=dry_run, echo=True, ) # If top-of-task status check wasn't all_okay, it means the code between # there and here was expected to alter state. Run another check to make # sure those actions actually succeeded! if not dry_run and not actions.all_okay: actions, state = status(c) if not actions.all_okay: raise Exit("Something went wrong! Please fix.") def _release_line(c): """ Examine current repo state to determine what type of release to prep. :returns: A two-tuple of ``(branch-name, line-type)`` where: - ``branch-name`` is the current branch name, e.g. ``1.1``, ``main``, ``gobbledygook`` (or, usually, ``HEAD`` if not on a branch). - ``line-type`` is a symbolic member of `.Release` representing what "type" of release the line appears to be for: - ``Release.BUGFIX`` if on a bugfix/stable release line, e.g. ``1.1``. - ``Release.FEATURE`` if on a feature-release branch (typically ``main``). - ``Release.UNDEFINED`` if neither of those appears to apply (usually means on some unmerged feature/dev branch). """ # TODO: I don't _think_ this technically overlaps with Releases (because # that only ever deals with changelog contents, and therefore full release # version numbers) but in case it does, move it there sometime. # TODO: this and similar calls in this module may want to be given an # explicit pointer-to-git-repo option (i.e. if run from outside project # context). # TODO: major releases? or are they big enough events we don't need to # bother with the script? Also just hard to gauge - when is main the next # 1.x feature vs 2.0? # TODO: yea, this is currently a slightly annoying hole, workaround is to: # - be on main still # - update your pyproject's version to eg 3.0.0 # - add the 3.0.0 release entry in your changelog (without that, this # module will croak on assumptions) # - commit # - branch to 3.0 # - running eg `inv release --dry-run` should now work as expected branch = c.run("git rev-parse --abbrev-ref HEAD", hide=True).stdout.strip() type_ = Release.UNDEFINED if BUGFIX_RE.match(branch): type_ = Release.BUGFIX if FEATURE_RE.match(branch): type_ = Release.FEATURE return branch, type_ def _latest_feature_bucket(changelog): """ Select 'latest'/'highest' unreleased feature bucket from changelog. :returns: a string key from ``changelog``. """ unreleased = [x for x in changelog if x.startswith("unreleased_")] return sorted( unreleased, key=lambda x: int(x.split("_")[1]), reverse=True )[0] # TODO: this feels like it should live in Releases, though that would imply # adding semantic_version as a dep there, grump def _versions_from_changelog(changelog): """ Return all released versions from given ``changelog``, sorted. :param dict changelog: A changelog dict as returned by ``releases.util.parse_changelog``. :returns: A sorted list of `semantic_version.Version` objects. """ versions = [Version(x) for x in changelog if BUGFIX_RELEASE_RE.match(x)] return sorted(versions) # TODO: may want to live in releases.util eventually def _release_and_issues(changelog, branch, release_type): """ Return most recent branch-appropriate release, if any, and its contents. :param dict changelog: Changelog contents, as returned by ``releases.util.parse_changelog``. :param str branch: Branch name. :param release_type: Member of `Release`, e.g. `Release.FEATURE`. :returns: Two-tuple of release (``str``) and issues (``list`` of issue numbers.) If there is no latest release for the given branch (e.g. if it's a feature or main branch), it will be ``None``. """ # Bugfix lines just use the branch to find issues bucket = branch # Features need a bit more logic if release_type is Release.FEATURE: bucket = _latest_feature_bucket(changelog) # Issues is simply what's in the bucket issues = changelog[bucket] # Latest release is undefined for feature lines release = None # And requires scanning changelog, for bugfix lines if release_type is Release.BUGFIX: versions = [str(x) for x in _versions_from_changelog(changelog)] release = [x for x in versions if x.startswith(bucket)][-1] return release, issues def _get_tags(c): """ Return sorted list of release-style tags as semver objects. """ tags_ = [] for tagstr in c.run("git tag", hide=True).stdout.strip().split("\n"): try: tags_.append(Version(tagstr)) # Ignore anything non-semver; most of the time they'll be non-release # tags, and even if they are, we can't reason about anything # non-semver anyways. # TODO: perhaps log these to DEBUG except ValueError: pass # Version objects sort semantically return sorted(tags_) def _latest_and_next_version(state): """ Determine latest version for current branch, and its increment. E.g. on the ``1.2`` branch, we take the latest ``1.2.x`` release and increment its tertiary number, so e.g. if the previous release was ``1.2.2``, this function returns ``1.2.3``. If on ``main`` and latest overall release was ``1.2.2``, it returns ``1.3.0``. :param dict state: The ``state`` dict as returned by / generated within `converge`. :returns: 2-tuple of ``semantic_version.Version``. """ if state.release_type == Release.FEATURE: previous_version = state.latest_overall_release next_version = previous_version.next_minor() else: previous_version = state.latest_line_release next_version = previous_version.next_patch() return previous_version, next_version def _find_package(c): """ Try to find 'the' One True Package for this project. Uses the ``packaging.package`` config setting if defined. If not defined, fallback is to look for a single top-level Python package (directory containing ``__init__.py``). (This search ignores a small blacklist of directories like ``tests/``, ``vendor/`` etc.) """ # TODO: try using importlib now it's in stdlib? configured_value = c.get("packaging", {}).get("package", None) if configured_value: return configured_value # TODO: tests covering this stuff here (most logic tests simply supply # config above) packages = [ path for path in os.listdir(".") if ( os.path.isdir(path) and os.path.exists(os.path.join(path, "__init__.py")) and path not in ("tests", "integration", "sites", "vendor") ) ] if not packages: raise Exit("Unable to find a local Python package!") if len(packages) > 1: raise Exit("Found multiple Python packages: {!r}".format(packages)) return packages[0] @task def build(c, sdist=True, wheel=True, directory=None, python=None, clean=False): """ Build sdist and/or wheel archives, optionally in a temp base directory. Uses the `build `_ tool from PyPA. All parameters/flags honor config settings of the same name, under the ``packaging`` tree. E.g. say ``.configure({'packaging': {'wheel': False}})`` to disable building wheel archives by default. :param bool sdist: Whether to build sdists/tgzs. Default: ``True``. :param bool wheel: Whether to build wheels (requires the ``wheel`` package from PyPI). Default: ``True``. :param str directory: Allows specifying a specific directory in which to perform dist creation. Useful when running as a subroutine from ``publish`` which sets up a temporary directory. Defaults to the current working directory + ``dist/`` (the same as ``pypa/build``'s default behavior, albeit explicitly derived to support our own functionality). :param clean: Whether to remove the dist directory before building. :param str python: Which Python binary to use when invoking ``-m build``. Defaults to ``"python"``. If ``wheel=True``, then this Python must have ``wheel`` installed in its default ``site-packages`` (or similar) location. .. versionchanged:: 2.0 ``clean`` now defaults to False instead of True, cleans both dist and build dirs when True, and honors configuration. .. versionchanged:: 2.0 ``wheel`` now defaults to True instead of False. .. versionchanged:: 4.0 Switched to using ``pypa/build`` and made related changes to args (eg, ``directory`` now only controls dist output location). """ # Config hooks config = c.config.get("packaging", {}) # Check bool flags to see if they were overridden by config. # TODO: this wants something explicit at the Invoke layer, technically this # prevents someone from giving eg --sdist on CLI to override a falsey # config value for it. if sdist is True and "sdist" in config: sdist = config["sdist"] if wheel is True and "wheel" in config: wheel = config["wheel"] if clean is False and "clean" in config: clean = config["clean"] if directory is None: directory = Path(config.get("directory", Path.cwd() / "dist")) if directory.is_absolute(): directory = directory.relative_to(Path.cwd()) print(f"Building into {directory}...") if python is None: python = config.get("python", "python") # buffalo buffalo # Sanity if not sdist and not wheel: raise Exit( "You said no sdists and no wheels..." "what DO you want to build exactly?" ) # Start building command parts = [python, "-m build"] # Set, clean directory as needed parts.append(f"--outdir {directory}") if clean: rmtree(directory, ignore_errors=True) if sdist: parts.append("--sdist") if wheel: parts.append("--wheel") c.run(" ".join(parts)) print("Result:") c.run(f"ls -l {directory}", echo=True, hide=False) def find_gpg(c): for candidate in "gpg gpg1 gpg2".split(): if c.run("which {}".format(candidate), hide=True, warn=True).ok: return candidate @task def publish( c, sdist=True, wheel=True, index=None, sign=False, dry_run=False, directory=None, ): """ Publish code to PyPI or index of choice. Wraps ``build`` and ``upload``. This uses the ``twine`` command under the hood, both its pre-upload ``check`` subcommand (which verifies the archives to be uploaded, including checking your PyPI readme) and the ``upload`` one. All parameters save ``dry_run`` and ``directory`` honor config settings of the same name, under the ``packaging`` tree. E.g. say ``.configure({'packaging': {'wheel': True}})`` to force building wheel archives by default. :param bool sdist: Whether to upload sdists/tgzs. Default: ``True``. :param bool wheel: Whether to upload wheels (requires the ``wheel`` package from PyPI). Default: ``True``. :param str index: Custom upload index/repository name. See ``upload`` help for details. :param bool sign: Whether to sign the built archive(s) via GPG. :param bool dry_run: Skip upload step if ``True``. This also prevents cleanup of the temporary build/dist directories, so you can examine the build artifacts. Note that this does not skip the ``twine check`` step, just the final upload. :param str directory: Used for ``build(directory=)`` and thus affects where dists go. Defaults to a temporary directory which is cleaned up after the run finishes. """ # Don't hide by default, this step likes to be verbose most of the time. c.config.run.hide = False # Including echoing! c.config.run.echo = True # Config hooks # TODO: this pattern is too widespread. Really needs something in probably # Executor that automatically does this on our behalf for any kwargs we # indicate should be configurable config = c.config.get("packaging", {}) if index is None and "index" in config: index = config["index"] if sign is False and "sign" in config: sign = config["sign"] # Build, into controlled temp dir (avoids attempting to re-upload old # files) with tmpdir(skip_cleanup=dry_run, explicit=directory) as tmp: # Build default archives builder = partial(build, c, sdist=sdist, wheel=wheel, directory=tmp) builder() # Rebuild with env (mostly for Fabric 2) # TODO: code smell; implies this really wants to be class/hook based? # TODO: or at least invert sometime so it's easier to say "do random # stuff to arrive at dists, then test and upload". rebuild_with_env = config.get("rebuild_with_env", None) if rebuild_with_env: old_environ = os.environ.copy() os.environ.update(rebuild_with_env) try: builder() finally: os.environ.update(old_environ) for key in rebuild_with_env: if key not in old_environ: del os.environ[key] # Use twine's check command on built artifacts (at present this just # validates long_description) print(c.config.run.echo_format.format(command="twine check")) failure = twine_check(dists=[os.path.join(tmp, "*")]) if failure: raise Exit(1) # Test installation of built artifacts into virtualenvs (even during # dry run) test_install(c, directory=tmp) # Do the thing! (Maybe.) upload(c, directory=tmp, index=index, sign=sign, dry_run=dry_run) @task def test_install(c, directory, verbose=False, skip_import=False): """ Test installation of build artifacts found in ``$directory``. Uses the `venv` module to build temporary virtualenvs. :param bool verbose: Whether to print subprocess output. :param bool skip_import: If True, don't try importing the installed module or checking it for type hints. """ # TODO: wants contextmanager or similar for only altering a setting within # a given scope or block - this may pollute subsequent subroutine calls if verbose: old_hide = c.config.run.hide c.config.run.hide = False builder = venv.EnvBuilder(with_pip=True) archives = get_archives(directory) if not archives: raise Exit(f"No archive files found in {directory}!") for archive in archives: with tmpdir() as tmp: # Make temp venv builder.create(tmp) # Obligatory: make inner pip match outer pip (version obtained from # this file's executable env, up in import land); very frequently # venv-made envs have a bundled, older pip :( envbin = Path(tmp) / "bin" pip = envbin / "pip" c.run(f"{pip} install pip=={pip_version}") # Does the package under test install cleanly? c.run(f"{pip} install --disable-pip-version-check {archive}") # Can we actually import it? (Will catch certain classes of # import-time-but-not-install-time explosions, eg busted dependency # specifications or imports). if not skip_import: package = _find_package(c) # Import, generally c.run(f"{envbin / 'python'} -c 'import {package}'") # Import, typecheck version (ie dependent package typechecking # both itself and us). Assumes task is run from project root. # TODO: is py.typed still mandatory these days? pytyped = Path(package) / "py.typed" if pytyped.exists(): # TODO: pin a specific mypy version? c.run(f"{envbin / 'pip'} install mypy") # Use some other dir (our cwd is probably the project root, # whose local $package dir may confuse mypy into a false # positive!) with tmpdir() as tmp2: mypy_check = f"{envbin / 'mypy'} -c 'import {package}'" c.run(f"cd {tmp2} && {mypy_check}") if verbose: c.config.run.hide = old_hide def get_archives(directory: Union[str, Path]) -> list[Path]: """ Obtain list of archive filenames, then ensure any wheels come first so their improved metadata is what PyPI sees initially (otherwise, it only honors the sdist's lesser data). """ target = Path(directory) return sorted(target.glob("*.whl")) + sorted(target.glob("*.tar.gz")) @task def upload(c, directory, index=None, sign=False, dry_run=False): """ Upload (potentially also signing) all artifacts in ``directory/dist``. :param str index: Custom upload index/repository name. By default, uses whatever the invoked ``pip`` is configured to use. Modify your ``pypirc`` file to add new named repositories. :param bool sign: Whether to sign the built archive(s) via GPG. :param bool dry_run: Skip actual publication step (and dry-run actions like signing) if ``True``. This also prevents cleanup of the temporary build/dist directories, so you can examine the build artifacts. """ archives = get_archives(directory) # Sign each archive in turn # NOTE: twine has a --sign option but it's not quite flexible enough & # doesn't allow you to dry-run or upload manually when API is borked... if sign: prompt = "Please enter GPG passphrase for signing: " passphrase = "" if dry_run else getpass.getpass(prompt) input_ = StringIO(passphrase + "\n") gpg_bin = find_gpg(c) if not gpg_bin: raise Exit( "You need to have one of `gpg`, `gpg1` or `gpg2` " "installed to GPG-sign!" ) for archive in archives: cmd = "{} --detach-sign --armor --passphrase-fd=0 --batch --pinentry-mode=loopback {{}}".format( # noqa gpg_bin ) c.run(cmd.format(archive), in_stream=input_, dry=dry_run) input_.seek(0) # So it can be replayed by subsequent iterations # Upload parts = ["twine", "upload"] if index: parts.append(f"--repository {index}") paths = [str(x) for x in archives] if sign and not dry_run: paths.append(os.path.join(directory, "*.asc")) parts.extend(paths) cmd = " ".join(parts) if dry_run: print(f"Would publish via: {cmd}") print("Files that would be published:") c.run(f"ls -l {' '.join(paths)}") else: c.run(cmd) @task def push(c, dry_run=False): """ Push current branch and tags to default Git remote. """ # Push tags, not just branches; and at this stage pre-push hooks will be # more trouble than they're worth. opts = "--follow-tags --no-verify" # Dry run: echo, and either tack on git's own dry-run (if not CI) or # dry-run the run() itself (if CI - which probably can't push to the remote # and might thus error uselessly) kwargs = dict() if dry_run: kwargs["echo"] = True if in_ci(): kwargs["dry"] = True else: opts += " --dry-run" c.run("git push {}".format(opts), **kwargs) # TODO: still need time to solve the 'just myself pls' problem ns = Collection( "release", all_, status, prepare, build, publish, push, test_install, upload, ) # Hide stdout by default, preferring to explicitly enable it when necessary. ns.configure({"run": {"hide": "stdout"}}) pyinvoke-invocations-ea947a4/invocations/packaging/semantic_version_monkey.py000066400000000000000000000020551504415344000301160ustar00rootroot00000000000000""" Monkey patches for ``semantic_version.Version``. We never like monkey-patching, but for now this is easier than either vendoring or distributing our own fork. """ from semantic_version import Version def clone(self): """ Return a new copy of this Version object. Useful when you need to generate a new object that can be mutated separately from the original. """ return Version(str(self)) Version.clone = clone def next_minor(self): """ Return a Version whose minor number is one greater than self's. .. note:: The new Version will always have a zeroed-out bugfix/tertiary version number, because the "next minor release" of e.g. 1.2.1 is 1.3.0, not 1.3.1. """ clone = self.clone() clone.minor += 1 clone.patch = 0 return clone Version.next_minor = next_minor def next_patch(self): """ Return a Version whose patch/bugfix number is one greater than self's. """ clone = self.clone() clone.patch += 1 return clone Version.next_patch = next_patch pyinvoke-invocations-ea947a4/invocations/packaging/vendorize.py000066400000000000000000000104571504415344000251760ustar00rootroot00000000000000""" Tasks for importing external code into a vendor subdirectory. """ from os import chdir from pathlib import Path from shutil import copy, copytree, rmtree from invoke import task from ..util import tmpdir def _unpack(c, tmp, package, version, git_url=None): """ Download + unpack given package into temp dir ``tmp``. Return ``(real_version, source)`` where ``real_version`` is the "actual" version downloaded (e.g. if a Git main branch was indicated, it will be the SHA of ``main`` HEAD) and ``source`` is the source directory (relative to unpacked source) to import into ``/vendor``. """ real_version = version[:] source = None if git_url: pass # git clone into tempdir # git checkout # set target to checkout # if version does not look SHA-ish: # in the checkout, obtain SHA from that branch # set real_version to that value else: cwd = Path.cwd() print(f"Moving into temp dir {tmp}") chdir(tmp) try: # Nab from index. Skip wheels; we want to unpack an sdist. flags = "--download=. --build=build --no-use-wheel" cmd = f"pip install {flags} {package}=={version}" c.run(cmd) # Identify basename # TODO: glob is bad here because pip install --download gets all # dependencies too! ugh. Figure out best approach for that. globs = [] globexpr = "" for extension, opener in ( ("zip", "unzip"), ("tgz", "tar xzvf"), ("tar.gz", "tar xzvf"), ): globexpr = "*.{}".format(extension) globs = cwd.glob(globexpr) if globs: break archive = globs[0].name # TODO: weird how there's no "mega-.stem" in Pathlib, o well source, _, _ = archive.rpartition(".{}".format(extension)) c.run("{} {}".format(opener, globexpr)) finally: chdir(cwd) return real_version, source @task def vendorize( c, distribution, version, vendor_dir, package=None, git_url=None, license=None, ): """ Vendorize Python package ``distribution`` at version/SHA ``version``. Specify the vendor folder (e.g. ``/vendor``) as ``vendor_dir``. For Crate/PyPI releases, ``package`` should be the name of the software entry on those sites, and ``version`` should be a specific version number. E.g. ``vendorize('lexicon', '0.1.2')``. For Git releases, ``package`` should be the name of the package folder within the checkout that needs to be vendorized and ``version`` should be a Git identifier (branch, tag, SHA etc.) ``git_url`` must also be given, something suitable for ``git clone ``. For SVN releases: xxx. For packages where the distribution name is not the same as the package directory name, give ``package='name'``. By default, no explicit license seeking is done -- we assume the license info is in file headers or otherwise within the Python package vendorized. This is not always true; specify ``license=/path/to/license/file`` to trigger copying of a license into the vendored folder from the checkout/download (relative to its root.) """ with tmpdir() as tmp: package = package or distribution target = Path(vendor_dir) / package # Unpack source real_version, source = _unpack(c, tmp, distribution, version, git_url) abs_source = tmp / source source_package = abs_source / package # Ensure source package exists if not source_package.exists(): rel_package = source_package.relative_to(Path.cwd()) raise ValueError(f"Source package {rel_package} doesn't exist!") # Nuke target if exists if target.exists(): print(f"Removing pre-existing vendorized folder {target}") rmtree(target) # Perform the copy print(f"Copying {source_package} => {target}") copytree(source_package, target) # Explicit license if needed if license: copy(abs_source / license, target) # git commit -a -m "Update $package to $version ($real_version if different)" # noqa pyinvoke-invocations-ea947a4/invocations/pytest.py000066400000000000000000000116211504415344000225670ustar00rootroot00000000000000""" Pytest-using variant of testing.py. Will eventually replace the latter. """ from invoke import task @task def test( c, verbose=True, color=True, capture="sys", module=None, k=None, x=False, opts="", pty=True, warnings=True, ): """ Run pytest with given options. :param bool verbose: Whether to run tests in verbose mode. :param bool color: Whether to request colorized output (typically only works when ``verbose=True``.) :param str capture: What type of stdout/err capturing pytest should use. Defaults to ``sys`` since pytest's own default, ``fd``, tends to trip up subprocesses trying to detect PTY status. Can be set to ``no`` for no capturing / useful print-debugging / etc. :param str module: Select a specific test module to focus on, e.g. ``main`` to only run ``tests/main.py``. (Note that this is a specific idiom aside from the use of ``-o '-k pattern'``.) Default: ``None``. :param str k: Convenience passthrough for ``pytest -k``, i.e. test selection. Default: ``None``. :param bool x: Convenience passthrough for ``pytest -x``, i.e. fail-fast. Default: ``False``. :param str opts: Extra runtime options to hand to ``pytest``. :param bool pty: Whether to use a pty when executing pytest. Default: ``True``. :param bool warnings: Inverse alias for the pytest ``--disable_warnings`` flag; when this is False (i.e. called on CLI as ``--no-warnings``), ``--disable-warnings`` will be given. Default: ``True``. .. versionadded:: 2.0 """ # TODO: really need better tooling around these patterns # TODO: especially the problem of wanting to be configurable, but # sometimes wanting to override one's config via kwargs; and also needing # non-None defaults in the kwargs to inform the parser (or have to # configure it explicitly...?) flags = [] if verbose: flags.append("--verbose") if color: flags.append("--color=yes") flags.append("--capture={}".format(capture)) if opts: flags.append(opts) if k is not None and not ("-k" in opts if opts else False): flags.append("-k '{}'".format(k)) if x and not ("-x" in opts if opts else False): flags.append("-x") if not warnings and not ("--disable-warnings" in opts if opts else False): flags.append("--disable-warnings") modstr = "" if module is not None: modstr = " tests/{}.py".format(module) c.run("pytest {}{}".format(" ".join(flags), modstr), pty=pty) @task(help=test.help) def integration( c, opts=None, pty=True, x=False, k=None, verbose=True, color=True, capture="sys", module=None, ): """ Run the integration test suite. May be slow! See ``pytest.test`` for description of most arguments. """ opts = opts or "" opts += " integration/" if module is not None: opts += "{}.py".format(module) test( c, opts=opts, pty=pty, x=x, k=k, verbose=verbose, color=color, capture=capture, ) @task(iterable=["additional_testers"]) def coverage( c, report="term", opts="", tester=None, codecov=False, additional_testers=None, ): """ Run pytest with coverage enabled. Assumes the ``pytest-cov`` pytest plugin is installed. :param str report: Coverage report style to use. If 'html', will also open in browser. :param str opts: Extra runtime opts to pass to pytest. :param tester: Specific test task object to invoke. If ``None`` (default), uses this module's local `test`. :param bool codecov: Whether to build XML and upload to Codecov. Requires ``codecov`` tool. Default: ``False``. :param additional_testers: List of additional test functions to call besides ``tester``. If given, implies the use of ``--cov-append`` on these subsequent test runs. .. versionchanged:: 2.4 Added the ``additional_testers`` argument. """ my_opts = "--cov --no-cov-on-fail --cov-report={}".format(report) if opts: my_opts += " " + opts # TODO: call attached suite's test(), not the one in here, if they differ # TODO: arguably wants ability to lookup task string when tester(s) given # on CLI, but, eh (tester or test)(c, opts=my_opts) if additional_testers: my_opts += " --cov-append" for tester in additional_testers: tester(c, opts=my_opts) if report == "html": c.run("open htmlcov/index.html") if codecov: # Generate XML report from that already-gathered data (otherwise # codecov generates it on its own and gets it wrong!) c.run("coverage xml") # Upload to Codecov c.run("codecov") pyinvoke-invocations-ea947a4/invocations/testing.py000066400000000000000000000134261504415344000227210ustar00rootroot00000000000000import sys import time from collections import defaultdict from invoke import task from tqdm import tqdm from .watch import watch @task( help={ "module": "Just runs tests/STRING.py.", "runner": "Use STRING to run tests instead of 'spec'.", "opts": "Extra flags for the test runner", "pty": "Whether to run tests under a pseudo-tty", } ) def test(c, module=None, runner=None, opts=None, pty=True): """ Run a Spec or Nose-powered internal test suite. """ runner = runner or "spec" # Allow selecting specific submodule specific_module = f" --tests=tests/{module}.py" args = specific_module if module else "" if opts: args += " " + opts # Always enable timing info by default. OPINIONATED args += " --with-timing" # Allow client to configure some other Nose-related things. logformat = c.config.get("tests", {}).get("logformat", None) if logformat is not None: args += f" --logging-format='{logformat}'" # Use pty by default so the spec/nose/Python process buffers "correctly" c.run(runner + args, pty=pty) @task(help=test.help) def integration(c, module=None, runner=None, opts=None, pty=True): """ Run the integration test suite. May be slow! """ opts = opts or "" override = " --tests=integration/" if module: override += f"{module}.py" opts += override test(c, runner=runner, opts=opts, pty=pty) @task def watch_tests(c, module=None, opts=None): """ Watch source tree and test tree for changes, rerunning tests as necessary. Honors ``tests.package`` setting re: which source directory to watch for changes. """ package = c.config.get("tests", {}).get("package") patterns = [r"\./tests/"] if package: patterns.append(r"\./{}/".format(package)) kwargs = {"module": module, "opts": opts} # Kick things off with an initial test (making sure it doesn't exit on its # own if tests currently fail) c.config.run.warn = True test(c, **kwargs) # Then watch watch(c, test, patterns, [r".*/\..*\.swp"], **kwargs) @task def coverage(c, html=True, integration_=True): """ Run tests w/ coverage enabled, optionally generating HTML & opening it. :param bool html: Whether to generate & open an HTML report. Default: ``True``. :param bool integration_: Whether to run integration test suite (``integration/``) in addition to unit test suite (``tests/``). Default: ``True``. """ if not c.run("which coverage", hide=True, warn=True).ok: sys.exit("You need to 'pip install coverage' to use this task!") # Generate actual coverage data. NOTE: this will honor a local .coveragerc test_opts = "--with-coverage" test(c, opts=test_opts) # Coverage naturally accumulates unless --cover-erase is used - so the # resulting .coverage file parsed by 'coverage html' will contain the union # of both suites, if integration suite is run too. if integration_: integration(c, opts=test_opts) if html: c.run("coverage html && open htmlcov/index.html") # TODO: rename to like find_errors or something more generic @task def count_errors(c, command, trials=10, verbose=False, fail_fast=False): """ Run ``command`` multiple times and tally statistics about failures. Use Ctrl-C or other SIGINT to abort early (also see ``fail_fast``.) :param str command: The command to execute. Make sure to escape special shell characters! :param int trials: Number of trials to execute (default 10.) :param bool verbose: Whether to emit stdout/err from failed runs at end of execution. Default: ``False``. :param bool fail_fast: Whether to exit after the first error (i.e. "count runs til error is exhibited" mode.) Default: ``False``. Say ``verbose=True`` to see stderr from failed runs at the end. Say ``--fail-fast`` to error out, with error output, on the first error. """ # TODO: allow defining failure as something besides "exited 1", e.g. # "stdout contained " or whatnot goods, bads = [], [] prev_error = time.time() for num_runs in tqdm(range(trials), unit="trial"): result = c.run(command, hide=True, warn=True) if result.failed: now = time.time() result.since_prev_error = int(now - prev_error) prev_error = now bads.append(result) # -2 is typically indicative of SIGINT in most shells if fail_fast or result.exited == -2: break else: goods.append(result) num_runs += 1 # for count starting at 1, not 0 if verbose or fail_fast: # TODO: would be nice to show interwoven stdout/err but I don't believe # we track that at present... for result in bads: print("") print(result.stdout) print(result.stderr) # Stats! TODO: errors only jeez successes = len(goods) failures = len(bads) overall = "{}/{} trials failed".format(failures, num_runs) # Short-circuit if no errors if not bads: print(overall) return periods = [x.since_prev_error for x in bads] # Period mean mean = int(sum(periods) / float(len(periods))) # Period mode # TODO: use collections.Counter now that we've dropped 2.6 counts = defaultdict(int) for period in periods: counts[period] += 1 mode = sorted((value, key) for key, value in counts.items())[-1][1] # Emission of stats! if fail_fast: print("First failure occurred after {} successes".format(successes)) else: print(overall) print( "Stats: min={}s, mean={}s, mode={}s, max={}s".format( min(periods), mean, mode, max(periods) ) ) pyinvoke-invocations-ea947a4/invocations/util.py000066400000000000000000000011741504415344000222160ustar00rootroot00000000000000from contextlib import contextmanager from shutil import rmtree from tempfile import mkdtemp @contextmanager def tmpdir(skip_cleanup=False, explicit=None): """ Context-manage a temporary directory. Can be given ``skip_cleanup`` to skip cleanup, and ``explicit`` to choose a specific location. (If both are given, this is basically not doing anything, but it allows code that normally requires a secure temporary directory to 'dry run' instead.) """ tmp = explicit if explicit is not None else mkdtemp() try: yield tmp finally: if not skip_cleanup: rmtree(tmp) pyinvoke-invocations-ea947a4/invocations/watch.py000066400000000000000000000023601504415344000223450ustar00rootroot00000000000000""" File-watching subroutines, built on watchdog. """ import sys import time def make_handler(ctx, task_, regexes, ignore_regexes, *args, **kwargs): args = [ctx] + list(args) try: from watchdog.events import RegexMatchingEventHandler except ImportError: sys.exit("If you want to use this, 'pip install watchdog' first.") class Handler(RegexMatchingEventHandler): def on_any_event(self, event): try: task_(*args, **kwargs) except BaseException: pass return Handler(regexes=regexes, ignore_regexes=ignore_regexes) def observe(*handlers): try: from watchdog.observers import Observer except ImportError: sys.exit("If you want to use this, 'pip install watchdog' first.") observer = Observer() # TODO: Find parent directory of tasks.py and use that. for handler in handlers: observer.schedule(handler, ".", recursive=True) observer.start() try: while True: time.sleep(1) except KeyboardInterrupt: observer.stop() observer.join() def watch(c, task_, regexes, ignore_regexes, *args, **kwargs): observe(make_handler(c, task_, regexes, ignore_regexes, *args, **kwargs)) pyinvoke-invocations-ea947a4/pyproject.toml000066400000000000000000000051541504415344000212510ustar00rootroot00000000000000[project] name = "invocations" requires-python = ">=3.9" version = "4.0.2" description = "Common/best-practice Invoke tasks and collections" readme = "README.rst" license = "BSD-2-Clause" license-files = ["LICENSE"] authors = [{name = "Jeff Forcier", email = "jeff@bitprophet.org"}] dependencies = [ # Core dependency "invoke>=1.7.2", # Dependencies for various subpackages. # NOTE: these used to be all optional (only complained about at import # time if missing), but that got hairy fast, and these are all # pure-Python packages, so it shouldn't be a huge burden for users to # obtain them. "blessings>=1.6", "build>=1.3", # For envs that don't actually have pip - we use some of its tooling atm. "pip~=25.2", "releases>=1.6", "semantic_version>=2.4,<2.7", "tabulate>=0.7.5", "tqdm>=4.8.1", "twine>=1.15", "wheel>=0.24.0", ] classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "Operating System :: POSIX", "Operating System :: Unix", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development", "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Software Distribution", "Topic :: System :: Systems Administration", ] [project.urls] Homepage = "https://invocations.readthedocs.io" Source = "https://github.com/pyinvoke/invocations" Changelog = "https://invocations.readthedocs.io/en/latest/changelog.html" CI = "https://app.circleci.com/pipelines/github/pyinvoke/invocations" Issues = "https://github.com/pyinvoke/invocations/issues" [build-system] requires = ["setuptools >= 77"] build-backend = "setuptools.build_meta" [dependency-groups] dev = [ # For testing "pytest-relaxed>=2", "invoke>=2", "releases>=2.0.1", "pytest-cov==2.4.0", "pytest-mock==3.2.0", # Oldest version that works on Python >3.11 "watchdog==1.0.2", "coverage==4.4.2", "icecream==2.1.3", # Formatting "black==22.12.0", # Linting "flake8~=7.3.0", ] pyinvoke-invocations-ea947a4/pytest.ini000066400000000000000000000000541504415344000203600ustar00rootroot00000000000000[pytest] testpaths = tests python_files = * pyinvoke-invocations-ea947a4/tasks.py000066400000000000000000000011751504415344000200330ustar00rootroot00000000000000from invoke import Collection from invocations import docs, checks from invocations.packaging import release from invocations.pytest import test, coverage ns = Collection(release, test, coverage, docs, checks.blacken, checks) ns.configure( { "packaging": {"wheel": True, "changelog_file": "docs/changelog.rst"}, "blacken": {"find_opts": r"-and -not -path '*.cci_pycache*'"}, "run": { "env": { # Our ANSI color tests test against hardcoded codes appropriate # for this terminal, for now. "TERM": "xterm-256color" }, }, } ) pyinvoke-invocations-ea947a4/tests/000077500000000000000000000000001504415344000174725ustar00rootroot00000000000000pyinvoke-invocations-ea947a4/tests/autodoc/000077500000000000000000000000001504415344000211305ustar00rootroot00000000000000pyinvoke-invocations-ea947a4/tests/autodoc/_support/000077500000000000000000000000001504415344000230035ustar00rootroot00000000000000pyinvoke-invocations-ea947a4/tests/autodoc/_support/conf.py000066400000000000000000000004031504415344000242770ustar00rootroot00000000000000from os.path import dirname import sys # Add local support dir to path so tasks modules may be imported by autodoc sys.path.insert(0, dirname(__file__)) master_doc = "index" extensions = ["invocations.autodoc"] autodoc_default_options = dict(members=True) pyinvoke-invocations-ea947a4/tests/autodoc/_support/docs/000077500000000000000000000000001504415344000237335ustar00rootroot00000000000000pyinvoke-invocations-ea947a4/tests/autodoc/_support/docs/api.rst000066400000000000000000000000451504415344000252350ustar00rootroot00000000000000=== API === .. automodule:: mytasks pyinvoke-invocations-ea947a4/tests/autodoc/_support/docs/index.rst000066400000000000000000000000451504415344000255730ustar00rootroot00000000000000==== Test ==== .. toctree:: api pyinvoke-invocations-ea947a4/tests/autodoc/_support/mytasks.py000066400000000000000000000006511504415344000250520ustar00rootroot00000000000000""" Some fake tasks to test task autodoc. """ from invoke import task def not_a_task(c): """ I am a regular function. """ pass @task def undocumented(c): # I have no docstring so I may not show up. Or...may I?! pass @task def base_case(c): """ Literally the smallest possible task. """ pass @task def simple_case(c, simple_arg): """ Parameterization! """ pass pyinvoke-invocations-ea947a4/tests/autodoc/base.py000066400000000000000000000053001504415344000224120ustar00rootroot00000000000000from os.path import join, dirname import re import shutil from unittest.mock import Mock from invoke import Context from invocations.autodoc import setup as our_setup, TaskDocumenter def _build(): """ Build local support docs tree and return the build target dir for cleanup. """ c = Context() support = join(dirname(__file__), "_support") docs = join(support, "docs") build = join(support, "_build") command = "sphinx-build -c {} -W {} {}".format(support, docs, build) with c.cd(support): # Turn off stdin mirroring to avoid irritating pytest. c.run(command, in_stream=False) return build class autodoc_: @classmethod def setup_class(self): # Build once, introspect many...for now self.build_dir = _build() with open(join(self.build_dir, "api.html")) as fd: self.api_docs = fd.read() @classmethod def teardown_class(self): shutil.rmtree(self.build_dir, ignore_errors=True) def setup_requires_autodoc_and_adds_autodocumenter(self): app = Mock() our_setup(app) app.setup_extension.assert_called_once_with("sphinx.ext.autodoc") app.add_autodocumenter.assert_called_once_with(TaskDocumenter) def module_docstring_unmodified(self): # Just a sanity test, really. assert "Some fake tasks to test task autodoc." in self.api_docs def regular_functions_only_appear_once(self): # Paranoid sanity check re: our # very-much-like-FunctionDocumenter-documenter not accidentally loading # up non-task objects (and thus having them autodoc'd twice: once # regularly and once incorrectly 'as tasks'). SHRUG. # NOTE: as of Sphinx 5.2, ToC now shows name of object too, so we test # for the identifier and the docstring separately (and expect the 1st # twice) assert len(re.findall(">not_a_task", self.api_docs)) == 2 assert len(re.findall(">I am a regular function", self.api_docs)) == 1 def undocumented_members_do_not_appear_by_default(self): # This really just tests basic Sphinx/autodoc stuff for now...meh assert "undocumented" not in self.api_docs def base_case_of_no_argument_docstringed_task(self): for sentinel in ("base_case", "smallest possible task"): assert sentinel in self.api_docs def simple_case_of_single_argument_task(self): # TODO: OK we really need something that scales better soon re: # viewing the output as a non-HTML string / something that is not # super tied to sphinx/theme output...heh for sentinel in ("simple_case", "simple_arg", "Parameterization!"): assert sentinel in self.api_docs pyinvoke-invocations-ea947a4/tests/checks.py000066400000000000000000000074411504415344000213120ustar00rootroot00000000000000from unittest.mock import call import pytest from invocations.checks import blacken, lint, all_ as all_task class checks: class blacken_: @pytest.mark.parametrize( "kwargs,command", [ (dict(), "find . -name '*.py' | xargs black -l 79"), ( dict(line_length=80), "find . -name '*.py' | xargs black -l 80", ), ( dict(folders=["foo", "bar"]), "find foo bar -name '*.py' | xargs black -l 79", ), ( # Explicit invocation that matches a default CLI # invocation, since 'folders' is an iterable and thus shows # up as an empty list in real life. Ehhh. dict(folders=[]), "find . -name '*.py' | xargs black -l 79", ), ( dict(check=True), "find . -name '*.py' | xargs black -l 79 --check", ), ( dict(diff=True), "find . -name '*.py' | xargs black -l 79 --diff", ), ( dict( diff=True, check=True, line_length=80, folders=["foo", "bar"], ), "find foo bar -name '*.py' | xargs black -l 80 --check --diff", # noqa ), ( dict(find_opts="-and -not -name foo"), "find . -name '*.py' -and -not -name foo | xargs black -l 79", # noqa ), ], ids=[ "base case is all files and 79 characters", "line length controllable", "folders controllable", "folders real default value", "check flag passed through", "diff flag passed through", "most args combined", "find opts controllable", ], ) def runs_black(self, ctx, kwargs, command): blacken(ctx, **kwargs) ctx.run.assert_called_once_with(command, pty=True) def folders_configurable(self, ctx): # Just config -> works fine ctx.blacken = dict(folders=["elsewhere"]) blacken(ctx) assert "elsewhere" in ctx.run_command def folders_config_loses_to_runtime(self, ctx): # Config + CLI opt -> CLI opt wins ctx.blacken = dict(folders=["nowhere"]) blacken(ctx, folders=["nowhere"]) assert "nowhere" in ctx.run_command def find_opts_configurable(self, ctx): ctx.blacken = dict(find_opts="-and -not -name foo.py") blacken(ctx) assert ( "find . -name '*.py' -and -not -name foo.py" in ctx.run_command ) def find_opts_config_loses_to_runtime(self, ctx): ctx.blacken = dict(find_opts="-and -not -name foo.py") blacken(ctx, find_opts="-or -name '*.js'") assert "find . -name '*.py' -or -name '*.js'" in ctx.run_command def aliased_to_format(self): assert blacken.aliases == ["format"] class lint_: def runs_flake8_by_default(self, ctx): lint(ctx) assert ctx.run_command == "flake8" class all_: def runs_blacken_and_lint(self, ctx): all_task(ctx) assert ctx.run.call_args_list == [ call("find . -name '*.py' | xargs black -l 79", pty=True), call("flake8", pty=True, warn=True), ] def is_default_task(self): assert all_task.is_default pyinvoke-invocations-ea947a4/tests/conftest.py000066400000000000000000000070661504415344000217020ustar00rootroot00000000000000from pathlib import Path from unittest.mock import patch, Mock, MagicMock, call from pytest import fixture from invoke import MockContext # Set up icecream globally for convenience. from icecream import install install() @fixture def ctx(): # TODO: this would be a nice convenience in MockContext itself, though most # uses of it really just want responses-style "assert if expected calls did # not happen" behavior MockContext.run_command = property(lambda self: self.run.call_args[0][0]) return MockContext(run=True) class Mocks: pass # For use in packaging.release.publish tests @fixture def fakepub(mocker): mocks = Mocks() mocks.rmtree = mocker.patch("invocations.util.rmtree") mocks.twine_check = mocker.patch( "invocations.packaging.release.twine_check", return_value=False ) mocks.upload = mocker.patch("invocations.packaging.release.upload") mocks.build = mocker.patch("invocations.packaging.release.build") mocks.test_install = mocker.patch( "invocations.packaging.release.test_install" ) mocks.mkdtemp = mocker.patch("invocations.util.mkdtemp") mocks.mkdtemp.return_value = "tmpdir" c = MockContext(run=True) yield c, mocks # For use in packaging.release.test_install tests @fixture def install(): with patch("invocations.packaging.release.pip_version", "lmao"), patch( "invocations.util.rmtree", Mock("rmtree") ), patch( "invocations.packaging.release._find_package", lambda c: "foo" ), patch( "venv.EnvBuilder" ) as builder, patch( "invocations.util.mkdtemp" ) as mkdtemp, patch( "invocations.packaging.release.get_archives" ) as get_archives, patch( "invocations.packaging.release.Path" ) as fakePath: # Setup & run c = MockContext(run=True, repeat=True) mkdtemp.return_value = "tmpdir" get_archives.return_value = ["foo.tgz", "foo.whl"] # I hate this but don't see a cleaner way to mock out a nested # 'exists()' w/o breaking everything else, or using a real tmpdir. def set_exists(value): def fakediv(self, arg): root = Path(mkdtemp.return_value) bindir = root / "bin" if arg == "bin": return bindir elif arg == "pip": return bindir / "pip" elif arg == "python": return bindir / "python" elif arg == "py.typed": path = Path("foo") / "py.typed" ret = MagicMock(wraps=path) ret.exists.return_value = value return ret fakePath.return_value.__truediv__ = fakediv c.set_exists = set_exists # so caller can run it c.set_exists(False) # default yield c # Create factory builder.assert_called_once_with(with_pip=True) # Used helper to get artifacts get_archives.assert_called_once_with("whatever") # venv factory ran twice in some temp dir builder.return_value.create.assert_has_calls( [call("tmpdir"), call("tmpdir")] ) pip_base = "tmpdir/bin/pip install --disable-pip-version-check" for wanted in ( # Pip installed to same version as running interpreter's pip call("tmpdir/bin/pip install pip==lmao"), # Archives installed into venv call("{} foo.tgz".format(pip_base)), call("{} foo.whl".format(pip_base)), ): assert wanted in c.run.mock_calls pyinvoke-invocations-ea947a4/tests/console.py000066400000000000000000000035421504415344000215120ustar00rootroot00000000000000import sys from unittest.mock import patch from pytest_relaxed import trap from invocations.console import confirm class confirm_: @patch("invocations.console.input", return_value="yes") def displays_question_with_yes_no_suffix(self, mock_input): confirm("Are you sure?") assert mock_input.call_args[0][0] == "Are you sure? [Y/n] " @patch("invocations.console.input") def returns_True_for_yeslike_responses(self, mock_input): for value in ("y", "Y", "yes", "YES", "yES", "Yes"): mock_input.return_value = value assert confirm("Meh") is True @patch("invocations.console.input") def returns_False_for_nolike_responses(self, mock_input): for value in ("n", "N", "no", "NO", "nO", "No"): mock_input.return_value = value assert confirm("Meh") is False @trap @patch("invocations.console.input", side_effect=["wat", "y"]) def reprompts_on_bad_input(self, mock_input): assert confirm("O rly?") is True assert "I didn't understand you" in sys.stderr.getvalue() @patch("invocations.console.input", return_value="y") def suffix_changes_when_assume_yes_False(self, mock_input): confirm("Are you sure?", assume_yes=False) assert mock_input.call_args[0][0] == "Are you sure? [y/N] " @patch("invocations.console.input", return_value="") def default_on_empty_response_is_True_by_default(self, mock_input): assert confirm("Are you sure?") is True @patch("invocations.console.input", return_value="") def default_on_empty_response_is_False_if_assume_yes_False( self, mock_input ): assert confirm("Are you sure?", assume_yes=False) is False @patch("invocations.console.input", return_value=" y ") def whitespace_is_trimmed(self, mock_input): assert confirm("Are you sure?") is True pyinvoke-invocations-ea947a4/tests/environment.py000066400000000000000000000021341504415344000224100ustar00rootroot00000000000000from unittest.mock import patch from pytest import mark from invocations.environment import in_ci @mark.parametrize( "environ,expected", [ (dict(), False), (dict(WHATEVS="true", SURE_WHYNOT=""), False), (dict(CIRCLECI=""), False), (dict(TRAVIS=""), False), (dict(CIRCLECI="", WHATEVS="yo"), False), (dict(CIRCLECI="", TRAVIS=""), False), (dict(CIRCLECI="true"), True), (dict(CIRCLECI="false"), True), # yup (dict(CIRCLECI="no"), True), (dict(CIRCLECI="1"), True), (dict(CIRCLECI="0"), True), (dict(TRAVIS="true"), True), (dict(CIRCLECI="true", TRAVIS=""), True), (dict(CIRCLECI="", TRAVIS="true"), True), (dict(CIRCLECI="true", TRAVIS="true"), True), (dict(CIRCLECI="false", TRAVIS="no"), True), (dict(CIRCLECI="true", WHATEVS=""), True), (dict(CIRCLECI="true", WHATEVS="huh?"), True), ], ) def in_ci_true_when_any_expected_vars_nonempty(environ, expected): with patch("invocations.environment.os.environ", environ): assert in_ci() is expected pyinvoke-invocations-ea947a4/tests/main.py000066400000000000000000000002351504415344000207700ustar00rootroot00000000000000from importlib import metadata import invocations def package_has_dunder_version(): assert invocations.__version__ == metadata.version("invocations") pyinvoke-invocations-ea947a4/tests/packaging/000077500000000000000000000000001504415344000214165ustar00rootroot00000000000000pyinvoke-invocations-ea947a4/tests/packaging/_support/000077500000000000000000000000001504415344000232715ustar00rootroot00000000000000pyinvoke-invocations-ea947a4/tests/packaging/_support/conf.py000066400000000000000000000000001504415344000245560ustar00rootroot00000000000000pyinvoke-invocations-ea947a4/tests/packaging/_support/index.rst000066400000000000000000000000621504415344000251300ustar00rootroot00000000000000.. Dummy index file so nearby changelogs can load pyinvoke-invocations-ea947a4/tests/packaging/_support/no_unreleased_1.1_bugs.rst000066400000000000000000000004051504415344000302440ustar00rootroot00000000000000========= Changelog ========= * :release:`1.1.2 <2016-10-24>` * :release:`1.0.2 <2016-10-24>` * :bug:`2` Yup. * :release:`1.1.1 <2016-10-17>` * :release:`1.0.1 <2014-01-02>` * :bug:`1` Fix a bug. * :release:`1.1.0 <2014-01-01>` * :release:`1.0.0 <2014-01-01>` pyinvoke-invocations-ea947a4/tests/packaging/_support/no_unreleased_1.x_features.rst000066400000000000000000000002631504415344000312330ustar00rootroot00000000000000========= Changelog ========= * :release:`1.1.0 <2016-10-17>` * :release:`1.0.1 <2014-01-02>` * :feature:`2` New! Improved! * :bug:`1` Fix a bug. * :release:`1.0.0 <2014-01-01>` pyinvoke-invocations-ea947a4/tests/packaging/_support/unreleased_1.1_bugs.rst000066400000000000000000000003451504415344000275530ustar00rootroot00000000000000========= Changelog ========= * :release:`1.0.2 <2016-10-24>` * :bug:`2` Yup. * :release:`1.1.1 <2016-10-24>` * :release:`1.0.1 <2014-01-02>` * :bug:`1` Fix a bug. * :release:`1.1.0 <2014-01-01>` * :release:`1.0.0 <2014-01-01>` pyinvoke-invocations-ea947a4/tests/packaging/_support/unreleased_1.x_features.rst000066400000000000000000000002711504415344000305360ustar00rootroot00000000000000========= Changelog ========= .. Notably, no release yet for 1.1.0 * :release:`1.0.1 <2014-01-02>` * :feature:`2` New! Improved! * :bug:`1` Fix a bug. * :release:`1.0.0 <2014-01-01>` pyinvoke-invocations-ea947a4/tests/packaging/release.py000066400000000000000000001331441504415344000234160ustar00rootroot00000000000000from contextlib import contextmanager from os import path from pathlib import Path import re import sys from invoke.vendor.lexicon import Lexicon from invoke import MockContext, Result, Config, Exit from docutils.utils import Reporter from unittest.mock import patch, call import pytest from pytest import skip from pytest_relaxed import trap, raises from invocations.packaging.semantic_version_monkey import Version from invocations.packaging.release import ( Changelog, Release, Tag, UndefinedReleaseType, VersionFile, _latest_and_next_version, _latest_feature_bucket, _release_and_issues, _release_line, all_, prepare, push, build, publish, status, upload, test_install as install_test_task, # to avoid pytest treating as test func ns as release_ns, ) class release_line_: def assumes_bugfix_if_release_branch(self): c = MockContext(run=Result("2.7")) assert _release_line(c)[1] == Release.BUGFIX def assumes_feature_if_main(self): c = MockContext(run=Result("main")) assert _release_line(c)[1] == Release.FEATURE def assumes_feature_if_master(self): c = MockContext(run=Result("master")) assert _release_line(c)[1] == Release.FEATURE def is_undefined_if_arbitrary_branch_name(self): c = MockContext(run=Result("yea-whatever")) assert _release_line(c)[1] == Release.UNDEFINED def is_undefined_if_specific_commit_checkout(self): # Just a sanity check; current logic doesn't differentiate between e.g. # 'gobbledygook' and 'HEAD'. c = MockContext(run=Result("HEAD")) assert _release_line(c)[1] == Release.UNDEFINED class latest_feature_bucket_: def base_case_of_single_release_family(self): bucket = _latest_feature_bucket( dict.fromkeys(["unreleased_1_feature"]) ) assert bucket == "unreleased_1_feature" def simple_ordering_by_bucket_number(self): bucket = _latest_feature_bucket( dict.fromkeys(["unreleased_1_feature", "unreleased_2_feature"]) ) assert bucket == "unreleased_2_feature" def ordering_goes_by_numeric_not_lexical_order(self): bucket = _latest_feature_bucket( dict.fromkeys( [ "unreleased_1_feature", # Yes, releases like 10.x or 17.x are unlikely, but # definitely plausible - think modern Firefox for example. "unreleased_10_feature", "unreleased_23_feature", "unreleased_202_feature", "unreleased_17_feature", "unreleased_2_feature", ] ) ) assert bucket == "unreleased_202_feature" class release_and_issues_: class bugfix: # TODO: factor out into setup() so each test has some excluded/ignored # data in it - helps avoid naive implementation returning x[0] etc. def no_unreleased(self): release, issues = _release_and_issues( changelog={"1.1": [], "1.1.0": [1, 2]}, branch="1.1", release_type=Release.BUGFIX, ) assert release == "1.1.0" assert issues == [] def has_unreleased(self): skip() class feature: def no_unreleased(self): # release is None, issues is empty list release, issues = _release_and_issues( changelog={"1.0.1": [1], "unreleased_1_feature": []}, branch="main", release_type=Release.FEATURE, ) assert release is None assert issues == [] def has_unreleased(self): # release is still None, issues is nonempty list release, issues = _release_and_issues( changelog={"1.0.1": [1], "unreleased_1_feature": [2, 3]}, branch="main", release_type=Release.FEATURE, ) assert release is None assert issues == [2, 3] def undefined_always_returns_None_and_empty_list(self): skip() class find_package_: def can_be_short_circuited_with_config_value(self): skip() def seeks_directories_with_init_py_in_em(self): skip() def blacklists_common_non_public_modules(self): skip() def errors_if_cannot_find_anything(self): skip() def errors_if_ambiguous_results(self): # I.e. >1 possible result skip() class latest_and_next_version_: def next_patch_of_bugfix_release(self): versions = _latest_and_next_version( Lexicon( { "release_type": Release.BUGFIX, "latest_line_release": Version("1.2.2"), "latest_overall_release": Version("1.4.1"), # realism! } ) ) assert versions == (Version("1.2.2"), Version("1.2.3")) def next_minor_of_feature_release(self): versions = _latest_and_next_version( Lexicon( { "release_type": Release.FEATURE, "latest_line_release": None, # realism! "latest_overall_release": Version("1.2.2"), } ) ) assert versions == (Version("1.2.2"), Version("1.3.0")) # Multi-dimensional scenarios, in relatively arbitrary nesting order: # - what type of release we're talking about (based on branch name) # - whether there appear to be unreleased issues in the changelog # - comparison of version file contents w/ latest release in changelog # TODO: ... (pypi release, etc) support_dir = Path(__file__).parent / "_support" # Sentinel for targeted __import__ mocking. Is a string so that it can be # expected in tests about the version file, etc. # NOTE: needs to not shadow any real imported module name! FAKE_PACKAGE = "fakey_mcfakerson_not_real_in_any_way" # NOTE: can't easily slap this on the test class itself due to using inner # classes. If we can get the inner classes to not only copy attributes but also # decorators (seems unlikely?), we could organize more "naturally". # NOTE: OTOH, it's actually nice to use this in >1 top level class, so...meh? @contextmanager def _mock_context(self): """ Context manager for a mocked Invoke context + other external patches. Specifically: - Examine test class attributes for configuration; this allows easy multidimensional test setup. - Where possible, the code under test relies on calling shell commands via the Context object, so we pass in a MockContext for that. - Where not possible (eg things which must be Python-level and not shell-level, such as version imports), mock with the 'mock' lib as usual. :yields: an `invoke.context.MockContext` created & modified as described above. """ # # Generate config & context from attrs # changelog_file = "{}.rst".format(self._changelog) config = Config( overrides={ "packaging": { "changelog_file": path.join(support_dir, changelog_file), "package": FAKE_PACKAGE, } } ) tag_output = "" if hasattr(self, "_tags"): tag_output = "\n".join(self._tags) + "\n" # NOTE: Result first posarg is stdout string data. run_results = { # Branch detection "git rev-parse --abbrev-ref HEAD": self._branch, # Changelog update action - just here so it can be called re.compile(r"\$EDITOR.*"): True, # Git tags "git tag": tag_output, # Git status/commit/tagging re.compile("git tag .*"): True, re.compile("git commit.*"): True, # NOTE: some tests will need to override this, for now default to a # result that implies a commit is needed 'git status --porcelain | egrep -v "^\\?"': Result( "M somefile", exited=0 ), } # Make cwd appear to be inside our support dir for eg pyproject.toml with patch( "invocations.packaging.release._read_pyproject_toml" ) as mock_pyproject: mock_pyproject.return_value = dict(project=dict(version=self._version)) yield MockContext(config=config, run=run_results, repeat=True) def _mock_status(self): with _mock_context(self) as c: return status(c) @trap def _expect_actions(self, *actions): _mock_status(self) stdout = sys.stdout.getvalue() for action in actions: # Check for action's text value in the table which gets printed. # (Actual table formatting is tested in an individual test.) err = "Didn't find {} in stdout:\n\n{}".format(action, stdout) assert action.value in stdout, err class status_: class overall_behavior: _branch = "1.1" _changelog = "unreleased_1.1_bugs" _version = "1.1.1" _tags = ("1.1.0", "1.1.1") @trap def displays_expectations_and_component_statuses(self): _mock_status(self) # TODO: make things more organic/specific/less tabular: # # current git branch: xxx (implies type yyy) # changelog: xxx # so the next release would be: a.b.c (or: 'so the release we're # cutting/expecting is a.b.c') # version file: # git tag: (maybe including # latest that is found? that's extra logic...) # etc... parts = dict( changelog=Changelog.NEEDS_RELEASE.value, version=VersionFile.NEEDS_BUMP.value, tag=Tag.NEEDS_CUTTING.value, ) for part in parts: parts[part] = re.escape(parts[part]) parts["header_footer"] = r"-+( +-+)?" # NOTE: forces impl to follow specific order, which is good regex = r""" {header_footer} Changelog +{changelog} Version +{version} Tag +{tag} {header_footer} """.format( **parts ).strip() output = sys.stdout.getvalue() err = "Expected:\n\n{}\n\nGot:\n\n{}".format(regex, output) err += "\n\nRepr edition...\n\n" err += "Expected:\n\n{!r}\n\nGot:\n\n{!r}".format(regex, output) assert re.match(regex, output) is not None, err @trap # just for cleaner test output def returns_lexica_for_reuse(self): actions = Lexicon( changelog=Changelog.NEEDS_RELEASE, version=VersionFile.NEEDS_BUMP, tag=Tag.NEEDS_CUTTING, all_okay=False, ) found_actions, found_state = _mock_status(self) assert found_actions == actions # Spot check state, don't need to check whole thing... assert found_state.branch == self._branch assert found_state.latest_version == Version("1.1.1") assert found_state.tags == [Version(x) for x in self._tags] # TODO: I got this attribute jazz working in pytest but see if there is a # 'native' pytest feature that works better (while still in conjunction # with nested tasks, ideally) class release_line_branch: _branch = "1.1" class unreleased_issues: _changelog = "unreleased_1.1_bugs" class file_version_equals_latest_in_changelog: _version = "1.1.1" class tags_only_exist_for_past_releases: _tags = ("1.1.0", "1.1.1") def changelog_release_version_update_tag_update(self): _expect_actions( self, Changelog.NEEDS_RELEASE, VersionFile.NEEDS_BUMP, Tag.NEEDS_CUTTING, ) class version_file_is_newer: _version = "1.1.2" class tags_only_exist_for_past_releases: _tags = ("1.1.0", "1.1.1") def changelog_release_version_okay_tag_update(self): _expect_actions( self, Changelog.NEEDS_RELEASE, VersionFile.OKAY, Tag.NEEDS_CUTTING, ) class changelog_version_is_newer: _version = "1.1.0" # Undefined situation - unsure how/whether to test class no_unreleased_issues: _changelog = "no_unreleased_1.1_bugs" class file_version_equals_latest_in_changelog: _version = "1.1.2" class tag_for_new_version_present: _tags = ("1.1.0", "1.1.1", "1.1.2") def no_updates_necessary(self): _expect_actions( self, Changelog.OKAY, VersionFile.OKAY, Tag.OKAY ) class tag_for_new_version_missing: _tags = ("1.1.0", "1.1.1") def tag_needs_cutting_still(self): _expect_actions( self, Changelog.OKAY, VersionFile.OKAY, Tag.NEEDS_CUTTING, ) class version_file_out_of_date: _version = "1.1.1" class tag_missing: _tags = ("1.1.0", "1.1.1") # no 1.1.2 def changelog_okay_version_needs_bump_tag_needs_cut(self): _expect_actions( self, Changelog.OKAY, VersionFile.NEEDS_BUMP, Tag.NEEDS_CUTTING, ) # TODO: as in other TODOs, tag can't be expected to exist/be up # to date if any other files are also not up to date. so tag # present but version file out of date, makes no sense, would # be an error. class version_file_is_newer: _version = "1.1.3" def both_technically_okay(self): skip() # see TODO below _expect_actions( self, # TODO: display a 'warning' state noting that your # version outpaces your changelog despite your # changelog having no unreleased stuff in it. Still # "Okay" (no action needed), not an error per se, but # still "strange". Changelog.OKAY, VersionFile.OKAY, ) class main_branch: _branch = "main" class unreleased_issues: _changelog = "unreleased_1.x_features" class file_version_equals_latest_in_changelog: _version = "1.0.1" class latest_tag_same_as_file_version: _tags = ("1.0.0", "1.0.1") def changelog_release_version_update_tag_cut(self): # TODO: do we want some sort of "and here's _what_ you # ought to be adding as the new release and/or version # value" aspect to the actions? can leave up to user # for now, but, more automation is better. _expect_actions( self, Changelog.NEEDS_RELEASE, VersionFile.NEEDS_BUMP, Tag.NEEDS_CUTTING, ) # TODO: if there's somehow a tag present for a release as yet # uncut...which makes no sense as changelog still has no # release. Would represent error state! # TODO: what if the version file is newer _but not what it needs to # be for the branch_? e.g. if it was 1.0.2 here (where latest # release is 1.0.1 but branch (main) implies desire is 1.1.0)? class version_file_is_newer: _version = "1.1.0" class new_tag_not_present: _tags = ("1.0.1",) def changelog_release_version_okay(self): _expect_actions( self, # TODO: same as above re: suggesting the release # value to the edit step Changelog.NEEDS_RELEASE, VersionFile.OKAY, Tag.NEEDS_CUTTING, ) class changelog_version_is_newer: _version = "1.2.0" # TODO: as with bugfix branches, this is undefined, except here # it's even moreso because...well it's even more wacky. why # would we have anything >1.1.0 when the changelog itself only # even goes up to 1.0.x?? class no_unreleased_issues: _changelog = "no_unreleased_1.x_features" class file_version_equals_latest_in_changelog: _version = "1.1.0" class tag_present: _tags = ("1.0.2", "1.1.0") def all_okay(self): _expect_actions( self, Changelog.OKAY, VersionFile.OKAY, Tag.OKAY ) class tag_missing: _tags = "1.0.2" def changelog_and_version_okay_tag_needs_cut(self): _expect_actions( self, Changelog.OKAY, VersionFile.OKAY, Tag.NEEDS_CUTTING, ) class undefined_branch: _branch = "whatever" _changelog = "nah" _tags = ("nope",) _version = "irrelevant" @raises(UndefinedReleaseType) def raises_exception(self): _mock_status(self) def _confirm(which): path = "invocations.packaging.release.confirm" def _wrapper(f): return trap(patch(path, return_value=which)(f)) return _wrapper _confirm_true = _confirm(True) _confirm_false = _confirm(False) # This is shit but I'm too tired and angry right now to give a fuck. def _run_prepare(c, mute=True, **kwargs): try: return prepare(c, **kwargs) except Exit: if not mute: raise class prepare_: # NOTE: mostly testing the base case of 'everything needs updating', # all the permutations are tested elsewhere. _branch = "1.1" _changelog = "unreleased_1.1_bugs" _version = "1.1.1" _tags = ("1.1.0",) @_confirm_false def displays_status_output(self, _): with _mock_context(self) as c: _run_prepare(c) output = sys.stdout.getvalue() for action in ( Changelog.NEEDS_RELEASE, VersionFile.NEEDS_BUMP, Tag.NEEDS_CUTTING, ): err = "Didn't see '{}' text in status output!".format(action.name) assert action.value in output, err @patch("invocations.packaging.release.status") def short_circuits_when_no_work_to_do(self, status): status.return_value = Lexicon(all_okay=True), Lexicon() with _mock_context(self) as c: # True retval, one call to status(), and no barfing on lack of # run() mocking, all point to the short circuit happening assert _run_prepare(c) is True assert status.call_count == 1 @trap @patch("invocations.console.input", return_value="no") def prompts_before_taking_action(self, mock_input): with _mock_context(self) as c: _run_prepare(c) assert mock_input.call_args[0][0] == "Take the above actions? [Y/n] " @_confirm_false def if_prompt_response_negative_no_action_taken(self, _): with _mock_context(self) as c: _run_prepare(c) # TODO: move all action-y code into subroutines, then mock them and # assert they were never called? # Expect that only the status-y run() calls were made. assert c.run.call_count == 2 commands = [x[0][0] for x in c.run.call_args_list] assert commands[0].startswith("git rev-parse") assert commands[1].startswith("git tag") @_confirm_true def opens_EDITOR_with_changelog_when_it_needs_update(self, _): with _mock_context(self) as c: _run_prepare(c) # Grab changelog path from the context config, why not path = c.config.packaging.changelog_file # TODO: real code should probs expand EDITOR explicitly so it can # run w/o a shell wrap / require a full env? cmd = "$EDITOR {}".format(path) c.run.assert_any_call(cmd, pty=True, hide=False, dry=False) @_confirm_true def opens_EDITOR_with_version_file_when_it_needs_update(self, _): with _mock_context(self) as c: _run_prepare(c) # TODO: real code should probs expand EDITOR explicitly so it can # run w/o a shell wrap / require a full env? cmd = "$EDITOR pyproject.toml" c.run.assert_any_call(cmd, pty=True, hide=False, dry=False) @_confirm_true def commits_and_adds_git_tag_when_needs_cutting(self, _): with _mock_context(self) as c: _run_prepare(c) version = "1.1.2" # as changelog has issues & prev was 1.1.1 # Ensure the commit necessity test happened. (Default mock_context # sets it up to result in a commit being necessary.) check = 'git status --porcelain | egrep -v "^\\?"' c.run.assert_any_call(check, hide=True, warn=True) commit = 'git commit -am "Cut {}"'.format(version) tag = 'git tag -a {} -m ""'.format(version) for cmd in (commit, tag): c.run.assert_any_call(cmd, hide=False, dry=False, echo=True) @_confirm_true def does_not_commit_if_no_commit_necessary(self, _): with _mock_context(self) as c: # Set up for a no-commit-necessary result to check command check = 'git status --porcelain | egrep -v "^\\?"' c.set_result_for("run", check, Result("", exited=1)) _run_prepare(c) # Expect NO git commit commands = [x[0][0] for x in c.run.call_args_list] assert not any(x.startswith("git commit") for x in commands) # Expect git tag c.run.assert_any_call( 'git tag -a 1.1.2 -m ""', hide=False, dry=False, echo=True ) class final_status_check: @_confirm_true @patch("invocations.packaging.release.status") def run_twice_when_not_short_circuiting(self, status, _): status.side_effect = [ ( Lexicon( changelog=Changelog.NEEDS_RELEASE, version=VersionFile.OKAY, tag=Tag.OKAY, all_okay=False, ), Lexicon(), ), (Lexicon(all_okay=True), Lexicon()), ] with _mock_context(self) as c: # Mute off - want kaboom if Exit raised _run_prepare(c, mute=False) assert status.call_count == 2 @_confirm_true @patch("invocations.packaging.release.status") def exits_if_still_not_all_okay(self, status, _): status.side_effect = [ ( Lexicon( changelog=Changelog.NEEDS_RELEASE, version=VersionFile.OKAY, tag=Tag.OKAY, all_okay=False, ), Lexicon(), ), (Lexicon(all_okay=False), Lexicon()), ] with _mock_context(self) as c: with pytest.raises(Exit, match=r"Something went wrong"): _run_prepare(c, mute=False) assert status.call_count == 2 class dry_run_prepare: @patch("invocations.packaging.release.status") def exits_early_like_non_dry_run_on_all_okay(self, status): status.return_value = Lexicon(all_okay=True), Lexicon() with _mock_context(self) as c: assert _run_prepare(c, dry_run=True) is True assert status.call_count == 1 @patch("invocations.packaging.release.status") def does_not_fail_fast_on_bad_release_type(self, status): status.side_effect = UndefinedReleaseType with _mock_context(self) as c: _run_prepare(c, dry_run=True) @patch("invocations.console.input") def does_not_prompt_to_confirm(self, mock_input): with _mock_context(self) as c: _run_prepare(c, dry_run=True) assert not mock_input.called def dry_runs_all_prep_commands(self): # Reminder: default state of mocked context is "everything needs # updates" with _mock_context(self) as c: _run_prepare(c, dry_run=True) dry_runs = [ x[1][0] for x in c.run.mock_calls if x[2].get("dry", False) ] for pattern in ( r"\$EDITOR .*\.rst", r"\$EDITOR pyproject\.toml", r"git commit.*", r"git tag -a.*", ): assert any(re.match(pattern, x) for x in dry_runs) @patch("invocations.packaging.release.status") def does_not_run_final_status_check(self, status): # Slight cheat: other actions all actually ok even tho all_okay is # false. means no needing to mock the run() calls etc. status.return_value = ( Lexicon( changelog=Changelog.OKAY, version=VersionFile.OKAY, tag=Tag.OKAY, all_okay=False, ), Lexicon(), ) with _mock_context(self) as c: _run_prepare(c, dry_run=True) # The end step was skipped assert status.call_count == 1 # Don't want a full re-enactment of status_ test tree, but do want to spot # check that actions not needing to be taken, aren't... class lack_of_action: _changelog = "no_unreleased_1.1_bugs" @_confirm_true def no_changelog_update_needed_means_no_changelog_edit(self, _): with _mock_context(self) as c: _run_prepare(c) # TODO: as with the 'took no actions at all' test above, # proving a negative sucks - eventually make this subroutine # assert based. Meh. path = c.config.packaging.changelog_file cmd = "$EDITOR {}".format(path) err = "Saw {!r} despite changelog not needing update!".format( cmd ) assert cmd not in [x[0][0] for x in c.run.call_args_list], err # NOTE: yea...this kinda pushes the limits of sane TDD...meh # NOTE: possible that the actual codes blessings emits differ based on # termcap/etc; consider sucking it up and just calling blessings directly in # that case, even though it makes the tests kinda tautological. # TODO: yes, when I personally went from TERM=xterm-256color to # TERM=screen-256color, that made these tests break! Updating test machinery to # account for now, but...not ideal! class component_state_enums_contain_human_readable_values: class changelog: def okay(self): expected = "\x1b[32m\u2714 no unreleased issues\x1b(B\x1b[m" assert Changelog.OKAY.value == expected def needs_release(self): expected = "\x1b[31m\u2718 needs :release: entry\x1b(B\x1b[m" assert Changelog.NEEDS_RELEASE.value == expected class version_file: def okay(self): expected = "\x1b[32m\u2714 version up to date\x1b(B\x1b[m" assert VersionFile.OKAY.value == expected def needs_bump(self): expected = "\x1b[31m\u2718 needs version bump\x1b(B\x1b[m" assert VersionFile.NEEDS_BUMP.value == expected class tag: def okay(self): assert Tag.OKAY.value == "\x1b[32m\u2714 all set\x1b(B\x1b[m" def needs_cutting(self): expected = "\x1b[31m\u2718 needs cutting\x1b(B\x1b[m" assert Tag.NEEDS_CUTTING.value == expected @contextmanager def _expect_pypa_build( flags, python="python", config=None, yield_rmtree=False ): kwargs = dict(run=True) if config is not None: kwargs["config"] = config c = MockContext(**kwargs) # Make sure we don't actually run rmtree regardless with patch("invocations.packaging.release.rmtree") as rmtree: if yield_rmtree: yield c, rmtree else: yield c print(c.run.mock_calls) c.run.assert_any_call(f"{python} -m build {flags}") class build_: _sdist_flags = "--outdir dist --sdist" _wheel_flags = "--outdir dist --wheel" _both_flags = "--outdir dist --sdist --wheel" class sdist: def on_by_default(self): with _expect_pypa_build(self._both_flags) as c: build(c) def can_be_disabled_via_config(self): config = Config(dict(packaging=dict(sdist=False))) with _expect_pypa_build(self._wheel_flags, config=config) as c: build(c) def kwarg_wins_over_config(self): config = Config(dict(packaging=dict(sdist=True))) with _expect_pypa_build(self._wheel_flags, config=config) as c: build(c, sdist=False) class wheel: def indicates_explicit_build_and_wheel(self): with _expect_pypa_build(self._wheel_flags) as c: build(c, sdist=False, wheel=True) def on_by_default(self): with _expect_pypa_build(self._wheel_flags) as c: build(c, sdist=False) def can_be_disabled_via_config(self): config = Config(dict(packaging=dict(wheel=False))) with _expect_pypa_build(self._sdist_flags, config=config) as c: build(c) def kwarg_wins_over_config(self): config = Config(dict(packaging=dict(wheel=True))) with _expect_pypa_build(self._sdist_flags, config=config) as c: build(c, wheel=False) @raises(Exit) def kabooms_if_sdist_and_wheel_both_False(self): build(MockContext(), sdist=False, wheel=False) class directory: def defaults_to_cwd_dist(self): with _expect_pypa_build(self._both_flags) as c: build(c) def may_be_given_via_config(self): config = Config(dict(packaging=dict(directory="dir"))) with _expect_pypa_build( "--outdir dir --sdist --wheel", config=config ) as c: build(c) def kwarg_wins_over_config(self): config = Config(dict(packaging=dict(directory="NOTdir"))) with _expect_pypa_build( "--outdir dir --sdist --wheel", config=config ) as c: build(c, directory="dir") class python: def defaults_to_python(self): with _expect_pypa_build(self._both_flags, python="python") as c: build(c, python="python") def may_be_overridden(self): with _expect_pypa_build(self._both_flags, python="fython") as c: build(c, python="fython") def can_be_given_via_config(self): config = Config(dict(packaging=dict(python="python17"))) with _expect_pypa_build( self._both_flags, config=config, python="python17" ) as c: build(c) def kwarg_wins_over_config(self): config = Config(dict(packaging=dict(python="python17"))) with _expect_pypa_build( self._both_flags, config=config, python="python99" ) as c: build(c, python="python99") class clean: def _expect_with_rmtree(self): return _expect_pypa_build(self._both_flags, yield_rmtree=True) def defaults_to_False_meaning_no_clean(self): with self._expect_with_rmtree() as (c, rmtree): build(c) assert not rmtree.called def True_means_clean_dist_dir(self): with self._expect_with_rmtree() as (c, rmtree): build(c, clean=True) rmtree.assert_any_call(Path("dist"), ignore_errors=True) def understands_directory_option(self): with _expect_pypa_build( "--outdir custom --sdist --wheel", yield_rmtree=True ) as ( c, rmtree, ): build(c, directory="custom", clean=True) rmtree.assert_any_call("custom", ignore_errors=True) def may_be_configured(self): config = Config(dict(packaging=dict(clean=True))) with _expect_pypa_build( self._both_flags, yield_rmtree=True, config=config ) as (c, rmtree): build(c) rmtree.assert_any_call(Path("dist"), ignore_errors=True) def kwarg_wins_over_config(self): config = Config(dict(packaging=dict(clean=True))) with _expect_pypa_build( self._both_flags, yield_rmtree=True, config=config ) as (c, rmtree): build(c, clean=False) rmtree.assert_any_call(Path("dist"), ignore_errors=True) class upload_: def _check_upload(self, c, kwargs=None, flags=None, extra=None): """ Expect/call upload() with common environment and settings/mocks. Returns the full command constructed, typically for further examination. """ def mkpath(x): return path.join("somedir", x) with patch("invocations.packaging.release.Path") as mock_Path: tgz, whl = mkpath("foo.tar.gz"), mkpath("foo.whl") glob = mock_Path.return_value.glob glob.side_effect = lambda x: [tgz if x.endswith("gz") else whl] # Do the thing! upload(c, "somedir", **(kwargs or {})) mock_Path.assert_any_call("somedir") glob.assert_any_call("*.tar.gz") glob.assert_any_call("*.whl") self.files = "{} {}".format(whl, tgz) cmd = "twine upload" if flags: cmd += " {}".format(flags) cmd += " {}".format(self.files) if extra: cmd += " {}".format(extra) return cmd def twine_uploads_dist_contents_with_wheels_first(self): c = MockContext(run=True) c.run.assert_called_once_with(self._check_upload(c)) def may_target_alternate_index(self): c = MockContext(run=True) cmd = self._check_upload( c, kwargs=dict(index="lol"), flags="--repository lol" ) c.run.assert_called_once_with(cmd) @patch("builtins.print") def dry_run_just_prints_and_ls(self, print): c = MockContext(run=True) cmd = self._check_upload(c, kwargs=dict(dry_run=True)) print.assert_any_call("Would publish via: {}".format(cmd)) c.run.assert_called_once_with("ls -l {}".format(self.files)) @patch("invocations.packaging.release.getpass.getpass") def allows_signing_via_gpg(self, getpass): c = MockContext(run=True, repeat=True) getpass.return_value = "super sekrit" twine_upload = self._check_upload( c, kwargs=dict(sign=True), extra="somedir/*.asc" ) calls = c.run.mock_calls # Looked for gpg assert calls[0] == call("which gpg", hide=True, warn=True) # Signed wheel flags = "--detach-sign --armor --passphrase-fd=0 --batch --pinentry-mode=loopback" # noqa template = "gpg {} somedir/foo.{{}}".format(flags) assert calls[1][1][0] == template.format("whl") # Spot check: did use in_stream to submit passphrase assert "in_stream" in calls[1][2] # Signed tgz assert calls[2][1][0] == template.format("tar.gz") # Uploaded (and w/ asc's) c.run.assert_any_call(twine_upload) class _Kaboom(Exception): pass class publish_: class base_case: def does_all_the_things(self, fakepub): c, mocks = fakepub # Execution publish(c) # Unhides stdout assert c.config.run.hide is False # Build mocks.build.assert_called_once_with( c, sdist=True, wheel=True, directory="tmpdir" ) # Twine check splat = path.join("tmpdir", "*") mocks.twine_check.assert_called_once_with(dists=[splat]) # Install test mocks.test_install.assert_called_once_with(c, directory="tmpdir") # Upload mocks.upload.assert_called_once_with( c, directory="tmpdir", index=None, sign=False, dry_run=False ) # Tmpdir cleaned up mocks.rmtree.assert_called_once_with("tmpdir") def cleans_up_on_error(self, fakepub): c, mocks = fakepub mocks.build.side_effect = _Kaboom with pytest.raises(_Kaboom): publish(MockContext(run=True)) mocks.rmtree.assert_called_once_with(mocks.mkdtemp.return_value) def monkeypatches_readme_renderer(self, fakepub): # Happens at module load time but is just a data structure change import readme_renderer.rst assert ( readme_renderer.rst.SETTINGS["halt_level"] == Reporter.INFO_LEVEL ) assert ( readme_renderer.rst.SETTINGS["report_level"] == Reporter.INFO_LEVEL ) class index: def passed_to_upload(self, fakepub): c, mocks = fakepub publish(c, index="dev") assert mocks.upload.call_args[1]["index"] == "dev" def honors_config(self, fakepub): c, mocks = fakepub c.config.packaging = dict(index="prod") publish(c) assert mocks.upload.call_args[1]["index"] == "prod" def kwarg_beats_config(self, fakepub): c, mocks = fakepub c.config.packaging = dict(index="prod") publish(c, index="dev") assert mocks.upload.call_args[1]["index"] == "dev" class sign: def passed_to_upload(self, fakepub): c, mocks = fakepub publish(c, sign=True) assert mocks.upload.call_args[1]["sign"] is True def honors_config(self, fakepub): c, mocks = fakepub c.config.packaging = dict(sign=True) publish(c) assert mocks.upload.call_args[1]["sign"] is True def kwarg_beats_config(self, fakepub): c, mocks = fakepub c.config.packaging = dict(sign=False) publish(c, sign=True) assert mocks.upload.call_args[1]["sign"] is True class sdist: def defaults_True_and_passed_to_build(self, fakepub): c, mocks = fakepub publish(c) assert mocks.build.call_args[1]["sdist"] is True def may_be_overridden(self, fakepub): c, mocks = fakepub publish(c, sdist=False) assert mocks.build.call_args[1]["sdist"] is False class wheel: def defaults_True_and_passed_to_build(self, fakepub): c, mocks = fakepub publish(c) assert mocks.build.call_args[1]["wheel"] is True def may_be_overridden(self, fakepub): c, mocks = fakepub publish(c, wheel=False) assert mocks.build.call_args[1]["wheel"] is False def directory_affects_tmpdir(self, fakepub): c, mocks = fakepub publish(c, directory="explicit") assert not mocks.mkdtemp.called assert mocks.build.call_args[1]["directory"] == "explicit" class dry_run: def causes_tmpdir_cleanup_to_be_skipped(self, fakepub): c, mocks = fakepub publish(c, dry_run=True) assert not mocks.rmtree.called def causes_tmpdir_cleanup_to_be_skipped_on_exception(self, fakepub): c, mocks = fakepub mocks.build.side_effect = _Kaboom with pytest.raises(_Kaboom): publish(c, dry_run=True) assert not mocks.rmtree.called def passed_to_upload(self, fakepub): c, mocks = fakepub publish(c, dry_run=True) assert mocks.upload.call_args[1]["dry_run"] is True class test_install_: def installs_all_archives_in_fresh_venv_with_matching_pip(self, install): c = install # Basic test, uses guts of fixture install_test_task(c, directory="whatever") # Import attempt was made c.run.assert_any_call("tmpdir/bin/python -c 'import foo'") def skips_import_test_when_asked_to(self, install): c = install install_test_task(c, directory="whatever", skip_import=True) # No import attempt for unwanted in (call("tmpdir/bin/python -c 'import foo'"),): assert unwanted not in c.run.mock_calls def does_mypy_import_when_py_typed_present(self, install): c = install # Mock out the pathlib exists call as positive (default is negative) c.set_exists(True) install_test_task(c, directory="whatever") # Mypy installed and executed c.run.assert_any_call("tmpdir/bin/pip install mypy") # NOTE: not actually the same 2 tmpdirs here but I'm already so sick of # all these mocks, jeez c.run.assert_any_call("cd tmpdir && tmpdir/bin/mypy -c 'import foo'") def skips_mypy_import_when_no_py_typed(self, install): c = install # Mock out the pathlib exists call as explicitly false, why not c.set_exists(False) install_test_task(c, directory="whatever") # Mypy NOT installed or executed for unwanted in ( call("tmpdir/bin/pip install mypy"), call("cd tmpdir && tmpdir/bin/mypy -c 'import foo'"), ): assert unwanted not in c.run.mock_calls def skips_mypy_import_when_skipping_regular_import(self, install): c = install c.set_exists(True) install_test_task(c, directory="whatever", skip_import=True) # Mypy NOT installed or executed for unwanted in ( call("tmpdir/bin/python -c 'import foo'"), call("tmpdir/bin/pip install mypy"), call("cd tmpdir && tmpdir/bin/mypy -c 'import foo'"), ): assert unwanted not in c.run.mock_calls class push_: def pushes_with_follow_tags(self): "git-pushes with --follow-tags" c = MockContext(run=True) push(c) c.run.assert_called_once_with("git push --follow-tags --no-verify") @trap @patch("invocations.environment.os.environ", dict(CIRCLECI="")) def honors_dry_run(self): c = MockContext(run=True) push(c, dry_run=True) c.run.assert_called_once_with( "git push --follow-tags --no-verify --dry-run", echo=True ) @trap @patch("invocations.environment.os.environ", dict(CIRCLECI="true")) def dry_run_dry_runs_the_invocation_itself_if_in_ci(self): c = MockContext(run=True) push(c, dry_run=True) c.run.assert_called_once_with( "git push --follow-tags --no-verify", echo=True, dry=True ) @trap @patch("invocations.environment.os.environ", dict(CIRCLECI="true")) def ci_check_only_applies_to_dry_run_behavior(self): # Yes, technically already covered by base tests, but... c = MockContext(run=True) push(c, dry_run=False) c.run.assert_called_once_with("git push --follow-tags --no-verify") class all_task: @patch("invocations.packaging.release.prepare") @patch("invocations.packaging.release.publish") @patch("invocations.packaging.release.push") def runs_primary_workflow(self, push, publish, prepare): c = MockContext(run=True) all_(c) # TODO: this doesn't actually prove order of operations. not seeing an # unhairy way to do that, but not really that worried either...:P prepare.assert_called_once_with(c, dry_run=False) publish.assert_called_once_with(c, dry_run=False) push.assert_called_once_with(c, dry_run=False) @patch("invocations.packaging.release.prepare") @patch("invocations.packaging.release.publish") @patch("invocations.packaging.release.push") def passes_through_dry_run_flag(self, push, publish, prepare): c = MockContext(run=True) all_(c, dry_run=True) prepare.assert_called_once_with(c, dry_run=True) publish.assert_called_once_with(c, dry_run=True) push.assert_called_once_with(c, dry_run=True) def bound_to_name_without_underscore(self): assert all_.name == "all" class namespace: def contains_all_tasks(self): names = """ all build prepare publish push status test-install upload """.split() assert set(release_ns.task_names) == set(names) def all_is_default_task(self): assert release_ns.default == "all" def hides_stdout_by_default(self): assert release_ns.configuration()["run"]["hide"] == "stdout" pyinvoke-invocations-ea947a4/tests/pytest_.py000066400000000000000000000056361504415344000215450ustar00rootroot00000000000000from contextlib import contextmanager from invoke import MockContext from invocations.pytest import test as _test_task, coverage from unittest.mock import Mock, call @contextmanager def _expect(flags=None, extra_flags=None, kwargs=None): if kwargs is None: kwargs = dict(pty=True) flags = flags or "--verbose --color=yes --capture=sys" if extra_flags is not None: flags = flags + " " + extra_flags c = MockContext(run=True) yield c c.run.assert_called_once_with("pytest {}".format(flags), **kwargs) class test_: def defaults_to_verbose_color_and_syscapture_with_pty_True(self): # Relies on default flags within expect helper with _expect() as c: _test_task(c) def can_turn_off_or_change_defaults(self): with _expect(flags="--capture=no", kwargs=dict(pty=False)) as c: _test_task(c, verbose=False, color=False, pty=False, capture="no") def can_passthru_k_x_and_arbitrary_opts(self): with _expect(extra_flags="--whatever -man -k 'lmao' -x") as c: _test_task(c, k="lmao", x=True, opts="--whatever -man") def can_disable_warnings(self): with _expect(extra_flags="--disable-warnings") as c: _test_task(c, warnings=False) class coverage_: _FLAGS = "--cov --no-cov-on-fail --cov-report={}" def default_args(self): with _expect(extra_flags=self._FLAGS.format("term")) as c: coverage(c) def report_type(self): with _expect(extra_flags=self._FLAGS.format("xml")) as c: coverage(c, report="xml") def opts(self): with _expect(extra_flags=self._FLAGS.format("term") + " --meh") as c: coverage(c, opts="--meh") def test_function(self): c = MockContext() faketest = Mock() coverage(c, tester=faketest) faketest.assert_called_once_with(c, opts=self._FLAGS.format("term")) def can_append_additional_test_tasks(self): c = MockContext(run=True, repeat=True) faketest1, faketest2 = Mock(), Mock() coverage(c, additional_testers=[faketest1, faketest2]) # Uses coverage-appending arg to pytest-cov flags = self._FLAGS.format("term") + " --cov-append" faketest1.assert_called_once_with(c, opts=flags) faketest2.assert_called_once_with(c, opts=flags) def open_html_report(self): c = MockContext(run=True, repeat=True) coverage(c, report="html") print(c.run.mock_calls) c.run.assert_any_call("open htmlcov/index.html") class codecov_support: def defaults_False(self): c = MockContext(run=True, repeat=True) coverage(c) assert call("codecov") not in c.run.mock_calls def runs_xml_and_codecov_when_True(self): c = MockContext(run=True, repeat=True) coverage(c, codecov=True) c.run.assert_has_calls([call("coverage xml"), call("codecov")]) pyinvoke-invocations-ea947a4/uv.lock000066400000000000000000004562441504415344000176530ustar00rootroot00000000000000version = 1 revision = 2 requires-python = ">=3.9" resolution-markers = [ "python_full_version >= '3.11'", "python_full_version == '3.10.*'", "python_full_version < '3.10'", ] [[package]] name = "alabaster" version = "0.7.16" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" }, ] [[package]] name = "alabaster" version = "1.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", "python_full_version == '3.10.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] [[package]] name = "asttokens" version = "3.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload-time = "2024-11-30T04:30:14.439Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" }, ] [[package]] name = "babel" version = "2.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, ] [[package]] name = "backports-tarfile" version = "1.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, ] [[package]] name = "black" version = "22.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "platformdirs" }, { name = "tomli" }, { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/58/8a3443a5034685152270f9012a9d196c9f165791ed3f2777307708b15f6c/black-22.1.0.tar.gz", hash = "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5", size = 559521, upload-time = "2022-01-29T20:38:08.749Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fd/ae/c401710dabb32bac39d799417ab25bd59ffb1336652bcb04f4bdd7126b79/black-22.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6", size = 2391069, upload-time = "2022-01-29T20:39:31.168Z" }, { url = "https://files.pythonhosted.org/packages/40/4e/fa8299630a4957f543675b2d8999a80428a7e35a66ec21e8a7d250c97dab/black-22.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866", size = 1341037, upload-time = "2022-01-29T20:39:37.254Z" }, { url = "https://files.pythonhosted.org/packages/b5/cb/d9799d8bd5f95e36ea4a04a80a0a48c24c638734a257d3b22fa16ec9a4ac/black-22.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71", size = 1214880, upload-time = "2022-01-29T20:39:42.597Z" }, { url = "https://files.pythonhosted.org/packages/b3/4b/e490650ee69bd53bad29956969346fa9d345422eb9ed9e201ec9533688eb/black-22.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab", size = 1476248, upload-time = "2022-01-29T20:39:49.335Z" }, { url = "https://files.pythonhosted.org/packages/28/2d/fbc5948cfca9f6e8ccb4f97d27eb96f70a6884fc6b71a8c64b89b96be3de/black-22.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5", size = 1140556, upload-time = "2022-01-29T20:39:55.463Z" }, { url = "https://files.pythonhosted.org/packages/c2/e2/6198c928e9cee46233463f30a8faf39a5752e75c07c8d30a908865a05a51/black-22.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f", size = 2390187, upload-time = "2022-01-29T20:38:59.123Z" }, { url = "https://files.pythonhosted.org/packages/a6/5e/5e3d6145ae5c8127abe1734878fff2ca6a494799cfa18fe585c33cae9198/black-22.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0", size = 1340496, upload-time = "2022-01-29T20:39:04.932Z" }, { url = "https://files.pythonhosted.org/packages/38/95/e3f3796278da6c399003db92d3254f330f928777230cda43a3607dc0f913/black-22.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c", size = 1214562, upload-time = "2022-01-29T20:39:10.299Z" }, { url = "https://files.pythonhosted.org/packages/56/25/c625a190347b5f6d940cfdeeb15958c04436328c29dc17b5bafb6dafa3ec/black-22.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2", size = 1475362, upload-time = "2022-01-29T20:39:15.19Z" }, { url = "https://files.pythonhosted.org/packages/ed/c6/817f8f025f9ede44d8f028b9f9ee2a66abe6c605cfcb41029600cda1b205/black-22.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321", size = 1139692, upload-time = "2022-01-29T20:39:21.989Z" }, { url = "https://files.pythonhosted.org/packages/a5/59/bd6d44da2b364fd2bd7a0b2ce2edfe200b79faad1cde14ce5ef13d504393/black-22.1.0-py3-none-any.whl", hash = "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d", size = 160408, upload-time = "2022-01-29T20:38:07.336Z" }, ] [[package]] name = "blessings" version = "1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5c/f8/9f5e69a63a9243448350b44c87fae74588aa634979e6c0c501f26a4f6df7/blessings-1.7.tar.gz", hash = "sha256:98e5854d805f50a5b58ac2333411b0482516a8210f23f43308baeb58d77c157d", size = 28194, upload-time = "2018-06-21T14:00:25.518Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/03/74/489f85a78247609c6b4f13733cbf3ba0d864b11aa565617b645d6fdf2a4a/blessings-1.7-py3-none-any.whl", hash = "sha256:b1fdd7e7a675295630f9ae71527a8ebc10bfefa236b3d6aa4932ee4462c17ba3", size = 18460, upload-time = "2018-06-21T14:00:24.412Z" }, ] [[package]] name = "build" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "os_name == 'nt'" }, { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, { name = "packaging" }, { name = "pyproject-hooks" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/25/1c/23e33405a7c9eac261dff640926b8b5adaed6a6eb3e1767d441ed611d0c0/build-1.3.0.tar.gz", hash = "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", size = 48544, upload-time = "2025-08-01T21:27:09.268Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl", hash = "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4", size = 23382, upload-time = "2025-08-01T21:27:07.844Z" }, ] [[package]] name = "certifi" version = "2025.8.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] [[package]] name = "cffi" version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910, upload-time = "2024-09-04T20:45:05.315Z" }, { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200, upload-time = "2024-09-04T20:45:06.903Z" }, { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565, upload-time = "2024-09-04T20:45:08.975Z" }, { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635, upload-time = "2024-09-04T20:45:10.64Z" }, { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218, upload-time = "2024-09-04T20:45:12.366Z" }, { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486, upload-time = "2024-09-04T20:45:13.935Z" }, { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911, upload-time = "2024-09-04T20:45:15.696Z" }, { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632, upload-time = "2024-09-04T20:45:17.284Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671, upload-time = "2025-05-02T08:34:12.696Z" }, { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744, upload-time = "2025-05-02T08:34:14.665Z" }, { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993, upload-time = "2025-05-02T08:34:17.134Z" }, { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382, upload-time = "2025-05-02T08:34:19.081Z" }, { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536, upload-time = "2025-05-02T08:34:21.073Z" }, { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349, upload-time = "2025-05-02T08:34:23.193Z" }, { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365, upload-time = "2025-05-02T08:34:25.187Z" }, { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499, upload-time = "2025-05-02T08:34:27.359Z" }, { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735, upload-time = "2025-05-02T08:34:29.798Z" }, { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786, upload-time = "2025-05-02T08:34:31.858Z" }, { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203, upload-time = "2025-05-02T08:34:33.88Z" }, { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436, upload-time = "2025-05-02T08:34:35.907Z" }, { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772, upload-time = "2025-05-02T08:34:37.935Z" }, { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] [[package]] name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] [[package]] name = "click" version = "8.2.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", "python_full_version == '3.10.*'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "coverage" version = "4.4.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0b/e1/190ef1a264144c9b073b7353c259ca5431b5ddc8861b452e858fcbd0e9de/coverage-4.4.2.tar.gz", hash = "sha256:309d91bd7a35063ec7a0e4d75645488bfab3f0b66373e7722f23da7f5b0f34cc", size = 374581, upload-time = "2017-11-05T13:35:39.042Z" } [[package]] name = "cryptography" version = "45.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903, upload-time = "2025-07-02T13:06:25.941Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926, upload-time = "2025-07-02T13:05:04.741Z" }, { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235, upload-time = "2025-07-02T13:05:07.084Z" }, { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785, upload-time = "2025-07-02T13:05:09.321Z" }, { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050, upload-time = "2025-07-02T13:05:11.069Z" }, { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379, upload-time = "2025-07-02T13:05:13.32Z" }, { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355, upload-time = "2025-07-02T13:05:15.017Z" }, { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087, upload-time = "2025-07-02T13:05:16.945Z" }, { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873, upload-time = "2025-07-02T13:05:18.743Z" }, { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651, upload-time = "2025-07-02T13:05:21.382Z" }, { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780, upload-time = "2025-07-02T13:05:29.299Z" }, { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091, upload-time = "2025-07-02T13:05:31.221Z" }, { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711, upload-time = "2025-07-02T13:05:33.062Z" }, { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299, upload-time = "2025-07-02T13:05:34.94Z" }, { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558, upload-time = "2025-07-02T13:05:37.288Z" }, { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020, upload-time = "2025-07-02T13:05:39.102Z" }, { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759, upload-time = "2025-07-02T13:05:41.398Z" }, { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991, upload-time = "2025-07-02T13:05:43.64Z" }, { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189, upload-time = "2025-07-02T13:05:46.045Z" }, { url = "https://files.pythonhosted.org/packages/8b/5d/a19441c1e89afb0f173ac13178606ca6fab0d3bd3ebc29e9ed1318b507fc/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c648025b6840fe62e57107e0a25f604db740e728bd67da4f6f060f03017d5097", size = 4140906, upload-time = "2025-07-02T13:05:55.914Z" }, { url = "https://files.pythonhosted.org/packages/4b/db/daceb259982a3c2da4e619f45b5bfdec0e922a23de213b2636e78ef0919b/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b8fa8b0a35a9982a3c60ec79905ba5bb090fc0b9addcfd3dc2dd04267e45f25e", size = 4374411, upload-time = "2025-07-02T13:05:57.814Z" }, { url = "https://files.pythonhosted.org/packages/6a/35/5d06ad06402fc522c8bf7eab73422d05e789b4e38fe3206a85e3d6966c11/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:14d96584701a887763384f3c47f0ca7c1cce322aa1c31172680eb596b890ec30", size = 4140942, upload-time = "2025-07-02T13:06:00.137Z" }, { url = "https://files.pythonhosted.org/packages/65/79/020a5413347e44c382ef1f7f7e7a66817cd6273e3e6b5a72d18177b08b2f/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57c816dfbd1659a367831baca4b775b2a5b43c003daf52e9d57e1d30bc2e1b0e", size = 4374079, upload-time = "2025-07-02T13:06:02.043Z" }, { url = "https://files.pythonhosted.org/packages/f0/63/83516cfb87f4a8756eaa4203f93b283fda23d210fc14e1e594bd5f20edb6/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bd4c45986472694e5121084c6ebbd112aa919a25e783b87eb95953c9573906d6", size = 4152447, upload-time = "2025-07-02T13:06:08.345Z" }, { url = "https://files.pythonhosted.org/packages/22/11/d2823d2a5a0bd5802b3565437add16f5c8ce1f0778bf3822f89ad2740a38/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:982518cd64c54fcada9d7e5cf28eabd3ee76bd03ab18e08a48cad7e8b6f31b18", size = 4386778, upload-time = "2025-07-02T13:06:10.263Z" }, { url = "https://files.pythonhosted.org/packages/5f/38/6bf177ca6bce4fe14704ab3e93627c5b0ca05242261a2e43ef3168472540/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:12e55281d993a793b0e883066f590c1ae1e802e3acb67f8b442e721e475e6463", size = 4151627, upload-time = "2025-07-02T13:06:13.097Z" }, { url = "https://files.pythonhosted.org/packages/38/6a/69fc67e5266bff68a91bcb81dff8fb0aba4d79a78521a08812048913e16f/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:5aa1e32983d4443e310f726ee4b071ab7569f58eedfdd65e9675484a4eb67bd1", size = 4385593, upload-time = "2025-07-02T13:06:15.689Z" }, ] [[package]] name = "decorator" version = "5.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] [[package]] name = "docutils" version = "0.21.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, ] [[package]] name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] [[package]] name = "executing" version = "2.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693, upload-time = "2025-01-22T15:41:29.403Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702, upload-time = "2025-01-22T15:41:25.929Z" }, ] [[package]] name = "flake8" version = "3.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mccabe" }, { name = "pycodestyle" }, { name = "pyflakes" }, { name = "setuptools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d0/27/c0d1274b86a8f71ec1a6e4d4c1cfe3b20d6f95b090ec7545320150952c93/flake8-3.6.0.tar.gz", hash = "sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670", size = 144684, upload-time = "2018-10-24T04:05:16.803Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/34/a6/49e2849a0e5464e1b5d621f63bc8453066f0f367bb3b744a33fca0bc1ddd/flake8-3.6.0-py2.py3-none-any.whl", hash = "sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2", size = 68881, upload-time = "2018-10-24T04:05:14.754Z" }, ] [[package]] name = "icecream" version = "2.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asttokens" }, { name = "colorama" }, { name = "executing" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/8b/ae6ebc9fc423f9397a0982990c86f1fe94077df729ef452c9a847fb16ae6/icecream-2.1.3.tar.gz", hash = "sha256:0aa4a7c3374ec36153a1d08f81e3080e83d8ac1eefd97d2f4fe9544e8f9b49de", size = 14722, upload-time = "2022-07-21T09:18:18.823Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/06/4e/21e309c7087695cf500a1827597f8510641f2c9a50ed7741bf7fc38736ff/icecream-2.1.3-py2.py3-none-any.whl", hash = "sha256:757aec31ad4488b949bc4f499d18e6e5973c40cc4d4fc607229e78cfaec94c34", size = 8425, upload-time = "2022-07-21T09:18:16.303Z" }, ] [[package]] name = "id" version = "1.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237, upload-time = "2024-12-04T19:53:05.575Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611, upload-time = "2024-12-04T19:53:03.02Z" }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] name = "imagesize" version = "1.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, ] [[package]] name = "importlib-metadata" version = "8.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] name = "invocations" version = "4.0.0" source = { editable = "." } dependencies = [ { name = "blessings" }, { name = "build" }, { name = "invoke" }, { name = "releases" }, { name = "semantic-version" }, { name = "tabulate" }, { name = "tqdm" }, { name = "twine" }, { name = "wheel" }, ] [package.dev-dependencies] dev = [ { name = "black" }, { name = "coverage" }, { name = "flake8" }, { name = "icecream" }, { name = "invoke" }, { name = "pytest-cov" }, { name = "pytest-mock" }, { name = "pytest-relaxed" }, { name = "releases" }, { name = "watchdog" }, ] [package.metadata] requires-dist = [ { name = "blessings", specifier = ">=1.6" }, { name = "build", specifier = ">=1.3" }, { name = "invoke", specifier = ">=1.7.2" }, { name = "releases", specifier = ">=1.6" }, { name = "semantic-version", specifier = ">=2.4,<2.7" }, { name = "tabulate", specifier = ">=0.7.5" }, { name = "tqdm", specifier = ">=4.8.1" }, { name = "twine", specifier = ">=1.15" }, { name = "wheel", specifier = ">=0.24.0" }, ] [package.metadata.requires-dev] dev = [ { name = "black", specifier = "==22.1.0" }, { name = "coverage", specifier = "==4.4.2" }, { name = "flake8", specifier = "==3.6.0" }, { name = "icecream", specifier = "==2.1.3" }, { name = "invoke", specifier = ">=2" }, { name = "pytest-cov", specifier = "==2.4.0" }, { name = "pytest-mock", specifier = "==3.2.0" }, { name = "pytest-relaxed", specifier = ">=2" }, { name = "releases", specifier = ">=2.0.1" }, { name = "watchdog", specifier = "==1.0.2" }, ] [[package]] name = "invoke" version = "2.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/42/127e6d792884ab860defc3f4d80a8f9812e48ace584ffc5a346de58cdc6c/invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5", size = 299835, upload-time = "2023-07-12T18:05:17.998Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0a/66/7f8c48009c72d73bc6bbe6eb87ac838d6a526146f7dab14af671121eb379/invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820", size = 160274, upload-time = "2023-07-12T18:05:16.294Z" }, ] [[package]] name = "jaraco-classes" version = "3.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, ] [[package]] name = "jaraco-context" version = "6.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, ] [[package]] name = "jaraco-functools" version = "4.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/1c/831faaaa0f090b711c355c6d8b2abf277c72133aab472b6932b03322294c/jaraco_functools-4.2.1.tar.gz", hash = "sha256:be634abfccabce56fa3053f8c7ebe37b682683a4ee7793670ced17bab0087353", size = 19661, upload-time = "2025-06-21T19:22:03.201Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f3/fd/179a20f832824514df39a90bb0e5372b314fea99f217f5ab942b10a8a4e8/jaraco_functools-4.2.1-py3-none-any.whl", hash = "sha256:590486285803805f4b1f99c60ca9e94ed348d4added84b74c7a12885561e524e", size = 10349, upload-time = "2025-06-21T19:22:02.039Z" }, ] [[package]] name = "jeepney" version = "0.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, ] [[package]] name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "keyring" version = "25.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, { name = "jaraco-classes" }, { name = "jaraco-context" }, { name = "jaraco-functools" }, { name = "jeepney", marker = "sys_platform == 'linux'" }, { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, { name = "secretstorage", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, ] [[package]] name = "markdown-it-py" version = "3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" }, { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" }, { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" }, { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" }, { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" }, { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" }, { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" }, { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" }, { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" }, { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, ] [[package]] name = "mccabe" version = "0.6.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/06/18/fa675aa501e11d6d6ca0ae73a101b2f3571a565e0f7d38e062eec18a91ee/mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f", size = 8612, upload-time = "2017-01-26T22:13:15.699Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/87/89/479dc97e18549e21354893e4ee4ef36db1d237534982482c3681ee6e7b57/mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", size = 8556, upload-time = "2017-01-26T22:13:14.36Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "more-itertools" version = "10.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671, upload-time = "2025-04-22T14:17:41.838Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278, upload-time = "2025-04-22T14:17:40.49Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] name = "nh3" version = "0.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/96cff0977357f60f06ec4368c4c7a7a26cccfe7c9fcd54f5378bf0428fd3/nh3-0.3.0.tar.gz", hash = "sha256:d8ba24cb31525492ea71b6aac11a4adac91d828aadeff7c4586541bf5dc34d2f", size = 19655, upload-time = "2025-07-17T14:43:37.05Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b4/11/340b7a551916a4b2b68c54799d710f86cf3838a4abaad8e74d35360343bb/nh3-0.3.0-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a537ece1bf513e5a88d8cff8a872e12fe8d0f42ef71dd15a5e7520fecd191bbb", size = 1427992, upload-time = "2025-07-17T14:43:06.848Z" }, { url = "https://files.pythonhosted.org/packages/ad/7f/7c6b8358cf1222921747844ab0eef81129e9970b952fcb814df417159fb9/nh3-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c915060a2c8131bef6a29f78debc29ba40859b6dbe2362ef9e5fd44f11487c2", size = 798194, upload-time = "2025-07-17T14:43:08.263Z" }, { url = "https://files.pythonhosted.org/packages/63/da/c5fd472b700ba37d2df630a9e0d8cc156033551ceb8b4c49cc8a5f606b68/nh3-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba0caa8aa184196daa6e574d997a33867d6d10234018012d35f86d46024a2a95", size = 837884, upload-time = "2025-07-17T14:43:09.233Z" }, { url = "https://files.pythonhosted.org/packages/4c/3c/cba7b26ccc0ef150c81646478aa32f9c9535234f54845603c838a1dc955c/nh3-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80fe20171c6da69c7978ecba33b638e951b85fb92059259edd285ff108b82a6d", size = 996365, upload-time = "2025-07-17T14:43:10.243Z" }, { url = "https://files.pythonhosted.org/packages/f3/ba/59e204d90727c25b253856e456ea61265ca810cda8ee802c35f3fadaab00/nh3-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e90883f9f85288f423c77b3f5a6f4486375636f25f793165112679a7b6363b35", size = 1071042, upload-time = "2025-07-17T14:43:11.57Z" }, { url = "https://files.pythonhosted.org/packages/10/71/2fb1834c10fab6d9291d62c95192ea2f4c7518bd32ad6c46aab5d095cb87/nh3-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0649464ac8eee018644aacbc103874ccbfac80e3035643c3acaab4287e36e7f5", size = 995737, upload-time = "2025-07-17T14:43:12.659Z" }, { url = "https://files.pythonhosted.org/packages/33/c1/8f8ccc2492a000b6156dce68a43253fcff8b4ce70ab4216d08f90a2ac998/nh3-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1adeb1062a1c2974bc75b8d1ecb014c5fd4daf2df646bbe2831f7c23659793f9", size = 980552, upload-time = "2025-07-17T14:43:13.763Z" }, { url = "https://files.pythonhosted.org/packages/2f/d6/f1c6e091cbe8700401c736c2bc3980c46dca770a2cf6a3b48a175114058e/nh3-0.3.0-cp313-cp313t-win32.whl", hash = "sha256:7275fdffaab10cc5801bf026e3c089d8de40a997afc9e41b981f7ac48c5aa7d5", size = 593618, upload-time = "2025-07-17T14:43:15.098Z" }, { url = "https://files.pythonhosted.org/packages/23/1e/80a8c517655dd40bb13363fc4d9e66b2f13245763faab1a20f1df67165a7/nh3-0.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:423201bbdf3164a9e09aa01e540adbb94c9962cc177d5b1cbb385f5e1e79216e", size = 598948, upload-time = "2025-07-17T14:43:16.064Z" }, { url = "https://files.pythonhosted.org/packages/9a/e0/af86d2a974c87a4ba7f19bc3b44a8eaa3da480de264138fec82fe17b340b/nh3-0.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:16f8670201f7e8e0e05ed1a590eb84bfa51b01a69dd5caf1d3ea57733de6a52f", size = 580479, upload-time = "2025-07-17T14:43:17.038Z" }, { url = "https://files.pythonhosted.org/packages/0c/e0/cf1543e798ba86d838952e8be4cb8d18e22999be2a24b112a671f1c04fd6/nh3-0.3.0-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ec6cfdd2e0399cb79ba4dcffb2332b94d9696c52272ff9d48a630c5dca5e325a", size = 1442218, upload-time = "2025-07-17T14:43:18.087Z" }, { url = "https://files.pythonhosted.org/packages/5c/86/a96b1453c107b815f9ab8fac5412407c33cc5c7580a4daf57aabeb41b774/nh3-0.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5e7185599f89b0e391e2f29cc12dc2e206167380cea49b33beda4891be2fe1", size = 823791, upload-time = "2025-07-17T14:43:19.721Z" }, { url = "https://files.pythonhosted.org/packages/97/33/11e7273b663839626f714cb68f6eb49899da5a0d9b6bc47b41fe870259c2/nh3-0.3.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:389d93d59b8214d51c400fb5b07866c2a4f79e4e14b071ad66c92184fec3a392", size = 811143, upload-time = "2025-07-17T14:43:20.779Z" }, { url = "https://files.pythonhosted.org/packages/6a/1b/b15bd1ce201a1a610aeb44afd478d55ac018b4475920a3118ffd806e2483/nh3-0.3.0-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e9e6a7e4d38f7e8dda9edd1433af5170c597336c1a74b4693c5cb75ab2b30f2a", size = 1064661, upload-time = "2025-07-17T14:43:21.839Z" }, { url = "https://files.pythonhosted.org/packages/8f/14/079670fb2e848c4ba2476c5a7a2d1319826053f4f0368f61fca9bb4227ae/nh3-0.3.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7852f038a054e0096dac12b8141191e02e93e0b4608c4b993ec7d4ffafea4e49", size = 997061, upload-time = "2025-07-17T14:43:23.179Z" }, { url = "https://files.pythonhosted.org/packages/a3/e5/ac7fc565f5d8bce7f979d1afd68e8cb415020d62fa6507133281c7d49f91/nh3-0.3.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af5aa8127f62bbf03d68f67a956627b1bd0469703a35b3dad28d0c1195e6c7fb", size = 924761, upload-time = "2025-07-17T14:43:24.23Z" }, { url = "https://files.pythonhosted.org/packages/39/2c/6394301428b2017a9d5644af25f487fa557d06bc8a491769accec7524d9a/nh3-0.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f416c35efee3e6a6c9ab7716d9e57aa0a49981be915963a82697952cba1353e1", size = 803959, upload-time = "2025-07-17T14:43:26.377Z" }, { url = "https://files.pythonhosted.org/packages/4e/9a/344b9f9c4bd1c2413a397f38ee6a3d5db30f1a507d4976e046226f12b297/nh3-0.3.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37d3003d98dedca6cd762bf88f2e70b67f05100f6b949ffe540e189cc06887f9", size = 844073, upload-time = "2025-07-17T14:43:27.375Z" }, { url = "https://files.pythonhosted.org/packages/66/3f/cd37f76c8ca277b02a84aa20d7bd60fbac85b4e2cbdae77cb759b22de58b/nh3-0.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:634e34e6162e0408e14fb61d5e69dbaea32f59e847cfcfa41b66100a6b796f62", size = 1000680, upload-time = "2025-07-17T14:43:28.452Z" }, { url = "https://files.pythonhosted.org/packages/ee/db/7aa11b44bae4e7474feb1201d8dee04fabe5651c7cb51409ebda94a4ed67/nh3-0.3.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:b0612ccf5de8a480cf08f047b08f9d3fecc12e63d2ee91769cb19d7290614c23", size = 1076613, upload-time = "2025-07-17T14:43:30.031Z" }, { url = "https://files.pythonhosted.org/packages/97/03/03f79f7e5178eb1ad5083af84faff471e866801beb980cc72943a4397368/nh3-0.3.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c7a32a7f0d89f7d30cb8f4a84bdbd56d1eb88b78a2434534f62c71dac538c450", size = 1001418, upload-time = "2025-07-17T14:43:31.429Z" }, { url = "https://files.pythonhosted.org/packages/ce/55/1974bcc16884a397ee699cebd3914e1f59be64ab305533347ca2d983756f/nh3-0.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3f1b4f8a264a0c86ea01da0d0c390fe295ea0bcacc52c2103aca286f6884f518", size = 986499, upload-time = "2025-07-17T14:43:32.459Z" }, { url = "https://files.pythonhosted.org/packages/c9/50/76936ec021fe1f3270c03278b8af5f2079038116b5d0bfe8538ffe699d69/nh3-0.3.0-cp38-abi3-win32.whl", hash = "sha256:6d68fa277b4a3cf04e5c4b84dd0c6149ff7d56c12b3e3fab304c525b850f613d", size = 599000, upload-time = "2025-07-17T14:43:33.852Z" }, { url = "https://files.pythonhosted.org/packages/8c/ae/324b165d904dc1672eee5f5661c0a68d4bab5b59fbb07afb6d8d19a30b45/nh3-0.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:bae63772408fd63ad836ec569a7c8f444dd32863d0c67f6e0b25ebbd606afa95", size = 604530, upload-time = "2025-07-17T14:43:34.95Z" }, { url = "https://files.pythonhosted.org/packages/5b/76/3165e84e5266d146d967a6cc784ff2fbf6ddd00985a55ec006b72bc39d5d/nh3-0.3.0-cp38-abi3-win_arm64.whl", hash = "sha256:d97d3efd61404af7e5721a0e74d81cdbfc6e5f97e11e731bb6d090e30a7b62b2", size = 585971, upload-time = "2025-07-17T14:43:35.936Z" }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] [[package]] name = "platformdirs" version = "4.3.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pycodestyle" version = "2.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/28/ad/cae9654d7fd64eb3d2ab2c44c9bf8dc5bd4fb759625beab99532239aa6e8/pycodestyle-2.4.0.tar.gz", hash = "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a", size = 96665, upload-time = "2018-04-10T11:28:42.235Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e5/c6/ce130213489969aa58610042dff1d908c25c731c9575af6935c2dfad03aa/pycodestyle-2.4.0-py2.py3-none-any.whl", hash = "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", size = 62342, upload-time = "2018-04-10T11:28:37.618Z" }, ] [[package]] name = "pycparser" version = "2.22" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, ] [[package]] name = "pyflakes" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/92/9e/386c0d9deef14996eb90d9deebbcb9d3ceb70296840b09615cb61b2ae231/pyflakes-2.0.0.tar.gz", hash = "sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49", size = 49002, upload-time = "2018-05-20T17:13:29.386Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/44/98/af7a72c9a543b1487d92813c648cb9b9adfbc96faef5455d60f4439aa99b/pyflakes-2.0.0-py2.py3-none-any.whl", hash = "sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae", size = 53379, upload-time = "2018-05-20T17:13:31.65Z" }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pyproject-hooks" version = "1.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, ] [[package]] name = "pytest" version = "8.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] [[package]] name = "pytest-cov" version = "2.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/c0/2bfd1fcdb9d407b8ac8185b1cb5ff458105c6b207a9a7f0e13032de9828f/pytest-cov-2.4.0.tar.gz", hash = "sha256:53d4179086e1eec1c688705977387432c01031b0a7bd91b8ff6c912c08c3820d", size = 37071, upload-time = "2016-10-10T19:32:06.706Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/85/e6/f041bcf77bcf7bf11ccb9b8a6cdb3a2ee70c1bd2ab49d87d2269cfd4f3e0/pytest_cov-2.4.0-py2.py3-none-any.whl", hash = "sha256:10e37e876f49ddec80d6c83a54b657157f1387ebc0f7755285f8c156130014a1", size = 20287, upload-time = "2016-10-10T19:32:03.995Z" }, ] [[package]] name = "pytest-mock" version = "3.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c3/42/3fc2c47f322913acda6c3b52a654f0779d3117fd79240cfd924332ac480d/pytest-mock-3.2.0.tar.gz", hash = "sha256:7122d55505d5ed5a6f3df940ad174b3f606ecae5e9bc379569cdcbd4cd9d2b83", size = 25569, upload-time = "2020-07-11T16:11:40.916Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/32/fd/52de218c335039f4ac25803101d5c3337c69045c68166b969d85821e49dc/pytest_mock-3.2.0-py3-none-any.whl", hash = "sha256:5564c7cd2569b603f8451ec77928083054d8896046830ca763ed68f4112d17c7", size = 10648, upload-time = "2020-07-11T16:11:39.622Z" }, ] [[package]] name = "pytest-relaxed" version = "2.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "decorator" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b8/e4/4153d3a7bfe5d5cfea0320f41ed63c00e193a91dd2feacc784189720454b/pytest-relaxed-2.0.2.tar.gz", hash = "sha256:956ea028ec30dbbfb680dd8e7b4a7fb8f80a239595e88bace018bf2c0d718248", size = 27876, upload-time = "2024-03-29T15:53:26.896Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/aa/3d/1f6c7bd9b7b47de5c2ef83462255c0b8f5803f49876f73bd94b8ec53624d/pytest_relaxed-2.0.2-py3-none-any.whl", hash = "sha256:b763256255586b3ec9b3397dc6632a242b4838749a5133638db9b47267edfae2", size = 15786, upload-time = "2024-03-29T15:53:25.028Z" }, ] [[package]] name = "pywin32-ctypes" version = "0.2.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, ] [[package]] name = "readme-renderer" version = "44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, { name = "nh3" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, ] [[package]] name = "releases" version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "semantic-version" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/71/a7/37cbf3e9f7c2591b4ba0453ef916cb2df9c4fd3cd06d5ddab2175a4c023b/releases-2.1.1.tar.gz", hash = "sha256:ae0683c3f309931a3717c6976e0f079929d05d34214b432a95764e61367f58d1", size = 43034, upload-time = "2023-04-28T19:12:12.259Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2e/05/9e0553803381b24d6e807bcbc1d1c5c11610dfefa70772eee36773d40dbc/releases-2.1.1-py3-none-any.whl", hash = "sha256:cd06449c915ce9729aaebe19a836744998f83f9ebb0cf92e54949c88ab3402f1", size = 20969, upload-time = "2023-04-28T19:12:10.907Z" }, ] [[package]] name = "requests" version = "2.32.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] [[package]] name = "requests-toolbelt" version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] [[package]] name = "rfc3986" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, ] [[package]] name = "rich" version = "14.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, ] [[package]] name = "roman-numerals-py" version = "3.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017, upload-time = "2025-02-22T07:34:54.333Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" }, ] [[package]] name = "secretstorage" version = "3.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "jeepney" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739, upload-time = "2022-08-13T16:22:46.976Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221, upload-time = "2022-08-13T16:22:44.457Z" }, ] [[package]] name = "semantic-version" version = "2.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/83/f76958017f3094b072d8e3a72d25c3ed65f754cc607fdb6a7b33d84ab1d5/semantic_version-2.6.0.tar.gz", hash = "sha256:2a4328680073e9b243667b201119772aefc5fc63ae32398d6afafff07c4f54c0", size = 13919, upload-time = "2016-09-25T14:22:57.563Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/28/be/3a7241d731ba89063780279a5433f5971c1cf41735b64a9f874b7c3ff995/semantic_version-2.6.0-py3-none-any.whl", hash = "sha256:2d06ab7372034bcb8b54f2205370f4aa0643c133b7e6dbd129c5200b83ab394b", size = 14175, upload-time = "2016-09-25T14:23:00.423Z" }, ] [[package]] name = "setuptools" version = "80.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "snowballstemmer" version = "3.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, ] [[package]] name = "sphinx" version = "7.4.7" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "babel", marker = "python_full_version < '3.10'" }, { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, { name = "docutils", marker = "python_full_version < '3.10'" }, { name = "imagesize", marker = "python_full_version < '3.10'" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2", marker = "python_full_version < '3.10'" }, { name = "packaging", marker = "python_full_version < '3.10'" }, { name = "pygments", marker = "python_full_version < '3.10'" }, { name = "requests", marker = "python_full_version < '3.10'" }, { name = "snowballstemmer", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10'" }, { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10'" }, { name = "tomli", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624, upload-time = "2024-07-20T14:46:52.142Z" }, ] [[package]] name = "sphinx" version = "8.1.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "babel", marker = "python_full_version == '3.10.*'" }, { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, { name = "docutils", marker = "python_full_version == '3.10.*'" }, { name = "imagesize", marker = "python_full_version == '3.10.*'" }, { name = "jinja2", marker = "python_full_version == '3.10.*'" }, { name = "packaging", marker = "python_full_version == '3.10.*'" }, { name = "pygments", marker = "python_full_version == '3.10.*'" }, { name = "requests", marker = "python_full_version == '3.10.*'" }, { name = "snowballstemmer", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.10.*'" }, { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.10.*'" }, { name = "tomli", marker = "python_full_version == '3.10.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, ] [[package]] name = "sphinx" version = "8.2.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", ] dependencies = [ { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "babel", marker = "python_full_version >= '3.11'" }, { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, { name = "docutils", marker = "python_full_version >= '3.11'" }, { name = "imagesize", marker = "python_full_version >= '3.11'" }, { name = "jinja2", marker = "python_full_version >= '3.11'" }, { name = "packaging", marker = "python_full_version >= '3.11'" }, { name = "pygments", marker = "python_full_version >= '3.11'" }, { name = "requests", marker = "python_full_version >= '3.11'" }, { name = "roman-numerals-py", marker = "python_full_version >= '3.11'" }, { name = "snowballstemmer", marker = "python_full_version >= '3.11'" }, { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" }, { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" }, { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11'" }, { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" }, { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" }, { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" }, ] [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, ] [[package]] name = "sphinxcontrib-devhelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, ] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, ] [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, ] [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, ] [[package]] name = "sphinxcontrib-serializinghtml" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] [[package]] name = "tabulate" version = "0.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] [[package]] name = "tqdm" version = "4.67.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] [[package]] name = "twine" version = "6.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "id" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, { name = "packaging" }, { name = "readme-renderer" }, { name = "requests" }, { name = "requests-toolbelt" }, { name = "rfc3986" }, { name = "rich" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c8/a2/6df94fc5c8e2170d21d7134a565c3a8fb84f9797c1dd65a5976aaf714418/twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd", size = 168404, upload-time = "2025-01-21T18:45:26.758Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7c/b6/74e927715a285743351233f33ea3c684528a0d374d2e43ff9ce9585b73fe/twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384", size = 40791, upload-time = "2025-01-21T18:45:24.584Z" }, ] [[package]] name = "typing-extensions" version = "4.14.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] [[package]] name = "urllib3" version = "2.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] [[package]] name = "watchdog" version = "1.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5f/39/e9e2aa6f22983b682dda2daccd3151c706d3da66d989c78c038d6b94b962/watchdog-1.0.2.tar.gz", hash = "sha256:376cbc2a35c0392b0fe7ff16fbc1b303fd99d4dd9911ab5581ee9d69adc88982", size = 98483, upload-time = "2020-12-18T11:15:49.816Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ed/2a/ff2716d49e3953fd8afdb2ac2a0a1984fff6c6630c1236ccd35184843e10/watchdog-1.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:27d9b4666938d5d40afdcdf2c751781e9ce36320788b70208d0f87f7401caf93", size = 80486, upload-time = "2020-12-18T11:15:31.52Z" }, { url = "https://files.pythonhosted.org/packages/73/9a/1597c678e18c8eec1a0a3b76514436bd51e100d9d6d76196b4d12d6422d7/watchdog-1.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d948ad9ab9aba705f9836625b32e965b9ae607284811cd98334423f659ea537a", size = 72571, upload-time = "2020-12-18T11:15:35.788Z" }, { url = "https://files.pythonhosted.org/packages/23/0c/5f86bc499e5b95e242f8841b7e4d5531816e5b7cef737bde1adea540ccd3/watchdog-1.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:101532b8db506559e52a9b5d75a308729b3f68264d930670e6155c976d0e52a0", size = 72570, upload-time = "2020-12-18T11:15:37.354Z" }, { url = "https://files.pythonhosted.org/packages/66/f0/e94c49630cff54bc8fc15abe58bd8dd038de825681eea12d41c29dafc000/watchdog-1.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:b1d723852ce90a14abf0ec0ca9e80689d9509ee4c9ee27163118d87b564a12ac", size = 72568, upload-time = "2020-12-18T11:15:38.563Z" }, { url = "https://files.pythonhosted.org/packages/43/ab/aeb7fb23e06306c5650cb9bacb8db49f9f5deebee3074729757734cb4585/watchdog-1.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:68744de2003a5ea2dfbb104f9a74192cf381334a9e2c0ed2bbe1581828d50b61", size = 72568, upload-time = "2020-12-18T11:15:40.065Z" }, { url = "https://files.pythonhosted.org/packages/6a/a4/9ba99e93f58d34f8d215f648085e90c491cef802c44363c03f558c9026dc/watchdog-1.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:602dbd9498592eacc42e0632c19781c3df1728ef9cbab555fab6778effc29eeb", size = 72572, upload-time = "2020-12-18T11:15:41.453Z" }, { url = "https://files.pythonhosted.org/packages/7e/bf/4120ce3c66eabbd439deeae1fde3288486805800ed9575d5f53bfb8f5865/watchdog-1.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:016b01495b9c55b5d4126ed8ae75d93ea0d99377084107c33162df52887cee18", size = 72569, upload-time = "2020-12-18T11:15:42.848Z" }, { url = "https://files.pythonhosted.org/packages/83/d9/3d1f46b428fd7b646725896b58d2eddb84f79fd76912773e6193cf74263d/watchdog-1.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:5f1f3b65142175366ba94c64d8d4c8f4015825e0beaacee1c301823266b47b9b", size = 72572, upload-time = "2020-12-18T11:15:44.124Z" }, { url = "https://files.pythonhosted.org/packages/c7/2e/26058560e6836ae654af7a67069effd0f4942216c52b2960b593bf8f0ba1/watchdog-1.0.2-py3-none-win32.whl", hash = "sha256:57f05e55aa603c3b053eed7e679f0a83873c540255b88d58c6223c7493833bac", size = 72556, upload-time = "2020-12-18T11:15:45.476Z" }, { url = "https://files.pythonhosted.org/packages/b5/db/8b7c15e98bf54931a980e5430838fb35f3a5b112a0f4fb9609a07e80f949/watchdog-1.0.2-py3-none-win_amd64.whl", hash = "sha256:f84146f7864339c8addf2c2b9903271df21d18d2c721e9a77f779493234a82b5", size = 72559, upload-time = "2020-12-18T11:15:46.961Z" }, { url = "https://files.pythonhosted.org/packages/b7/cb/8a592d3670c61a99d71544821458c2f9038bba427be8c1996d5490a5d9ec/watchdog-1.0.2-py3-none-win_ia64.whl", hash = "sha256:ee21aeebe6b3e51e4ba64564c94cee8dbe7438b9cb60f0bb350c4fa70d1b52c2", size = 72557, upload-time = "2020-12-18T11:15:48.235Z" }, ] [[package]] name = "wheel" version = "0.45.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload-time = "2024-11-23T00:18:23.513Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" }, ] [[package]] name = "zipp" version = "3.23.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ]