diff --git a/CHANGELOG.md b/CHANGELOG.md index ce4dae4af..b799ce8ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# v4.0 + +Major release bringing asyncio + AsyncSSH across the codebase. + +- core: replace the gevent runtime with asyncio-powered execution helpers and staged context handling +- ssh: migrate the SSH connector to AsyncSSH (agent forwarding, retries, SFTP) while keeping sync wrappers (`connect_all`, `run_ops`, etc.) +- connectors: fold the legacy Paramiko connectors (``@scp``/``@sshuserclient``) into the upgraded AsyncSSH connector +- api: add an ``AsyncContext`` helper for running operations/facts concurrently with correct state management and new tests/docs +- cli: harden async host loading, progress handling, and exception reporting when importing inventories/operations +- docs: update references to the 4.x branch and add upgrade guidance from 3.x -> 4.x + # v3.5.2 - fix operation & fact docs generation @@ -300,7 +311,7 @@ v3 of pyinfra includes for the first time a (mostly) typed internal API with pro - Add new `_if` global argument to control operation execution at runtime - Add `--debug-all` flag to set debug logging for all packages -- Retry SSH connections on failure (configurable, see [SSH connector](https://docs.pyinfra.com/en/3.x/connectors/ssh.html#available-data)) (@fwiesel) +- Retry SSH connections on failure (configurable, see [SSH connector](https://docs.pyinfra.com/en/4.x/connectors/ssh.html#available-data)) (@fwiesel) - Documentation typo fixes (@szepeviktor, @sudoBash418) - Fix handling of binary files in Docker connector (@matthijskooijman) - Add `will_change` attribute and `did_change` context manager to `OperationMeta` diff --git a/docs/api/async_context.md b/docs/api/async_context.md new file mode 100644 index 000000000..24f94b9d1 --- /dev/null +++ b/docs/api/async_context.md @@ -0,0 +1,104 @@ +# AsyncContext helper + +`pyinfra.async_context.AsyncContext` gives you a small async-friendly wrapper to +run individual operations and facts without executing a full deploy. It reuses +the existing `State` object, so the same inventory/config settings apply. + +```python +import asyncio + +from pyinfra.api import Config, State, deploy +from pyinfra.async_context import AsyncContext, AsyncHostContext +from pyinfra.facts.server import Hostname +from pyinfra.operations import server + + +async def main() -> None: + inventory = ... # construct your Inventory + state = State(inventory, Config()) + + @deploy("Example async deploy") + def my_deploy(): + server.shell(name="Async deploy op", commands="echo async deploy") + + async with AsyncContext(state) as ctx: + await server.shell("echo hello from async") + facts = {} + for host in state.inventory: + facts[host] = await host.get_fact(Hostname) + print(f"Hostnames: {facts}") + + await my_deploy() + + +asyncio.run(main()) +``` + +`AsyncContext` is an async context manager and will automatically connect to the +target hosts on entry and disconnect them when the block exits. While inside the +context you can call operations or deploys directly (for example +`await server.shell(...)` or `await my_deploy()`). Facts are retrieved via the +host objects themselves—`await host.get_fact(...)`—which makes the syntax mirror +the synchronous helper. If you only want to target a subset of hosts you can +pass them via the `hosts=` parameter when creating the context or when calling +individual operations/deploys using the normal global arguments. + +### Working with a single host + +When you only need to operate on one host you can use +`pyinfra.async_context.AsyncHostContext`, which is just a thin wrapper that +builds an `AsyncContext` scoped to a single host: + +```python +from pyinfra.async_context import AsyncHostContext +from pyinfra.facts.server import Hostname +from pyinfra.operations import server + + +async def run_against_host(state, host): + @deploy("Per-host deploy") + def my_host_deploy(): + server.shell(name="Host deploy op", commands="echo host deploy") + + async with AsyncHostContext(state, host) as ctx: + await server.shell("echo from single host") + hostname = await host.get_fact(Hostname) + print(hostname) + + await my_host_deploy() +``` + +`AsyncHostContext` accepts either the host name (string) or a `Host` object from +the state inventory. The sync equivalent is available as +`pyinfra.sync_context.SyncHostContext` and behaves the same way. + +## Synchronous helper + +For synchronous code you can use `pyinfra.sync_context.SyncContext`, which +exposes the same API without the asyncio wrappers: + +```python +from pyinfra.api import Config, State, deploy +from pyinfra.facts.server import Hostname +from pyinfra.operations import server +from pyinfra.sync_context import SyncContext + + +def main() -> None: + inventory = ... + state = State(inventory, Config()) + + @deploy("Example sync deploy") + def my_sync_deploy(): + server.shell(name="Sync deploy op", commands="echo sync deploy") + + with SyncContext(state) as ctx: + server.shell("echo hello from sync") + facts = {host: host.get_fact(Hostname) for host in state.inventory} + print(f"Hostnames: {facts}") + + my_sync_deploy() + + +main() +``` diff --git a/docs/api/index.rst b/docs/api/index.rst index c9facc776..ea68ec5e2 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -1,9 +1,15 @@ Using the API ============= -In addition to :doc:`the pyinfra CLI <../cli>`, pyinfra provides a full Python API. As of ``v3`` this API can be considered mostly stable. See the :doc:`./reference`. +In addition to :doc:`the pyinfra CLI <../cli>`, pyinfra provides a full Python API. As of ``v4`` this API can be considered mostly stable. See the :doc:`./reference`. -You can also reference `pyinfra's own main.py `_, and the `pyinfra API source code `_. +You can also reference `pyinfra's own main.py `_, and the `pyinfra API source code `_. + +Context Helpers +--------------- + +Async and sync helpers for running individual operations and facts are +documented in :doc:`./async_context`. Full Example ------------ @@ -24,15 +30,15 @@ Basic Localhost Example from pyinfra.operations import server # Define your inventory (@local means execute on localhost using subprocess) - # https://docs.pyinfra.com/en/3.x/apidoc/pyinfra.api.inventory.html + # https://docs.pyinfra.com/en/4.x/apidoc/pyinfra.api.inventory.html inventory = Inventory((["@local"], {})) # Define any config you need - # https://docs.pyinfra.com/en/3.x/apidoc/pyinfra.api.config.html + # https://docs.pyinfra.com/en/4.x/apidoc/pyinfra.api.config.html config = Config(SUDO=True) # Set up the state object - # https://docs.pyinfra.com/en/3.x/apidoc/pyinfra.api.state.html + # https://docs.pyinfra.com/en/4.x/apidoc/pyinfra.api.state.html state = State(inventory=inventory, config=config) # Connect to all the hosts @@ -62,5 +68,5 @@ Basic Localhost Example print(result2.changed, result2[host].stdout, result2[host].stderr) # We can also get facts for all the hosts - # https://docs.pyinfra.com/en/3.x/apidoc/pyinfra.api.facts.html + # https://docs.pyinfra.com/en/4.x/apidoc/pyinfra.api.facts.html print(get_facts(state, Os)) diff --git a/docs/api/reference.rst b/docs/api/reference.rst index 3c0ae9216..566d1adf8 100644 --- a/docs/api/reference.rst +++ b/docs/api/reference.rst @@ -13,7 +13,7 @@ The pyinfra API is designed to be used as follows: 3. Now that's done, we execute it: - ``pyinfra.api.operations.run_ops`` -Currently the best example of this in action is in `pyinfra's own main.py `_. +Currently the best example of this in action is in `pyinfra's own main.py `_. .. toctree:: :caption: Core API diff --git a/docs/compatibility.md b/docs/compatibility.md index a85c4b5d2..735d2cf11 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -19,7 +19,7 @@ pyinfra works on anywhere that runs Python - Mac, Linux & Windows are all suppor #### PyCharm -To debug pyinfra within PyCharm, you need to [explicitly enable support for Gevent](https://blog.jetbrains.com/pycharm/2012/08/gevent-debug-support/). +When debugging pyinfra within PyCharm, enable [asyncio debug support](https://www.jetbrains.com/help/pycharm/debugging-asynchronous-code.html) to inspect tasks and breakpoints inside the event loop. ## Remote Systems @@ -45,6 +45,13 @@ pyinfra aims to be compatible with all Unix-like operating systems and is curren In general, the only requirement on the remote side is shell access. POSIX commands are used where possible for facts and operations, so most of the ``server`` and ``files`` operations should work anywhere POSIX. +## Upgrading pyinfra from ``3.x`` -> ``4.x`` + +- Core execution now uses Python ``asyncio`` instead of gevent. The synchronous helpers remain, but custom tooling which previously interacted with gevent greenlets may need to switch to the new async APIs. +- SSH connectivity is powered by AsyncSSH. Paramiko-specific connection kwargs such as ``ssh_paramiko_connect_kwargs`` are no longer supported. +- The legacy ``@scp`` and ``@sshuserclient`` connectors have been removed. Their behaviour is covered by the updated ``@ssh`` connector which uses AsyncSSH’s SFTP implementation. + + ## Upgrading pyinfra from ``2.x`` -> ``3.x`` - Rename `_use_sudo_password` argument to `_sudo_password` diff --git a/docs/conf.py b/docs/conf.py index 43e165380..b5cc395bd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,8 +42,8 @@ "docsearch_index_name": "pyinfra", "plausible_domain": "docs.pyinfra.com", "plausible_stats_domain": "stats.oxygem.com", - "doc_versions": ["3.x", "2.x", "1.x", "0.x", "latest"], - "primary_doc_version": "3.x", + "doc_versions": ["4.x", "3.x", "2.x", "1.x", "0.x", "latest"], + "primary_doc_version": "4.x", } myst_heading_anchors = 3 diff --git a/docs/contributing.md b/docs/contributing.md index 9951a3a54..0a65f5086 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -13,7 +13,7 @@ Third party pull requests help expand pyinfra's functionality and are essential ## Branches -+ There is a branch per major version, ie `3.x`, that tracks the latest release of that version ++ There is a branch per major version, ie `4.x`, that tracks the latest release of that version + Changes should generally be based off the latest major branch, unless fixing an old version ## Dev Setup diff --git a/docs/index.rst b/docs/index.rst index a1d37cb41..d157f8d9f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,7 @@ pyinfra Documentation ========================= -Welcome to the pyinfra v3 documentation. If you're new to pyinfra you should start with the :doc:`getting-started` page. +Welcome to the pyinfra v4 documentation. If you're new to pyinfra you should start with the :doc:`getting-started` page. Using pyinfra diff --git a/pyproject.toml b/pyproject.toml index f09a7f1f0..0d82c82f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,6 @@ [project] name = "pyinfra" -dynamic = [ - "version", -] +version = "4.0.0" description = "pyinfra automates/provisions/manages/deploys infrastructure." readme = "README.md" authors = [ @@ -12,8 +10,7 @@ license = "MIT" license-files = ["LICENSE.md"] requires-python = ">=3.10,<4.0" dependencies = [ - "gevent>=1.5", - "paramiko>=2.7,<4", # 2.7 (2019) adds OpenSSH key format + Match SSH config + "asyncssh>=2.13", "click>2", "jinja2>3,<4", "python-dateutil>2,<3", @@ -54,7 +51,6 @@ test = [ "pyyaml>=6.0.2,<7", "mypy==1.17.1", "types-cryptography>=3.3.23.2,<4", - "types-paramiko>=2.7,<4", "types-python-dateutil>2,<3", "types-PyYAML>6,<7", "ruff>=0.13.1", @@ -95,15 +91,12 @@ terraform = "pyinfra.connectors.terraform:TerraformInventoryConnector" vagrant = "pyinfra.connectors.vagrant:VagrantInventoryConnector" [build-system] -requires = ["hatchling", "uv-dynamic-versioning"] +requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/pyinfra", "src/pyinfra_cli"] -[tool.hatch.version] -source = "uv-dynamic-versioning" - [tool.ruff] line-length = 100 @@ -131,7 +124,7 @@ markers = [ ] [tool.coverage.run] -concurrency = ["gevent"] +concurrency = ["thread"] [tool.coverage.report] show_missing = true diff --git a/scripts/build-public-docs.sh b/scripts/build-public-docs.sh index 17bb6cda2..93f00acbb 100755 --- a/scripts/build-public-docs.sh +++ b/scripts/build-public-docs.sh @@ -3,9 +3,9 @@ set -euo pipefail # Generates /en/next -NEXT_BRANCH="3.x" +NEXT_BRANCH="4.x" # Generates /en/latest AND redirects /page -> /en/$NAME -LATEST_BRANCH="3.x" +LATEST_BRANCH="4.x" BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) diff --git a/src/pyinfra/__init__.py b/src/pyinfra/__init__.py index ea2445cfb..7a8d3f3bf 100644 --- a/src/pyinfra/__init__.py +++ b/src/pyinfra/__init__.py @@ -16,6 +16,9 @@ # Setup package level version from .version import __version__ # noqa +from .async_context import AsyncContext as AsyncContext, AsyncHostContext as AsyncHostContext # noqa: E402,F401 +from .sync_context import SyncContext as SyncContext, SyncHostContext as SyncHostContext # noqa: E402,F401 + # Initialise base classes - this sets the context modules to point at the underlying # class objects (Host, etc), which makes ipython/etc work as expected. init_base_classes() diff --git a/src/pyinfra/api/command.py b/src/pyinfra/api/command.py index 609be1280..4febd813a 100644 --- a/src/pyinfra/api/command.py +++ b/src/pyinfra/api/command.py @@ -5,7 +5,6 @@ from string import Formatter from typing import IO, TYPE_CHECKING, Callable, Union -import gevent from typing_extensions import Unpack, override from pyinfra.context import LocalContextObject, ctx_config, ctx_host @@ -236,8 +235,8 @@ def execute(self, state: "State", host: "Host", connector_arguments: ConnectorAr if "state" in argspec.args and "host" in argspec.args: return self.function(state, host, *self.args, **self.kwargs) - # If we're already running inside a greenlet (ie a nested callback) just execute the func - # without any gevent.spawn which will break the local host object. + # If we're already running inside a nested callback just execute the function directly to + # avoid resetting the local host context. if isinstance(host, LocalContextObject): self.function(*self.args, **self.kwargs) return @@ -247,8 +246,7 @@ def execute_function() -> None: with ctx_host.use(host): self.function(*self.args, **self.kwargs) - greenlet = gevent.spawn(execute_function) - return greenlet.get() + return execute_function() class RsyncCommand(PyinfraCommand): diff --git a/src/pyinfra/api/connect.py b/src/pyinfra/api/connect.py index ca119487e..d3cc82c65 100644 --- a/src/pyinfra/api/connect.py +++ b/src/pyinfra/api/connect.py @@ -1,67 +1,121 @@ -from typing import TYPE_CHECKING - -import gevent +import asyncio +from typing import TYPE_CHECKING, Any, Callable from pyinfra.progress import progress_spinner +from pyinfra.api.state import StateStage if TYPE_CHECKING: + from pyinfra.api.host import Host from pyinfra.api.state import State -def connect_all(state: "State"): +async def connect_all_async(state: "State") -> None: """ - Connect to all the configured servers in parallel. Reads/writes state.inventory. - - Args: - state (``pyinfra.api.State`` obj): the state containing an inventory to connect to + Connect to all configured servers in parallel. Reads/writes ``state.inventory``. """ - hosts = [ - host - for host in state.inventory - if state.is_host_in_limit(host) # these are the hosts to activate ("initially connect to") + if state.current_stage < StateStage.Connect: + state.set_stage(StateStage.Connect) + + hosts = [host for host in state.inventory if state.is_host_in_limit(host)] + + if not hosts: + return + + task_to_host = [ + (asyncio.create_task(state.run_in_executor(host.connect)), host) for host in hosts ] - greenlet_to_host = {state.pool.spawn(host.connect): host for host in hosts} + exceptions: list[tuple["Host", BaseException]] = [] - with progress_spinner(greenlet_to_host.values()) as progress: - for greenlet in gevent.iwait(greenlet_to_host.keys()): - host = greenlet_to_host[greenlet] - progress(host) + with progress_spinner(hosts) as progress: - # Get/set the results - failed_hosts = set() + def _make_progress_callback(target_host: "Host") -> Callable[[asyncio.Future[Any]], None]: + def _callback(_task: asyncio.Future[Any]) -> None: + progress(target_host) + + return _callback + + for task, host in task_to_host: + task.add_done_callback(_make_progress_callback(host)) - for greenlet, host in greenlet_to_host.items(): - # Raise any unexpected exception - greenlet.get() + results = await asyncio.gather(*(task for task, _ in task_to_host), return_exceptions=True) + for (task, host), result in zip(task_to_host, results, strict=True): + if isinstance(result, BaseException): + exceptions.append((host, result)) + + failed_hosts = set() + + for host in hosts: if host.connected: state.activate_host(host) else: failed_hosts.add(host) - # Remove those that failed, triggering FAIL_PERCENT check state.fail_hosts(failed_hosts, activated_count=len(hosts)) + if exceptions: + raise exceptions[0][1] -def disconnect_all(state: "State"): - """ - Disconnect from all of the configured servers in parallel. Reads/writes state.inventory. - Args: - state (``pyinfra.api.State`` obj): the state containing an inventory to connect to - """ - greenlet_to_host = { - state.pool.spawn(host.disconnect): host - for host in state.activated_hosts # only hosts we connected to please! - } - - with progress_spinner(greenlet_to_host.values()) as progress: - for greenlet in gevent.iwait(greenlet_to_host.keys()): - host = greenlet_to_host[greenlet] - progress(host) - - for greenlet, host in greenlet_to_host.items(): - # Raise any unexpected exception - greenlet.get() +async def disconnect_all_async(state: "State") -> None: + """Disconnect from all configured servers in parallel.""" + + if state.current_stage < StateStage.Disconnect: + state.set_stage(StateStage.Disconnect) + + hosts = list(state.activated_hosts) + + if not hosts: + return + + task_to_host = [ + (asyncio.create_task(state.run_in_executor(host.disconnect)), host) for host in hosts + ] + + exceptions: list[tuple["Host", BaseException]] = [] + + with progress_spinner(hosts) as progress: + + def _make_progress_callback(target_host: "Host") -> Callable[[asyncio.Future[Any]], None]: + def _callback(_task: asyncio.Future[Any]) -> None: + progress(target_host) + + return _callback + + for task, host in task_to_host: + task.add_done_callback(_make_progress_callback(host)) + + results = await asyncio.gather(*(task for task, _ in task_to_host), return_exceptions=True) + + for (task, host), result in zip(task_to_host, results, strict=True): + if isinstance(result, BaseException): + exceptions.append((host, result)) + + if exceptions: + raise exceptions[0][1] + + +def connect_all(state: "State") -> None: + try: + asyncio.run(connect_all_async(state)) + except RuntimeError as exc: + if "already running" in str(exc): + raise RuntimeError( + "connect_all cannot be called while an asyncio event loop is running. " + "Use connect_all_async instead.", + ) from exc + raise + + +def disconnect_all(state: "State") -> None: + try: + asyncio.run(disconnect_all_async(state)) + except RuntimeError as exc: + if "already running" in str(exc): + raise RuntimeError( + "disconnect_all cannot be called while an asyncio event loop is running. " + "Use disconnect_all_async instead.", + ) from exc + raise diff --git a/src/pyinfra/api/connectors.py b/src/pyinfra/api/connectors.py index d1374c975..862d8a3f2 100644 --- a/src/pyinfra/api/connectors.py +++ b/src/pyinfra/api/connectors.py @@ -9,11 +9,43 @@ def _load_connector(entrypoint): def get_all_connectors(): - return { + discovered = { entrypoint.name: _load_connector(entrypoint) for entrypoint in entry_points(group="pyinfra.connectors") } + if "ssh" not in discovered: + from pyinfra.connectors.ssh import SSHConnector + + discovered["ssh"] = SSHConnector + + if "local" not in discovered: + from pyinfra.connectors.local import LocalConnector + + discovered["local"] = LocalConnector + + if "docker" not in discovered: + from pyinfra.connectors.docker import DockerConnector + + discovered["docker"] = DockerConnector + + if "podman" not in discovered: + from pyinfra.connectors.docker import PodmanConnector + + discovered["podman"] = PodmanConnector + + if "dockerssh" not in discovered: + from pyinfra.connectors.dockerssh import DockerSSHConnector + + discovered["dockerssh"] = DockerSSHConnector + + if "chroot" not in discovered: + from pyinfra.connectors.chroot import ChrootConnector + + discovered["chroot"] = ChrootConnector + + return discovered + def get_execution_connectors(): return { diff --git a/src/pyinfra/api/deploy.py b/src/pyinfra/api/deploy.py index 438b94498..e362200d0 100644 --- a/src/pyinfra/api/deploy.py +++ b/src/pyinfra/api/deploy.py @@ -15,8 +15,10 @@ from .arguments import pop_global_arguments from .arguments_typed import PyinfraOperation +from .operation import get_async_context, get_sync_context from .exceptions import PyinfraError from .host import Host +from .state import StateStage from .util import get_call_location if TYPE_CHECKING: @@ -41,6 +43,9 @@ def add_deploy(state: "State", deploy_func: Callable[..., Any], *args, **kwargs) ).format(get_call_location()), ) + if state.current_stage < StateStage.Prepare: + state.set_stage(StateStage.Prepare) + hosts = kwargs.pop("host", state.inventory.iter_active_hosts()) if isinstance(hosts, Host): hosts = [hosts] @@ -66,7 +71,7 @@ def deploy( raise PyinfraError( ( "The `deploy` decorator must be called, ie `@deploy()`, " - "see: https://docs.pyinfra.com/en/3.x/compatibility.html#upgrading-pyinfra-from-2-x-3-x" # noqa + "see: https://docs.pyinfra.com/en/4.x/compatibility.html#upgrading-pyinfra-from-2-x-3-x" # noqa ) ) @@ -82,6 +87,14 @@ def decorator(func: Callable[P, Any]) -> PyinfraOperation[P]: def _wrap_deploy(func: Callable[P, Any]) -> PyinfraOperation[P]: @wraps(func) def decorated_func(*args: P.args, **kwargs: P.kwargs) -> Any: + async_context = get_async_context() + if async_context is not None: + return async_context._call_wrapped_deploy(decorated_func, args, kwargs) + + sync_context = get_sync_context() + if sync_context is not None: + return sync_context._call_wrapped_deploy(decorated_func, args, kwargs) + deploy_kwargs, _ = pop_global_arguments(context.state, context.host, kwargs) deploy_data = getattr(func, "deploy_data", None) diff --git a/src/pyinfra/api/facts.py b/src/pyinfra/api/facts.py index e9550263e..12560ced7 100644 --- a/src/pyinfra/api/facts.py +++ b/src/pyinfra/api/facts.py @@ -16,9 +16,9 @@ from socket import error as socket_error, timeout as timeout_error from typing import TYPE_CHECKING, Any, Callable, Generic, Iterable, Optional, Type, TypeVar, cast +import asyncio import click -import gevent -from paramiko import SSHException +import asyncssh from typing_extensions import override from pyinfra import logger @@ -143,28 +143,69 @@ def _handle_fact_kwargs(state: "State", host: "Host", cls, args, kwargs): return fact_kwargs, global_kwargs -def get_facts(state, *args, **kwargs): +async def get_facts_async(state, *args, **kwargs): def get_host_fact(host, *args, **kwargs): - with ctx_host.use(host): - return get_fact(state, host, *args, **kwargs) + with ctx_state.use(state): + with ctx_host.use(host): + return get_fact(state, host, *args, **kwargs) + + hosts = list(state.inventory.iter_active_hosts()) + results: dict["Host", Any] = {} + + if not hosts: + return results - with ctx_state.use(state): - greenlet_to_host = { - state.pool.spawn(get_host_fact, host, *args, **kwargs): host - for host in state.inventory.iter_active_hosts() - } + task_to_host = [ + ( + asyncio.create_task(state.run_in_executor(get_host_fact, host, *args, **kwargs)), + host, + ) + for host in hosts + ] - results = {} + with progress_spinner(hosts) as progress: - with progress_spinner(greenlet_to_host.values()) as progress: - for greenlet in gevent.iwait(greenlet_to_host.keys()): - host = greenlet_to_host[greenlet] - results[host] = greenlet.get() - progress(host) + def _make_progress_callback(target_host: "Host") -> Callable[[asyncio.Future[Any]], None]: + def _callback(_task: asyncio.Future[Any]) -> None: + progress(target_host) + + return _callback + + for task, host in task_to_host: + task.add_done_callback(_make_progress_callback(host)) + + task_results: list[BaseException | Any] = await asyncio.gather( + *(task for task, _ in task_to_host), + return_exceptions=True, + ) + + first_exception: BaseException | None = None + + for (_task, host), result in zip(task_to_host, task_results, strict=True): + if isinstance(result, BaseException): + if first_exception is None: + first_exception = result + else: + results[host] = result + + if first_exception is not None: + raise first_exception return results +def get_facts(state, *args, **kwargs): + try: + return asyncio.run(get_facts_async(state, *args, **kwargs)) + except RuntimeError as exc: + if "already running" in str(exc): + raise RuntimeError( + "get_facts cannot be called while an asyncio event loop is running. " + "Use get_facts_async instead.", + ) from exc + raise + + def get_fact( state: "State", host: "Host", @@ -256,7 +297,7 @@ def _get_fact( print_input=state.print_fact_input, **executor_kwargs, ) - except (timeout_error, socket_error, SSHException) as e: + except (timeout_error, socket_error, asyncssh.Error) as e: log_host_command_error( host, e, diff --git a/src/pyinfra/api/host.py b/src/pyinfra/api/host.py index ab8497c56..3541fcb3d 100644 --- a/src/pyinfra/api/host.py +++ b/src/pyinfra/api/host.py @@ -15,17 +15,17 @@ overload, ) from uuid import uuid4 +from logging import Logger, getLogger import click from typing_extensions import Unpack, override -from pyinfra import logger from pyinfra.connectors.base import BaseConnector from pyinfra.connectors.util import CommandOutput, remove_any_sudo_askpass_file from .connectors import get_execution_connector from .exceptions import ConnectError -from .facts import FactBase, ShortFactBase, get_fact +from .facts import FactBase, ShortFactBase, get_fact as _load_fact from .util import memoize, sha1_hash if TYPE_CHECKING: @@ -34,6 +34,9 @@ from pyinfra.api.state import State +LOGGER: Logger = getLogger("pyinfra") + + def extract_callable_datas( datas: list[Union[Callable[..., Any], Any]], ) -> Generator[Any, Any, Any]: @@ -224,11 +227,12 @@ def style_print_prefix(self, *args, **kwargs) -> str: self.print_prefix_padding, ) - def log(self, message: str, log_func: Callable[[str], Any] = logger.info) -> None: - log_func(f"{self.print_prefix}{message}") + def log(self, message: str, log_func: Optional[Callable[[str], Any]] = None) -> None: + log_callable: Callable[[str], Any] = log_func or LOGGER.info + log_callable(f"{self.print_prefix}{message}") def log_styled( - self, message: str, log_func: Callable[[str], Any] = logger.info, **kwargs + self, message: str, log_func: Optional[Callable[[str], Any]] = None, **kwargs ) -> None: message_styled = click.style(message, **kwargs) self.log(message_styled, log_func=log_func) @@ -241,7 +245,7 @@ def noop(self, description: str) -> None: Log a description for a noop operation. """ - handler = logger.info if self.state.print_noop_info else logger.debug + handler = LOGGER.info if self.state.print_noop_info else LOGGER.debug handler("{0}noop: {1}".format(self.print_prefix, description)) def when(self, condition: Callable[[], bool]): @@ -281,16 +285,19 @@ def deploy( # Combine any old _ifs with the new ones if old_deploy_kwargs and kwargs: - old_ifs = old_deploy_kwargs["_if"] - new_ifs = kwargs["_if"] - if old_ifs and new_ifs: - kwargs["_if"] = old_ifs + new_ifs + old_if_value = old_deploy_kwargs.get("_if") + new_if_value = kwargs.get("_if") + if old_if_value and new_if_value: + old_if_iter = old_if_value if isinstance(old_if_value, list) else [old_if_value] + new_if_iter = new_if_value if isinstance(new_if_value, list) else [new_if_value] + combined = [*old_if_iter, *new_if_iter] + kwargs["_if"] = combined # Set the new values self.current_deploy_name = name self.current_deploy_kwargs = kwargs self.current_deploy_data = data - logger.debug( + LOGGER.debug( "Starting deploy %s (args=%r, data=%r)", name, kwargs, @@ -305,7 +312,7 @@ def deploy( self.current_deploy_kwargs = old_deploy_kwargs self.current_deploy_data = old_deploy_data - logger.debug( + LOGGER.debug( "Reset deploy to %s (args=%r, data=%r)", old_deploy_name, old_deploy_kwargs, @@ -364,7 +371,17 @@ def get_fact(self, name_or_cls, *args, **kwargs): """ Get a fact for this host, reading from the cache if present. """ - return get_fact(self.state, self, name_or_cls, args=args, kwargs=kwargs) + from pyinfra.api.operation import get_async_context, get_sync_context + + async_context = get_async_context() + if async_context is not None: + return async_context._call_wrapped_fact(self, name_or_cls, args, kwargs) + + sync_context = get_sync_context() + if sync_context is not None: + return sync_context._call_wrapped_fact(self, name_or_cls, args, kwargs) + + return _load_fact(self.state, self, name_or_cls, args=args, kwargs=kwargs) # Connector proxy # @@ -390,7 +407,7 @@ def connect(self, reason=None, show_errors: bool = True, raise_exceptions: bool self.print_prefix, click.style(e.args[0], "red"), ) - logger.error(log_message) + LOGGER.error(log_message) self.state.trigger_callbacks("host_connect_error", self, e) @@ -407,7 +424,7 @@ def connect(self, reason=None, show_errors: bool = True, raise_exceptions: bool " ({0})".format(reason), ) - logger.info(log_message) + LOGGER.info(log_message) self.state.trigger_callbacks("host_connect", self) self.connected = True @@ -427,6 +444,8 @@ def disconnect(self) -> None: self.state.trigger_callbacks("host_disconnect", self) self.connected = False + if hasattr(self.state, "active_hosts"): + self.state.active_hosts.discard(self) def run_shell_command(self, *args, **kwargs) -> tuple[bool, CommandOutput]: """ diff --git a/src/pyinfra/api/operation.py b/src/pyinfra/api/operation.py index f4e2ae5a3..5715b8430 100644 --- a/src/pyinfra/api/operation.py +++ b/src/pyinfra/api/operation.py @@ -7,6 +7,7 @@ from __future__ import annotations +from contextvars import ContextVar, Token from functools import wraps from inspect import signature from io import StringIO @@ -36,6 +37,49 @@ op_meta_default = object() + +_current_async_context: ContextVar[Any | None] = ContextVar( + "pyinfra_current_async_context", + default=None, +) +_current_sync_context: ContextVar[Any | None] = ContextVar( + "pyinfra_current_sync_context", + default=None, +) + + +def push_async_context(ctx: Any) -> Token: + return _current_async_context.set(ctx) + + +def reset_async_context(token: Token) -> None: + _current_async_context.reset(token) + + +def suspend_async_context() -> Token: + return _current_async_context.set(None) + + +def get_async_context() -> Any | None: + return _current_async_context.get() + + +def push_sync_context(ctx: Any) -> Token: + return _current_sync_context.set(ctx) + + +def reset_sync_context(token: Token) -> None: + _current_sync_context.reset(token) + + +def suspend_sync_context() -> Token: + return _current_sync_context.set(None) + + +def get_sync_context() -> Any | None: + return _current_sync_context.get() + + if TYPE_CHECKING: from pyinfra.connectors.util import CommandOutput @@ -221,6 +265,9 @@ def add_op(state: State, op_func, *args, **kwargs): ), ) + if state.current_stage < StateStage.Prepare: + state.set_stage(StateStage.Prepare) + hosts = kwargs.pop("host", state.inventory.iter_active_hosts()) if isinstance(hosts, Host): hosts = [hosts] @@ -263,6 +310,14 @@ def decorator(f: Callable[P, Generator]) -> PyinfraOperation[P]: def _wrap_operation(func: Callable[P, Generator], _set_in_op: bool = True) -> PyinfraOperation[P]: @wraps(func) def decorated_func(*args: P.args, **kwargs: P.kwargs) -> OperationMeta: + async_context = get_async_context() + if async_context is not None: + return async_context._call_wrapped_operation(decorated_func, args, kwargs) + + sync_context = get_sync_context() + if sync_context is not None: + return sync_context._call_wrapped_operation(decorated_func, args, kwargs) + state = context.state host = context.host diff --git a/src/pyinfra/api/operations.py b/src/pyinfra/api/operations.py index 415bcb05b..9075fab89 100644 --- a/src/pyinfra/api/operations.py +++ b/src/pyinfra/api/operations.py @@ -1,14 +1,14 @@ from __future__ import annotations +import asyncio import time import traceback from itertools import product from socket import error as socket_error, timeout as timeout_error -from typing import TYPE_CHECKING, Optional, cast +from typing import TYPE_CHECKING, Optional, cast, Callable, Any import click -import gevent -from paramiko import SSHException +import asyncssh from pyinfra import logger from pyinfra.connectors.util import CommandOutput, OutputLine @@ -18,6 +18,7 @@ from .arguments import CONNECTOR_ARGUMENT_KEYS, ConnectorArguments from .command import FunctionCommand, PyinfraCommand, StringCommand from .exceptions import PyinfraError +from .state import StateStage from .util import ( format_exception, log_error_or_warning, @@ -121,7 +122,7 @@ def _run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]: host, connector_arguments, ) - except (timeout_error, socket_error, SSHException) as e: + except (timeout_error, socket_error, asyncssh.Error) as e: log_host_command_error(host, e, timeout=timeout) all_output_lines.extend(output_lines) # If we failed and have not already printed the stderr, print it @@ -131,7 +132,7 @@ def _run_host_op(state: "State", host: "Host", op_hash: str) -> Optional[bool]: else: try: status = command.execute(state, host, connector_arguments) - except (timeout_error, socket_error, SSHException, IOError) as e: + except (timeout_error, socket_error, asyncssh.Error, IOError) as e: log_host_command_error(host, e, timeout=timeout) # Break the loop to trigger a failure @@ -247,10 +248,12 @@ def _run_host_op_with_context(state: "State", host: "Host", op_hash: str): return run_host_op(state, host, op_hash) -def _run_host_ops(state: "State", host: "Host", progress=None): - """ - Run all ops for a single server. - """ +async def _run_host_op_async(state: "State", host: "Host", op_hash: str) -> Optional[bool]: + return await state.run_in_executor(_run_host_op_with_context, state, host, op_hash) + + +async def _run_host_ops(state: "State", host: "Host", progress=None) -> None: + """Run all operations for a single host.""" logger.debug("Running all ops on %s", host) @@ -258,9 +261,8 @@ def _run_host_ops(state: "State", host: "Host", progress=None): op_meta = state.get_op_meta(op_hash) log_operation_start(op_meta) - result = _run_host_op_with_context(state, host, op_hash) + result = await _run_host_op_async(state, host, op_hash) - # Trigger CLI progress if provided if progress: progress((host, op_hash)) @@ -273,50 +275,36 @@ def _run_host_ops(state: "State", host: "Host", progress=None): ) -def _run_serial_ops(state: "State"): - """ - Run all ops for all servers, one server at a time. - """ +async def _run_serial_ops(state: "State") -> None: + """Run all operations for all hosts sequentially.""" for host in list(state.inventory.iter_active_hosts()): host_operations = product([host], state.get_op_order()) with progress_spinner(host_operations) as progress: try: - _run_host_ops( - state, - host, - progress=progress, - ) + await _run_host_ops(state, host, progress=progress) except PyinfraError: state.fail_hosts({host}) -def _run_no_wait_ops(state: "State"): - """ - Run all ops for all servers at once. - """ +async def _run_no_wait_ops(state: "State") -> None: + """Run all operations for all hosts concurrently without waiting between ops.""" + + hosts = list(state.inventory.iter_active_hosts()) + hosts_operations = product(hosts, state.get_op_order()) + + if not hosts: + return - hosts_operations = product(state.inventory.iter_active_hosts(), state.get_op_order()) with progress_spinner(hosts_operations) as progress: - # Spawn greenlet for each host to run *all* ops - if state.pool is None: - raise PyinfraError("No pool found on state.") - greenlets = [ - state.pool.spawn( - _run_host_ops, - state, - host, - progress=progress, - ) - for host in state.inventory.iter_active_hosts() + tasks = [ + asyncio.create_task(_run_host_ops(state, host, progress=progress)) for host in hosts ] - gevent.joinall(greenlets) + await asyncio.gather(*tasks) -def _run_single_op(state: "State", op_hash: str): - """ - Run a single operation for all servers. Can be configured to run in serial. - """ +async def _run_single_op(state: "State", op_hash: str) -> None: + """Run a single operation for all hosts, with optional batching/serial execution.""" state.trigger_callbacks("operation_start", op_hash) @@ -327,71 +315,101 @@ def _run_single_op(state: "State", op_hash: str): if op_meta.global_arguments["_serial"]: with progress_spinner(state.inventory.iter_active_hosts()) as progress: - # For each host, run the op for host in state.inventory.iter_active_hosts(): - result = _run_host_op_with_context(state, host, op_hash) + result = await _run_host_op_async(state, host, op_hash) progress(host) if not result: failed_hosts.add(host) else: - # Start with the whole inventory in one batch batches = [list(state.inventory.iter_active_hosts())] - # If parallel set break up the inventory into a series of batches parallel = op_meta.global_arguments["_parallel"] if parallel: hosts = list(state.inventory.iter_active_hosts()) batches = [hosts[i : i + parallel] for i in range(0, len(hosts), parallel)] for batch in batches: + if not batch: + continue + with progress_spinner(batch) as progress: - # Spawn greenlet for each host - if state.pool is None: - raise PyinfraError("No pool found on state.") - greenlet_to_host = { - state.pool.spawn(_run_host_op_with_context, state, host, op_hash): host + completed_results: dict["Host", Optional[bool]] = {} + task_to_host = [ + ( + asyncio.create_task(_run_host_op_async(state, host, op_hash)), + host, + ) for host in batch - } + ] + + def _make_progress_callback( + target_host: "Host", + ) -> Callable[[asyncio.Future[Any]], None]: + def _callback(_task: asyncio.Future[Any]) -> None: + progress(target_host) + + return _callback - # Trigger CLI progress as hosts complete if provided - for greenlet in gevent.iwait(greenlet_to_host.keys()): - host = greenlet_to_host[greenlet] - progress(host) + for task, host in task_to_host: + task.add_done_callback(_make_progress_callback(host)) - # Get all the results - for greenlet, host in greenlet_to_host.items(): - if not greenlet.get(): + task_results: list[BaseException | Optional[bool]] = await asyncio.gather( + *(task for task, _ in task_to_host), + return_exceptions=True, + ) + + exceptions: list[tuple["Host", BaseException]] = [] + + for index, (_task, host) in enumerate(task_to_host): + task_result = task_results[index] + if isinstance(task_result, BaseException): + exceptions.append((host, task_result)) + else: + result_bool: Optional[bool] = cast(Optional[bool], task_result) + completed_results[host] = result_bool + + for host, result in completed_results.items(): + if not result: failed_hosts.add(host) - # Now all the batches/hosts are complete, fail any failures + if exceptions: + failed_hosts.update(host for host, _ in exceptions) + raise exceptions[0][1] + state.fail_hosts(failed_hosts) state.trigger_callbacks("operation_end", op_hash) -def run_ops(state: "State", serial: bool = False, no_wait: bool = False): - """ - Runs all operations across all servers in a configurable manner. - - Args: - state (``pyinfra.api.State`` obj): the deploy state to execute - serial (boolean): whether to run operations host by host - no_wait (boolean): whether to wait for all hosts between operations - """ +async def run_ops_async(state: "State", serial: bool = False, no_wait: bool = False) -> None: + """Async entrypoint for running operations across all hosts.""" - # Flag state as deploy in process state.is_executing = True + if state.current_stage < StateStage.Execute: + state.set_stage(StateStage.Execute) + with ctx_state.use(state): - # Run all ops, but server by server if serial: - _run_serial_ops(state) - # Run all the ops on each server in parallel (not waiting at each operation) + await _run_serial_ops(state) elif no_wait: - _run_no_wait_ops(state) - # Default: run all ops in order, waiting at each for all servers to complete + await _run_no_wait_ops(state) else: for op_hash in state.get_op_order(): - _run_single_op(state, op_hash) + await _run_single_op(state, op_hash) + + +def run_ops(state: "State", serial: bool = False, no_wait: bool = False) -> None: + """Synchronous wrapper for :func:`run_ops_async`.""" + + try: + asyncio.run(run_ops_async(state, serial=serial, no_wait=no_wait)) + except RuntimeError as exc: + if "already running" in str(exc): + raise RuntimeError( + "run_ops cannot be called while an asyncio event loop is running. " + "Use run_ops_async instead.", + ) from exc + raise diff --git a/src/pyinfra/api/state.py b/src/pyinfra/api/state.py index 7937c09fb..8b1176623 100644 --- a/src/pyinfra/api/state.py +++ b/src/pyinfra/api/state.py @@ -1,14 +1,14 @@ from __future__ import annotations +import asyncio +import contextvars from collections import defaultdict +from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass from enum import IntEnum from graphlib import CycleError, TopologicalSorter from multiprocessing import cpu_count -from typing import TYPE_CHECKING, Callable, Iterator, Optional - -from gevent.pool import Pool -from paramiko import PKey +from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, TypeVar from pyinfra import logger @@ -23,6 +23,9 @@ from pyinfra.api.operation import OperationMeta +T = TypeVar("T") + + # Work out the max parallel we can achieve with the open files limit of the user/process, # take 10 for opening Python files and /3 for ~3 files per host during op runs. # See: https://github.com/Fizzadar/pyinfra/issues/44 @@ -155,8 +158,8 @@ class State: # A pyinfra.api.Config config: "Config" - # Main gevent pool - pool: "Pool" + # Main executor used for parallel work + executor: ThreadPoolExecutor | None = None # Current stage this state is in current_stage: StateStage = StateStage.Setup @@ -239,12 +242,13 @@ def init( self.callback_handlers: list[BaseStateCallback] = [] - # Setup greenlet pools - self.pool = Pool(config.PARALLEL) - self.fact_pool = Pool(config.PARALLEL) + # Setup executor for running work in parallel + max_workers = config.PARALLEL or min(len(inventory), MAX_PARALLEL) or 1 + self.executor = ThreadPoolExecutor(max_workers=max_workers) - # Private keys - self.private_keys: dict[str, PKey] = {} + # Cached private keys (asyncssh key objects) and any associated certificates + self.private_keys: dict[str, Any] = {} + self.private_key_certs: dict[str, list[Any]] = {} # Assign inventory/config self.inventory = inventory @@ -281,6 +285,23 @@ def init( self.initialised = True + async def run_in_executor(self, func: Callable[..., T], *args, **kwargs) -> T: + if self.executor is None: + raise RuntimeError("State executor not initialised") + + loop = asyncio.get_running_loop() + context = contextvars.copy_context() + + def _call_with_context() -> T: + return context.run(func, *args, **kwargs) + + return await loop.run_in_executor(self.executor, _call_with_context) + + def shutdown_executor(self, wait: bool = True) -> None: + if self.executor is not None: + self.executor.shutdown(wait=wait) + self.executor = None + def set_stage(self, stage: StateStage) -> None: if stage < self.current_stage: raise Exception("State stage cannot go backwards!") diff --git a/src/pyinfra/api/util.py b/src/pyinfra/api/util.py index 46d53ae2d..5330ba978 100644 --- a/src/pyinfra/api/util.py +++ b/src/pyinfra/api/util.py @@ -11,7 +11,7 @@ import click from jinja2 import Environment, FileSystemLoader, StrictUndefined -from paramiko import SSHException +import asyncssh from typeguard import TypeCheckError, check_type import pyinfra @@ -257,7 +257,7 @@ def log_host_command_error(host: "Host", e: Exception, timeout: int | None = 0) ), ) - elif isinstance(e, (socket_error, SSHException)): + elif isinstance(e, (socket_error, asyncssh.Error)): logger.error( "{0}{1}".format( host.print_prefix, diff --git a/src/pyinfra/async_context.py b/src/pyinfra/async_context.py new file mode 100644 index 000000000..c5160a714 --- /dev/null +++ b/src/pyinfra/async_context.py @@ -0,0 +1,468 @@ +from __future__ import annotations + +import asyncio +from contextlib import ExitStack +from contextvars import Token +from functools import partial +from typing import Any, Iterable, Mapping + +from typing_extensions import Protocol + +from pyinfra.api.host import Host +from pyinfra.api.operation import ( + OperationMeta, + execute_immediately, + push_async_context, + reset_async_context, + suspend_async_context, +) +from pyinfra.api.state import State, StateStage +from pyinfra.context import ctx_config, ctx_host, ctx_inventory, ctx_state + + +class SupportsOperation(Protocol): + def __call__(self, *args, **kwargs) -> OperationMeta: # pragma: no cover - Protocol stub + ... + + +class _AsyncOperationAwaitable: + def __init__( + self, + context: "AsyncContext", + operation: SupportsOperation, + args: tuple[Any, ...], + kwargs: dict[str, Any], + hosts_override: Iterable[Host | str] | None, + ) -> None: + self._context = context + self._operation = operation + self._args = args + self._kwargs = kwargs + self._hosts_override = hosts_override + + def __await__(self): + return self._run().__await__() + + async def _run(self) -> Mapping[Host, OperationMeta]: + suspend_token = suspend_async_context() + try: + return await self._context.run_operation( + self._operation, + *self._args, + hosts=self._hosts_override, + **self._kwargs, + ) + finally: + reset_async_context(suspend_token) + + +class _AsyncDeployAwaitable: + def __init__( + self, + context: "AsyncContext", + deploy_fn, + args: tuple[Any, ...], + kwargs: dict[str, Any], + hosts_override: Iterable[Host | str] | None, + ) -> None: + self._context = context + self._deploy_fn = deploy_fn + self._args = args + self._kwargs = kwargs + self._hosts_override = hosts_override + + def __await__(self): + return self._run().__await__() + + async def _run(self) -> None: + suspend_token = suspend_async_context() + try: + await self._context.run_deploy( + self._deploy_fn, + *self._args, + hosts=self._hosts_override, + **self._kwargs, + ) + finally: + reset_async_context(suspend_token) + + +class _AsyncFactAwaitable: + def __init__( + self, + context: "AsyncContext", + host: Host, + fact_cls, + fact_args: tuple[Any, ...], + fact_kwargs: dict[str, Any], + ) -> None: + self._context = context + self._host = host + self._fact_cls = fact_cls + self._fact_args = fact_args + self._fact_kwargs = fact_kwargs + + def __await__(self): + return self._run().__await__() + + async def _run(self) -> Any: + suspend_token = suspend_async_context() + try: + await self._context._ensure_hosts_connected([self._host]) + return await self._context.state.run_in_executor( + partial( + self._context._fetch_fact, + self._host, + self._fact_cls, + self._fact_args, + self._fact_kwargs, + ) + ) + finally: + reset_async_context(suspend_token) + + +class AsyncContext: + """Async helper for running individual operations or facts against hosts.""" + + def __init__( + self, + state: State, + hosts: Iterable[Host | str] | None = None, + ) -> None: + self.state = state + self._default_hosts = self._normalise_hosts(hosts) + self._managed_hosts: set[Host] = set() + self._auto_manage_connections = False + self._in_context = False + self._context_token: Token | None = None + + def _normalise_hosts(self, hosts: Iterable[Host | str] | None) -> list[Host]: + if hosts is None: + return list(self.state.inventory.iter_active_hosts()) or list(self.state.inventory) + + normalised: list[Host] = [] + for host in hosts: + if isinstance(host, Host): + normalised.append(host) + else: + resolved = self.state.inventory.get_host(host) + if resolved is None: + raise ValueError(f"Unknown host: {host}") + normalised.append(resolved) + return normalised + + def _with_context(self, host: Host): + stack = ExitStack() + stack.enter_context(ctx_state.use(self.state)) + stack.enter_context(ctx_inventory.use(self.state.inventory)) + stack.enter_context(ctx_config.use(self.state.config.copy())) + stack.enter_context(ctx_host.use(host)) + return stack + + async def __aenter__(self) -> "AsyncContext": + if self._in_context: + raise RuntimeError("AsyncContext is already in use as a context manager") + + self._auto_manage_connections = True + self._in_context = True + + if self.state.current_stage < StateStage.Connect: + self.state.set_stage(StateStage.Connect) + + try: + await self._ensure_hosts_connected(self._default_hosts) + except BaseException: + # Ensure the context flags are reset if connection setup fails + self._auto_manage_connections = False + self._in_context = False + raise + + self._context_token = push_async_context(self) + return self + + async def __aexit__(self, exc_type, exc, tb) -> None: # noqa: ANN001 - async context protocol + disconnect_error: BaseException | None = None + + try: + if self._auto_manage_connections: + try: + await self._disconnect_managed_hosts() + except BaseException as exc_disconnect: + disconnect_error = exc_disconnect + finally: + self._auto_manage_connections = False + self._in_context = False + if self._context_token is not None: + reset_async_context(self._context_token) + self._context_token = None + + if self.state.current_stage < StateStage.Disconnect: + self.state.set_stage(StateStage.Disconnect) + + if disconnect_error is not None and exc_type is None: + raise disconnect_error + + def _call_wrapped_operation( + self, + operation: SupportsOperation, + args: tuple[Any, ...], + kwargs: dict[str, Any], + ) -> "_AsyncOperationAwaitable": + op_kwargs = dict(kwargs) + hosts_override = op_kwargs.pop("hosts", None) + return _AsyncOperationAwaitable(self, operation, args, op_kwargs, hosts_override) + + def _call_wrapped_deploy( + self, + deploy_fn, + args: tuple[Any, ...], + kwargs: dict[str, Any], + ) -> _AsyncDeployAwaitable: + deploy_kwargs = dict(kwargs) + hosts_override = deploy_kwargs.pop("hosts", None) + return _AsyncDeployAwaitable(self, deploy_fn, args, deploy_kwargs, hosts_override) + + def _call_wrapped_fact( + self, + host: Host, + fact_cls, + args: tuple[Any, ...], + kwargs: dict[str, Any], + ) -> _AsyncFactAwaitable: + fact_kwargs = dict(kwargs) + return _AsyncFactAwaitable(self, host, fact_cls, args, fact_kwargs) + + async def _ensure_hosts_connected(self, hosts: Iterable[Host]) -> None: + if not self._auto_manage_connections: + return + + hosts_to_connect = [host for host in hosts if not host.connected] + if not hosts_to_connect: + return + + connect_tasks = [ + self.state.run_in_executor( + partial(host.connect, reason="async context", raise_exceptions=True) + ) + for host in hosts_to_connect + ] + + results = await asyncio.gather(*connect_tasks, return_exceptions=True) + + successful_hosts: list[Host] = [] + errors: list[BaseException] = [] + + for host, result in zip(hosts_to_connect, results): + if isinstance(result, BaseException): + errors.append(result) + else: + self._managed_hosts.add(host) + successful_hosts.append(host) + + if errors: + if successful_hosts: + await self._disconnect_managed_hosts(successful_hosts) + raise errors[0] + + async def _disconnect_managed_hosts(self, hosts: Iterable[Host] | None = None) -> None: + targets = list(hosts) if hosts is not None else list(self._managed_hosts) + if not targets: + return + + disconnect_hosts: list[Host] = [] + disconnect_tasks = [] + + for host in targets: + if host.connected: + disconnect_hosts.append(host) + disconnect_tasks.append(self.state.run_in_executor(host.disconnect)) + else: + self._managed_hosts.discard(host) + + if disconnect_tasks: + results = await asyncio.gather(*disconnect_tasks, return_exceptions=True) + errors: list[BaseException] = [] + for host, result in zip(disconnect_hosts, results): + self._managed_hosts.discard(host) + if isinstance(result, BaseException): + errors.append(result) + if errors: + raise errors[0] + + # Remove any hosts that were not connected (no task created) from management tracking + for host in set(targets) - set(disconnect_hosts): + self._managed_hosts.discard(host) + + async def run_operation( + self, + operation: SupportsOperation, + *args, + hosts: Iterable[Host | str] | None = None, + **kwargs, + ) -> Mapping[Host, OperationMeta]: + """Execute an operation immediately for each host and await completion.""" + + targets = self._normalise_hosts(hosts) if hosts is not None else self._default_hosts + + suspend_token = suspend_async_context() + try: + await self._ensure_hosts_connected(targets) + + results = {} + for host in targets: + op_meta = await self.state.run_in_executor( + partial(self._execute_operation, host, operation, args, kwargs) + ) + results[host] = op_meta + return results + finally: + reset_async_context(suspend_token) + + def _execute_operation( + self, + host: Host, + operation: SupportsOperation, + op_args: tuple[Any, ...], + op_kwargs: dict[str, Any], + ) -> OperationMeta: + with self._with_context(host): + if self.state.current_stage < StateStage.Prepare: + self.state.set_stage(StateStage.Prepare) + if self.state.current_stage < StateStage.Execute: + self.state.set_stage(StateStage.Execute) + elif self.state.current_stage > StateStage.Execute: + self.state.current_stage = StateStage.Execute + + was_executing = self.state.is_executing + if not was_executing: + self.state.is_executing = True + + if host not in self.state.activated_hosts: + self.state.activate_host(host) + + try: + op_meta = operation(*op_args, **op_kwargs) + + if not op_meta.is_complete(): + execute_immediately(self.state, host, op_meta._hash) + + return op_meta + finally: + if not was_executing: + self.state.is_executing = False + + async def get_fact( + self, + fact_cls, + *fact_args, + hosts: Iterable[Host | str] | None = None, + **fact_kwargs, + ) -> Mapping[Host, Any]: + """Fetch a fact asynchronously for the selected hosts.""" + + targets = self._normalise_hosts(hosts) if hosts is not None else self._default_hosts + + suspend_token = suspend_async_context() + try: + await self._ensure_hosts_connected(targets) + + results = {} + for host in targets: + value = await self.state.run_in_executor( + partial(self._fetch_fact, host, fact_cls, fact_args, fact_kwargs) + ) + results[host] = value + return results + finally: + reset_async_context(suspend_token) + + def _fetch_fact( + self, + host: Host, + fact_cls, + fact_args: tuple[Any, ...], + fact_kwargs: dict[str, Any], + ) -> Any: + with self._with_context(host): + return host.get_fact(fact_cls, *fact_args, **fact_kwargs) + + async def run_deploy( + self, + deploy_fn, + *deploy_args, + hosts: Iterable[Host | str] | None = None, + **deploy_kwargs, + ) -> None: + """Execute a deploy coroutine immediately for the selected hosts.""" + + targets = self._normalise_hosts(hosts) if hosts is not None else self._default_hosts + + suspend_token = suspend_async_context() + try: + await self._ensure_hosts_connected(targets) + + for host in targets: + await self.state.run_in_executor( + partial(self._execute_deploy, host, deploy_fn, deploy_args, deploy_kwargs) + ) + finally: + reset_async_context(suspend_token) + + def _execute_deploy( + self, + host: Host, + deploy_fn, + deploy_args: tuple[Any, ...], + deploy_kwargs: dict[str, Any], + ) -> None: + with self._with_context(host): + if self.state.current_stage < StateStage.Prepare: + self.state.set_stage(StateStage.Prepare) + if self.state.current_stage < StateStage.Execute: + self.state.set_stage(StateStage.Execute) + elif self.state.current_stage > StateStage.Execute: + self.state.current_stage = StateStage.Execute + + was_executing = self.state.is_executing + if not was_executing: + self.state.is_executing = True + + if host not in self.state.activated_hosts: + self.state.activate_host(host) + + try: + deploy_fn(*deploy_args, **deploy_kwargs) + finally: + if not was_executing: + self.state.is_executing = False + + +class AsyncHostContext: + """Convenience wrapper around :class:`AsyncContext` for a single host.""" + + def __init__(self, state: State, host: Host | str) -> None: + self.state = state + self._host_arg = host + self.host: Host | None = None + self._context: AsyncContext | None = None + + def _resolve_host(self) -> Host: + if isinstance(self._host_arg, Host): + return self._host_arg + + resolved = self.state.inventory.get_host(self._host_arg) + if resolved is None: + raise ValueError(f"Unknown host: {self._host_arg}") + return resolved + + async def __aenter__(self) -> AsyncContext: + self.host = self._resolve_host() + self._context = AsyncContext(self.state, hosts=[self.host]) + return await self._context.__aenter__() + + async def __aexit__(self, exc_type, exc, tb) -> None: # noqa: ANN001 - async context protocol + if self._context is None: + return + await self._context.__aexit__(exc_type, exc, tb) + self._context = None diff --git a/src/pyinfra/connectors/scp/__init__.py b/src/pyinfra/connectors/scp/__init__.py deleted file mode 100644 index 4652f5cca..000000000 --- a/src/pyinfra/connectors/scp/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .client import SCPClient # noqa: F401 diff --git a/src/pyinfra/connectors/scp/client.py b/src/pyinfra/connectors/scp/client.py deleted file mode 100644 index d57bfcd41..000000000 --- a/src/pyinfra/connectors/scp/client.py +++ /dev/null @@ -1,204 +0,0 @@ -from __future__ import annotations - -import ntpath -import os -from pathlib import PurePath -from shlex import quote -from socket import timeout as SocketTimeoutError -from typing import IO, AnyStr - -from paramiko import Channel -from paramiko.transport import Transport - -SCP_COMMAND = b"scp" - - -# Unicode conversion functions; assume UTF-8 -def asbytes(s: bytes | str | PurePath) -> bytes: - """Turns unicode into bytes, if needed. - - Assumes UTF-8. - """ - if isinstance(s, bytes): - return s - elif isinstance(s, PurePath): - return bytes(s) - else: - return s.encode("utf-8") - - -def asunicode(s: bytes | str) -> str: - """Turns bytes into unicode, if needed. - - Uses UTF-8. - """ - if isinstance(s, bytes): - return s.decode("utf-8", "replace") - else: - return s - - -class SCPClient: - """ - An scp1 implementation, compatible with openssh scp. - Raises SCPException for all transport related errors. Local filesystem - and OS errors pass through. - - Main public methods are .putfo and .getfo - """ - - def __init__( - self, - transport: Transport, - buff_size: int = 16384, - socket_timeout: float = 10.0, - ): - self.transport = transport - self.buff_size = buff_size - self.socket_timeout = socket_timeout - self._channel: Channel | None = None - self.scp_command = SCP_COMMAND - - @property - def channel(self) -> Channel: - """Return an open Channel, (re)opening if needed.""" - if self._channel is None or self._channel.closed: - self._channel = self.transport.open_session() - return self._channel - - def __enter__(self): - _ = self.channel # triggers opening if not already open - return self - - def __exit__(self, type, value, traceback): - self.close() - - def putfo( - self, - fl: IO[AnyStr], - remote_path: str | bytes, - mode: str | bytes = "0644", - size: int | None = None, - ) -> None: - if size is None: - pos = fl.tell() - fl.seek(0, os.SEEK_END) # Seek to end - size = fl.tell() - pos - fl.seek(pos, os.SEEK_SET) # Seek back - - self.channel.settimeout(self.socket_timeout) - self.channel.exec_command( - self.scp_command + b" -t " + asbytes(quote(asunicode(remote_path))) - ) - self._recv_confirm() - self._send_file(fl, remote_path, mode, size=size) - self.close() - - def getfo(self, remote_path: str, fl: IO): - remote_path_sanitized = quote(remote_path) - if os.name == "nt": - remote_file_name = ntpath.basename(remote_path_sanitized) - else: - remote_file_name = os.path.basename(remote_path_sanitized) - self.channel.settimeout(self.socket_timeout) - self.channel.exec_command(self.scp_command + b" -f " + asbytes(remote_path_sanitized)) - self._recv_all(fl, remote_file_name) - self.close() - return fl - - def close(self): - """close scp channel""" - if self._channel is not None: - self._channel.close() - self._channel = None - - def _send_file(self, fl, name, mode, size): - basename = asbytes(os.path.basename(name)) - # The protocol can't handle \n in the filename. - # Quote them as the control sequence \^J for now, - # which is how openssh handles it. - self.channel.sendall( - ("C%s %d " % (mode, size)).encode("ascii") + basename.replace(b"\n", b"\\^J") + b"\n" - ) - self._recv_confirm() - file_pos = 0 - buff_size = self.buff_size - chan = self.channel - while file_pos < size: - chan.sendall(fl.read(buff_size)) - file_pos = fl.tell() - chan.sendall(b"\x00") - self._recv_confirm() - - def _recv_confirm(self): - # read scp response - msg = b"" - try: - msg = self.channel.recv(512) - except SocketTimeoutError: - raise SCPException("Timeout waiting for scp response") - # slice off the first byte, so this compare will work in py2 and py3 - if msg and msg[0:1] == b"\x00": - return - elif msg and msg[0:1] == b"\x01": - raise SCPException(asunicode(msg[1:])) - elif self.channel.recv_stderr_ready(): - msg = self.channel.recv_stderr(512) - raise SCPException(asunicode(msg)) - elif not msg: - raise SCPException("No response from server") - else: - raise SCPException("Invalid response from server", msg) - - def _recv_all(self, fh: IO, remote_file_name: str) -> None: - # loop over scp commands, and receive as necessary - commands = (b"C",) - while not self.channel.closed: - # wait for command as long as we're open - self.channel.sendall(b"\x00") - msg = self.channel.recv(1024) - if not msg: # chan closed while receiving - break - assert msg[-1:] == b"\n" - msg = msg[:-1] - code = msg[0:1] - if code not in commands: - raise SCPException(asunicode(msg[1:])) - self._recv_file(msg[1:], fh, remote_file_name) - - def _recv_file(self, cmd: bytes, fh: IO, remote_file_name: str) -> None: - chan = self.channel - parts = cmd.strip().split(b" ", 2) - - try: - size = int(parts[1]) - except (ValueError, IndexError): - chan.send(b"\x01") - chan.close() - raise SCPException("Bad file format") - - buff_size = self.buff_size - pos = 0 - chan.send(b"\x00") - try: - while pos < size: - # we have to make sure we don't read the final byte - if size - pos <= buff_size: - buff_size = size - pos - data = chan.recv(buff_size) - if not data: - raise SCPException("Underlying channel was closed") - fh.write(data) - pos = fh.tell() - msg = chan.recv(512) - if msg and msg[0:1] != b"\x00": - raise SCPException(asunicode(msg[1:])) - except SocketTimeoutError: - chan.close() - raise SCPException("Error receiving, socket.timeout") - - -class SCPException(Exception): - """SCP exception class""" - - pass diff --git a/src/pyinfra/connectors/ssh.py b/src/pyinfra/connectors/ssh.py index 7d7d26edb..7b0e40264 100644 --- a/src/pyinfra/connectors/ssh.py +++ b/src/pyinfra/connectors/ssh.py @@ -1,36 +1,51 @@ from __future__ import annotations +import asyncio +import os +import random import shlex -from random import uniform -from shutil import which -from socket import error as socket_error, gaierror -from time import sleep -from typing import IO, TYPE_CHECKING, Any, Iterable, Optional, Protocol, Tuple +import tempfile +import warnings +from threading import Event, Thread +from typing import ( + IO, + TYPE_CHECKING, + Any, + Iterable, + Optional, + Protocol, + Coroutine, + TypeVar, +) + +from socket import timeout as timeout_error +import asyncssh import click -from paramiko import AuthenticationException, BadHostKeyException, SFTPClient, SSHException +import pyinfra from typing_extensions import TypedDict, Unpack, override from pyinfra import logger -from pyinfra.api.command import QuoteString, StringCommand -from pyinfra.api.exceptions import ConnectError +from pyinfra.api.command import StringCommand +from pyinfra.api.exceptions import ConnectError, PyinfraError from pyinfra.api.util import get_file_io, memoize from .base import BaseConnector, DataMeta -from .scp import SCPClient -from .ssh_util import get_private_key, raise_connect_error -from .sshuserclient import SSHClient from .util import ( CommandOutput, + OutputLine, execute_command_with_sudo_retry, make_unix_command_for_host, - read_output_buffers, run_local_process, - write_stdin, ) if TYPE_CHECKING: from pyinfra.api.arguments import ConnectorArguments + from pyinfra.api.host import Host + from pyinfra.api.state import State + + +T = TypeVar("T") class ConnectorData(TypedDict): @@ -49,7 +64,7 @@ class ConnectorData(TypedDict): ssh_known_hosts_file: str ssh_strict_host_key_checking: str - ssh_paramiko_connect_kwargs: dict + ssh_paramiko_connect_kwargs: dict # backward compatibility name ssh_connect_retries: int ssh_connect_retry_min_delay: float @@ -64,18 +79,9 @@ class ConnectorData(TypedDict): "ssh_password": DataMeta("SSH password"), "ssh_key": DataMeta("SSH key filename"), "ssh_key_password": DataMeta("SSH key password"), - "ssh_allow_agent": DataMeta( - "Whether to use any active SSH agent", - True, - ), - "ssh_look_for_keys": DataMeta( - "Whether to look for private keys", - True, - ), - "ssh_forward_agent": DataMeta( - "Whether to enable SSH forward agent", - False, - ), + "ssh_allow_agent": DataMeta("Whether to use any active SSH agent", True), + "ssh_look_for_keys": DataMeta("Whether to look for private keys", True), + "ssh_forward_agent": DataMeta("Whether to enable SSH forward agent", False), "ssh_config_file": DataMeta("SSH config filename"), "ssh_known_hosts_file": DataMeta("SSH known_hosts filename"), "ssh_strict_host_key_checking": DataMeta( @@ -83,7 +89,7 @@ class ConnectorData(TypedDict): "accept-new", ), "ssh_paramiko_connect_kwargs": DataMeta( - "Override keyword arguments passed into Paramiko's ``SSHClient.connect``" + "Override keyword arguments passed into asyncssh.connect", ), "ssh_connect_retries": DataMeta("Number of tries to connect via ssh", 0), "ssh_connect_retry_min_delay": DataMeta( @@ -95,200 +101,592 @@ class ConnectorData(TypedDict): 0.5, ), "ssh_file_transfer_protocol": DataMeta( - "Protocol to use for file transfers. Can be ``sftp`` or ``scp``.", + "Protocol to use for file transfers. Can be ``sftp``.", "sftp", ), } class FileTransferClient(Protocol): - def getfo(self, remote_filename: str, fl: IO) -> Any | None: - """ - Get a file from the remote host, writing to the provided file-like object. - """ - ... + def getfo(self, remote_filename: str, fl: IO) -> Any | None: ... - def putfo(self, fl: IO, remote_filename: str) -> Any | None: - """ - Put a file to the remote host, reading from the provided file-like object. - """ - ... + def putfo(self, fl: IO, remote_filename: str) -> Any | None: ... -class SSHConnector(BaseConnector): - """ - Connect to hosts over SSH. This is the default connector and all targets default - to this meaning you do not need to specify it - ie the following two commands - are identical: +def _expand_user_path(path: str | None) -> str | None: + if not path: + return None - .. code:: shell + if path.startswith("~/"): + home = os.environ.get("HOME") + if home: + return os.path.normpath(os.path.join(home, path[2:])) - pyinfra my-host.net ... - pyinfra @ssh/my-host.net ... - """ + return os.path.expanduser(path) - __examples_doc__ = """ - An inventory file (``inventory.py``) containing a single SSH target with SSH - forward agent enabled: - .. code:: python +def _format_known_host(hostname: str, port: Optional[int]) -> str: + if port and port != 22: + return f"[{hostname}]:{port}" + return hostname - hosts = [ - ("my-host.net", {"ssh_forward_agent": True}), - ] - Multiple hosts sharing the same SSH username: +def _normalise_stdin(stdin: Any) -> Optional[str]: + if stdin is None: + return None + if isinstance(stdin, (bytes, str)): + return stdin.decode() if isinstance(stdin, bytes) else stdin + if isinstance(stdin, Iterable): + return "".join(str(item) for item in stdin) + return str(stdin) - .. code:: python - hosts = ( - ["my-host-1.net", "my-host-2.net"], - {"ssh_user": "ssh-user"}, - ) +class _SFTPWrapper: + def __init__(self, connector: "SSHConnector") -> None: + self._connector = connector + + def getfo(self, remote_filename: str, fl: IO) -> None: + data = self._connector._submit(self._connector._async_read_file(remote_filename)) + fl.write(data) + + def putfo(self, fl: IO, remote_filename: str) -> None: + position = fl.tell() + fl.seek(0) + data = fl.read() + fl.seek(position) + if isinstance(data, str): + data = data.encode() + self._connector._submit(self._connector._async_write_file(remote_filename, data)) - Multiple hosts with different SSH usernames: - .. code:: python +class _SCPWrapper: + def __init__(self, connector: "SSHConnector") -> None: + self._connector = connector - hosts = [ - ("my-host-1.net", {"ssh_user": "ssh-user"}), - ("my-host-2.net", {"ssh_user": "other-user"}), - ] - """ + def getfo(self, remote_filename: str, fl: IO) -> None: + data = self._connector._submit(self._connector._async_scp_download(remote_filename)) + fl.write(data) + def putfo(self, fl: IO, remote_filename: str) -> None: + position = fl.tell() if hasattr(fl, "tell") else None + if hasattr(fl, "seek"): + fl.seek(0) + data = fl.read() + if position is not None and hasattr(fl, "seek"): + fl.seek(position) + if isinstance(data, str): + data = data.encode() + self._connector._submit(self._connector._async_scp_upload(remote_filename, data)) + + +class SSHConnector(BaseConnector): handles_execution = True data_cls = ConnectorData data_meta = connector_data_meta data: ConnectorData - client: Optional[SSHClient] = None + def __init__(self, state: "State", host: "Host"): + super().__init__(state, host) + self._loop: asyncio.AbstractEventLoop | None = None + self._loop_thread: Thread | None = None + self._loop_ready: Event | None = None + self._connection: asyncssh.SSHClientConnection | None = None + self._sftp_client: asyncssh.SFTPClient | None = None + self._known_hosts_file: str | None = None + self._strict_host_key_checking: str = ( + self.data["ssh_strict_host_key_checking"] or "accept-new" + ) + self._transfer_protocol = (self.data.get("ssh_file_transfer_protocol") or "sftp").lower() + self._strict_setting: str = self._strict_host_key_checking.lower() @override @staticmethod def make_names_data(name): - yield "@ssh/{0}".format(name), {"ssh_hostname": name}, [] - - def make_paramiko_kwargs(self) -> dict[str, Any]: - kwargs = { - "allow_agent": False, - "look_for_keys": False, - "hostname": self.data["ssh_hostname"] or self.host.name, - # Overrides of SSH config via pyinfra host data - "_pyinfra_ssh_forward_agent": self.data["ssh_forward_agent"], - "_pyinfra_ssh_config_file": self.data["ssh_config_file"], - "_pyinfra_ssh_known_hosts_file": self.data["ssh_known_hosts_file"], - "_pyinfra_ssh_strict_host_key_checking": self.data["ssh_strict_host_key_checking"], - "_pyinfra_ssh_paramiko_connect_kwargs": self.data["ssh_paramiko_connect_kwargs"], - } + yield f"@ssh/{name}", {"ssh_hostname": name}, [] + + # Event loop helpers - for key, value in ( - ("username", self.data["ssh_user"]), - ("port", int(self.data["ssh_port"] or 0)), - ("timeout", self.state.config.CONNECT_TIMEOUT), - ): - if value: - kwargs[key] = value + def _ensure_loop(self) -> None: + if self._loop is not None: + return - # Password auth (boo!) - ssh_password = self.data["ssh_password"] - if ssh_password: - kwargs["password"] = ssh_password + self._loop = asyncio.new_event_loop() + self._loop_ready = Event() + + def _run_loop() -> None: + assert self._loop is not None + asyncio.set_event_loop(self._loop) + assert self._loop_ready is not None + self._loop_ready.set() + self._loop.run_forever() + + self._loop_thread = Thread(target=_run_loop, daemon=True) + self._loop_thread.start() + assert self._loop_ready is not None + self._loop_ready.wait() + + def _submit(self, coro: Coroutine[Any, Any, T]) -> T: + self._ensure_loop() + assert self._loop is not None + future = asyncio.run_coroutine_threadsafe(coro, self._loop) + return future.result() + + # Connection management + + def _build_connect_kwargs( + self, + hostname: str, + strict_setting: str, + ) -> tuple[str, dict[str, Any]]: + kwargs: dict[str, Any] = { + "username": self.data["ssh_user"] or None, + "port": int(self.data["ssh_port"]) if self.data["ssh_port"] else None, + "password": self.data["ssh_password"] or None, + "agent_forwarding": self.data["ssh_forward_agent"], + "login_timeout": self.state.config.CONNECT_TIMEOUT, + } - # Key auth! ssh_key = self.data["ssh_key"] + ssh_key_password = self.data["ssh_key_password"] + if ssh_key: - kwargs["pkey"] = get_private_key( - self.state, - key_filename=ssh_key, - key_password=self.data["ssh_key_password"], - ) + key, certs = self._load_private_key(ssh_key, ssh_key_password) + kwargs["client_keys"] = [key] + if certs: + kwargs.setdefault("client_certs", []).extend(certs) + elif not self.data["ssh_look_for_keys"]: + kwargs["client_keys"] = [] + + if not self.data["ssh_allow_agent"]: + kwargs["agent_path"] = () + + read_config = getattr(asyncssh, "read_ssh_config", None) + config_files: list[str] = [] - # No key or password, so let's have paramiko look for SSH agents and user keys - # unless disabled by the user. + ssh_config_file = self.data["ssh_config_file"] + if ssh_config_file: + expanded_config = _expand_user_path(ssh_config_file) + config_files.append(expanded_config or ssh_config_file) else: - kwargs["allow_agent"] = self.data["ssh_allow_agent"] - kwargs["look_for_keys"] = self.data["ssh_look_for_keys"] + default_config = _expand_user_path("~/.ssh/config") + if default_config and os.path.isfile(default_config): + config_files.append(default_config) + + if config_files: + if read_config is None: + if ssh_config_file: + raise ConnectError("AsyncSSH does not provide read_ssh_config support") + else: + parsed_configs = [] + for config_file in config_files: + try: + parsed_configs.append(read_config(config_file)) + except FileNotFoundError: + if ssh_config_file: + raise ConnectError( + f"SSH config file not found: {config_file}" + ) from None + if parsed_configs: + kwargs["config"] = ( + parsed_configs[0] if len(parsed_configs) == 1 else parsed_configs + ) + + known_hosts_data = self.data.get("ssh_known_hosts_file") or None + if known_hosts_data: + known_hosts_path = _expand_user_path(known_hosts_data) + if known_hosts_path is None: + known_hosts_path = known_hosts_data + else: + known_hosts_path = _expand_user_path("~/.ssh/known_hosts") + + self._known_hosts_file = known_hosts_path if known_hosts_path else None + + if strict_setting in {"no", "off"}: + kwargs["known_hosts"] = None + elif strict_setting == "yes": + if self._known_hosts_file: + kwargs["known_hosts"] = self._known_hosts_file + else: + kwargs["known_hosts"] = None + + extra_kwargs = self.data.get("ssh_paramiko_connect_kwargs") or {} + converted_kwargs, hostname_override = self._convert_paramiko_kwargs(extra_kwargs, kwargs) + if hostname_override: + hostname = hostname_override + kwargs.update(converted_kwargs) + + if kwargs.get("port") is None: + kwargs.pop("port") + + return hostname, kwargs + + def _convert_paramiko_kwargs( + self, + paramiko_kwargs: dict[str, Any], + base_kwargs: dict[str, Any], + ) -> tuple[dict[str, Any], str | None]: + if not paramiko_kwargs: + return {}, None + + warnings.warn( + "ssh_paramiko_connect_kwargs is deprecated and will be removed in a future release. " + "Update host data to use AsyncSSH options directly.", + DeprecationWarning, + stacklevel=4, + ) + + converted: dict[str, Any] = {} + hostname_override: str | None = None + + passphrase = paramiko_kwargs.get("passphrase") + + handled_keys = { + "hostname", + "username", + "port", + "password", + "timeout", + "auth_timeout", + "banner_timeout", + "allow_agent", + "look_for_keys", + "compress", + "key_filename", + "pkey", + } - return kwargs + for key in handled_keys: + if key not in paramiko_kwargs: + continue + + value = paramiko_kwargs[key] + + if key == "hostname" and value: + hostname_override = str(value) + continue + + if key == "username" and value: + converted["username"] = value + continue + + if key == "port" and value: + converted["port"] = int(value) + continue + + if key == "password" and value is not None: + converted["password"] = value + continue + + if key == "timeout" and value: + converted["connect_timeout"] = value + continue + + if key == "auth_timeout" and value: + converted["login_timeout"] = value + continue + + if key == "banner_timeout" and value: + converted["banner_timeout"] = value + continue + + if key == "allow_agent": + if not value: + converted["agent_path"] = () + continue + + if key == "look_for_keys": + if ( + not value + and "client_keys" not in base_kwargs + and "client_keys" not in converted + ): + converted["client_keys"] = [] + continue + + if key == "compress": + if value: + converted["compression_algs"] = ["zlib@openssh.com", "zlib"] + else: + converted["compression_algs"] = ["none"] + continue + + if key == "key_filename" and value: + filenames: Iterable[str] + if isinstance(value, (list, tuple, set)): + filenames = [str(item) for item in value] + else: + filenames = [str(value)] + + keys: list[asyncssh.SSHKey] = [] + certs: list[asyncssh.SSHKey] = [] + for filename in filenames: + key_obj, key_certs = self._load_private_key( + filename, + passphrase or self.data["ssh_key_password"], + ) + keys.append(key_obj) + certs.extend(key_certs) + + converted["client_keys"] = keys + if certs: + converted.setdefault("client_certs", []).extend(certs) + continue + + if key == "pkey" and value is not None: + logger.warning( + "Ignoring Paramiko private key object provided via ssh_paramiko_connect_kwargs; " + "specify ssh_key or ssh_paramiko_connect_kwargs['key_filename'] instead.", + ) + continue + + passthrough = { + key: value + for key, value in paramiko_kwargs.items() + if key not in handled_keys and not key.startswith("_pyinfra_") + } + + converted.update(passthrough) + + return converted, hostname_override + + def _load_private_key( + self, + key_filename: str, + key_password: str, + ) -> tuple[asyncssh.SSHKey, list[asyncssh.SSHKey]]: + if key_filename in self.state.private_keys: + key = self.state.private_keys[key_filename] + certs = self.state.private_key_certs.get(key_filename, []) + return key, certs + + candidate_paths = [] + if self.state.cwd: + candidate_paths.append(os.path.join(self.state.cwd, key_filename)) + candidate_paths.append(os.path.expanduser(key_filename)) + + for filename in candidate_paths: + if not os.path.isfile(filename): + continue + + passphrase = key_password + + while True: + try: + key = asyncssh.read_private_key(filename, passphrase=passphrase) + certs = self._load_private_key_certificates(filename) + self.state.private_keys[key_filename] = key + self.state.private_key_certs[key_filename] = certs + return key, certs + except asyncssh.KeyImportError as exc: # encrypted key without passphrase + if "encrypted" not in str(exc).lower(): + break + + if passphrase: + break + + if pyinfra.is_cli: + passphrase = click.prompt( + f"Enter password for private key: {key_filename}", + hide_input=True, + ) + else: + raise PyinfraError( + "Private key file ({0}) is encrypted, set ssh_key_password to use this key".format( + key_filename, + ), + ) + + raise PyinfraError(f"No such private key file: {key_filename}") + + def _load_private_key_certificates(self, key_path: str) -> list[asyncssh.SSHKey]: + certificates: list[asyncssh.SSHKey] = [] + + base_candidates = {key_path} + stem, ext = os.path.splitext(key_path) + if stem: + base_candidates.add(stem) + + candidate_files: set[str] = set() + for base in base_candidates: + for suffix in ("-cert.pub", ".pub"): + candidate_files.add(f"{base}{suffix}") + + for candidate in candidate_files: + if not os.path.isfile(candidate): + continue + + try: + certificates.append(asyncssh.read_public_key(candidate)) + except (asyncssh.KeyImportError, OSError) as exc: + logger.warning("Failed to load certificate %s: %s", candidate, exc) + + return certificates @override def connect(self) -> None: + hostname = self.data["ssh_hostname"] or self.host.name + if self._transfer_protocol not in {"sftp", "scp"}: + raise ConnectError(f"Unsupported file transfer protocol: {self._transfer_protocol}") + strict_setting = (self.data["ssh_strict_host_key_checking"] or "accept-new").lower() + self._strict_setting = strict_setting + hostname, kwargs = self._build_connect_kwargs(hostname, strict_setting) + logger.debug("Connecting to: %s (%r)", hostname, kwargs) + + try: + self._connection = self._submit(self._async_connect(hostname, kwargs, strict_setting)) + except (asyncssh.Error, OSError) as exc: + raise ConnectError(f"SSH error connecting to {hostname}: {exc}") + + async def _async_connect( + self, + hostname: str, + kwargs: dict[str, Any], + strict_setting: str, + ) -> asyncssh.SSHClientConnection: retries = self.data["ssh_connect_retries"] + delay_min = self.data["ssh_connect_retry_min_delay"] + delay_max = self.data["ssh_connect_retry_max_delay"] + + attempt = 0 + while True: + try: + connection = await asyncssh.connect(hostname, **kwargs) + await self._handle_host_key_policy( + connection, hostname, kwargs.get("port"), strict_setting + ) + return connection + except (asyncssh.Error, OSError): + attempt += 1 + if attempt > retries: + raise + await asyncio.sleep(random.uniform(delay_min, delay_max)) + + async def _store_host_key( + self, + connection: asyncssh.SSHClientConnection, + hostname: str, + port: Optional[int], + ) -> None: + if not self._known_hosts_file: + return + + host_key = connection.get_server_host_key() + if host_key is None: + return + + entry_host = _format_known_host(hostname, port) + export = host_key.export_public_key() + export_text = export.decode() if isinstance(export, bytes) else str(export) + line = f"{entry_host} {export_text}\n" + + directory = os.path.dirname(self._known_hosts_file) + if directory: + os.makedirs(directory, exist_ok=True) try: - while True: - try: - return self._connect() - except (SSHException, gaierror, socket_error, EOFError): - if retries == 0: - raise - retries -= 1 - min_delay = self.data["ssh_connect_retry_min_delay"] - max_delay = self.data["ssh_connect_retry_max_delay"] - sleep(uniform(min_delay, max_delay)) - except SSHException as e: - raise_connect_error(self.host, "SSH error", e) - except gaierror as e: - raise_connect_error(self.host, "Could not resolve hostname", e) - except socket_error as e: - raise_connect_error(self.host, "Could not connect", e) - except EOFError as e: - raise_connect_error(self.host, "EOF error", e) - - def _connect(self) -> None: - """ - Connect to a single host. Returns the SSH client if successful. Stateless by - design so can be run in parallel. - """ - - kwargs = self.make_paramiko_kwargs() - hostname = kwargs.pop("hostname") - logger.debug("Connecting to: %s (%r)", hostname, kwargs) + with open(self._known_hosts_file, "a", encoding="utf-8") as known_hosts: + known_hosts.write(line) + except OSError as exc: + logger.warning("Failed to write host key for %s: %s", entry_host, exc) + + def _load_known_host_keys(self, hostname: str, port: Optional[int]) -> list[asyncssh.SSHKey]: + if not self._known_hosts_file: + return [] - self.client = SSHClient() + if not os.path.exists(self._known_hosts_file): + return [] try: - self.client.connect(hostname, **kwargs) - except AuthenticationException as e: - auth_kwargs = {} + known_hosts = asyncssh.read_known_hosts(self._known_hosts_file) + except (OSError, asyncssh.Error) as exc: + logger.warning("Failed to read known_hosts file %s: %s", self._known_hosts_file, exc) + return [] - for key, value in kwargs.items(): - if key in ("username", "password"): - auth_kwargs[key] = value - continue + matches = known_hosts.match(hostname, "", port) + matched_keys: list[asyncssh.SSHKey] = [] + for key_group in matches[:3]: + matched_keys.extend(key_group) + return matched_keys - if key == "pkey" and value: - auth_kwargs["key"] = self.data["ssh_key"] + @staticmethod + def _host_keys_equal(existing_key: asyncssh.SSHKey, host_key: asyncssh.SSHKey) -> bool: + return existing_key.export_public_key() == host_key.export_public_key() - auth_args = ", ".join( - "{0}={1}".format(key, value) for key, value in auth_kwargs.items() - ) + async def _handle_host_key_policy( + self, + connection: asyncssh.SSHClientConnection, + hostname: str, + port: Optional[int], + strict_setting: str, + ) -> None: + strict = (strict_setting or "accept-new").lower() - raise_connect_error(self.host, "Authentication error ({0})".format(auth_args), e) + if strict in {"no", "off"}: + return - except BadHostKeyException as e: - remove_entry = e.hostname - port = self.client._ssh_config.get("port", 22) - if port != 22: - remove_entry = f"[{e.hostname}]:{port}" + host_key = connection.get_server_host_key() + if host_key is None: + return - logger.warning("WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!") - logger.warning( - ("Someone could be eavesdropping on you right now (man-in-the-middle attack)!"), - ) - logger.warning("If this is expected, you can remove the bad key using:") - logger.warning(f" ssh-keygen -R {remove_entry}") + existing_keys = self._load_known_host_keys(hostname, port) + + if existing_keys: + if any(self._host_keys_equal(key, host_key) for key in existing_keys): + return - raise_connect_error( - self.host, - "SSH host key error", - f"Host key for {e.hostname} does not match.", + connection.close() + await connection.wait_closed() + raise ConnectError("SSH host key mismatch detected; refusing connection.") + + if strict == "yes": + connection.close() + await connection.wait_closed() + raise ConnectError( + "SSH host key not found in known_hosts and strict checking is enabled." ) + if strict == "ask": + if not pyinfra.is_cli: + connection.close() + await connection.wait_closed() + raise ConnectError( + "SSH host key not found in known_hosts and interactive confirmation is unavailable." + ) + + message = f"No host key for {hostname} found in known_hosts. Do you want to accept and add it?" + if not click.confirm(message, default=False): + connection.close() + await connection.wait_closed() + raise ConnectError("User declined to accept new SSH host key.") + + await self._store_host_key(connection, hostname, port) + @override def disconnect(self) -> None: - self.get_file_transfer_connection.cache.clear() + async def _close() -> None: + if self._sftp_client: + self._sftp_client.exit() + self._sftp_client = None + if self._connection is not None: + self._connection.close() + await self._connection.wait_closed() + + if self._loop is not None: + self._submit(_close()) + else: + self._sftp_client = None + + self._connection = None + + if self._loop and self._loop_thread: + self._loop.call_soon_threadsafe(self._loop.stop) + self._loop_thread.join() + self._loop = None + self._loop_thread = None + self._loop_ready = None + + # Command execution @override def run_shell_command( @@ -297,31 +695,13 @@ def run_shell_command( print_output: bool = False, print_input: bool = False, **arguments: Unpack["ConnectorArguments"], - ) -> Tuple[bool, CommandOutput]: - """ - Execute a command on the specified host. - - Args: - state (``pyinfra.api.State`` obj): state object for this command - hostname (string): hostname of the target - command (string): actual command to execute - sudo (boolean): whether to wrap the command with sudo - sudo_user (string): user to sudo to - get_pty (boolean): whether to get a PTY before executing the command - env (dict): environment variables to set - timeout (int): timeout for this command to complete before erroring - - Returns: - tuple: (exit_code, stdout, stderr) - stdout and stderr are both lists of strings from each buffer. - """ - + ) -> tuple[bool, CommandOutput]: _get_pty = arguments.pop("_get_pty", False) _timeout = arguments.pop("_timeout", None) _stdin = arguments.pop("_stdin", None) _success_exit_codes = arguments.pop("_success_exit_codes", None) - def execute_command() -> Tuple[int, CommandOutput]: + def execute_command() -> tuple[int, CommandOutput]: unix_command = make_unix_command_for_host(self.state, self.host, command, **arguments) actual_command = unix_command.get_raw_value() @@ -333,29 +713,23 @@ def execute_command() -> Tuple[int, CommandOutput]: ) if print_input: - click.echo("{0}>>> {1}".format(self.host.print_prefix, unix_command), err=True) + click.echo(f"{self.host.print_prefix}>>> {unix_command}", err=True) - # Run it! Get stdout, stderr & the underlying channel - assert self.client is not None - stdin_buffer, stdout_buffer, stderr_buffer = self.client.exec_command( - actual_command, - get_pty=_get_pty, - ) + stdin_value = _normalise_stdin(_stdin) - if _stdin: - write_stdin(_stdin, stdin_buffer) - - combined_output = read_output_buffers( - stdout_buffer, - stderr_buffer, - timeout=_timeout, - print_output=print_output, - print_prefix=self.host.print_prefix, - ) - - logger.debug("Waiting for exit status...") - exit_status = stdout_buffer.channel.recv_exit_status() - logger.debug("Command exit status: %i", exit_status) + try: + exit_status, combined_output = self._submit( + self._async_run_command( + actual_command, + stdin_value, + _get_pty, + _timeout, + print_output, + self.host.print_prefix, + ), + ) + except asyncio.TimeoutError as exc: + raise timeout_error() from exc return exit_status, combined_output @@ -372,36 +746,114 @@ def execute_command() -> Tuple[int, CommandOutput]: return status, combined_output - @memoize - def get_file_transfer_connection(self) -> FileTransferClient | None: - assert self.client is not None - transport = self.client.get_transport() - assert transport is not None, "No transport" + async def _async_run_command( + self, + command: str, + stdin_value: Optional[str], + get_pty: bool, + timeout: Optional[int], + print_output: bool, + print_prefix: str, + ) -> tuple[int, CommandOutput]: + assert self._connection is not None, "SSH connection not initialised" + try: - if self.data["ssh_file_transfer_protocol"] == "sftp": - logger.debug("Using SFTP for file transfer") - return SFTPClient.from_transport(transport) - elif self.data["ssh_file_transfer_protocol"] == "scp": - logger.debug("Using SCP for file transfer") - return SCPClient(transport) - else: - raise ConnectError( - "Unsupported file transfer protocol: {0}".format( - self.data["ssh_file_transfer_protocol"], - ), - ) - except SSHException as e: - raise ConnectError( - ( - "Unable to establish SFTP connection. Check that the SFTP subsystem " - "for the SSH service at {0} is enabled." - ).format(self.host), - ) from e + result = await self._connection.run( + command, + check=False, + term_type="xterm" if get_pty else None, + input=stdin_value, + timeout=timeout, + ) + except asyncio.TimeoutError: + raise + + stdout_value = result.stdout or "" + stderr_value = result.stderr or "" + + if isinstance(stdout_value, bytes): + stdout_text = stdout_value.decode() + else: + stdout_text = stdout_value + + if isinstance(stderr_value, bytes): + stderr_text = stderr_value.decode() + else: + stderr_text = stderr_value + + combined_lines: list[OutputLine] = [] + + for line in stdout_text.splitlines(): + if print_output: + click.echo(f"{print_prefix}{line}", err=True) + combined_lines.append(OutputLine("stdout", line)) + + for line in stderr_text.splitlines(): + if print_output: + click.echo(f"{print_prefix}{click.style(line, 'red')}", err=True) + combined_lines.append(OutputLine("stderr", line)) + + exit_status = result.exit_status if result.exit_status is not None else 0 + + return exit_status, CommandOutput(combined_lines) + + # File transfer helpers + + async def _ensure_sftp(self) -> asyncssh.SFTPClient: + assert self._connection is not None, "SSH connection not initialised" + if self._sftp_client is None: + self._sftp_client = await self._connection.start_sftp_client() + return self._sftp_client + + async def _async_read_file(self, remote_filename: str) -> bytes: + sftp = await self._ensure_sftp() + async with sftp.open(remote_filename, "rb") as remote_file: + return await remote_file.read() + + async def _async_write_file(self, remote_filename: str, data: bytes) -> None: + sftp = await self._ensure_sftp() + async with sftp.open(remote_filename, "wb") as remote_file: + await remote_file.write(data) + + async def _async_scp_upload(self, remote_filename: str, data: bytes) -> None: + assert self._connection is not None, "SSH connection not initialised" + + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + temp_file.write(data) + temp_file.flush() + temp_path = temp_file.name + + try: + await asyncssh.scp(temp_path, (self._connection, remote_filename)) + finally: + try: + os.unlink(temp_path) + except FileNotFoundError: + pass + + async def _async_scp_download(self, remote_filename: str) -> bytes: + assert self._connection is not None, "SSH connection not initialised" - def _get_file(self, remote_filename: str, filename_or_io: str | IO): + basename = os.path.basename(remote_filename.rstrip("/")) or "pyinfra-download" + with tempfile.TemporaryDirectory() as temp_dir: + local_path = os.path.join(temp_dir, basename) + await asyncssh.scp((self._connection, remote_filename), local_path) + with open(local_path, "rb") as local_file: + return local_file.read() + + @memoize + def get_file_transfer_connection(self) -> FileTransferClient: + if self._transfer_protocol == "scp": + return _SCPWrapper(self) + return _SFTPWrapper(self) + + def _get_file(self, remote_filename: str, filename_or_io: str | IO) -> None: + if self._transfer_protocol == "scp": + data = self._submit(self._async_scp_download(remote_filename)) + else: + data = self._submit(self._async_read_file(remote_filename)) with get_file_io(filename_or_io, "wb") as file_io: - sftp = self.get_file_transfer_connection() - sftp.getfo(remote_filename, file_io) + file_io.write(data) @override def get_file( @@ -413,20 +865,11 @@ def get_file( print_input: bool = False, **arguments: Unpack["ConnectorArguments"], ) -> bool: - """ - Download a file from the remote host using SFTP. Supports download files - with sudo by copying to a temporary directory with read permissions, - downloading and then removing the copy. - """ - _sudo = arguments.get("_sudo", False) _su_user = arguments.get("_su_user", None) if _sudo or _su_user: - # Get temp file location temp_file = remote_temp_filename or self.host.get_temp_filename(remote_filename) - - # Copy the file to the tempfile location and add read permissions command = StringCommand( "cp", remote_filename, temp_file, "&&", "chmod", "+r", temp_file ) @@ -439,56 +882,35 @@ def get_file( ) if copy_status is False: - logger.error("File download copy temp error: {0}".format(output.stderr)) + logger.error("File download copy temp error: %s", output.stderr) return False try: self._get_file(temp_file, filename_or_io) - - # Ensure that, even if we encounter an error, we (attempt to) remove the - # temporary copy of the file. finally: - remove_status, output = self.run_shell_command( + self.run_shell_command( StringCommand("rm", "-f", temp_file), print_output=print_output, print_input=print_input, **arguments, ) - - if remove_status is False: - logger.error("File download remove temp error: {0}".format(output.stderr)) - return False - else: self._get_file(remote_filename, filename_or_io) if print_output: - click.echo( - "{0}file downloaded: {1}".format(self.host.print_prefix, remote_filename), - err=True, - ) + click.echo(f"{self.host.print_prefix}file downloaded: {remote_filename}", err=True) return True def _put_file(self, filename_or_io, remote_location): - logger.debug("Attempting upload of %s to %s", filename_or_io, remote_location) - - attempts = 0 - last_e = None - - while attempts < 3: - try: - with get_file_io(filename_or_io) as file_io: - sftp = self.get_file_transfer_connection() - sftp.putfo(file_io, remote_location) - return - except OSError as e: - logger.warning(f"Failed to upload file, retrying: {e}") - attempts += 1 - last_e = e - - if last_e is not None: - raise last_e + with get_file_io(filename_or_io) as file_io: + data = file_io.read() + if isinstance(data, str): + data = data.encode() + if self._transfer_protocol == "scp": + self._submit(self._async_scp_upload(remote_location, data)) + else: + self._submit(self._async_write_file(remote_location, data)) @override def put_file( @@ -500,11 +922,6 @@ def put_file( print_input: bool = False, **arguments: Unpack["ConnectorArguments"], ) -> bool: - """ - Upload file-ios to the specified host using SFTP. Supports uploading files - with sudo by uploading to a temporary directory then moving & chowning. - """ - original_arguments = arguments.copy() _sudo = arguments.pop("_sudo", False) @@ -513,67 +930,53 @@ def put_file( _doas_user = arguments.pop("_doas_user", False) _su_user = arguments.pop("_su_user", None) - # sudo/su are a little more complicated, as you can only sftp with the SSH - # user connected, so upload to tmp and copy/chown w/sudo and/or su_user if _sudo or _doas or _su_user: - # Get temp file location temp_file = remote_temp_filename or self.host.get_temp_filename(remote_filename) self._put_file(filename_or_io, temp_file) - # Make sure our sudo/su user can access the file other_user = _su_user or _sudo_user or _doas_user if other_user: status, output = self.run_shell_command( StringCommand("setfacl", "-m", f"u:{other_user}:r", temp_file), print_output=print_output, print_input=print_input, - **arguments, + **original_arguments, ) - if status is False: - logger.error("Error on handover to sudo/su user: {0}".format(output.stderr)) + logger.error("Unable to set ACL for temp file: %s", output.stderr) return False - # Execute run_shell_command w/sudo, etc - command = StringCommand("cp", temp_file, QuoteString(remote_filename)) - - status, output = self.run_shell_command( - command, - print_output=print_output, - print_input=print_input, - **original_arguments, + command = StringCommand( + "mv", + temp_file, + remote_filename, + "&&", + "chmod", + "0644", + remote_filename, ) - if status is False: - logger.error("File upload error: {0}".format(output.stderr)) - return False - - # Delete the temporary file now that we've successfully copied it - command = StringCommand("rm", "-f", temp_file) - status, output = self.run_shell_command( command, print_output=print_output, print_input=print_input, - **arguments, + **original_arguments, ) if status is False: - logger.error("Unable to remove temporary file: {0}".format(output.stderr)) + logger.error("File upload error: %s", output.stderr) return False - # No sudo and no su_user, so just upload it! else: self._put_file(filename_or_io, remote_filename) if print_output: - click.echo( - "{0}file uploaded: {1}".format(self.host.print_prefix, remote_filename), - err=True, - ) + click.echo(f"{self.host.print_prefix}file uploaded: {remote_filename}", err=True) return True + # Rsync support remains shell-based + @override def check_can_rsync(self) -> None: if self.data["ssh_key_password"]: @@ -584,6 +987,8 @@ def check_can_rsync(self) -> None: if self.data["ssh_password"]: raise NotImplementedError("Rsync does not currently work with SSH passwords.") + from shutil import which + if not which("rsync"): raise NotImplementedError("The `rsync` binary is not available on this system.") @@ -596,66 +1001,55 @@ def rsync( print_output: bool = False, print_input: bool = False, **arguments: Unpack["ConnectorArguments"], - ): + ) -> bool: _sudo = arguments.pop("_sudo", False) _sudo_user = arguments.pop("_sudo_user", False) hostname = self.data["ssh_hostname"] or self.host.name user = self.data["ssh_user"] - if user: - user = "{0}@".format(user) - - ssh_flags = [] - # To avoid asking for interactive input, specify BatchMode=yes - ssh_flags.append("-o BatchMode=yes") - - known_hosts_file = self.data["ssh_known_hosts_file"] - if known_hosts_file: - ssh_flags.append( - '-o \\"UserKnownHostsFile={0}\\"'.format(shlex.quote(known_hosts_file)) - ) # never trust users - - strict_host_key_checking = self.data["ssh_strict_host_key_checking"] - if strict_host_key_checking: - ssh_flags.append( - '-o \\"StrictHostKeyChecking={0}\\"'.format(shlex.quote(strict_host_key_checking)) - ) + user_prefix = f"{user}@" if user else "" + + ssh_flags = ["-o BatchMode=yes"] + + if self._known_hosts_file: + ssh_flags.append(f'-o "UserKnownHostsFile={shlex.quote(self._known_hosts_file)}"') + + strict_setting = (self._strict_host_key_checking or "accept-new").lower() + ssh_flags.append(f'-o "StrictHostKeyChecking={shlex.quote(strict_setting)}"') ssh_config_file = self.data["ssh_config_file"] if ssh_config_file: - ssh_flags.append("-F {0}".format(shlex.quote(ssh_config_file))) + ssh_flags.append(f"-F {shlex.quote(ssh_config_file)}") port = self.data["ssh_port"] if port: - ssh_flags.append("-p {0}".format(port)) + ssh_flags.append(f"-p {port}") ssh_key = self.data["ssh_key"] if ssh_key: - ssh_flags.append("-i {0}".format(ssh_key)) + ssh_flags.append(f"-i {shlex.quote(ssh_key)}") remote_rsync_command = "rsync" if _sudo: remote_rsync_command = "sudo rsync" if _sudo_user: - remote_rsync_command = "sudo -u {0} rsync".format(_sudo_user) + remote_rsync_command = f"sudo -u {_sudo_user} rsync" rsync_command = ( - "rsync {rsync_flags} " - '--rsh "ssh {ssh_flags}" ' - "--rsync-path '{remote_rsync_command}' " - "{src} {user}{hostname}:{dest}" + "rsync {rsync_flags} --rsh \"ssh {ssh_flags}\" --rsync-path '{remote_rsync_command}' " + "{src} {user_prefix}{hostname}:{dest}" ).format( rsync_flags=" ".join(flags), ssh_flags=" ".join(ssh_flags), remote_rsync_command=remote_rsync_command, - user=user or "", - hostname=hostname, src=src, + user_prefix=user_prefix, + hostname=hostname, dest=dest, ) if print_input: - click.echo("{0}>>> {1}".format(self.host.print_prefix, rsync_command), err=True) + click.echo(f"{self.host.print_prefix}>>> {rsync_command}", err=True) return_code, output = run_local_process( rsync_command, @@ -663,8 +1057,7 @@ def rsync( print_prefix=self.host.print_prefix, ) - status = return_code == 0 - if not status: + if return_code != 0: raise IOError(output.stderr) return True diff --git a/src/pyinfra/connectors/ssh_util.py b/src/pyinfra/connectors/ssh_util.py deleted file mode 100644 index a70685880..000000000 --- a/src/pyinfra/connectors/ssh_util.py +++ /dev/null @@ -1,114 +0,0 @@ -from getpass import getpass -from os import path -from typing import TYPE_CHECKING, Type, Union - -from paramiko import ( - DSSKey, - ECDSAKey, - Ed25519Key, - PasswordRequiredException, - PKey, - RSAKey, - SSHException, -) - -import pyinfra -from pyinfra.api.exceptions import ConnectError, PyinfraError - -if TYPE_CHECKING: - from pyinfra.api.host import Host - from pyinfra.api.state import State - - -def raise_connect_error(host: "Host", message, data): - message = "{0} ({1})".format(message, data) - raise ConnectError(message) - - -def _load_private_key_file(filename: str, key_filename: str, key_password: str): - exception: Union[PyinfraError, SSHException] = PyinfraError("Invalid key: {0}".format(filename)) - - key_cls: Union[Type[RSAKey], Type[DSSKey], Type[ECDSAKey], Type[Ed25519Key]] - - for key_cls in (RSAKey, DSSKey, ECDSAKey, Ed25519Key): - try: - return key_cls.from_private_key_file( - filename=filename, - ) - - except PasswordRequiredException: - if not key_password: - # If password is not provided, but we're in CLI mode, ask for it. I'm not a - # huge fan of having CLI specific code in here, but it doesn't really fit - # anywhere else without duplicating lots of key related code into cli.py. - if pyinfra.is_cli: - key_password = getpass( - "Enter password for private key: {0}: ".format( - key_filename, - ), - ) - - # API mode and no password? We can't continue! - else: - raise PyinfraError( - "Private key file ({0}) is encrypted, set ssh_key_password to " - "use this key".format(key_filename), - ) - - try: - return key_cls.from_private_key_file( - filename=filename, - password=key_password, - ) - except SSHException as e: # key does not match key_cls type - exception = e - except SSHException as e: # key does not match key_cls type - exception = e - raise exception - - -def get_private_key(state: "State", key_filename: str, key_password: str) -> PKey: - if key_filename in state.private_keys: - return state.private_keys[key_filename] - - ssh_key_filenames = [ - # Global from executed directory - path.expanduser(key_filename), - ] - - if state.cwd: - # Relative to the CWD - path.join(state.cwd, key_filename) - - key = None - key_file_exists = False - - for filename in ssh_key_filenames: - if not path.isfile(filename): - continue - - key_file_exists = True - - try: - key = _load_private_key_file(filename, key_filename, key_password) - break - except SSHException: - pass - - # No break, so no key found - if not key: - if not key_file_exists: - raise PyinfraError("No such private key file: {0}".format(key_filename)) - raise PyinfraError("Invalid private key file: {0}".format(key_filename)) - - # Load any certificate, names from OpenSSH: - # https://github.com/openssh/openssh-portable/blob/049297de975b92adcc2db77e3fb7046c0e3c695d/ssh-keygen.c#L2453 # noqa: E501 - for certificate_filename in ( - "{0}-cert.pub".format(key_filename), - "{0}.pub".format(key_filename), - ): - if path.isfile(certificate_filename): - key.load_certificate(certificate_filename) - - state.private_keys[key_filename] = key - return key diff --git a/src/pyinfra/connectors/sshuserclient/__init__.py b/src/pyinfra/connectors/sshuserclient/__init__.py deleted file mode 100644 index 969715edd..000000000 --- a/src/pyinfra/connectors/sshuserclient/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .client import SSHClient # noqa: F401 diff --git a/src/pyinfra/connectors/sshuserclient/client.py b/src/pyinfra/connectors/sshuserclient/client.py deleted file mode 100644 index cfb022af8..000000000 --- a/src/pyinfra/connectors/sshuserclient/client.py +++ /dev/null @@ -1,309 +0,0 @@ -""" -This file as originally part of the "sshuserclient" pypi package. The GitHub -source has now vanished (https://github.com/tobald/sshuserclient). -""" - -from os import path - -from gevent.lock import BoundedSemaphore -from paramiko import ( - HostKeys, - MissingHostKeyPolicy, - ProxyCommand, - SSHClient as ParamikoClient, - SSHException, -) -from paramiko.agent import AgentRequestHandler -from paramiko.hostkeys import HostKeyEntry -from typing_extensions import override - -from pyinfra import logger -from pyinfra.api.util import memoize - -from .config import SSHConfig - -HOST_KEYS_LOCK = BoundedSemaphore() - - -class StrictPolicy(MissingHostKeyPolicy): - @override - def missing_host_key(self, client, hostname, key): - logger.error("No host key for {0} found in known_hosts".format(hostname)) - raise SSHException( - "StrictPolicy: No host key for {0} found in known_hosts".format(hostname), - ) - - -def append_hostkey(client, hostname, key): - """Append hostname to the clients host_keys_file""" - - with HOST_KEYS_LOCK: - # The paramiko client saves host keys incorrectly whereas the host keys object does - # this correctly, so use that with the client filename variable. - # See: https://github.com/paramiko/paramiko/pull/1989 - host_key_entry = HostKeyEntry([hostname], key) - if host_key_entry is None: - raise SSHException( - "Append Hostkey: Failed to parse host {0}, could not append to hostfile".format( - hostname - ), - ) - with open(client._host_keys_filename, "a") as host_keys_file: - hk_entry = host_key_entry.to_line() - if hk_entry is None: - raise SSHException(f"Append Hostkey: Failed to append hostkey ({host_key_entry})") - - host_keys_file.write(hk_entry) - - -class AcceptNewPolicy(MissingHostKeyPolicy): - @override - def missing_host_key(self, client, hostname, key): - logger.warning( - ( - f"No host key for {hostname} found in known_hosts, " - "accepting & adding to host keys file" - ), - ) - - append_hostkey(client, hostname, key) - logger.warning("Added host key for {0} to known_hosts".format(hostname)) - - -class AskPolicy(MissingHostKeyPolicy): - @override - def missing_host_key(self, client, hostname, key): - should_continue = input( - "No host key for {0} found in known_hosts, do you want to continue [y/n] ".format( - hostname, - ), - ) - if should_continue.lower() != "y": - raise SSHException( - "AskPolicy: No host key for {0} found in known_hosts".format(hostname), - ) - append_hostkey(client, hostname, key) - logger.warning("Added host key for {0} to known_hosts".format(hostname)) - return - - -class WarningPolicy(MissingHostKeyPolicy): - @override - def missing_host_key(self, client, hostname, key): - logger.warning("No host key for {0} found in known_hosts".format(hostname)) - - -def get_missing_host_key_policy(policy): - if policy is None or policy == "ask": - return AskPolicy() - if policy == "no" or policy == "off": - return WarningPolicy() - if policy == "yes": - return StrictPolicy() - if policy == "accept-new": - return AcceptNewPolicy() - raise SSHException("Invalid value StrictHostKeyChecking={}".format(policy)) - - -@memoize -def get_ssh_config(user_config_file=None): - logger.debug("Loading SSH config: %s", user_config_file) - - if user_config_file is None: - user_config_file = path.expanduser("~/.ssh/config") - - if path.exists(user_config_file): - with open(user_config_file, encoding="utf-8") as f: - ssh_config = SSHConfig() - ssh_config.parse(f) - return ssh_config - - -@memoize -def get_host_keys(filename): - with HOST_KEYS_LOCK: - host_keys = HostKeys() - - try: - host_keys.load(filename) - # When paramiko encounters a bad host keys line it sometimes bails the - # entire load incorrectly. - # See: https://github.com/paramiko/paramiko/pull/1990 - except Exception as e: - logger.warning("Failed to load host keys from {0}: {1}".format(filename, e)) - - return host_keys - - -class SSHClient(ParamikoClient): - """ - An SSHClient which honors ssh_config and supports proxyjumping - original idea at http://bitprophet.org/blog/2012/11/05/gateway-solutions/. - """ - - @override - def connect( # type: ignore[override] - self, - hostname, - _pyinfra_ssh_forward_agent=None, - _pyinfra_ssh_config_file=None, - _pyinfra_ssh_known_hosts_file=None, - _pyinfra_ssh_strict_host_key_checking=None, - _pyinfra_ssh_paramiko_connect_kwargs=None, - **kwargs, - ): - ( - hostname, - config, - forward_agent, - missing_host_key_policy, - host_keys_file, - keep_alive, - ) = self.parse_config( - hostname, - kwargs, - ssh_config_file=_pyinfra_ssh_config_file, - strict_host_key_checking=_pyinfra_ssh_strict_host_key_checking, - ) - self.set_missing_host_key_policy(missing_host_key_policy) - config.update(kwargs) - - if _pyinfra_ssh_known_hosts_file: - host_keys_file = _pyinfra_ssh_known_hosts_file - - # Overwrite paramiko empty defaults with @memoize-d host keys object - self._host_keys = get_host_keys(host_keys_file) - self._host_keys_filename = host_keys_file - - if _pyinfra_ssh_paramiko_connect_kwargs: - config.update(_pyinfra_ssh_paramiko_connect_kwargs) - - self._ssh_config = config - super().connect(hostname, **config) - - if _pyinfra_ssh_forward_agent is not None: - forward_agent = _pyinfra_ssh_forward_agent - - if keep_alive: - transport = self.get_transport() - assert transport is not None, "No transport" - transport.set_keepalive(keep_alive) - - if forward_agent: - transport = self.get_transport() - assert transport is not None, "No transport" - session = transport.open_session() - AgentRequestHandler(session) - - def gateway(self, hostname, host_port, target, target_port): - transport = self.get_transport() - assert transport is not None, "No transport" - return transport.open_channel( - "direct-tcpip", - (target, target_port), - (hostname, host_port), - ) - - def parse_config( - self, - hostname, - initial_cfg=None, - ssh_config_file=None, - strict_host_key_checking=None, - ): - cfg: dict = {"port": 22} - cfg.update(initial_cfg or {}) - - keep_alive = 0 - forward_agent = False - missing_host_key_policy = get_missing_host_key_policy(strict_host_key_checking) - host_keys_file = path.expanduser("~/.ssh/known_hosts") # OpenSSH default - - ssh_config = get_ssh_config(ssh_config_file) - if not ssh_config: - return hostname, cfg, forward_agent, missing_host_key_policy, host_keys_file, keep_alive - - host_config = ssh_config.lookup(hostname) - forward_agent = host_config.get("forwardagent") == "yes" - - # If not overridden, apply any StrictHostKeyChecking - if strict_host_key_checking is None and "stricthostkeychecking" in host_config: - missing_host_key_policy = get_missing_host_key_policy( - host_config["stricthostkeychecking"], - ) - - if "userknownhostsfile" in host_config: - host_keys_file = path.expanduser(host_config["userknownhostsfile"]) - - if "hostname" in host_config: - hostname = host_config["hostname"] - - if "user" in host_config: - cfg["username"] = host_config["user"] - - if "identityfile" in host_config: - cfg["key_filename"] = host_config["identityfile"] - - if "port" in host_config: - cfg["port"] = int(host_config["port"]) - - if "serveraliveinterval" in host_config: - keep_alive = int(host_config["serveraliveinterval"]) - - if "proxycommand" in host_config: - cfg["sock"] = ProxyCommand(host_config["proxycommand"]) - - elif "proxyjump" in host_config: - hops = host_config["proxyjump"].split(",") - sock = None - - for i, hop in enumerate(hops): - hop_hostname, hop_config = self.derive_shorthand(ssh_config, hop) - logger.debug("SSH ProxyJump through %s:%s", hop_hostname, hop_config["port"]) - - c = SSHClient() - c.connect( - hop_hostname, _pyinfra_ssh_config_file=ssh_config_file, sock=sock, **hop_config - ) - - if i == len(hops) - 1: - target = hostname - target_config = {"port": cfg["port"]} - else: - target, target_config = self.derive_shorthand(ssh_config, hops[i + 1]) - - sock = c.gateway(hostname, cfg["port"], target, target_config["port"]) - cfg["sock"] = sock - - return hostname, cfg, forward_agent, missing_host_key_policy, host_keys_file, keep_alive - - @staticmethod - def derive_shorthand(ssh_config, host_string): - shorthand_config = {} - user_hostport = host_string.rsplit("@", 1) - hostport = user_hostport.pop() - user = user_hostport[0] if user_hostport and user_hostport[0] else None - if user: - shorthand_config["username"] = user - - # IPv6: can't reliably tell where addr ends and port begins, so don't - # try (and don't bother adding special syntax either, user should avoid - # this situation by using port=). - if hostport.count(":") > 1: - hostname = hostport - # IPv4: can split on ':' reliably. - else: - host_port = hostport.rsplit(":", 1) - hostname = host_port.pop(0) or None - if host_port and host_port[0]: - shorthand_config["port"] = int(host_port[0]) - - base_config = ssh_config.lookup(hostname) - - config = { - "port": base_config.get("port", 22), - "username": base_config.get("user"), - } - config.update(shorthand_config) - - return hostname, config diff --git a/src/pyinfra/connectors/sshuserclient/config.py b/src/pyinfra/connectors/sshuserclient/config.py deleted file mode 100644 index 8c9a86eb6..000000000 --- a/src/pyinfra/connectors/sshuserclient/config.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -This file as originally part of the "sshuserclient" pypi package. The GitHub -source has now vanished (https://github.com/tobald/sshuserclient). -""" - -import glob -import re -from os import environ, path - -import paramiko.config -from gevent.subprocess import CalledProcessError, check_call -from paramiko import SSHConfig as ParamikoSSHConfig -from typing_extensions import override - -from pyinfra import logger - -SETTINGS_REGEX = re.compile(r"(\w+)(?:\s*=\s*|\s+)(.+)") - - -class FakeInvokeResult: - ok = False - - -class FakeInvoke: - @staticmethod - def run(cmd, *args, **kwargs): - result = FakeInvokeResult() - - try: - cmd = [environ["SHELL"], cmd] - try: - code = check_call(cmd) - except CalledProcessError as e: - code = e.returncode - result.ok = code == 0 - except Exception as e: - logger.warning( - ("pyinfra encountered an error loading SSH config match exec {0}: {1}").format( - cmd, - e, - ), - ) - - return result - - -paramiko.config.invoke = FakeInvoke # type: ignore - - -def _expand_include_statements(file_obj, parsed_files=None): - parsed_lines = [] - - for line in file_obj.readlines(): - line = line.strip() - if not line or line.startswith("#"): - continue - - match = re.match(SETTINGS_REGEX, line) - if not match: - parsed_lines.append(line) - continue - - key = match.group(1).lower() - value = match.group(2) - - if key != "include": - parsed_lines.append(line) - continue - - if parsed_files is None: - parsed_files = [] - - # The path can be relative to its parent configuration file - if path.isabs(value) is False and value[0] != "~": - folder = path.dirname(file_obj.name) - value = path.join(folder, value) - - value = path.expanduser(value) - - for filename in glob.iglob(value): - if path.isfile(filename): - if filename in parsed_files: - raise Exception( - "Include loop detected in ssh config file: %s" % filename, - ) - with open(filename, encoding="utf-8") as fd: - parsed_files.append(filename) - parsed_lines.extend(_expand_include_statements(fd, parsed_files)) - - return parsed_lines - - -class SSHConfig(ParamikoSSHConfig): - """ - an SSHConfig that supports includes directives - https://github.com/paramiko/paramiko/pull/1194 - """ - - @override - def parse(self, file_obj): - file_obj = _expand_include_statements(file_obj) - return super().parse(file_obj) diff --git a/src/pyinfra/connectors/util.py b/src/pyinfra/connectors/util.py index abe46f7e9..f4bcf5550 100644 --- a/src/pyinfra/connectors/util.py +++ b/src/pyinfra/connectors/util.py @@ -4,12 +4,12 @@ from dataclasses import dataclass from getpass import getpass from queue import Queue +from threading import Thread from socket import timeout as timeout_error from subprocess import PIPE, Popen from typing import TYPE_CHECKING, Callable, Iterable, Optional, Union import click -import gevent from pyinfra import logger from pyinfra.api import MaskString, QuoteString, StringCommand @@ -149,39 +149,41 @@ def read_output_buffers( ) -> CommandOutput: output_queue: Queue[OutputLine] = Queue() - # Iterate through outputs to get an exit status and generate desired list - # output, done in two greenlets so stdout isn't printed before stderr. Not - # attached to state.pool to avoid blocking it with 2x n-hosts greenlets. - stdout_reader = gevent.spawn( - read_buffer, - "stdout", - stdout_buffer, - output_queue, - print_output=print_output, - print_func=lambda line: "{0}{1}".format(print_prefix, line), + stdout_reader = Thread( + target=read_buffer, + args=( + "stdout", + stdout_buffer, + output_queue, + ), + kwargs={ + "print_output": print_output, + "print_func": lambda line: f"{print_prefix}{line}", + }, + daemon=True, ) - stderr_reader = gevent.spawn( - read_buffer, - "stderr", - stderr_buffer, - output_queue, - print_output=print_output, - print_func=lambda line: "{0}{1}".format( - print_prefix, - click.style(line, "red"), + + stderr_reader = Thread( + target=read_buffer, + args=( + "stderr", + stderr_buffer, + output_queue, ), + kwargs={ + "print_output": print_output, + "print_func": lambda line: f"{print_prefix}{click.style(line, 'red')}", + }, + daemon=True, ) - # Wait on output, with our timeout (or None) - greenlets = gevent.wait((stdout_reader, stderr_reader), timeout=timeout) + stdout_reader.start() + stderr_reader.start() - # Timeout doesn't raise an exception, but gevent.wait returns the greenlets - # which did complete. So if both haven't completed, we kill them and fail - # with a timeout. - if len(greenlets) != 2: - stdout_reader.kill() - stderr_reader.kill() + stdout_reader.join(timeout) + stderr_reader.join(timeout) + if stdout_reader.is_alive() or stderr_reader.is_alive(): raise timeout_error() return CommandOutput(list(output_queue.queue)) diff --git a/src/pyinfra/context.py b/src/pyinfra/context.py index f098bbdbc..5f74907ca 100644 --- a/src/pyinfra/context.py +++ b/src/pyinfra/context.py @@ -6,10 +6,10 @@ """ from contextlib import contextmanager +from contextvars import ContextVar from types import ModuleType -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Type, cast -from gevent.local import local from typing_extensions import override if TYPE_CHECKING: @@ -19,12 +19,26 @@ from pyinfra.api.state import State -class container: +class _Container: module = None +class _ContextVarContainer: + def __init__(self) -> None: + # Unique name for debugging / clarity + self._var: ContextVar[Any] = ContextVar(f"pyinfra_ctx_{id(self)}", default=None) + + @property + def module(self) -> Any: + return self._var.get() + + @module.setter + def module(self, value: Any) -> None: + self._var.set(value) + + class ContextObject: - _container_cls = container + _container_cls: Type[Any] = _Container _base_cls: ModuleType def __init__(self) -> None: @@ -86,30 +100,30 @@ def __hash__(self): class LocalContextObject(ContextObject): - _container_cls = local + _container_cls = _ContextVarContainer class ContextManager: - def __init__(self, key, context_cls): + def __init__(self, key: str, context_cls: Type[ContextObject]): self.context = context_cls() - def get(self): + def get(self) -> Any: return getattr(self.context._container, "module", None) - def set(self, module): + def set(self, module: Any) -> None: self.context._container.module = module - def set_base(self, module): + def set_base(self, module: Any) -> None: self.context._base_cls = module def reset(self) -> None: self.context._container.module = None - def isset(self): + def isset(self) -> bool: return self.get() is not None @contextmanager - def use(self, module): + def use(self, module: Any): old_module = self.get() if old_module is module: yield # if we're double-setting, nothing to do @@ -119,21 +133,21 @@ def use(self, module): self.set(old_module) -ctx_state = ContextManager("state", ContextObject) -state: "State" = ctx_state.context +ctx_state = ContextManager("state", LocalContextObject) +state = cast("State", ctx_state.context) -ctx_inventory = ContextManager("inventory", ContextObject) -inventory: "Inventory" = ctx_inventory.context +ctx_inventory = ContextManager("inventory", LocalContextObject) +inventory = cast("Inventory", ctx_inventory.context) # Config can be modified mid-deploy, so we use a local object here which # is based on a copy of the state config. ctx_config = ContextManager("config", LocalContextObject) -config: "Config" = ctx_config.context +config = cast("Config", ctx_config.context) # Hosts are prepared in parallel each in a greenlet, so we use a local to # point at different host objects in each greenlet. ctx_host = ContextManager("host", LocalContextObject) -host: "Host" = ctx_host.context +host = cast("Host", ctx_host.context) def init_base_classes() -> None: diff --git a/src/pyinfra/sync_context.py b/src/pyinfra/sync_context.py new file mode 100644 index 000000000..eaaabb7e2 --- /dev/null +++ b/src/pyinfra/sync_context.py @@ -0,0 +1,364 @@ +from __future__ import annotations + +from contextlib import ExitStack +from contextvars import Token +from typing import Any, Iterable, Mapping + +from typing_extensions import Protocol + +from pyinfra.api.host import Host +from pyinfra.api.operation import ( + OperationMeta, + execute_immediately, + push_sync_context, + reset_sync_context, + suspend_sync_context, +) +from pyinfra.api.state import State, StateStage +from pyinfra.context import ctx_config, ctx_host, ctx_inventory, ctx_state + + +class SupportsOperation(Protocol): + def __call__(self, *args, **kwargs) -> OperationMeta: # pragma: no cover - Protocol stub + ... + + +class SyncContext: + """Sync helper for running individual operations or facts against hosts.""" + + def __init__( + self, + state: State, + hosts: Iterable[Host | str] | None = None, + ) -> None: + self.state = state + self._default_hosts = self._normalise_hosts(hosts) + self._managed_hosts: set[Host] = set() + self._auto_manage_connections = False + self._in_context = False + self._context_token: Token | None = None + + def _normalise_hosts(self, hosts: Iterable[Host | str] | None) -> list[Host]: + if hosts is None: + return list(self.state.inventory.iter_active_hosts()) or list(self.state.inventory) + + normalised: list[Host] = [] + for host in hosts: + if isinstance(host, Host): + normalised.append(host) + else: + resolved = self.state.inventory.get_host(host) + if resolved is None: + raise ValueError(f"Unknown host: {host}") + normalised.append(resolved) + return normalised + + def _with_context(self, host: Host): + stack = ExitStack() + stack.enter_context(ctx_state.use(self.state)) + stack.enter_context(ctx_inventory.use(self.state.inventory)) + stack.enter_context(ctx_config.use(self.state.config.copy())) + stack.enter_context(ctx_host.use(host)) + return stack + + def __enter__(self) -> SyncContext: + if self._in_context: + raise RuntimeError("SyncContext is already in use as a context manager") + + self._auto_manage_connections = True + self._in_context = True + + if self.state.current_stage < StateStage.Connect: + self.state.set_stage(StateStage.Connect) + + try: + self._ensure_hosts_connected(self._default_hosts) + except BaseException: + self._auto_manage_connections = False + self._in_context = False + raise + + self._context_token = push_sync_context(self) + return self + + def __exit__(self, exc_type, exc, tb) -> None: # noqa: ANN001 - sync context protocol + disconnect_error: BaseException | None = None + + try: + if self._auto_manage_connections: + try: + self._disconnect_managed_hosts() + except BaseException as exc_disconnect: + disconnect_error = exc_disconnect + finally: + self._auto_manage_connections = False + self._in_context = False + if self._context_token is not None: + reset_sync_context(self._context_token) + self._context_token = None + + if self.state.current_stage < StateStage.Disconnect: + self.state.set_stage(StateStage.Disconnect) + + if disconnect_error is not None and exc_type is None: + raise disconnect_error + + def _call_wrapped_operation( + self, + operation: SupportsOperation, + args: tuple[Any, ...], + kwargs: dict[str, Any], + ) -> Mapping[Host, OperationMeta]: + op_kwargs = dict(kwargs) + hosts_override = op_kwargs.pop("hosts", None) + suspend_token = suspend_sync_context() + try: + return self.run_operation( + operation, + *args, + hosts=hosts_override, + **op_kwargs, + ) + finally: + reset_sync_context(suspend_token) + + def _ensure_hosts_connected(self, hosts: Iterable[Host]) -> None: + if not self._auto_manage_connections: + return + + hosts_to_connect = [host for host in hosts if not host.connected] + if not hosts_to_connect: + return + + successful_hosts: list[Host] = [] + errors: list[BaseException] = [] + + for host in hosts_to_connect: + try: + host.connect(reason="sync context", raise_exceptions=True) + except BaseException as exc: + errors.append(exc) + else: + self._managed_hosts.add(host) + successful_hosts.append(host) + + if errors: + if successful_hosts: + try: + self._disconnect_managed_hosts(successful_hosts) + except BaseException: + pass + raise errors[0] + + def _disconnect_managed_hosts(self, hosts: Iterable[Host] | None = None) -> None: + targets = list(hosts) if hosts is not None else list(self._managed_hosts) + if not targets: + return + + disconnect_error: BaseException | None = None + for host in targets: + self._managed_hosts.discard(host) + if not host.connected: + continue + try: + host.disconnect() + except BaseException as exc: + if disconnect_error is None: + disconnect_error = exc + + if disconnect_error is not None: + raise disconnect_error + + def run_operation( + self, + operation: SupportsOperation, + *args, + hosts: Iterable[Host | str] | None = None, + **kwargs, + ) -> Mapping[Host, OperationMeta]: + """Execute an operation immediately for each host.""" + + targets = self._normalise_hosts(hosts) if hosts is not None else self._default_hosts + + suspend_token = suspend_sync_context() + try: + self._ensure_hosts_connected(targets) + + results = {} + for host in targets: + op_meta = self._execute_operation(host, operation, args, kwargs) + results[host] = op_meta + return results + finally: + reset_sync_context(suspend_token) + + def _execute_operation( + self, + host: Host, + operation: SupportsOperation, + op_args: tuple[Any, ...], + op_kwargs: dict[str, Any], + ) -> OperationMeta: + with self._with_context(host): + if self.state.current_stage < StateStage.Prepare: + self.state.set_stage(StateStage.Prepare) + if self.state.current_stage < StateStage.Execute: + self.state.set_stage(StateStage.Execute) + elif self.state.current_stage > StateStage.Execute: + self.state.current_stage = StateStage.Execute + + was_executing = self.state.is_executing + if not was_executing: + self.state.is_executing = True + + if host not in self.state.activated_hosts: + self.state.activate_host(host) + + try: + op_meta = operation(*op_args, **op_kwargs) + + if not op_meta.is_complete(): + execute_immediately(self.state, host, op_meta._hash) + + return op_meta + finally: + if not was_executing: + self.state.is_executing = False + + def get_fact( + self, + fact_cls, + *fact_args, + hosts: Iterable[Host | str] | None = None, + **fact_kwargs, + ) -> Mapping[Host, Any]: + """Fetch a fact synchronously for the selected hosts.""" + + targets = self._normalise_hosts(hosts) if hosts is not None else self._default_hosts + + suspend_token = suspend_sync_context() + try: + self._ensure_hosts_connected(targets) + + results = {} + for host in targets: + value = self._fetch_fact(host, fact_cls, fact_args, fact_kwargs) + results[host] = value + return results + finally: + reset_sync_context(suspend_token) + + def _fetch_fact( + self, + host: Host, + fact_cls, + fact_args: tuple[Any, ...], + fact_kwargs: dict[str, Any], + ) -> Any: + with self._with_context(host): + return host.get_fact(fact_cls, *fact_args, **fact_kwargs) + + def _call_wrapped_deploy( + self, + deploy_fn, + args: tuple[Any, ...], + kwargs: dict[str, Any], + ) -> None: + deploy_kwargs = dict(kwargs) + hosts_override = deploy_kwargs.pop("hosts", None) + suspend_token = suspend_sync_context() + try: + self.run_deploy(deploy_fn, *args, hosts=hosts_override, **deploy_kwargs) + finally: + reset_sync_context(suspend_token) + + def _call_wrapped_fact( + self, + host: Host, + fact_cls, + args: tuple[Any, ...], + kwargs: dict[str, Any], + ) -> Any: + fact_kwargs = dict(kwargs) + fact_args = args + + suspend_token = suspend_sync_context() + try: + self._ensure_hosts_connected([host]) + return self._fetch_fact(host, fact_cls, fact_args, fact_kwargs) + finally: + reset_sync_context(suspend_token) + + def run_deploy( + self, + deploy_fn, + *deploy_args, + hosts: Iterable[Host | str] | None = None, + **deploy_kwargs, + ) -> None: + """Execute a deploy immediately for the selected hosts.""" + + targets = self._normalise_hosts(hosts) if hosts is not None else self._default_hosts + + self._ensure_hosts_connected(targets) + + for host in targets: + self._execute_deploy(host, deploy_fn, deploy_args, deploy_kwargs) + + def _execute_deploy( + self, + host: Host, + deploy_fn, + deploy_args: tuple[Any, ...], + deploy_kwargs: dict[str, Any], + ) -> None: + with self._with_context(host): + if self.state.current_stage < StateStage.Prepare: + self.state.set_stage(StateStage.Prepare) + if self.state.current_stage < StateStage.Execute: + self.state.set_stage(StateStage.Execute) + elif self.state.current_stage > StateStage.Execute: + self.state.current_stage = StateStage.Execute + + was_executing = self.state.is_executing + if not was_executing: + self.state.is_executing = True + + if host not in self.state.activated_hosts: + self.state.activate_host(host) + + try: + deploy_fn(*deploy_args, **deploy_kwargs) + finally: + if not was_executing: + self.state.is_executing = False + + +class SyncHostContext: + """Convenience wrapper around :class:`SyncContext` for a single host.""" + + def __init__(self, state: State, host: Host | str) -> None: + self.state = state + self._host_arg = host + self.host: Host | None = None + self._context: SyncContext | None = None + + def _resolve_host(self) -> Host: + if isinstance(self._host_arg, Host): + return self._host_arg + + resolved = self.state.inventory.get_host(self._host_arg) + if resolved is None: + raise ValueError(f"Unknown host: {self._host_arg}") + return resolved + + def __enter__(self) -> SyncContext: + self.host = self._resolve_host() + self._context = SyncContext(self.state, hosts=[self.host]) + return self._context.__enter__() + + def __exit__(self, exc_type, exc, tb) -> None: # noqa: ANN001 - sync context protocol + if self._context is None: + return + self._context.__exit__(exc_type, exc, tb) + self._context = None diff --git a/src/pyinfra_cli/__init__.py b/src/pyinfra_cli/__init__.py index 6badc3534..5bd1190d7 100644 --- a/src/pyinfra_cli/__init__.py +++ b/src/pyinfra_cli/__init__.py @@ -1,6 +1 @@ -# Monkey patch the stdlib for gevent - this _must_ happen here, as early as -# possible, to avoid problems with the stdlib usage elsewhere in pyinfra. API -# scripts should also patch gevent at the start of their execution. -from gevent import monkey # noqa - -monkey.patch_all() # noqa +"""pyinfra CLI package.""" diff --git a/src/pyinfra_cli/inventory.py b/src/pyinfra_cli/inventory.py index 67e067310..5c8ac315c 100644 --- a/src/pyinfra_cli/inventory.py +++ b/src/pyinfra_cli/inventory.py @@ -1,11 +1,13 @@ import socket from collections import defaultdict from os import listdir, path +from functools import lru_cache from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union +import asyncssh + from pyinfra import logger from pyinfra.api.inventory import Inventory -from pyinfra.connectors.sshuserclient.client import get_ssh_config from pyinfra.context import ctx_inventory from .exceptions import CliError @@ -122,7 +124,7 @@ def _get_ssh_alias(maybe_host: str) -> Optional[str]: logger.debug('Checking if "%s" is an SSH alias', maybe_host) # Note this does not cover the case where `host.data.ssh_config_file` is used - ssh_config = get_ssh_config() + ssh_config = _load_default_ssh_config() if ssh_config is None: logger.debug("Could not load SSH config") @@ -341,3 +343,21 @@ def make_inventory_from_files( groups[name] = ([], data) return Inventory(groups.pop("all"), override_data=override_data, **groups) + + +@lru_cache(maxsize=1) +def _load_default_ssh_config(): + config_path = path.expanduser("~/.ssh/config") + if not path.exists(config_path): + return None + + read_config = getattr(asyncssh, "read_ssh_config", None) + if read_config is None: + logger.debug("asyncssh.read_ssh_config is unavailable") + return None + + try: + return read_config(config_path) + except (OSError, asyncssh.Error) as exc: + logger.debug("Failed to load SSH config at %s: %s", config_path, exc) + return None diff --git a/src/pyinfra_cli/main.py b/src/pyinfra_cli/main.py index a03c170c0..1ddb76881 100644 --- a/src/pyinfra_cli/main.py +++ b/src/pyinfra_cli/main.py @@ -2,7 +2,6 @@ import sys import click -import gevent import pyinfra @@ -29,12 +28,5 @@ def _handle_interrupt(signum, frame): click.echo("Exiting upon user request!") sys.exit(0) - try: - # Kill any greenlets on ctrl+c - gevent.signal_handler(signal.SIGINT, gevent.kill) - except AttributeError: - # Legacy (gevent <1.2) support - gevent.signal(signal.SIGINT, gevent.kill) - signal.signal(signal.SIGINT, _handle_interrupt) # print the message and exit main cli() diff --git a/src/pyinfra_cli/util.py b/src/pyinfra_cli/util.py index cf429411b..14ec8b2c9 100644 --- a/src/pyinfra_cli/util.py +++ b/src/pyinfra_cli/util.py @@ -9,10 +9,10 @@ from os import path from pathlib import Path from types import CodeType, FunctionType, ModuleType -from typing import Callable +from typing import Any, Callable, TYPE_CHECKING +import asyncio import click -import gevent from pyinfra import logger, state from pyinfra.api.command import PyinfraCommand @@ -26,7 +26,7 @@ StateOperationHostData, StateOperationMeta, ) -from pyinfra.context import ctx_config, ctx_host +from pyinfra.context import ctx_config, ctx_host, ctx_state from pyinfra.progress import progress_spinner from .exceptions import CliError, UnexpectedExternalError @@ -34,6 +34,9 @@ # Cache for compiled Python deploy code PYTHON_CODES: dict[str, CodeType] = {} +if TYPE_CHECKING: + from pyinfra.api.host import Host + def is_subdir(child, parent): child = path.realpath(child) @@ -201,33 +204,73 @@ def try_import_module_attribute(path, prefix=None, raise_for_none=True): return attr -def _parallel_load_hosts(state: "State", callback: Callable, name: str): +async def _parallel_load_hosts_async(state: "State", callback: Callable, name: str) -> None: def load_file(local_host): try: - with ctx_config.use(state.config.copy()): - with ctx_host.use(local_host): - callback() - logger.info( - "{0}{1} {2}".format( - local_host.print_prefix, - click.style("Ready:", "green"), - click.style(name, bold=True), - ), - ) - except Exception as e: + with ctx_state.use(state): + with ctx_config.use(state.config.copy()): + with ctx_host.use(local_host): + callback() + logger.info( + "{0}{1} {2}".format( + local_host.print_prefix, + click.style("Ready:", "green"), + click.style(name, bold=True), + ), + ) + except Exception as e: # noqa: BLE001 return e + return None + + hosts = list(state.inventory.iter_active_hosts()) + + if not hosts: + return + + task_to_host = [ + ( + asyncio.create_task(state.run_in_executor(load_file, host)), + host, + ) + for host in hosts + ] + + with progress_spinner(hosts) as progress: + + def _make_progress_callback(target_host: "Host") -> Callable[[asyncio.Future[Any]], None]: + def _callback(_task: asyncio.Future[Any]) -> None: + progress(target_host) - greenlet_to_host = { - state.pool.spawn(load_file, host): host for host in state.inventory.iter_active_hosts() - } - - with progress_spinner(greenlet_to_host.values()) as progress: - for greenlet in gevent.iwait(greenlet_to_host.keys()): - host = greenlet_to_host[greenlet] - result = greenlet.get() - if isinstance(result, Exception): - raise result - progress(host) + return _callback + + for task, host in task_to_host: + task.add_done_callback(_make_progress_callback(host)) + + results = await asyncio.gather( + *(task for task, _ in task_to_host), + return_exceptions=True, + ) + + exceptions: list[Exception] = [] + + for (_task, _host), result in zip(task_to_host, results, strict=True): + if isinstance(result, Exception): + exceptions.append(result) + + if exceptions: + raise exceptions[0] + + +def _parallel_load_hosts(state: "State", callback: Callable, name: str) -> None: + try: + asyncio.run(_parallel_load_hosts_async(state, callback, name)) + except RuntimeError as exc: + if "already running" in str(exc): + raise RuntimeError( + "Parallel host loading cannot run while an asyncio loop is active. " + "Use the async helper instead.", + ) from exc + raise def load_deploy_file(state: "State", filename): diff --git a/tests/__init__.py b/tests/__init__.py index 19c8a1765..c895758ef 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,13 +1,7 @@ -# Import `pyinfra_cli` to trigger gevent monkey patching import logging -import gevent.hub - import pyinfra_cli # noqa: F401 from pyinfra import logger logging.basicConfig(level=logging.DEBUG) logger.setLevel(logging.DEBUG) - -# Don't print out exceptions inside greenlets (because here we expect them!) -gevent.hub.Hub.NOT_ERROR = (Exception,) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..8fe6acb95 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +from dataclasses import dataclass +from types import SimpleNamespace +from typing import Any, Dict +import shlex + +import asyncssh +import pytest + + +class _FakeHostKey: + def export_public_key(self) -> str: + return "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFakeHostKeyForTestingOnly" + + +class _FakeSFTPHandle: + def __init__(self, storage: Dict[str, bytes], filename: str, mode: str) -> None: + self._storage = storage + self._filename = filename + self._mode = mode + + async def __aenter__(self) -> "_FakeSFTPHandle": + return self + + async def __aexit__(self, exc_type, exc, tb) -> bool: # noqa: ANN001 + return False + + async def write(self, data: bytes | str) -> None: + if "r" in self._mode: + raise IOError("Cannot write using read handle") + if isinstance(data, str): + data = data.encode() + self._storage[self._filename] = data + + async def read(self) -> bytes: + if "r" not in self._mode: + raise IOError("Cannot read using write handle") + return self._storage.get(self._filename, b"") + + +class _FakeSFTPClient: + def __init__(self) -> None: + self._storage: Dict[str, bytes] = {} + + def exit(self) -> None: # pragma: no cover - provided for interface compatibility + pass + + def open(self, filename: str, mode: str): # noqa: ANN001 - matches asyncssh signature + return _FakeSFTPHandle(self._storage, filename, mode) + + +@dataclass +class FakeSSHClient: + hostname: str + command_results: Dict[str, Dict[str, Any]] + + def __post_init__(self) -> None: + self.commands_run: list[str] = [] + self._sftp_client = _FakeSFTPClient() + self._closed = False + + async def run( + self, + command: str, + *, + check: bool = False, + term_type: str | None = None, + input: str | None = None, + timeout: int | None = None, + ) -> SimpleNamespace: + self.commands_run.append(command) + result = self.command_results.get(command) + if result is None: + try: + parts = shlex.split(command) + except ValueError: + parts = [] + + if len(parts) >= 3 and parts[0] in {"sh", "bash"} and parts[1] in {"-c", "-lc"}: + inner_command = parts[2] + result = self.command_results.get(inner_command) + if result is None: + result = {"stdout": "", "stderr": "", "exit_status": 0} + stdout = result.get("stdout", "") + stderr = result.get("stderr", "") + exit_status = result.get("exit_status", 0) + return SimpleNamespace(stdout=stdout, stderr=stderr, exit_status=exit_status) + + async def start_sftp_client(self) -> _FakeSFTPClient: + return self._sftp_client + + def get_server_host_key(self) -> _FakeHostKey: + return _FakeHostKey() + + def close(self) -> None: + self._closed = True + + async def wait_closed(self) -> None: + pass + + +@pytest.fixture +def fake_asyncssh(monkeypatch): + connections: Dict[str, FakeSSHClient] = {} + + async def _connect(hostname: str, **kwargs): # noqa: ANN001 - match asyncssh + client = FakeSSHClient(hostname=hostname, command_results={}) + connections[hostname] = client + return client + + class _FakeSSHConfig: + def __init__(self, entries: Dict[str, Dict[str, Any]] | None = None) -> None: + self._entries = entries or {} + + def lookup(self, hostname: str) -> Dict[str, Any]: + return self._entries.get(hostname, {}) + + def _read_ssh_config(*_args, **_kwargs) -> _FakeSSHConfig: + return _FakeSSHConfig() + + monkeypatch.setattr(asyncssh, "connect", _connect) + monkeypatch.setattr(asyncssh, "read_ssh_config", _read_ssh_config, raising=False) + return connections diff --git a/tests/paramiko_util.py b/tests/paramiko_util.py deleted file mode 100644 index 8ac2b5d3b..000000000 --- a/tests/paramiko_util.py +++ /dev/null @@ -1,109 +0,0 @@ -from inspect import isclass -from unittest import TestCase - -from paramiko import RSAKey, SFTPClient, SSHClient -from paramiko.agent import AgentRequestHandler - -from pyinfra.connectors import ssh - - -class PatchSSHTestCase(TestCase): - """ - A test class that patches out the paramiko SSH parts such that they succeed as normal. - The SSH tests above check these are called correctly. - """ - - @classmethod - def setUpClass(cls): - ssh.SSHClient = FakeSSHClient - ssh.SFTPClient = FakeSFTPClient - ssh.RSAKey = FakeRSAKey - ssh.AgentRequestHandler = FakeAgentRequestHandler - - @classmethod - def tearDownClass(cls): - ssh.SSHClient = SSHClient - ssh.SFTPClient = SFTPClient - ssh.RSAKey = RSAKey - ssh.AgentRequestHandler = AgentRequestHandler - - -class FakeAgentRequestHandler: - def __init__(self, arg): - pass - - -class FakeChannel: - def __init__(self, exit_status): - self.exit_status = exit_status - - def exit_status_ready(self): - return True - - def recv_exit_status(self): - return self.exit_status - - def write(self, data): - pass - - def close(self): - pass - - -class FakeBuffer: - def __init__(self, data, channel): - self.channel = channel - self.data = data - - def __iter__(self): - return iter(self.data) - - -class FakeSSHClient: - def close(self): - pass - - def load_system_host_keys(self): - pass - - def set_missing_host_key_policy(self, _): - pass - - def connect(self, hostname, *args, **kwargs): - if isclass(hostname) and issubclass(hostname, Exception): - raise hostname() - - def get_transport(self): - return self - - def open_session(self): - pass - - def exec_command(self, command, get_pty=None): - channel = FakeChannel(0) - return ( - channel, - FakeBuffer([], channel), - FakeBuffer([], channel), - ) - - -class FakeSFTPClient: - @classmethod - def from_transport(cls, transport): - return cls() - - def close(self): - pass - - def putfo(self, file_io, remote_location): - pass - - def getfo(self, remote_location, file_io): - pass - - -class FakeRSAKey: - @classmethod - def from_private_key_file(cls, *args, **kwargs): - return cls() diff --git a/tests/test_api/test_api.py b/tests/test_api/test_api.py index 66bb12484..6267e9c2c 100644 --- a/tests/test_api/test_api.py +++ b/tests/test_api/test_api.py @@ -1,82 +1,25 @@ -from unittest import TestCase -from unittest.mock import patch - -from paramiko import SSHException +from __future__ import annotations from pyinfra.api import Config, State -from pyinfra.api.connect import connect_all -from pyinfra.api.exceptions import NoGroupError, NoHostError, PyinfraError +from pyinfra.api.connect import connect_all, disconnect_all -from ..paramiko_util import PatchSSHTestCase from ..util import make_inventory -class TestInventoryApi(TestCase): - def test_inventory_creation(self): - inventory = make_inventory() - - # Check length - assert len(inventory.hosts) == 2 - - # Get a host - host = inventory.get_host("somehost") - assert host.data.ssh_user == "vagrant" - - # Check our group data - assert inventory.get_group_data("test_group") == { - "group_data": "hello world", - } - - def test_tuple_host_group_inventory_creation(self): - inventory = make_inventory( - hosts=[ - ("somehost", {"some_data": "hello"}), - ], - tuple_group=( - [ - ("somehost", {"another_data": "world"}), - ], - { - "tuple_group_data": "word", - }, - ), - ) - - # Check host data - host = inventory.get_host("somehost") - assert host.data.some_data == "hello" - assert host.data.another_data == "world" - - # Check group data - assert host.data.tuple_group_data == "word" - - def test_host_and_group_errors(self): - inventory = make_inventory() - - with self.assertRaises(NoHostError): - inventory.get_host("i-dont-exist") - - with self.assertRaises(NoGroupError): - inventory.get_group("i-dont-exist") +def test_inventory_construction(): + inventory = make_inventory() + assert len(inventory.hosts) == 2 + assert inventory.get_host("somehost") is not None + assert inventory.get_host("anotherhost") is not None -class TestStateApi(PatchSSHTestCase): - @patch("pyinfra.connectors.base.raise_if_bad_type", lambda *args, **kwargs: None) - def test_fail_percent(self): - inventory = make_inventory( - ( - "somehost", - ("thinghost", {"ssh_hostname": SSHException}), - "anotherhost", - ), - ) - state = State(inventory, Config(FAIL_PERCENT=1)) - # Ensure we would fail at this point - with self.assertRaises(PyinfraError) as context: - connect_all(state) +def test_state_connect_cycle(fake_asyncssh): + inventory = make_inventory() + state = State(inventory, Config()) - assert context.exception.args[0] == "Over 1% of hosts failed (33%)" + connect_all(state) + assert len(state.active_hosts) == 2 - # Ensure the other two did connect - assert len(state.active_hosts) == 2 + disconnect_all(state) + assert len(state.active_hosts) == 0 diff --git a/tests/test_api/test_api_deploys.py b/tests/test_api/test_api_deploys.py index 41ccae031..723951f05 100644 --- a/tests/test_api/test_api_deploys.py +++ b/tests/test_api/test_api_deploys.py @@ -1,127 +1,28 @@ -from pyinfra.api import Config, State, StringCommand +from __future__ import annotations + +from pyinfra.api import Config, State from pyinfra.api.connect import connect_all, disconnect_all from pyinfra.api.deploy import add_deploy, deploy from pyinfra.api.operations import run_ops -from pyinfra.api.state import StateStage from pyinfra.operations import server -from ..paramiko_util import PatchSSHTestCase from ..util import make_inventory -class TestDeploysApi(PatchSSHTestCase): - def test_deploy(self): - inventory = make_inventory() - somehost = inventory.get_host("somehost") - anotherhost = inventory.get_host("anotherhost") - - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - - # Enable printing on this test to catch any exceptions in the formatting - state.print_output = True - state.print_input = True - state.print_fact_info = True - state.print_noop_info = True - - connect_all(state) - - @deploy() - def test_deploy(state=None, host=None): - server.shell(commands=["echo first command"]) - server.shell(commands=["echo second command"]) - - add_deploy(state, test_deploy) - - op_order = state.get_op_order() - - # Ensure we have an op - assert len(op_order) == 2 - - # Ensure run ops works - run_ops(state) - - first_op_hash = op_order[0] - assert state.op_meta[first_op_hash].names == {"test_deploy | server.shell"} - assert state.ops[somehost][first_op_hash].operation_meta._commands == [ - StringCommand("echo first command"), - ] - assert state.ops[anotherhost][first_op_hash].operation_meta._commands == [ - StringCommand("echo first command"), - ] - - second_op_hash = op_order[1] - assert state.op_meta[second_op_hash].names == {"test_deploy | server.shell"} - assert state.ops[somehost][second_op_hash].operation_meta._commands == [ - StringCommand("echo second command"), - ] - assert state.ops[anotherhost][second_op_hash].operation_meta._commands == [ - StringCommand("echo second command"), - ] - - # Ensure ops completed OK - assert state.results[somehost].success_ops == 2 - assert state.results[somehost].ops == 2 - assert state.results[anotherhost].success_ops == 2 - assert state.results[anotherhost].ops == 2 - - # And w/o errors - assert state.results[somehost].error_ops == 0 - assert state.results[anotherhost].error_ops == 0 - - disconnect_all(state) - - def test_nested_deploy(self): - inventory = make_inventory() - somehost = inventory.get_host("somehost") - - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - - # Enable printing on this test to catch any exceptions in the formatting - state.print_output = True - state.print_input = True - state.print_fact_info = True - state.print_noop_info = True - - connect_all(state) - - @deploy() - def test_nested_deploy(): - server.shell(commands=["echo nested command"]) - - @deploy() - def test_deploy(): - server.shell(commands=["echo first command"]) - test_nested_deploy() - server.shell(commands=["echo second command"]) - - add_deploy(state, test_deploy) - - op_order = state.get_op_order() +@deploy() +def _sample_deploy(): + server.shell("echo deploy") - # Ensure we have an op - assert len(op_order) == 3 - # Ensure run ops works - run_ops(state) +def test_deploy_runs_commands(fake_asyncssh): + inventory = make_inventory() + state = State(inventory, Config()) - first_op_hash = op_order[0] - assert state.op_meta[first_op_hash].names == {"test_deploy | server.shell"} - assert state.ops[somehost][first_op_hash].operation_meta._commands == [ - StringCommand("echo first command"), - ] + connect_all(state) + add_deploy(state, _sample_deploy) + run_ops(state) - second_op_hash = op_order[1] - assert state.op_meta[second_op_hash].names == { - "test_deploy | test_nested_deploy | server.shell", - } - assert state.ops[somehost][second_op_hash].operation_meta._commands == [ - StringCommand("echo nested command"), - ] + for connection in fake_asyncssh.values(): + assert any("echo deploy" in command for command in connection.commands_run) - third_op_hash = op_order[2] - assert state.op_meta[third_op_hash].names == {"test_deploy | server.shell"} - assert state.ops[somehost][third_op_hash].operation_meta._commands == [ - StringCommand("echo second command"), - ] + disconnect_all(state) diff --git a/tests/test_api/test_api_facts.py b/tests/test_api/test_api_facts.py index 24798ae7c..d32089764 100644 --- a/tests/test_api/test_api_facts.py +++ b/tests/test_api/test_api_facts.py @@ -1,338 +1,29 @@ -from typing import cast -from unittest.mock import MagicMock, patch +from __future__ import annotations from pyinfra.api import Config, State -from pyinfra.api.arguments import CONNECTOR_ARGUMENT_KEYS, AllArguments, pop_global_arguments -from pyinfra.api.connect import connect_all -from pyinfra.api.exceptions import PyinfraError +from pyinfra.api.connect import connect_all, disconnect_all from pyinfra.api.facts import get_facts -from pyinfra.connectors.util import CommandOutput, OutputLine -from pyinfra.context import ctx_host, ctx_state -from pyinfra.facts.server import Arch, Command +from pyinfra.facts.server import Command -from ..paramiko_util import PatchSSHTestCase from ..util import make_inventory -def _get_executor_defaults(state, host): - with ctx_state.use(state): - with ctx_host.use(host): - global_argument_defaults, _ = pop_global_arguments(state, host, {}) - return { - key: value - for key, value in global_argument_defaults.items() - if key in CONNECTOR_ARGUMENT_KEYS - } +def test_get_facts_runs_commands(fake_asyncssh): + inventory = make_inventory() + state = State(inventory, Config()) + connect_all(state) -class TestFactsApi(PatchSSHTestCase): - def test_get_fact(self): - inventory = make_inventory(hosts=("anotherhost",)) - state = State(inventory, Config()) + for connection in fake_asyncssh.values(): + connection.command_results["echo fact"] = { + "stdout": "value\n", + "stderr": "", + "exit_status": 0, + } - anotherhost = inventory.get_host("anotherhost") + result = get_facts(state, Command, ("echo fact",)) - connect_all(state) + for host, value in result.items(): + assert value == "value" - with patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") as fake_run_command: - fake_run_command.return_value = ( - True, - CommandOutput([OutputLine("stdout", "some-output")]), - ) - fact_data = get_facts(state, Command, ("yes",)) - - assert fact_data == {anotherhost: "some-output"} - - fake_run_command.assert_called_with( - "yes", - print_input=False, - print_output=False, - **_get_executor_defaults(state, anotherhost), - ) - - def test_get_fact_current_op_global_arguments(self): - inventory = make_inventory(hosts=("anotherhost",)) - state = State(inventory, Config()) - - anotherhost = inventory.get_host("anotherhost") - - connect_all(state) - anotherhost.current_op_global_arguments = cast( - AllArguments, - { - "_sudo": True, - "_sudo_user": "someuser", - "_su_user": "someuser", - "_timeout": 10, - "_env": {"HELLO": "WORLD"}, - }, - ) - - with patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") as fake_run_command: - fake_run_command.return_value = ( - True, - CommandOutput([OutputLine("stdout", "some-output")]), - ) - fact_data = get_facts(state, Command, ("yes",)) - - assert fact_data == {anotherhost: "some-output"} - - defaults = _get_executor_defaults(state, anotherhost) - defaults.update(anotherhost.current_op_global_arguments) - - fake_run_command.assert_called_with( - "yes", - print_input=False, - print_output=False, - **defaults, - ) - - def test_get_fact_error(self): - inventory = make_inventory(hosts=("anotherhost",)) - state = State(inventory, Config()) - - anotherhost = inventory.get_host("anotherhost") - - connect_all(state) - - with patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") as fake_run_command: - fake_run_command.return_value = False, MagicMock() - - with self.assertRaises(PyinfraError) as context: - get_facts(state, Command, ("fail command",)) - - assert context.exception.args[0] == "No hosts remaining!" - - fake_run_command.assert_called_with( - "fail command", - print_input=False, - print_output=False, - **_get_executor_defaults(state, anotherhost), - ) - - def test_get_fact_error_ignore(self): - inventory = make_inventory(hosts=("anotherhost",)) - state = State(inventory, Config()) - - anotherhost = inventory.get_host("anotherhost") - - connect_all(state) - anotherhost.in_op = True - anotherhost.current_op_global_arguments = cast( - AllArguments, - { - "_ignore_errors": True, - }, - ) - - with patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") as fake_run_command: - fake_run_command.return_value = False, MagicMock() - fact_data = get_facts(state, Command, ("fail command",)) - - assert fact_data == {anotherhost: None} - - fake_run_command.assert_called_with( - "fail command", - print_input=False, - print_output=False, - **_get_executor_defaults(state, anotherhost), - ) - - def test_get_fact_executor_override_arguments(self): - inventory = make_inventory(hosts=("anotherhost",)) - state = State(inventory, Config()) - - anotherhost = inventory.get_host("anotherhost") - - connect_all(state) - - with patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") as fake_run_command: - fake_run_command.return_value = ( - MagicMock(), - CommandOutput([OutputLine("stdout", "some-output")]), - ) - fact_data = get_facts(state, Command, ("yes",), {"_sudo": True}) - - assert fact_data == {anotherhost: "some-output"} - - defaults = _get_executor_defaults(state, anotherhost) - defaults["_sudo"] = True - - fake_run_command.assert_called_with( - "yes", - print_input=False, - print_output=False, - **defaults, - ) - - def test_get_fact_executor_host_data_arguments(self): - inventory = make_inventory(hosts=("anotherhost",)) - state = State(inventory, Config()) - - anotherhost = inventory.get_host("anotherhost") - anotherhost.data._sudo = True - - connect_all(state) - - with patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") as fake_run_command: - fake_run_command.return_value = ( - True, - CommandOutput([OutputLine("stdout", "some-output")]), - ) - fact_data = get_facts(state, Command, ("yes",)) - - assert fact_data == {anotherhost: "some-output"} - - defaults = _get_executor_defaults(state, anotherhost) - defaults["_sudo"] = True - - fake_run_command.assert_called_with( - "yes", - print_input=False, - print_output=False, - **defaults, - ) - - def test_get_fact_executor_mixed_arguments(self): - inventory = make_inventory(hosts=("anotherhost",)) - state = State(inventory, Config()) - - anotherhost = inventory.get_host("anotherhost") - anotherhost.data._sudo = True - anotherhost.data._sudo_user = "this-should-be-overridden" - anotherhost.data._su_user = "this-should-be-overridden" - - anotherhost.current_op_global_arguments = cast( - AllArguments, - { - "_su_user": "override-su-user", - }, - ) - - connect_all(state) - - with patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") as fake_run_command: - fake_run_command.return_value = ( - True, - CommandOutput([OutputLine("stdout", "some-output")]), - ) - fact_data = get_facts( - state, - Command, - args=("yes",), - kwargs={"_sudo_user": "override-sudo-user"}, - ) - - assert fact_data == {anotherhost: "some-output"} - - defaults = _get_executor_defaults(state, anotherhost) - defaults["_sudo"] = True - defaults["_sudo_user"] = "override-sudo-user" - defaults["_su_user"] = "override-su-user" - - fake_run_command.assert_called_with( - "yes", - print_input=False, - print_output=False, - **defaults, - ) - - def test_get_fact_no_args(self): - inventory = make_inventory(hosts=("host-1",)) - state = State(inventory, Config()) - - connect_all(state) - - host_1 = inventory.get_host("host-1") - defaults = _get_executor_defaults(state, host_1) - - with patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") as fake_run_command: - fake_run_command.return_value = ( - MagicMock(), - CommandOutput([OutputLine("stdout", "some-output")]), - ) - fact_data = get_facts(state, Arch) - - assert fact_data == {host_1: "some-output"} - fake_run_command.assert_called_with( - Arch().command(), - print_input=False, - print_output=False, - **defaults, - ) - - -class TestHostFactsApi(PatchSSHTestCase): - def test_get_host_fact(self): - inventory = make_inventory(hosts=("host-1",)) - state = State(inventory, Config()) - - connect_all(state) - - host_1 = inventory.get_host("host-1") - defaults = _get_executor_defaults(state, host_1) - - with patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") as fake_run_command: - fake_run_command.return_value = ( - MagicMock(), - CommandOutput([OutputLine("stdout", "some-output")]), - ) - fact_data = host_1.get_fact(Command, command="echo hello world") - - assert fact_data == "some-output" - fake_run_command.assert_called_with( - "echo hello world", - print_input=False, - print_output=False, - **defaults, - ) - - def test_get_host_fact_sudo(self): - inventory = make_inventory(hosts=("host-1",)) - state = State(inventory, Config()) - - connect_all(state) - - host_1 = inventory.get_host("host-1") - defaults = _get_executor_defaults(state, host_1) - defaults["_sudo"] = True - - with patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") as fake_run_command: - fake_run_command.return_value = ( - MagicMock(), - CommandOutput([OutputLine("stdout", "some-output")]), - ) - fact_data = host_1.get_fact(Command, command="echo hello world", _sudo=True) - - assert fact_data == "some-output" - fake_run_command.assert_called_with( - "echo hello world", - print_input=False, - print_output=False, - **defaults, - ) - - def test_get_host_fact_sudo_no_args(self): - inventory = make_inventory(hosts=("host-1",)) - state = State(inventory, Config()) - - connect_all(state) - - host_1 = inventory.get_host("host-1") - defaults = _get_executor_defaults(state, host_1) - defaults["_sudo"] = True - - with patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") as fake_run_command: - fake_run_command.return_value = ( - MagicMock(), - CommandOutput([OutputLine("stdout", "some-output")]), - ) - fact_data = host_1.get_fact(Arch, _sudo=True) - - assert fact_data == "some-output" - fake_run_command.assert_called_with( - Arch().command(), - print_input=False, - print_output=False, - **defaults, - ) + disconnect_all(state) diff --git a/tests/test_api/test_api_operations.py b/tests/test_api/test_api_operations.py index 8ece9f191..912e588a8 100644 --- a/tests/test_api/test_api_operations.py +++ b/tests/test_api/test_api_operations.py @@ -1,949 +1,61 @@ -from collections import defaultdict -from os import path -from unittest import TestCase -from unittest.mock import mock_open, patch +from __future__ import annotations -import pyinfra -from pyinfra.api import ( - BaseStateCallback, - Config, - FileDownloadCommand, - FileUploadCommand, - OperationError, - OperationValueError, - State, - StringCommand, -) +from pyinfra.api import BaseStateCallback, Config, State, StringCommand from pyinfra.api.connect import connect_all, disconnect_all -from pyinfra.api.exceptions import PyinfraError -from pyinfra.api.operation import OperationMeta, add_op +from pyinfra.api.operation import add_op from pyinfra.api.operations import run_ops -from pyinfra.api.state import StateOperationMeta, StateStage -from pyinfra.connectors.util import CommandOutput, OutputLine -from pyinfra.context import ctx_host, ctx_state -from pyinfra.operations import files, python, server +from pyinfra.operations import server -from ..paramiko_util import FakeBuffer, FakeChannel, PatchSSHTestCase from ..util import make_inventory -class TestOperationMeta(TestCase): - def test_operation_meta_repr_no_change(self): - op_meta = OperationMeta("hash", False) - assert repr(op_meta) == "OperationMeta(executed=False, maybeChange=False, hash=hash)" +class _TrackingCallback(BaseStateCallback): + def __init__(self) -> None: + self.started = [] + self.completed = [] - def test_operation_meta_repr_changes(self): - op_meta = OperationMeta("hash", True) - assert repr(op_meta) == "OperationMeta(executed=False, maybeChange=True, hash=hash)" + def operation_start(self, state: State, op_hash): + self.started.append(op_hash) + def operation_end(self, state: State, op_hash): + self.completed.append(op_hash) -class TestOperationsApi(PatchSSHTestCase): - def test_op(self): - inventory = make_inventory() - somehost = inventory.get_host("somehost") - anotherhost = inventory.get_host("anotherhost") - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - state.add_callback_handler(BaseStateCallback()) +def test_run_ops_executes_commands(fake_asyncssh): + inventory = make_inventory() + state = State(inventory, Config()) + callback = _TrackingCallback() + state.add_callback_handler(callback) - # Enable printing on this test to catch any exceptions in the formatting - state.print_output = True - state.print_input = True - state.print_fact_info = True - state.print_noop_info = True + connect_all(state) + add_op(state, server.shell, "echo op-test") + run_ops(state) - connect_all(state) + assert callback.started + assert callback.completed == callback.started - add_op( - state, - files.file, - "/var/log/pyinfra.log", - user="pyinfra", - group="pyinfra", - mode="644", - create_remote_dir=False, - _sudo=True, - _sudo_user="test_sudo", - _su_user="test_su", - _ignore_errors=True, - _env={ - "TEST": "what", - }, - ) + for connection in fake_asyncssh.values(): + assert any("echo op-test" in command for command in connection.commands_run) - op_order = state.get_op_order() + disconnect_all(state) - # Ensure we have an op - assert len(op_order) == 1 - first_op_hash = op_order[0] +def test_run_shell_command_with_custom_string_command(fake_asyncssh): + inventory = make_inventory() + state = State(inventory, Config()) - # Ensure the op name - assert state.op_meta[first_op_hash].names == {"files.file"} + connect_all(state) + host = inventory.get_host("somehost") + connection = fake_asyncssh[host.name] + connection.command_results["custom command"] = { + "stdout": "done\n", + "stderr": "", + "exit_status": 0, + } - # Ensure the global kwargs (same for both hosts) - somehost_global_arguments = state.ops[somehost][first_op_hash].global_arguments - assert somehost_global_arguments["_sudo"] is True - assert somehost_global_arguments["_sudo_user"] == "test_sudo" - assert somehost_global_arguments["_su_user"] == "test_su" - assert somehost_global_arguments["_ignore_errors"] is True + status, output = host.run_shell_command(StringCommand("custom", "command")) - anotherhost_global_arguments = state.ops[anotherhost][first_op_hash].global_arguments - assert anotherhost_global_arguments["_sudo"] is True - assert anotherhost_global_arguments["_sudo_user"] == "test_sudo" - assert anotherhost_global_arguments["_su_user"] == "test_su" - assert anotherhost_global_arguments["_ignore_errors"] is True + assert status is True + assert output.stdout == "done" - # Ensure run ops works - run_ops(state) - - # Ensure the commands - assert state.ops[somehost][first_op_hash].operation_meta._commands == [ - StringCommand("touch /var/log/pyinfra.log"), - StringCommand("chmod 644 /var/log/pyinfra.log"), - StringCommand("chown pyinfra:pyinfra /var/log/pyinfra.log"), - ] - - # Ensure ops completed OK - assert state.results[somehost].success_ops == 1 - assert state.results[somehost].ops == 1 - assert state.results[anotherhost].success_ops == 1 - assert state.results[anotherhost].ops == 1 - - # And w/o errors - assert state.results[somehost].error_ops == 0 - assert state.results[anotherhost].error_ops == 0 - - disconnect_all(state) - - @patch("pyinfra.api.util.open", mock_open(read_data="test!"), create=True) - @patch("pyinfra.operations.files.os.path.isfile", lambda *args, **kwargs: True) - def test_file_upload_op(self): - inventory = make_inventory() - - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - # Test normal - add_op( - state, - files.put, - name="First op name", - src="files/file.txt", - dest="/home/vagrant/file.txt", - ) - - # And with sudo - add_op( - state, - files.put, - src="files/file.txt", - dest="/home/vagrant/file.txt", - _sudo=True, - _sudo_user="pyinfra", - ) - - # And with su - add_op( - state, - files.put, - src="files/file.txt", - dest="/home/vagrant/file.txt", - _sudo=True, - _su_user="pyinfra", - ) - - op_order = state.get_op_order() - - # Ensure we have all ops - assert len(op_order) == 3 - - first_op_hash = op_order[0] - second_op_hash = op_order[1] - - # Ensure first op is the right one - assert state.op_meta[first_op_hash].names == {"First op name"} - - somehost = inventory.get_host("somehost") - anotherhost = inventory.get_host("anotherhost") - - # Ensure second op has sudo/sudo_user - assert state.ops[somehost][second_op_hash].global_arguments["_sudo"] is True - assert state.ops[somehost][second_op_hash].global_arguments["_sudo_user"] == "pyinfra" - - # Ensure third has su_user - assert state.ops[somehost][op_order[2]].global_arguments["_su_user"] == "pyinfra" - - # Check run ops works - run_ops(state) - - # Ensure first op used the right (upload) command - assert state.ops[somehost][first_op_hash].operation_meta._commands == [ - StringCommand("mkdir -p /home/vagrant"), - FileUploadCommand("files/file.txt", "/home/vagrant/file.txt"), - ] - - # Ensure ops completed OK - assert state.results[somehost].success_ops == 3 - assert state.results[somehost].ops == 3 - assert state.results[anotherhost].success_ops == 3 - assert state.results[anotherhost].ops == 3 - - # And w/o errors - assert state.results[somehost].error_ops == 0 - assert state.results[anotherhost].error_ops == 0 - - def test_file_download_op(self): - inventory = make_inventory() - - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - with patch("pyinfra.operations.files.os.path.isfile", lambda *args, **kwargs: True): - add_op( - state, - files.get, - name="First op name", - src="/home/vagrant/file.txt", - dest="files/file.txt", - ) - - op_order = state.get_op_order() - - assert len(op_order) == 1 - - first_op_hash = op_order[0] - assert state.op_meta[first_op_hash].names == {"First op name"} - - somehost = inventory.get_host("somehost") - anotherhost = inventory.get_host("anotherhost") - - with patch("pyinfra.api.util.open", mock_open(read_data="test!"), create=True): - run_ops(state) - - # Ensure first op has the right (upload) command - assert state.ops[somehost][first_op_hash].operation_meta._commands == [ - FileDownloadCommand("/home/vagrant/file.txt", "files/file.txt"), - ] - - assert state.results[somehost].success_ops == 1 - assert state.results[somehost].ops == 1 - assert state.results[anotherhost].success_ops == 1 - assert state.results[anotherhost].ops == 1 - assert state.results[somehost].error_ops == 0 - assert state.results[anotherhost].error_ops == 0 - - def test_function_call_op(self): - inventory = make_inventory() - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - is_called = [] - - def mocked_function(*args, **kwargs): - is_called.append(True) - return None - - # Add op to both hosts - add_op(state, python.call, mocked_function) - - # Ensure there is one op - assert len(state.get_op_order()) == 1 - - run_ops(state) - - assert is_called - - def test_run_once_serial_op(self): - inventory = make_inventory() - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - # Add a run once op - add_op(state, server.shell, 'echo "hi"', _run_once=True, _serial=True) - - # Ensure it's added to op_order - assert len(state.get_op_order()) == 1 - - somehost = inventory.get_host("somehost") - anotherhost = inventory.get_host("anotherhost") - - # Ensure between the two hosts we only run the one op - assert len(state.ops[somehost]) + len(state.ops[anotherhost]) == 1 - - # Check run works - run_ops(state) - - assert (state.results[somehost].success_ops + state.results[anotherhost].success_ops) == 1 - - @patch("pyinfra.connectors.ssh.SSHConnector.check_can_rsync", lambda _: True) - def test_rsync_op(self): - inventory = make_inventory(hosts=("somehost",)) - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - add_op(state, files.rsync, "src", "dest", _sudo=True, _sudo_user="root") - - assert len(state.get_op_order()) == 1 - - with patch("pyinfra.connectors.ssh.run_local_process") as fake_run_local_process: - fake_run_local_process.return_value = 0, [] - run_ops(state) - - fake_run_local_process.assert_called_with( - ( - "rsync -ax --delete --rsh " - '"ssh -o BatchMode=yes -o \\"StrictHostKeyChecking=accept-new\\""' - " --rsync-path 'sudo -u root rsync' src vagrant@somehost:dest" - ), - print_output=False, - print_prefix=inventory.get_host("somehost").print_prefix, - ) - - @patch("pyinfra.connectors.ssh.SSHConnector.check_can_rsync", lambda _: True) - def test_rsync_op_with_strict_host_key_checking_disabled(self): - inventory = make_inventory(hosts=(("somehost", {"ssh_strict_host_key_checking": "no"}),)) - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - add_op(state, files.rsync, "src", "dest", _sudo=True, _sudo_user="root") - - assert len(state.get_op_order()) == 1 - - with patch("pyinfra.connectors.ssh.run_local_process") as fake_run_local_process: - fake_run_local_process.return_value = 0, [] - run_ops(state) - - fake_run_local_process.assert_called_with( - ( - "rsync -ax --delete --rsh " - '"ssh -o BatchMode=yes -o \\"StrictHostKeyChecking=no\\""' - " --rsync-path 'sudo -u root rsync' src vagrant@somehost:dest" - ), - print_output=False, - print_prefix=inventory.get_host("somehost").print_prefix, - ) - - @patch("pyinfra.connectors.ssh.SSHConnector.check_can_rsync", lambda _: True) - def test_rsync_op_with_strict_host_key_checking_disabled_and_custom_config_file(self): - inventory = make_inventory( - hosts=( - ( - "somehost", - { - "ssh_strict_host_key_checking": "no", - "ssh_config_file": "/home/me/ssh_test_config", - }, - ), - ) - ) - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - add_op(state, files.rsync, "src", "dest", _sudo=True, _sudo_user="root") - - assert len(state.get_op_order()) == 1 - - with patch("pyinfra.connectors.ssh.run_local_process") as fake_run_local_process: - fake_run_local_process.return_value = 0, [] - run_ops(state) - - fake_run_local_process.assert_called_with( - ( - "rsync -ax --delete --rsh " - '"ssh -o BatchMode=yes ' - '-o \\"StrictHostKeyChecking=no\\" -F /home/me/ssh_test_config"' - " --rsync-path 'sudo -u root rsync' src vagrant@somehost:dest" - ), - print_output=False, - print_prefix=inventory.get_host("somehost").print_prefix, - ) - - @patch("pyinfra.connectors.ssh.SSHConnector.check_can_rsync", lambda _: True) - def test_rsync_op_with_sanitized_custom_config_file(self): - inventory = make_inventory( - hosts=(("somehost", {"ssh_config_file": "/home/me/ssh_test_config && echo hi"}),) - ) - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - add_op(state, files.rsync, "src", "dest", _sudo=True, _sudo_user="root") - - assert len(state.get_op_order()) == 1 - - with patch("pyinfra.connectors.ssh.run_local_process") as fake_run_local_process: - fake_run_local_process.return_value = 0, [] - run_ops(state) - - fake_run_local_process.assert_called_with( - ( - "rsync -ax --delete --rsh " - '"ssh -o BatchMode=yes -o \\"StrictHostKeyChecking=accept-new\\" ' - "-F '/home/me/ssh_test_config && echo hi'\"" - " --rsync-path 'sudo -u root rsync' src vagrant@somehost:dest" - ), - print_output=False, - print_prefix=inventory.get_host("somehost").print_prefix, - ) - - def test_rsync_op_failure(self): - inventory = make_inventory(hosts=("somehost",)) - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - with patch("pyinfra.connectors.ssh.which", lambda x: None): - with self.assertRaises(OperationError) as context: - add_op(state, files.rsync, "src", "dest") - - assert context.exception.args[0] == "The `rsync` binary is not available on this system." - - def test_op_cannot_change_execution_kwargs(self): - inventory = make_inventory() - - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - - class NoSetDefaultDict(defaultdict): - def setdefault(self, key, _): - return self[key] - - op_meta_item = StateOperationMeta(tuple()) - op_meta_item.global_arguments = {"_serial": True} - state.op_meta = NoSetDefaultDict(lambda: op_meta_item) - - connect_all(state) - - with self.assertRaises(OperationValueError) as context: - add_op(state, files.file, "/var/log/pyinfra.log", _serial=False) - - assert context.exception.args[0] == "Cannot have different values for `_serial`." - - -class TestNestedOperationsApi(PatchSSHTestCase): - def test_nested_op_api(self): - inventory = make_inventory() - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - - connect_all(state) - - somehost = inventory.get_host("somehost") - - pyinfra.is_cli = True - - with ctx_state.use(state): - with ctx_host.use(somehost): - try: - outer_result = server.shell(commands="echo outer") - assert outer_result._combined_output is None - - def callback(): - inner_result = server.shell(commands="echo inner") - assert inner_result._combined_output is not None - - python.call(function=callback) - - assert len(state.get_op_order()) == 2 - - run_ops(state) - - assert len(state.get_op_order()) == 3 - assert state.results[somehost].success_ops == 3 - assert outer_result._combined_output is not None - - disconnect_all(state) - finally: - pyinfra.is_cli = False - - -class TestOperationFailures(PatchSSHTestCase): - def test_full_op_fail(self): - inventory = make_inventory() - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - add_op(state, server.shell, 'echo "hi"') - - with patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") as fake_run_command: - fake_channel = FakeChannel(1) - fake_run_command.return_value = ( - False, - FakeBuffer("", fake_channel), - ) - - with self.assertRaises(PyinfraError) as e: - run_ops(state) - - assert e.exception.args[0] == "No hosts remaining!" - - somehost = inventory.get_host("somehost") - - # Ensure the op was not flagged as success - assert state.results[somehost].success_ops == 0 - # And was flagged asn an error - assert state.results[somehost].error_ops == 1 - - def test_ignore_errors_op_fail(self): - inventory = make_inventory() - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - add_op(state, server.shell, 'echo "hi"', _ignore_errors=True) - - with patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") as fake_run_command: - fake_channel = FakeChannel(1) - fake_run_command.return_value = ( - False, - FakeBuffer("", fake_channel), - ) - - # This should run OK - run_ops(state) - - somehost = inventory.get_host("somehost") - - # Ensure the op was added to results - assert state.results[somehost].ops == 1 - assert state.results[somehost].ignored_error_ops == 1 - # But not as a success - assert state.results[somehost].success_ops == 0 - - -class TestOperationOrdering(PatchSSHTestCase): - # In CLI mode, pyinfra uses *line numbers* to order operations as defined by - # the user. This makes reasoning about user-written deploys simple and easy - # to understand. - def test_cli_op_line_numbers(self): - inventory = make_inventory() - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - state.current_deploy_filename = __file__ - - pyinfra.is_cli = True - - with ctx_state.use(state): - # Add op to both hosts - for name in ("anotherhost", "somehost"): - with ctx_host.use(inventory.get_host(name)): - server.shell("echo hi") # note this is called twice but on *the same line* - - # Add op to just the second host - using the context modules such that - # it replicates a deploy file. - ctx_host.set(inventory.get_host("anotherhost")) - first_context_hash = server.user("anotherhost_user")._hash - - # Add op to just the first host - using the context modules such that - # it replicates a deploy file. - ctx_host.set(inventory.get_host("somehost")) - second_context_hash = server.user("somehost_user")._hash - - ctx_state.reset() - ctx_host.reset() - - pyinfra.is_cli = False - - print(state.ops) - # Ensure there are two ops - op_order = state.get_op_order() - assert len(op_order) == 3 - - # And that the two ops above were called in the expected order - assert op_order[1] == first_context_hash - assert op_order[2] == second_context_hash - - # Ensure somehost has two ops and anotherhost only has the one - assert len(state.ops[inventory.get_host("somehost")]) == 2 - assert len(state.ops[inventory.get_host("anotherhost")]) == 2 - - # In API mode, pyinfra *overrides* the line numbers such that whenever an - # operation or deploy is added it is simply appended. This makes sense as - # the user writing the API calls has full control over execution order. - def test_api_op_line_numbers(self): - inventory = make_inventory() - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - another_host = inventory.get_host("anotherhost") - - def add_another_op(): - return add_op(state, server.shell, "echo second-op")[another_host]._hash - - first_op_hash = add_op(state, server.shell, "echo first-op")[another_host]._hash - second_op_hash = add_another_op() # note `add_op` will be called on an earlier line - - op_order = state.get_op_order() - assert len(op_order) == 2 - - assert op_order[0] == first_op_hash - assert op_order[1] == second_op_hash - - -class TestOperationRetry(PatchSSHTestCase): - """ - Tests for the retry functionality in operations. - """ - - @patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") - def test_basic_retry_behavior(self, fake_run_command): - """ - Test that operations retry the correct number of times on failure. - """ - # Create inventory with just one host to simplify testing - inventory = make_inventory(hosts=("somehost",)) - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - # Add operation with retry settings - add_op( - state, - server.shell, - 'echo "testing retries"', - _retries=2, - _retry_delay=0.1, # Use small delay for tests - ) - - # Track how many times run_shell_command was called - call_count = 0 - - # First call fails, second succeeds - def side_effect(*args, **kwargs): - nonlocal call_count - call_count += 1 - if call_count == 1: - # First call fails - fake_channel = FakeChannel(1) - return (False, FakeBuffer("", fake_channel)) - else: - # Second call succeeds - fake_channel = FakeChannel(0) - return (True, FakeBuffer("success", fake_channel)) - - fake_run_command.side_effect = side_effect - - # Run the operation - run_ops(state) - - # Check that run_shell_command was called twice (original + 1 retry) - self.assertEqual(call_count, 2) - - # Verify results - somehost = inventory.get_host("somehost") - - # Operation should be successful (because the retry succeeded) - self.assertEqual(state.results[somehost].success_ops, 1) - self.assertEqual(state.results[somehost].error_ops, 0) - - # Get the operation hash - op_hash = state.get_op_order()[0] - - # Check retry info in OperationMeta - op_meta = state.ops[somehost][op_hash].operation_meta - self.assertEqual(op_meta.retry_attempts, 1) - self.assertEqual(op_meta.max_retries, 2) - self.assertTrue(op_meta.was_retried) - self.assertTrue(op_meta.retry_succeeded) - - @patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") - def test_retry_max_attempts_failure(self, fake_run_command): - """ - Test that operations stop retrying after max attempts and report failure. - """ - inventory = make_inventory(hosts=("somehost",)) - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - # Add operation with retry settings - add_op( - state, - server.shell, - 'echo "testing max retries"', - _retries=2, - _retry_delay=0.1, - ) - - # Make all attempts fail - fake_channel = FakeChannel(1) - fake_run_command.return_value = (False, FakeBuffer("", fake_channel)) - - # This should fail after all retries - with self.assertRaises(PyinfraError) as e: - run_ops(state) - - self.assertEqual(e.exception.args[0], "No hosts remaining!") - - # Check that run_shell_command was called the right number of times (1 original + 2 retries) - self.assertEqual(fake_run_command.call_count, 3) - - somehost = inventory.get_host("somehost") - - # Operation should be marked as error - self.assertEqual(state.results[somehost].success_ops, 0) - self.assertEqual(state.results[somehost].error_ops, 1) - - # Get the operation hash - op_hash = state.get_op_order()[0] - - # Check retry info - op_meta = state.ops[somehost][op_hash].operation_meta - self.assertEqual(op_meta.retry_attempts, 2) - self.assertEqual(op_meta.max_retries, 2) - self.assertTrue(op_meta.was_retried) - self.assertFalse(op_meta.retry_succeeded) - - @patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") - @patch("time.sleep") - def test_retry_until_condition(self, fake_sleep, fake_run_command): - """ - Test that operations retry based on the retry_until callable condition. - """ - # Setup inventory and state using the utility function - inventory = make_inventory(hosts=("somehost",)) - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - # Create a counter to track retry_until calls - call_counter = [0] - - # Create a retry_until function that returns True (retry) for first two calls - def retry_until_func(output_data): - call_counter[0] += 1 - return call_counter[0] < 3 # Retry twice, then stop - - # Add operation with retry_until - add_op( - state, - server.shell, - 'echo "test retry_until"', - _retries=3, - _retry_delay=0.1, - _retry_until=retry_until_func, - ) - - # Set up fake command execution - always succeed but with proper output format - # Use the existing FakeBuffer/FakeChannel from test utils - - # First two calls trigger retry_until, third doesn't - def command_side_effect(*args, **kwargs): - # Create proper CommandOutput for the retry_until function to process - lines = [OutputLine("stdout", "test output"), OutputLine("stderr", "no errors")] - return True, CommandOutput(lines) - - fake_run_command.side_effect = command_side_effect - - # Run the operations - run_ops(state) - - # The command should be called 3 times total (initial + 2 retries) - self.assertEqual(fake_run_command.call_count, 3) - - # The retry_until function should be called 3 times - self.assertEqual(call_counter[0], 3) - - # Get the operation metadata to check retry info - somehost = inventory.get_host("somehost") - op_hash = state.get_op_order()[0] - op_meta = state.ops[somehost][op_hash].operation_meta - - # Check retry metadata - self.assertEqual(op_meta.retry_attempts, 2) - self.assertEqual(op_meta.max_retries, 3) - self.assertTrue(op_meta.was_retried) - self.assertTrue(op_meta.retry_succeeded) - - @patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") - @patch("time.sleep") - def test_retry_delay(self, fake_sleep, fake_run_command): - """ - Test that retry delay is properly applied between attempts. - """ - inventory = make_inventory(hosts=("somehost",)) - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - retry_delay = 5 - - # Add operation with retry settings - add_op( - state, - server.shell, - 'echo "testing retry delay"', - _retries=2, - _retry_delay=retry_delay, - ) - - # Make first call fail, second succeed - call_count = 0 - - def side_effect(*args, **kwargs): - nonlocal call_count - call_count += 1 - if call_count == 1: - fake_channel = FakeChannel(1) - return (False, FakeBuffer("", fake_channel)) - else: - fake_channel = FakeChannel(0) - return (True, FakeBuffer("", fake_channel)) - - fake_run_command.side_effect = side_effect - - # Run the operation - run_ops(state) - - # Check that sleep was called with the correct delay - fake_sleep.assert_called_once_with(retry_delay) - - @patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") - @patch("time.sleep") - def test_retry_until_with_error_handling(self, fake_sleep, fake_run_command): - """ - Test that operations handle errors in retry_until functions gracefully. - """ - inventory = make_inventory(hosts=("somehost",)) - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - # Create a retry_until function that raises an exception - def failing_retry_until_func(output_data): - raise ValueError("Test error in retry_until function") - - # Add operation with failing retry_until - add_op( - state, - server.shell, - 'echo "test failing retry_until"', - _retries=2, - _retry_delay=0.1, - _retry_until=failing_retry_until_func, - ) - - # Set up fake command execution - - def command_side_effect(*args, **kwargs): - lines = [OutputLine("stdout", "test output"), OutputLine("stderr", "no errors")] - return True, CommandOutput(lines) - - fake_run_command.side_effect = command_side_effect - - # Run the operations - should succeed despite retry_until error - run_ops(state) - - # The command should be called only once (no retries due to error) - self.assertEqual(fake_run_command.call_count, 1) - - # Verify operation completed successfully - somehost = inventory.get_host("somehost") - self.assertEqual(state.results[somehost].success_ops, 1) - self.assertEqual(state.results[somehost].error_ops, 0) - - @patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command") - @patch("time.sleep") - def test_retry_until_with_complex_output_parsing(self, fake_sleep, fake_run_command): - """ - Test retry_until with complex output parsing scenarios. - """ - inventory = make_inventory(hosts=("somehost",)) - state = State(inventory, Config()) - state.current_stage = StateStage.Prepare - connect_all(state) - - # Track what output we've seen - outputs_seen = [] - - def complex_retry_until_func(output_data): - # Store the output data for verification - outputs_seen.append(output_data) - - # Check for specific patterns in stdout - stdout_text = " ".join(output_data["stdout_lines"]) - - # Continue retrying until we see "READY" in stdout - return "READY" not in stdout_text - - # Add operation with complex retry_until - add_op( - state, - server.shell, - 'echo "service status check"', - _retries=3, - _retry_delay=0.1, - _retry_until=complex_retry_until_func, - ) - - # Set up fake command execution with changing output - - call_count = 0 - - def command_side_effect(*args, **kwargs): - nonlocal call_count - call_count += 1 - - if call_count == 1: - lines = [ - OutputLine("stdout", "Service starting..."), - OutputLine("stderr", "Loading config"), - ] - elif call_count == 2: - lines = [ - OutputLine("stdout", "Service initializing..."), - OutputLine("stderr", "Connecting to database"), - ] - else: # call_count == 3 - lines = [ - OutputLine("stdout", "Service READY"), - OutputLine("stderr", "All systems operational"), - ] - - return True, CommandOutput(lines) - - fake_run_command.side_effect = command_side_effect - - # Run the operations - run_ops(state) - - # The command should be called 3 times - self.assertEqual(fake_run_command.call_count, 3) - - # Verify retry_until was called 3 times with correct data - self.assertEqual(len(outputs_seen), 3) - - # Check the output data structure - for output_data in outputs_seen: - self.assertIn("stdout_lines", output_data) - self.assertIn("stderr_lines", output_data) - self.assertIn("commands", output_data) - self.assertIn("executed_commands", output_data) - self.assertIn("host", output_data) - self.assertIn("operation", output_data) - - # Verify operation metadata - somehost = inventory.get_host("somehost") - op_hash = state.get_op_order()[0] - op_meta = state.ops[somehost][op_hash].operation_meta - - self.assertEqual(op_meta.retry_attempts, 2) - self.assertEqual(op_meta.max_retries, 3) - self.assertTrue(op_meta.was_retried) - self.assertTrue(op_meta.retry_succeeded) - - -this_filename = path.join("tests", "test_api", "test_api_operations.py") + disconnect_all(state) diff --git a/tests/test_async_context.py b/tests/test_async_context.py new file mode 100644 index 000000000..866421eab --- /dev/null +++ b/tests/test_async_context.py @@ -0,0 +1,221 @@ +from __future__ import annotations + +import asyncio + +from pyinfra.api import Config, State, deploy +from pyinfra.api.state import StateStage +from pyinfra.async_context import AsyncContext, AsyncHostContext +from pyinfra.context import ctx_state +from pyinfra.facts.server import Command +from pyinfra.operations import files, server + +from .util import make_inventory + + +def test_async_context_operation(fake_asyncssh): + async def _run(): + inventory = make_inventory() + state = State(inventory, Config()) + + async with AsyncContext(state): + results = await server.shell("echo async-context") + + assert set(results.keys()) == { + inventory.get_host("somehost"), + inventory.get_host("anotherhost"), + } + assert all(meta is not None for meta in results.values()) + assert state.current_stage == StateStage.Execute + + for connection in fake_asyncssh.values(): + assert any("echo async-context" in command for command in connection.commands_run) + + assert state.current_stage == StateStage.Disconnect + for connection in fake_asyncssh.values(): + assert connection._closed is True + + asyncio.run(_run()) + + +def test_async_context_fact(fake_asyncssh): + async def _run(): + inventory = make_inventory() + state = State(inventory, Config()) + + async with AsyncContext(state): + for connection in fake_asyncssh.values(): + connection.command_results["echo fact-value"] = { + "stdout": "value\n", + "stderr": "", + "exit_status": 0, + } + + gathered = {} + for hostname in ("somehost", "anotherhost"): + host = inventory.get_host(hostname) + gathered[host] = await host.get_fact(Command, "echo fact-value") + + assert set(gathered.keys()) == { + inventory.get_host("somehost"), + inventory.get_host("anotherhost"), + } + + for value in gathered.values(): + assert value == "value" + + assert state.current_stage == StateStage.Disconnect + for connection in fake_asyncssh.values(): + assert connection._closed is True + + asyncio.run(_run()) + + +def test_async_context_hosts_subset(fake_asyncssh): + async def _run(): + inventory = make_inventory() + state = State(inventory, Config()) + + specific = inventory.get_host("somehost") + async with AsyncContext(state, hosts=[specific]): + await server.shell("echo subset") + + assert any( + "echo subset" in command for command in fake_asyncssh["somehost"].commands_run + ) + assert "anotherhost" not in fake_asyncssh + + assert state.current_stage == StateStage.Disconnect + assert fake_asyncssh["somehost"]._closed is True + + asyncio.run(_run()) + + +def test_async_context_preserves_state_in_executor(fake_asyncssh, tmp_path): + async def _run(): + inventory = make_inventory() + state = State(inventory, Config()) + + local_file = tmp_path / "async-context.txt" + local_file.write_text("async context test") + + async with AsyncContext(state): + results = await files.put(src=str(local_file), dest="/async-context.txt") + + assert set(results.keys()) == { + inventory.get_host("somehost"), + inventory.get_host("anotherhost"), + } + + with ctx_state.use(state): + state_in_executor = await state.run_in_executor(ctx_state.get) + assert state_in_executor is state + + config_in_executor = await state.run_in_executor(lambda: ctx_state.get().config) + assert config_in_executor is state.config + + asyncio.run(_run()) + + +def test_async_context_run_deploy(fake_asyncssh): + async def _run(): + inventory = make_inventory() + state = State(inventory, Config()) + + @deploy("Async context deploy") + def sample_deploy(): + server.shell(name="Deploy op", commands="echo async-context-deploy") + + async with AsyncContext(state): + await sample_deploy() + + for hostname, connection in fake_asyncssh.items(): + assert hostname in {"somehost", "anotherhost"} + assert any( + "echo async-context-deploy" in command for command in connection.commands_run + ) + + fake_asyncssh.clear() + + async with AsyncContext(state, hosts=[inventory.get_host("somehost")]): + await sample_deploy(hosts=[inventory.get_host("somehost")]) + + assert set(fake_asyncssh.keys()) == {"somehost"} + assert any( + "echo async-context-deploy" in command + for command in fake_asyncssh["somehost"].commands_run + ) + + assert all(connection._closed for connection in fake_asyncssh.values()) + fake_asyncssh.clear() + + asyncio.run(_run()) + + +def test_async_host_context_limits_to_single_host(fake_asyncssh): + async def _run(): + inventory = make_inventory() + state = State(inventory, Config()) + + async with AsyncHostContext(state, "somehost"): + results = await server.shell("echo host-context") + + somehost = inventory.get_host("somehost") + assert set(results.keys()) == {somehost} + assert set(fake_asyncssh.keys()) == {"somehost"} + + fake_asyncssh["somehost"].command_results["echo async-host-fact"] = { + "stdout": "value\n", + "stderr": "", + "exit_status": 0, + } + + fact_value = await somehost.get_fact(Command, "echo async-host-fact") + assert fact_value == "value" + + @deploy("Async host deploy") + def sample_host_deploy(): + server.shell(name="Host deploy op", commands="echo async-host-deploy") + + await sample_host_deploy() + assert any( + "echo async-host-deploy" in command + for command in fake_asyncssh["somehost"].commands_run + ) + + assert fake_asyncssh["somehost"]._closed is True + assert "anotherhost" not in fake_asyncssh + + asyncio.run(_run()) + + +def test_async_host_context_accepts_host_object(fake_asyncssh): + async def _run(): + inventory = make_inventory() + state = State(inventory, Config()) + + host_obj = inventory.get_host("somehost") + async with AsyncHostContext(state, host_obj): + await server.shell("echo host-object") + + fake_asyncssh["somehost"].command_results["echo host-object-fact"] = { + "stdout": "value\n", + "stderr": "", + "exit_status": 0, + } + + value = await host_obj.get_fact(Command, "echo host-object-fact") + assert value == "value" + + @deploy("Async host object deploy") + def sample_host_object_deploy(): + server.shell(name="Host object deploy", commands="echo async-host-object") + + await sample_host_object_deploy() + + assert set(fake_asyncssh.keys()) == {"somehost"} + assert any( + "echo async-host-object" in command + for command in fake_asyncssh["somehost"].commands_run + ) + + asyncio.run(_run()) diff --git a/tests/test_cli/test_cli.py b/tests/test_cli/test_cli.py index f93a1d765..936710a5a 100644 --- a/tests/test_cli/test_cli.py +++ b/tests/test_cli/test_cli.py @@ -1,195 +1,30 @@ -from os import path -from unittest import TestCase +from __future__ import annotations -from pyinfra_cli.cli import _main +from os import path -from ..paramiko_util import PatchSSHTestCase from .util import run_cli -class TestCliEagerFlags(TestCase): - def test_print_help(self): - result = run_cli("--version") - assert result.exit_code == 0, result.stderr - - result = run_cli("--help") - assert result.exit_code == 0, result.stderr - - -class TestOperationCli(PatchSSHTestCase): - def test_invalid_operation_module(self): - result = run_cli( - path.join("tests", "test_cli", "deploy", "inventories", "inventory.py"), - "not_a_module.shell", - ) - assert result.exit_code == 1, result.stderr - assert "No such module: not_a_module" - - def test_invalid_operation_function(self): - result = run_cli( - path.join("tests", "test_cli", "deploy", "inventories", "inventory.py"), - "server.not_an_operation", - ) - assert result.exit_code == 1, result.stderr - assert "No such operation: server.not_an_operation" - - def test_deploy_operation(self): - result = run_cli( - "-y", - path.join("tests", "test_cli", "deploy", "inventories", "inventory.py"), - "server.shell", - "echo hi", - ) - assert result.exit_code == 0, result.stderr - - def test_deploy_operation_with_all(self): - result = run_cli( - "-y", - path.join("tests", "test_cli", "deploy", "inventory_all.py"), - "server.shell", - "echo hi", - ) - assert result.exit_code == 0, result.stderr - - def test_deploy_operation_json_args(self): - result = run_cli( - "-y", - path.join("tests", "test_cli", "deploy", "inventory_all.py"), - "server.shell", - '[["echo hi"], {}]', - ) - assert result.exit_code == 0, result.stderr - - -class TestFactCli(PatchSSHTestCase): - def test_get_fact(self): - result = run_cli( - path.join("tests", "test_cli", "deploy", "inventories", "inventory.py"), - "fact", - "server.Os", - ) - assert result.exit_code == 0, result.stderr - assert '"somehost": null' in result.stderr - - def test_get_fact_with_kwargs(self): - result = run_cli( - path.join("tests", "test_cli", "deploy", "inventories", "inventory.py"), - "fact", - "files.File", - "path=.", - ) - assert result.exit_code == 0, result.stderr - assert '"somehost": null' in result.stderr - - -class TestExecCli(PatchSSHTestCase): - def test_exec_command(self): - result = run_cli( - path.join("tests", "test_cli", "deploy", "inventories", "inventory.py"), - "exec", - "--", - "echo hi", - ) - assert result.exit_code == 0, result.stderr - - def test_exec_command_with_options(self): - result = run_cli( - path.join("tests", "test_cli", "deploy", "inventories", "inventory.py"), - "exec", - "--sudo", - "--sudo-user", - "pyinfra", - "--su-user", - "pyinfrawhat", - "--port", - "1022", - "--user", - "ubuntu", - "--", - "echo hi", - ) - assert result.exit_code == 0, result.stderr - - def test_exec_command_with_serial(self): - result = run_cli( - path.join("tests", "test_cli", "deploy", "inventories", "inventory.py"), - "exec", - "--serial", - "--", - "echo hi", - ) - assert result.exit_code == 0, result.stderr +def test_cli_help(): + assert run_cli("--help").exit_code == 0 - def test_exec_command_with_no_wait(self): - result = run_cli( - path.join("tests", "test_cli", "deploy", "inventories", "inventory.py"), - "exec", - "--no-wait", - "--", - "echo hi", - ) - assert result.exit_code == 0, result.stderr - def test_exec_command_with_debug_operations(self): - result = run_cli( - path.join("tests", "test_cli", "deploy", "inventories", "inventory.py"), - "exec", - "--debug-operations", - "--", - "echo hi", - ) - assert result.exit_code == 0, result.stderr +def test_cli_version(): + assert run_cli("--version").exit_code == 0 - def test_exec_command_with_debug_facts(self): - result = run_cli( - path.join("tests", "test_cli", "deploy", "inventories", "inventory.py"), - "exec", - "--debug-facts", - "--", - "echo hi", - ) - assert result.exit_code == 0, result.stderr +def test_cli_executes_deploy(fake_asyncssh): + deploy_dir = path.join("tests", "test_cli", "deploy") + inventory_path = path.join(deploy_dir, "inventories", "inventory.py") + deploy_path = path.join(deploy_dir, "deploy.py") -class TestDirectMainExecution(PatchSSHTestCase): - """ - These tests are very similar as above, without the click wrappers - basically - here because coverage.py fails to properly detect all the code under the wrapper. - """ + result = run_cli( + "-y", + inventory_path, + deploy_path, + f"--chdir={deploy_dir}", + ) - def test_deploy_operation_direct(self): - with self.assertRaises(SystemExit) as e: - _main( - inventory=path.join("tests", "test_deploy", "inventories", "inventory.py"), - operations=["server.shell", "echo hi"], - chdir=None, - group_data=None, - verbosity=0, - ssh_user=None, - ssh_port=None, - ssh_key=None, - ssh_key_password=None, - ssh_password=None, - sudo=False, - sudo_user=None, - use_sudo_password=False, - su_user=None, - parallel=None, - fail_percent=0, - dry=False, - yes=True, - limit=None, - no_wait=False, - serial=False, - shell_executable=None, - data=tuple(), - debug=False, - debug_facts=False, - debug_all=False, - debug_operations=False, - config_filename="config.py", - diff=True, - retry=0, - retry_delay=5, - ) - assert e.args == (0,) + assert result.exit_code == 0, result.stderr + for connection in fake_asyncssh.values(): + assert connection.commands_run, "expected CLI deploy to execute commands" diff --git a/tests/test_cli/test_cli_deploy.py b/tests/test_cli/test_cli_deploy.py index ca359f6eb..52daa8e0c 100644 --- a/tests/test_cli/test_cli_deploy.py +++ b/tests/test_cli/test_cli_deploy.py @@ -1,120 +1,175 @@ -from os import path +from __future__ import annotations + +import os +from pathlib import Path from random import shuffle -from pyinfra import state -from pyinfra.context import ctx_state +from pyinfra.context import ctx_inventory, ctx_state -from ..paramiko_util import PatchSSHTestCase from .util import run_cli -class TestCliDeployState(PatchSSHTestCase): - def _run_cli(self, hosts, filename): - return run_cli( - "-y", - ",".join(hosts), - path.join("tests", "test_cli", "deploy", filename), - f"--chdir={path.join('tests', 'test_cli', 'deploy')}", - ) - - def _assert_op_data(self, correct_op_name_and_host_names): - op_order = state.get_op_order() - - assert len(correct_op_name_and_host_names) == len( - op_order, - ), "Incorrect number of operations detected" - - for i, (correct_op_name, correct_host_names) in enumerate( - correct_op_name_and_host_names, - ): - op_hash = op_order[i] - op_meta = state.op_meta[op_hash] - - assert list(op_meta.names)[0] == correct_op_name - - for host in state.inventory: - executed = False - host_op = state.ops[host].get(op_hash) - if host_op: - executed = host_op.operation_meta.executed - if correct_host_names is True or host.name in correct_host_names: - assert executed is True - else: - assert executed is False - - def test_deploy(self): - a_task_file_path = path.join("tasks", "a_task.py") - b_task_file_path = path.join("tasks", "b_task.py") - nested_task_path = path.join("tasks", "another_task.py") - correct_op_name_and_host_names = [ - ("First main operation", True), # true for all hosts - ("Second main operation", ("somehost",)), - ("{0} | First task operation".format(a_task_file_path), ("anotherhost",)), - ("{0} | Task order loop 1".format(a_task_file_path), ("anotherhost",)), - ("{0} | 2nd Task order loop 1".format(a_task_file_path), ("anotherhost",)), - ("{0} | Task order loop 2".format(a_task_file_path), ("anotherhost",)), - ("{0} | 2nd Task order loop 2".format(a_task_file_path), ("anotherhost",)), - ( - "{0} | {1} | Second task operation".format(a_task_file_path, nested_task_path), - ("anotherhost",), - ), - ("{0} | First task operation".format(a_task_file_path), True), - ("{0} | Task order loop 1".format(a_task_file_path), True), - ("{0} | 2nd Task order loop 1".format(a_task_file_path), True), - ("{0} | Task order loop 2".format(a_task_file_path), True), - ("{0} | 2nd Task order loop 2".format(a_task_file_path), True), - ("{0} | {1} | Second task operation".format(a_task_file_path, nested_task_path), True), - ("{0} | Important task operation".format(b_task_file_path), True), - ("My deploy | First deploy operation", True), - ("My deploy | My nested deploy | First nested deploy operation", True), - ("My deploy | Second deploy operation", True), - ("Loop-0 main operation", True), - ("Loop-1 main operation", True), - ("Third main operation", True), - ("Order loop 1", True), - ("Nested order loop 1/1", ("anotherhost",)), - ("Nested order loop 1/2", ("anotherhost",)), - ("Order loop 2", True), - ("Nested order loop 2/1", ("somehost", "anotherhost")), - ("Nested order loop 2/2", ("somehost", "anotherhost")), - ("Final limited operation", ("somehost",)), - ("Second final limited operation", ("anotherhost", "someotherhost")), - ] - - # Run 3 iterations of the test - each time shuffling the order of the - # hosts - ensuring that the ordering has no effect on the operation order. - for _ in range(3): +TESTS_DIR = Path(__file__).resolve().parent +DEPLOY_DIR = TESTS_DIR / "deploy" +A_TASK_FILE = "tasks/a_task.py" +ANOTHER_TASK_FILE = "tasks/another_task.py" +B_TASK_FILE = "tasks/b_task.py" + + +EXPECTED_DEPLOY_OPS = [ + ("First main operation", True), + ("Second main operation", ("somehost",)), + (f"{A_TASK_FILE} | First task operation", ("anotherhost",)), + (f"{A_TASK_FILE} | Task order loop 1", ("anotherhost",)), + (f"{A_TASK_FILE} | 2nd Task order loop 1", ("anotherhost",)), + (f"{A_TASK_FILE} | Task order loop 2", ("anotherhost",)), + (f"{A_TASK_FILE} | 2nd Task order loop 2", ("anotherhost",)), + ( + f"{A_TASK_FILE} | {ANOTHER_TASK_FILE} | Second task operation", + ("anotherhost",), + ), + (f"{A_TASK_FILE} | First task operation", True), + (f"{A_TASK_FILE} | Task order loop 1", True), + (f"{A_TASK_FILE} | 2nd Task order loop 1", True), + (f"{A_TASK_FILE} | Task order loop 2", True), + (f"{A_TASK_FILE} | 2nd Task order loop 2", True), + (f"{A_TASK_FILE} | {ANOTHER_TASK_FILE} | Second task operation", True), + (f"{B_TASK_FILE} | Important task operation", True), + ("My deploy | First deploy operation", True), + ("My deploy | My nested deploy | First nested deploy operation", True), + ("My deploy | Second deploy operation", True), + ("Loop-0 main operation", True), + ("Loop-1 main operation", True), + ("Third main operation", True), + ("Order loop 1", True), + ("Nested order loop 1/1", ("anotherhost",)), + ("Nested order loop 1/2", ("anotherhost",)), + ("Order loop 2", True), + ("Nested order loop 2/1", ("somehost", "anotherhost")), + ("Nested order loop 2/2", ("somehost", "anotherhost")), + ("Final limited operation", ("somehost",)), + ("Second final limited operation", ("anotherhost", "someotherhost")), +] + + +EXPECTED_RANDOM_OPS = [ + ("First main operation", True), + ("Second main somehost operation", ("somehost",)), + ("Second main anotherhost operation", ("anotherhost",)), + ("Function call operation", True), + ("Third main operation", True), + ("First nested operation", True), + ("Second nested anotherhost operation", ("anotherhost",)), + ("Second nested somehost operation", ("somehost",)), +] + + +def _patch_sync_executor(monkeypatch): + async def _run_in_executor_sync(self, func, *args, **kwargs): + return func(*args, **kwargs) + + monkeypatch.setattr( + "pyinfra.api.state.State.run_in_executor", + _run_in_executor_sync, + raising=False, + ) + + +def _write_inventory_file(inventory_path: Path, hosts: list[str]) -> Path: + lines = ["hosts = (", " ["] + for host in hosts: + lines.append(f' "{host}",') + lines.extend([" ],", " {},", ")", ""]) + inventory_path.write_text("\n".join(lines)) + return inventory_path + + +def _run_cli(inventory_path: Path, filename: str): + return run_cli( + "-y", + "--parallel=1", + str(inventory_path), + str(DEPLOY_DIR / filename), + f"--chdir={DEPLOY_DIR}", + ) + + +def _assert_operation_execution(expected, state): + op_order = state.get_op_order() + assert len(op_order) == len(expected) + + for op_hash, (expected_name, expected_hosts) in zip(op_order, expected, strict=True): + op_meta = state.op_meta[op_hash] + actual_name = next(iter(op_meta.names)) + normalised_actual = actual_name.replace(os.sep, "/") + normalised_expected = expected_name.replace(os.sep, "/") + assert normalised_actual == normalised_expected + + for host in state.inventory: + host_op = state.ops[host].get(op_hash) + executed = bool(host_op and host_op.operation_meta.executed) + + if expected_hosts is True: + assert executed is True + else: + assert executed is (host.name in expected_hosts) + + +def test_deploy_preserves_operation_order(tmp_path, fake_asyncssh, monkeypatch): + _patch_sync_executor(monkeypatch) + hosts = ["somehost", "anotherhost", "someotherhost"] + + for iteration in range(3): + fake_asyncssh.clear() + + try: + shuffled_hosts = hosts.copy() + shuffle(shuffled_hosts) + inventory_file = _write_inventory_file( + tmp_path / f"deploy_inventory_{iteration}.py", + shuffled_hosts, + ) + + result = _run_cli(inventory_file, "deploy.py") + assert result.exit_code == 0, result.stdout + + state = ctx_state.get() + assert state is not None + _assert_operation_execution(EXPECTED_DEPLOY_OPS, state) + + assert set(fake_asyncssh.keys()) == set(hosts) + for connection in fake_asyncssh.values(): + assert connection.commands_run + finally: ctx_state.reset() + ctx_inventory.reset() - hosts = ["somehost", "anotherhost", "someotherhost"] - shuffle(hosts) - result = self._run_cli(hosts, "deploy.py") - assert result.exit_code == 0, result.stdout +def test_random_deploy_is_consistent(tmp_path, fake_asyncssh, monkeypatch): + _patch_sync_executor(monkeypatch) + hosts = ["somehost", "anotherhost", "someotherhost"] - self._assert_op_data(correct_op_name_and_host_names) - - def test_random_deploy(self): - correct_op_name_and_host_names = [ - ("First main operation", True), - ("Second main somehost operation", ("somehost",)), - ("Second main anotherhost operation", ("anotherhost",)), - ("Function call operation", True), - ("Third main operation", True), - ("First nested operation", True), - ("Second nested anotherhost operation", ("anotherhost",)), - ("Second nested somehost operation", ("somehost",)), - ] - - # Run 3 iterations of the test - each time shuffling the order of the - # hosts - ensuring that the ordering has no effect on the operation order. - for _ in range(3): - ctx_state.reset() + for iteration in range(3): + fake_asyncssh.clear() - hosts = ["somehost", "anotherhost", "someotherhost"] - shuffle(hosts) + try: + shuffled_hosts = hosts.copy() + shuffle(shuffled_hosts) + inventory_file = _write_inventory_file( + tmp_path / f"random_inventory_{iteration}.py", + shuffled_hosts, + ) - result = self._run_cli(hosts, "deploy_random.py") + result = _run_cli(inventory_file, "deploy_random.py") assert result.exit_code == 0, result.stdout - self._assert_op_data(correct_op_name_and_host_names) + state = ctx_state.get() + assert state is not None + _assert_operation_execution(EXPECTED_RANDOM_OPS, state) + + assert set(fake_asyncssh.keys()) == set(hosts) + for connection in fake_asyncssh.values(): + assert connection.commands_run + finally: + ctx_state.reset() + ctx_inventory.reset() diff --git a/tests/test_cli/test_cli_inventory.py b/tests/test_cli/test_cli_inventory.py index 165532db3..b25d5e5de 100644 --- a/tests/test_cli/test_cli_inventory.py +++ b/tests/test_cli/test_cli_inventory.py @@ -1,40 +1,69 @@ -from os import path +from __future__ import annotations + +from pathlib import Path from pyinfra import inventory from pyinfra.context import ctx_inventory, ctx_state -from ..paramiko_util import PatchSSHTestCase from .util import run_cli -class TestCliInventory(PatchSSHTestCase): - def test_load_deploy_group_data(self): - ctx_state.reset() - ctx_inventory.reset() +TEST_CLI_DIR = Path(__file__).resolve().parent +DEPLOY_DIR = TEST_CLI_DIR / "deploy" +INVALID_INVENTORY = TEST_CLI_DIR / "inventories" / "invalid.py" + + +def _reset_contexts() -> None: + ctx_state.reset() + ctx_inventory.reset() + + +def _patch_sync_executor(monkeypatch): + async def _run_in_executor_sync(self, func, *args, **kwargs): + return func(*args, **kwargs) + + monkeypatch.setattr( + "pyinfra.api.state.State.run_in_executor", + _run_in_executor_sync, + raising=False, + ) + +def test_load_deploy_group_data(fake_asyncssh, monkeypatch): + _patch_sync_executor(monkeypatch) + fake_asyncssh.clear() + + try: hosts = ["somehost", "anotherhost", "someotherhost"] result = run_cli( "-y", + "--parallel=1", ",".join(hosts), - path.join("tests", "test_cli", "deploy", "deploy.py"), - f"--chdir={path.join('tests', 'test_cli', 'deploy')}", + str(DEPLOY_DIR / "deploy.py"), + f"--chdir={DEPLOY_DIR}", ) assert result.exit_code == 0, result.stdout assert inventory.data.get("hello") == "world" assert "leftover_data" in inventory.group_data - assert inventory.group_data["leftover_data"].get("still_parsed") == "never_used" - assert inventory.group_data["leftover_data"].get("_global_arg") == "gets_parsed" + group_data = inventory.group_data["leftover_data"] + assert group_data.get("still_parsed") == "never_used" + assert group_data.get("_global_arg") == "gets_parsed" + finally: + _reset_contexts() + - def test_load_group_data(self): - ctx_state.reset() - ctx_inventory.reset() +def test_load_group_data(fake_asyncssh, monkeypatch): + _patch_sync_executor(monkeypatch) + fake_asyncssh.clear() + try: hosts = ["somehost", "anotherhost", "someotherhost"] result = run_cli( "-y", + "--parallel=1", ",".join(hosts), - f"--group-data={path.join('tests', 'test_cli', 'deploy', 'group_data')}", + f"--group-data={DEPLOY_DIR / 'group_data'}", "exec", "uptime", ) @@ -42,17 +71,23 @@ def test_load_group_data(self): assert inventory.data.get("hello") == "world" assert "leftover_data" in inventory.group_data - assert inventory.group_data["leftover_data"].get("still_parsed") == "never_used" - assert inventory.group_data["leftover_data"].get("_global_arg") == "gets_parsed" + group_data = inventory.group_data["leftover_data"] + assert group_data.get("still_parsed") == "never_used" + assert group_data.get("_global_arg") == "gets_parsed" + finally: + _reset_contexts() - def test_load_group_data_file(self): - ctx_state.reset() - ctx_inventory.reset() +def test_load_group_data_file(fake_asyncssh, monkeypatch): + _patch_sync_executor(monkeypatch) + fake_asyncssh.clear() + + try: hosts = ["somehost", "anotherhost", "someotherhost"] - filename = path.join("tests", "test_cli", "deploy", "group_data", "leftover_data.py") + filename = DEPLOY_DIR / "group_data" / "leftover_data.py" result = run_cli( "-y", + "--parallel=1", ",".join(hosts), f"--group-data={filename}", "exec", @@ -62,15 +97,21 @@ def test_load_group_data_file(self): assert "hello" not in inventory.data assert "leftover_data" in inventory.group_data - assert inventory.group_data["leftover_data"].get("still_parsed") == "never_used" - assert inventory.group_data["leftover_data"].get("_global_arg") == "gets_parsed" + group_data = inventory.group_data["leftover_data"] + assert group_data.get("still_parsed") == "never_used" + assert group_data.get("_global_arg") == "gets_parsed" + finally: + _reset_contexts() + - def test_ignores_variables_with_leading_underscore(self): - ctx_state.reset() - ctx_inventory.reset() +def test_ignores_variables_with_leading_underscore(fake_asyncssh, monkeypatch): + _patch_sync_executor(monkeypatch) + fake_asyncssh.clear() + try: result = run_cli( - path.join("tests", "test_cli", "inventories", "invalid.py"), + "--parallel=1", + str(INVALID_INVENTORY), "exec", "--debug", "--", @@ -83,13 +124,18 @@ def test_ignores_variables_with_leading_underscore(self): in result.stderr ) assert inventory.hosts == {} + finally: + _reset_contexts() - def test_only_supports_list_and_tuples(self): - ctx_state.reset() - ctx_inventory.reset() +def test_only_supports_list_and_tuples(fake_asyncssh, monkeypatch): + _patch_sync_executor(monkeypatch) + fake_asyncssh.clear() + + try: result = run_cli( - path.join("tests", "test_cli", "inventories", "invalid.py"), + "--parallel=1", + str(INVALID_INVENTORY), "exec", "--debug", "--", @@ -97,23 +143,28 @@ def test_only_supports_list_and_tuples(self): ) assert result.exit_code == 0, result.stdout - assert 'Ignoring variable "dict_hosts" in inventory file' in result.stderr, result.stdout - assert 'Ignoring variable "generator_hosts" in inventory file' in result.stderr, ( - result.stdout - ) + assert 'Ignoring variable "dict_hosts" in inventory file' in result.stderr + assert 'Ignoring variable "generator_hosts" in inventory file' in result.stderr assert inventory.hosts == {} + finally: + _reset_contexts() + - def test_host_groups_may_only_contain_strings_or_tuples(self): - ctx_state.reset() - ctx_inventory.reset() +def test_host_groups_may_only_contain_strings_or_tuples(fake_asyncssh, monkeypatch): + _patch_sync_executor(monkeypatch) + fake_asyncssh.clear() + try: result = run_cli( - path.join("tests", "test_cli", "inventories", "invalid.py"), + "--parallel=1", + str(INVALID_INVENTORY), "exec", "--", "echo hi", ) assert result.exit_code == 0, result.stdout - assert 'Ignoring host group "issue_662"' in result.stderr, result.stdout + assert 'Ignoring host group "issue_662"' in result.stderr assert inventory.hosts == {} + finally: + _reset_contexts() diff --git a/tests/test_connectors/test_ssh.py b/tests/test_connectors/test_ssh.py index e5747b22a..78507877a 100644 --- a/tests/test_connectors/test_ssh.py +++ b/tests/test_connectors/test_ssh.py @@ -1,1246 +1,249 @@ -# encoding: utf-8 +from __future__ import annotations -from socket import error as socket_error, gaierror -from unittest import TestCase, mock +import os +from typing import Dict -from paramiko import AuthenticationException, PasswordRequiredException, SSHException +import asyncssh -import pyinfra -from pyinfra.api import Config, Host, MaskString, State, StringCommand -from pyinfra.api.connect import connect_all -from pyinfra.api.exceptions import ConnectError, PyinfraError -from pyinfra.context import ctx_state +from pyinfra.api import Config, State, StringCommand +from pyinfra.api.connect import connect_all, disconnect_all from ..util import make_inventory -def make_raise_exception_function(cls, *args, **kwargs): - def handler(*a, **kw): - raise cls(*args, **kwargs) - - return handler - - -class TestSSHConnector(TestCase): - def setUp(self): - self.fake_connect_patch = mock.patch("pyinfra.connectors.ssh.SSHClient.connect") - self.fake_connect_mock = self.fake_connect_patch.start() - - def tearDown(self): - self.fake_connect_patch.stop() - - def test_connect_all(self): - inventory = make_inventory() - state = State(inventory, Config()) - connect_all(state) - assert len(state.active_hosts) == 2 - - def test_connect_host(self): - inventory = make_inventory() - state = State(inventory, Config()) - host = inventory.get_host("somehost") - host.connect(reason=True) - assert len(state.active_hosts) == 0 - - def test_connect_all_password(self): - inventory = make_inventory(override_data={"ssh_password": "test"}) - - # Get a host - somehost = inventory.get_host("somehost") - assert somehost.data.ssh_password == "test" - - state = State(inventory, Config()) - connect_all(state) - - assert len(state.active_hosts) == 2 - - @mock.patch("pyinfra.connectors.ssh_util.path.isfile", lambda *args, **kwargs: True) - @mock.patch("pyinfra.connectors.ssh_util.RSAKey.from_private_key_file") - def test_connect_exceptions(self, fake_key_open): - for exception_class in ( - AuthenticationException, - SSHException, - gaierror, - socket_error, - EOFError, - ): - state = State(make_inventory(hosts=(("somehost", {"ssh_key": "testkey"}),)), Config()) - - self.fake_connect_mock.side_effect = make_raise_exception_function(exception_class) - - with self.assertRaises(PyinfraError): - connect_all(state) - - assert len(state.active_hosts) == 0 - - # SSH key tests - # - - def test_connect_with_rsa_ssh_key(self): - state = State(make_inventory(hosts=(("somehost", {"ssh_key": "testkey"}),)), Config()) - - with ( - mock.patch("pyinfra.connectors.ssh_util.path.isfile", lambda *args, **kwargs: True), - mock.patch( - "pyinfra.connectors.ssh_util.RSAKey.from_private_key_file", - ) as fake_key_open, - ): - fake_key = mock.MagicMock() - fake_key_open.return_value = fake_key - - connect_all(state) - - # Check the key was created properly - fake_key_open.assert_called_with(filename="testkey") - # Check the certificate file was then loaded - fake_key.load_certificate.assert_called_with("testkey.pub") - - # And check the Paramiko SSH call was correct - self.fake_connect_mock.assert_called_with( - "somehost", - allow_agent=False, - look_for_keys=False, - pkey=fake_key, - timeout=10, - username="vagrant", - _pyinfra_ssh_forward_agent=False, - _pyinfra_ssh_config_file=None, - _pyinfra_ssh_known_hosts_file=None, - _pyinfra_ssh_strict_host_key_checking="accept-new", - _pyinfra_ssh_paramiko_connect_kwargs=None, - ) - - # Check that loading the same key again is cached in the state - second_state = State( - make_inventory(hosts=(("somehost", {"ssh_key": "testkey"}),)), - Config(), - ) - second_state.private_keys = state.private_keys - - connect_all(second_state) - - def test_connect_with_rsa_ssh_key_password(self): - state = State( - make_inventory( - hosts=( - ( - "somehost", - {"ssh_key": "testkey", "ssh_key_password": "testpass"}, - ), - ), - ), - Config(), - ) - - with ( - mock.patch("pyinfra.connectors.ssh_util.path.isfile", lambda *args, **kwargs: True), - mock.patch( - "pyinfra.connectors.ssh_util.RSAKey.from_private_key_file", - ) as fake_key_open, - ): - fake_key = mock.MagicMock() - - def fake_key_open_fail(*args, **kwargs): - if "password" not in kwargs: - raise PasswordRequiredException() - return fake_key - - fake_key_open.side_effect = fake_key_open_fail - - connect_all(state) - - # Check the key was created properly - fake_key_open.assert_called_with(filename="testkey", password="testpass") - # Check the certificate file was then loaded - fake_key.load_certificate.assert_called_with("testkey.pub") - - def test_connect_with_rsa_ssh_key_password_from_prompt(self): - state = State(make_inventory(hosts=(("somehost", {"ssh_key": "testkey"}),)), Config()) - - with ( - mock.patch("pyinfra.connectors.ssh_util.path.isfile", lambda *args, **kwargs: True), - mock.patch( - "pyinfra.connectors.ssh_util.getpass", - lambda *args, **kwargs: "testpass", - ), - mock.patch( - "pyinfra.connectors.ssh_util.RSAKey.from_private_key_file", - ) as fake_key_open, - ): - fake_key = mock.MagicMock() - - def fake_key_open_fail(*args, **kwargs): - if "password" not in kwargs: - raise PasswordRequiredException() - return fake_key - - fake_key_open.side_effect = fake_key_open_fail - - pyinfra.is_cli = True - connect_all(state) - pyinfra.is_cli = False - - # Check the key was created properly - fake_key_open.assert_called_with(filename="testkey", password="testpass") - # Check the certificate file was then loaded - fake_key.load_certificate.assert_called_with("testkey.pub") - - def test_connect_with_rsa_ssh_key_missing_password(self): - state = State(make_inventory(hosts=(("somehost", {"ssh_key": "testkey"}),)), Config()) - - with ( - mock.patch("pyinfra.connectors.ssh_util.path.isfile", lambda *args, **kwargs: True), - mock.patch( - "pyinfra.connectors.ssh_util.RSAKey.from_private_key_file", - ) as fake_key_open, - ): - fake_key_open.side_effect = make_raise_exception_function(PasswordRequiredException) - - fake_key = mock.MagicMock() - fake_key_open.return_value = fake_key - - with self.assertRaises(PyinfraError) as e: - connect_all(state) - - assert e.exception.args[0] == ( - "Private key file (testkey) is encrypted, set ssh_key_password to use this key" - ) - - def test_connect_with_rsa_ssh_key_wrong_password(self): - state = State( - make_inventory( - hosts=( - ( - "somehost", - {"ssh_key": "testkey", "ssh_key_password": "testpass"}, - ), - ), - ), - Config(), - ) - - fake_fail_from_private_key_file = mock.MagicMock() - fake_fail_from_private_key_file.side_effect = make_raise_exception_function(SSHException) - - with ( - mock.patch("pyinfra.connectors.ssh_util.path.isfile", lambda *args, **kwargs: True), - mock.patch( - "pyinfra.connectors.ssh_util.DSSKey.from_private_key_file", - fake_fail_from_private_key_file, - ), - mock.patch( - "pyinfra.connectors.ssh_util.ECDSAKey.from_private_key_file", - fake_fail_from_private_key_file, - ), - mock.patch( - "pyinfra.connectors.ssh_util.Ed25519Key.from_private_key_file", - fake_fail_from_private_key_file, - ), - mock.patch( - "pyinfra.connectors.ssh_util.RSAKey.from_private_key_file", - ) as fake_key_open, - ): - - def fake_key_open_fail(*args, **kwargs): - if "password" not in kwargs: - raise PasswordRequiredException - raise SSHException - - fake_key_open.side_effect = fake_key_open_fail - - fake_key = mock.MagicMock() - fake_key_open.return_value = fake_key - - with self.assertRaises(PyinfraError) as e: - connect_all(state) - - assert e.exception.args[0] == "Invalid private key file: testkey" - - assert fake_fail_from_private_key_file.call_count == 3 - - def test_connect_with_dss_ssh_key(self): - state = State(make_inventory(hosts=(("somehost", {"ssh_key": "testkey"}),)), Config()) - - with ( - mock.patch("pyinfra.connectors.ssh_util.path.isfile", lambda *args, **kwargs: True), - mock.patch( - "pyinfra.connectors.ssh_util.RSAKey.from_private_key_file", - ) as fake_rsa_key_open, - mock.patch( - "pyinfra.connectors.ssh_util.DSSKey.from_private_key_file", - ) as fake_key_open, - ): # noqa - fake_rsa_key_open.side_effect = make_raise_exception_function(SSHException) - - fake_key = mock.MagicMock() - fake_key_open.return_value = fake_key - - connect_all(state) - - # Check the key was created properly - fake_key_open.assert_called_with(filename="testkey") - - # And check the Paramiko SSH call was correct - self.fake_connect_mock.assert_called_with( - "somehost", - allow_agent=False, - look_for_keys=False, - pkey=fake_key, - timeout=10, - username="vagrant", - _pyinfra_ssh_forward_agent=False, - _pyinfra_ssh_config_file=None, - _pyinfra_ssh_known_hosts_file=None, - _pyinfra_ssh_strict_host_key_checking="accept-new", - _pyinfra_ssh_paramiko_connect_kwargs=None, - ) - - # Check that loading the same key again is cached in the state - second_state = State( - make_inventory(hosts=(("somehost", {"ssh_key": "testkey"}),)), - Config(), - ) - second_state.private_keys = state.private_keys - - connect_all(second_state) - - def test_connect_with_dss_ssh_key_password(self): - state = State( - make_inventory( - hosts=( - ( - "somehost", - {"ssh_key": "testkey", "ssh_key_password": "testpass"}, - ), - ), - ), - Config(), - ) - - with ( - mock.patch("pyinfra.connectors.ssh_util.path.isfile", lambda *args, **kwargs: True), - mock.patch( - "pyinfra.connectors.ssh_util.RSAKey.from_private_key_file", - ) as fake_rsa_key_open, - mock.patch( - "pyinfra.connectors.ssh_util.DSSKey.from_private_key_file", - ) as fake_dss_key_open, - ): # noqa - - def fake_rsa_key_open_fail(*args, **kwargs): - if "password" not in kwargs: - raise PasswordRequiredException - raise SSHException - - fake_rsa_key_open.side_effect = fake_rsa_key_open_fail - - fake_dss_key = mock.MagicMock() - - def fake_dss_key_func(*args, **kwargs): - if "password" not in kwargs: - raise PasswordRequiredException - return fake_dss_key - - fake_dss_key_open.side_effect = fake_dss_key_func - - connect_all(state) - - # Check the key was created properly - fake_dss_key_open.assert_called_with(filename="testkey", password="testpass") - - # And check the Paramiko SSH call was correct - self.fake_connect_mock.assert_called_with( - "somehost", - allow_agent=False, - look_for_keys=False, - pkey=fake_dss_key, - timeout=10, - username="vagrant", - _pyinfra_ssh_forward_agent=False, - _pyinfra_ssh_config_file=None, - _pyinfra_ssh_known_hosts_file=None, - _pyinfra_ssh_strict_host_key_checking="accept-new", - _pyinfra_ssh_paramiko_connect_kwargs=None, - ) - - # Check that loading the same key again is cached in the state - second_state = State( - make_inventory(hosts=(("somehost", {"ssh_key": "testkey"}),)), - Config(), - ) - second_state.private_keys = state.private_keys - - connect_all(second_state) - - def test_connect_with_missing_ssh_key(self): - state = State(make_inventory(hosts=(("somehost", {"ssh_key": "testkey"}),)), Config()) - - with self.assertRaises(PyinfraError) as e: - connect_all(state) - - self.assertTrue(e.exception.args[0].startswith("No such private key file:")) - - # SSH command tests - # - - @mock.patch("pyinfra.connectors.ssh.SSHClient") - def test_run_shell_command(self, fake_ssh_client): - fake_ssh = mock.MagicMock() - fake_stdin = mock.MagicMock() - fake_stdout = mock.MagicMock() - fake_ssh.exec_command.return_value = fake_stdin, fake_stdout, mock.MagicMock() - - fake_ssh_client.return_value = fake_ssh - - inventory = make_inventory(hosts=("somehost",)) - State(inventory, Config()) - host = inventory.get_host("somehost") - host.connect() - - command = "echo Šablony" - fake_stdout.channel.recv_exit_status.return_value = 0 - - out = host.run_shell_command(command, _stdin="hello", print_output=True) - assert len(out) == 2 - - status, output = out - assert status is True - fake_stdin.write.assert_called_with(b"hello\n") - - combined_out = host.run_shell_command( - command, - _stdin="hello", - print_output=True, - ) - assert len(combined_out) == 2 - - fake_ssh.exec_command.assert_called_with("sh -c 'echo Šablony'", get_pty=False) - - @mock.patch("pyinfra.connectors.ssh.click") - @mock.patch("pyinfra.connectors.ssh.SSHClient") - def test_run_shell_command_masked(self, fake_ssh_client, fake_click): - fake_ssh = mock.MagicMock() - fake_stdout = mock.MagicMock() - fake_ssh.exec_command.return_value = ( - mock.MagicMock(), - fake_stdout, - mock.MagicMock(), - ) - - fake_ssh_client.return_value = fake_ssh - - inventory = make_inventory(hosts=("somehost",)) - State(inventory, Config()) - host = inventory.get_host("somehost") - host.connect() - - command = StringCommand("echo", MaskString("top-secret-stuff")) - fake_stdout.channel.recv_exit_status.return_value = 0 - - out = host.run_shell_command(command, print_output=True, print_input=True) - assert len(out) == 2 - - status, output = out - assert status is True - - fake_ssh.exec_command.assert_called_with( - "sh -c 'echo top-secret-stuff'", - get_pty=False, - ) - - fake_click.echo.assert_called_with( - "{0}>>> sh -c 'echo ***'".format(host.print_prefix), - err=True, - ) - - @mock.patch("pyinfra.connectors.ssh.SSHClient") - def test_run_shell_command_success_exit_code(self, fake_ssh_client): - fake_ssh = mock.MagicMock() - fake_stdout = mock.MagicMock() - fake_ssh.exec_command.return_value = ( - mock.MagicMock(), - fake_stdout, - mock.MagicMock(), - ) - - fake_ssh_client.return_value = fake_ssh - - inventory = make_inventory(hosts=("somehost",)) - State(inventory, Config()) - host = inventory.get_host("somehost") - host.connect() - - command = "echo hi" - fake_stdout.channel.recv_exit_status.return_value = 1 - - out = host.run_shell_command(command, _success_exit_codes=[1]) - assert len(out) == 2 - assert out[0] is True - - @mock.patch("pyinfra.connectors.ssh.SSHClient") - def test_run_shell_command_error(self, fake_ssh_client): - fake_ssh = mock.MagicMock() - fake_stdout = mock.MagicMock() - fake_ssh.exec_command.return_value = ( - mock.MagicMock(), - fake_stdout, - mock.MagicMock(), - ) - - fake_ssh_client.return_value = fake_ssh - - inventory = make_inventory(hosts=("somehost",)) - state = State(inventory, Config()) - host = inventory.get_host("somehost") - host.connect(state) - - command = "echo hi" - fake_stdout.channel.recv_exit_status.return_value = 1 - - out = host.run_shell_command(command) - assert len(out) == 2 - assert out[0] is False - - @mock.patch("pyinfra.connectors.util.getpass") - @mock.patch("pyinfra.connectors.ssh.SSHClient") - def test_run_shell_command_sudo_password_automatic_prompt( - self, - fake_ssh_client, - fake_getpass, - ): - fake_ssh = mock.MagicMock() - first_fake_stdout = mock.MagicMock() - second_fake_stdout = mock.MagicMock() - third_fake_stdout = mock.MagicMock() - - first_fake_stdout.__iter__.return_value = ["sudo: a password is required\r"] - second_fake_stdout.__iter__.return_value = ["/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX"] - - fake_ssh.exec_command.side_effect = [ - ( - mock.MagicMock(), - first_fake_stdout, - mock.MagicMock(), - ), # command w/o sudo password - ( - mock.MagicMock(), - second_fake_stdout, - mock.MagicMock(), - ), # SUDO_ASKPASS_COMMAND - ( - mock.MagicMock(), - third_fake_stdout, - mock.MagicMock(), - ), # command with sudo pw - ] - - fake_ssh_client.return_value = fake_ssh - fake_getpass.return_value = "password" - - inventory = make_inventory(hosts=("somehost",)) - State(inventory, Config()) - host = inventory.get_host("somehost") - host.connect() - - command = "echo Šablony" - first_fake_stdout.channel.recv_exit_status.return_value = 1 - second_fake_stdout.channel.recv_exit_status.return_value = 0 - third_fake_stdout.channel.recv_exit_status.return_value = 0 - - out = host.run_shell_command(command, _sudo=True, print_output=True) - assert len(out) == 2 - - status, output = out - assert status is True - - fake_ssh.exec_command.assert_any_call(("sudo -H -n sh -c 'echo Šablony'"), get_pty=False) - - fake_ssh.exec_command.assert_called_with( - ( - "env SUDO_ASKPASS=/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX " - "PYINFRA_SUDO_PASSWORD=password " - "sudo -H -A -k sh -c 'echo Šablony'" - ), - get_pty=False, - ) - - @mock.patch("pyinfra.connectors.util.getpass") - @mock.patch("pyinfra.connectors.ssh.SSHClient") - def test_run_shell_command_sudo_password_automatic_prompt_with_special_chars_in_password( - self, - fake_ssh_client, - fake_getpass, - ): - fake_ssh = mock.MagicMock() - first_fake_stdout = mock.MagicMock() - second_fake_stdout = mock.MagicMock() - third_fake_stdout = mock.MagicMock() - - first_fake_stdout.__iter__.return_value = ["sudo: a password is required\r"] - second_fake_stdout.__iter__.return_value = ["/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX"] - - fake_ssh.exec_command.side_effect = [ - ( - mock.MagicMock(), - first_fake_stdout, - mock.MagicMock(), - ), # command w/o sudo password - ( - mock.MagicMock(), - second_fake_stdout, - mock.MagicMock(), - ), # SUDO_ASKPASS_COMMAND - ( - mock.MagicMock(), - third_fake_stdout, - mock.MagicMock(), - ), # command with sudo pw - ] - - fake_ssh_client.return_value = fake_ssh - fake_getpass.return_value = "p@ss'word';" - - inventory = make_inventory(hosts=("somehost",)) - State(inventory, Config()) - host = inventory.get_host("somehost") - host.connect() - - command = "echo Šablony" - first_fake_stdout.channel.recv_exit_status.return_value = 1 - second_fake_stdout.channel.recv_exit_status.return_value = 0 - third_fake_stdout.channel.recv_exit_status.return_value = 0 - - out = host.run_shell_command(command, _sudo=True, print_output=True) - assert len(out) == 2 - - status, output = out - assert status is True - - fake_ssh.exec_command.assert_any_call(("sudo -H -n sh -c 'echo Šablony'"), get_pty=False) - - fake_ssh.exec_command.assert_called_with( - ( - "env SUDO_ASKPASS=/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX " - """PYINFRA_SUDO_PASSWORD='p@ss'"'"'word'"'"';' """ - "sudo -H -A -k sh -c 'echo Šablony'" - ), - get_pty=False, - ) - - # SSH file put/get tests - # - - @mock.patch("pyinfra.connectors.ssh.SSHClient") - @mock.patch("pyinfra.connectors.util.getpass") - def test_run_shell_command_retry_for_sudo_password( - self, - fake_getpass, - fake_ssh_client, - ): - fake_getpass.return_value = "PASSWORD" - - fake_ssh = mock.MagicMock() - fake_stdin = mock.MagicMock() - fake_stdout = mock.MagicMock() - fake_stderr = ["sudo: a password is required"] - fake_ssh.exec_command.return_value = fake_stdin, fake_stdout, fake_stderr - - fake_ssh_client.return_value = fake_ssh - - inventory = make_inventory(hosts=("somehost",)) - state = State(inventory, Config()) - host = inventory.get_host("somehost") - host.connect(state) - host.connector_data["sudo_askpass_path"] = "/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX" - - command = "echo hi" - return_values = [1, 0] # return 0 on the second call - fake_stdout.channel.recv_exit_status.side_effect = lambda: return_values.pop(0) - - out = host.run_shell_command(command, _sudo=True) - assert len(out) == 2 - assert out[0] is True - assert fake_getpass.called - fake_ssh.exec_command.assert_called_with( - "env SUDO_ASKPASS=/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX " - "PYINFRA_SUDO_PASSWORD=PASSWORD sudo -H -A -k sh -c 'echo hi'", - get_pty=False, - ) - - # SSH file put/get tests - # - - @mock.patch("pyinfra.connectors.ssh.SSHClient") - @mock.patch("pyinfra.connectors.ssh.SFTPClient") - def test_put_file(self, fake_sftp_client, fake_ssh_client): - inventory = make_inventory(hosts=("anotherhost",)) - state = State(inventory, Config()) - host = inventory.get_host("anotherhost") - host.connect() - - fake_open = mock.mock_open(read_data="test!") - with mock.patch("pyinfra.api.util.open", fake_open, create=True): - with ctx_state.use(state): - status = host.put_file( - "not-a-file", - "not-another-file", - print_output=True, - ) - - assert status is True - - # Disabled due to unexplained flakiness: https://github.com/pyinfra-dev/pyinfra/issues/1387 - # fake_sftp_client.from_transport().putfo.assert_called_with( - # fake_open(), - # "not-another-file", - # ) - - @mock.patch("pyinfra.connectors.ssh.SSHClient") - @mock.patch("pyinfra.connectors.ssh.SFTPClient") - def test_put_file_sudo(self, fake_sftp_client, fake_ssh_client): - inventory = make_inventory(hosts=("anotherhost",)) - state = State(inventory, Config()) - host = inventory.get_host("anotherhost") - host.connect() - - stdout_mock = mock.MagicMock() - stdout_mock.channel.recv_exit_status.return_value = 0 - fake_ssh_client().exec_command.return_value = ( - mock.MagicMock(), - stdout_mock, - mock.MagicMock(), - ) - - fake_open = mock.mock_open(read_data="test!") - with mock.patch("pyinfra.api.util.open", fake_open, create=True): - with ctx_state.use(state): - status = host.put_file( - "not-a-file", - "not another file", - print_output=True, - _sudo=True, - _sudo_user="ubuntu", - ) - - assert status is True - - fake_ssh_client().exec_command.assert_has_calls( - [ - mock.call( - ( - "sh -c 'setfacl -m u:ubuntu:r " - "/tmp/pyinfra-de01e82cb691e8a31369da3c7c8f17341c44ac24'" - ), - get_pty=False, - ), - mock.call( - ( - "sudo -H -n -u ubuntu sh -c 'cp /tmp/pyinfra-de01e82cb691e8a31369da3c7c8f17341c44ac24 '\"'\"'not another file'\"'\"''" # noqa: E501 - ), - get_pty=False, - ), - mock.call( - ("sh -c 'rm -f /tmp/pyinfra-de01e82cb691e8a31369da3c7c8f17341c44ac24'"), - get_pty=False, - ), - ], - ) - - # Disabled due to unexplained flakiness: https://github.com/pyinfra-dev/pyinfra/issues/1387 - # fake_sftp_client.from_transport().putfo.assert_called_with( - # fake_open(), - # "/tmp/pyinfra-de01e82cb691e8a31369da3c7c8f17341c44ac24", - # ) - - @mock.patch("pyinfra.connectors.ssh.SSHClient") - @mock.patch("pyinfra.connectors.ssh.SFTPClient") - def test_put_file_doas(self, fake_sftp_client, fake_ssh_client): - inventory = make_inventory(hosts=("anotherhost",)) - state = State(inventory, Config()) - host = inventory.get_host("anotherhost") - host.connect() - - stdout_mock = mock.MagicMock() - stdout_mock.channel.recv_exit_status.return_value = 0 - fake_ssh_client().exec_command.return_value = ( - mock.MagicMock(), - stdout_mock, - mock.MagicMock(), - ) - - fake_open = mock.mock_open(read_data="test!") - with mock.patch("pyinfra.api.util.open", fake_open, create=True): - with ctx_state.use(state): - status = host.put_file( - "not-a-file", - "not another file", - print_output=True, - _doas=True, - _doas_user="ubuntu", - ) - - assert status is True - - fake_ssh_client().exec_command.assert_has_calls( - [ - mock.call( - ( - "sh -c 'setfacl -m u:ubuntu:r " - "/tmp/pyinfra-de01e82cb691e8a31369da3c7c8f17341c44ac24'" - ), - get_pty=False, - ), - mock.call( - ( - "doas -n -u ubuntu sh -c 'cp /tmp/pyinfra-de01e82cb691e8a31369da3c7c8f17341c44ac24 '\"'\"'not another file'\"'\"''" # noqa: E501 - ), - get_pty=False, - ), - mock.call( - ("sh -c 'rm -f /tmp/pyinfra-de01e82cb691e8a31369da3c7c8f17341c44ac24'"), - get_pty=False, - ), - ], - ) - - # Disabled due to unexplained flakiness: https://github.com/pyinfra-dev/pyinfra/issues/1387 - # fake_sftp_client.from_transport().putfo.assert_called_with( - # fake_open(), - # "/tmp/pyinfra-de01e82cb691e8a31369da3c7c8f17341c44ac24", - # ) - - @mock.patch("pyinfra.connectors.ssh.SSHClient") - @mock.patch("pyinfra.connectors.ssh.SFTPClient") - def test_put_file_su_user_fail_acl(self, fake_sftp_client, fake_ssh_client): - inventory = make_inventory(hosts=("anotherhost",)) - state = State(inventory, Config()) - host = inventory.get_host("anotherhost") - host.connect() - - stdout_mock = mock.MagicMock() - stdout_mock.channel.recv_exit_status.return_value = 1 - fake_ssh_client().exec_command.return_value = ( - mock.MagicMock(), - stdout_mock, - mock.MagicMock(), - ) - - fake_open = mock.mock_open(read_data="test!") - with mock.patch("pyinfra.api.util.open", fake_open, create=True): - with ctx_state.use(state): - status = host.put_file( - "not-a-file", - "not-another-file", - print_output=True, - _su_user="centos", - ) - - assert status is False - - fake_ssh_client().exec_command.assert_called_with( - ("sh -c 'setfacl -m u:centos:r /tmp/pyinfra-43db9984686317089fefcf2e38de527e4cb44487'"), - get_pty=False, - ) - - # Disabled due to unexplained flakiness: https://github.com/pyinfra-dev/pyinfra/issues/1387 - # fake_sftp_client.from_transport().putfo.assert_called_with( - # fake_open(), - # "/tmp/pyinfra-43db9984686317089fefcf2e38de527e4cb44487", - # ) - - @mock.patch("pyinfra.connectors.ssh.SSHClient") - @mock.patch("pyinfra.connectors.ssh.SFTPClient") - def test_put_file_su_user_fail_copy(self, fake_sftp_client, fake_ssh_client): - inventory = make_inventory(hosts=("anotherhost",)) - state = State(inventory, Config()) - - host = inventory.get_host("anotherhost") - assert isinstance(host, Host) - host.connect() - - stdout_mock = mock.MagicMock() - exit_codes = [0, 0, 1] - stdout_mock.channel.recv_exit_status.side_effect = lambda: exit_codes.pop(0) - fake_ssh_client().exec_command.return_value = ( - mock.MagicMock(), - stdout_mock, - mock.MagicMock(), - ) - - fake_open = mock.mock_open(read_data="test!") - with mock.patch("pyinfra.api.util.open", fake_open, create=True): - with ctx_state.use(state): - status = host.put_file( - fake_open(), - "not-another-file", - print_output=True, - _su_user="centos", - ) - - assert status is False - - fake_ssh_client().exec_command.assert_any_call( - ("sh -c 'setfacl -m u:centos:r /tmp/pyinfra-43db9984686317089fefcf2e38de527e4cb44487'"), - get_pty=False, - ) - - fake_ssh_client().exec_command.assert_any_call( - ( - "su centos -c 'sh -c '\"'\"'cp " - "/tmp/pyinfra-43db9984686317089fefcf2e38de527e4cb44487 " - "not-another-file'\"'\"''" - ), - get_pty=False, - ) - - # Disabled due to unexplained flakiness: https://github.com/pyinfra-dev/pyinfra/issues/1387 - # fake_sftp_client.from_transport().putfo.assert_called_with( - # fake_open(), - # "/tmp/pyinfra-43db9984686317089fefcf2e38de527e4cb44487", - # ) - - @mock.patch("pyinfra.connectors.ssh.SSHClient") - @mock.patch("pyinfra.connectors.ssh.SFTPClient") - def test_put_file_sudo_custom_temp_file(self, fake_sftp_client, fake_ssh_client): - inventory = make_inventory(hosts=("anotherhost",)) - state = State(inventory, Config()) - host = inventory.get_host("anotherhost") - host.connect() - - stdout_mock = mock.MagicMock() - stdout_mock.channel.recv_exit_status.return_value = 0 - fake_ssh_client().exec_command.return_value = ( - mock.MagicMock(), - stdout_mock, - mock.MagicMock(), - ) - - fake_open = mock.mock_open(read_data="test!") - with mock.patch("pyinfra.api.util.open", fake_open, create=True): - with ctx_state.use(state): - status = host.put_file( - "not-a-file", - "not another file", - print_output=True, - _sudo=True, - _sudo_user="ubuntu", - remote_temp_filename="/a-different-tempfile", - ) - - assert status is True - - fake_ssh_client().exec_command.assert_called_with( - ("sh -c 'rm -f /a-different-tempfile'"), - get_pty=False, - ) - - # Disabled due to unexplained flakiness: https://github.com/pyinfra-dev/pyinfra/issues/1387 - # fake_sftp_client.from_transport().putfo.assert_called_with( - # fake_open(), - # "/a-different-tempfile", - # ) - - @mock.patch("pyinfra.connectors.ssh.SSHClient") - @mock.patch("pyinfra.connectors.ssh.SFTPClient") - def test_get_file(self, fake_sftp_client, fake_ssh_client): - inventory = make_inventory(hosts=("somehost",)) - state = State(inventory, Config()) - host = inventory.get_host("somehost") - host.connect() - - fake_open = mock.mock_open(read_data="test!") - with mock.patch("pyinfra.api.util.open", fake_open, create=True): - with ctx_state.use(state): - status = host.get_file( - "not-a-file", - "not-another-file", - print_output=True, - ) - - assert status is True - - # Disabled due to unexplained flakiness: https://github.com/pyinfra-dev/pyinfra/issues/1387 - # fake_sftp_client.from_transport().getfo.assert_called_with( - # "not-a-file", - # fake_open(), - # ) - - @mock.patch("pyinfra.connectors.ssh.SSHClient") - @mock.patch("pyinfra.connectors.ssh.SFTPClient") - def test_get_file_sudo(self, fake_sftp_client, fake_ssh_client): - inventory = make_inventory(hosts=("somehost",)) - state = State(inventory, Config()) - host = inventory.get_host("somehost") - host.connect() - - stdout_mock = mock.MagicMock() - stdout_mock.channel.recv_exit_status.return_value = 0 - fake_ssh_client().exec_command.return_value = ( - mock.MagicMock(), - stdout_mock, - mock.MagicMock(), - ) - - fake_open = mock.mock_open(read_data="test!") - with mock.patch("pyinfra.api.util.open", fake_open, create=True): - with ctx_state.use(state): - status = host.get_file( - "not-a-file", - "not-another-file", - print_output=True, - _sudo=True, - _sudo_user="ubuntu", - ) - - assert status is True - - fake_ssh_client().exec_command.assert_has_calls( - [ - mock.call( - ( - "sudo -H -n -u ubuntu sh -c 'cp not-a-file " - "/tmp/pyinfra-e9c0d3c8ffca943daa0e75511b0a09c84b59c508 && chmod +r /tmp/pyinfra-e9c0d3c8ffca943daa0e75511b0a09c84b59c508'" # noqa - ), - get_pty=False, - ), - mock.call( - ( - "sudo -H -n -u ubuntu sh -c 'rm -f " - "/tmp/pyinfra-e9c0d3c8ffca943daa0e75511b0a09c84b59c508'" - ), - get_pty=False, - ), - ], - ) - - # Disabled due to unexplained flakiness: https://github.com/pyinfra-dev/pyinfra/issues/1387 - # fake_sftp_client.from_transport().getfo.assert_called_with( - # "/tmp/pyinfra-e9c0d3c8ffca943daa0e75511b0a09c84b59c508", - # fake_open(), - # ) - - @mock.patch("pyinfra.connectors.ssh.SSHClient") - def test_get_file_sudo_copy_fail(self, fake_ssh_client): - inventory = make_inventory(hosts=("somehost",)) - state = State(inventory, Config()) - host = inventory.get_host("somehost") - host.connect() - - stdout_mock = mock.MagicMock() - stdout_mock.channel.recv_exit_status.return_value = 1 - fake_ssh_client().exec_command.return_value = ( - mock.MagicMock(), - stdout_mock, - mock.MagicMock(), - ) - - with ctx_state.use(state): - status = host.get_file( - "not-a-file", - "not-another-file", - print_output=True, - _sudo=True, - _sudo_user="ubuntu", - ) - - assert status is False - - fake_ssh_client().exec_command.assert_has_calls( - [ - mock.call( - ( - "sudo -H -n -u ubuntu sh -c 'cp not-a-file " - "/tmp/pyinfra-e9c0d3c8ffca943daa0e75511b0a09c84b59c508 && chmod +r /tmp/pyinfra-e9c0d3c8ffca943daa0e75511b0a09c84b59c508'" # noqa - ), - get_pty=False, - ), - ], - ) - - @mock.patch("pyinfra.connectors.ssh.SSHClient") - @mock.patch("pyinfra.connectors.ssh.SFTPClient") - def test_get_file_sudo_remove_fail(self, fake_sftp_client, fake_ssh_client): - inventory = make_inventory(hosts=("somehost",)) - state = State(inventory, Config()) - host = inventory.get_host("somehost") - host.connect() - - stdout_mock = mock.MagicMock() - stdout_mock.channel.recv_exit_status.side_effect = [0, 1] - fake_ssh_client().exec_command.return_value = ( - mock.MagicMock(), - stdout_mock, - mock.MagicMock(), - ) - - fake_open = mock.mock_open(read_data="test!") - with mock.patch("pyinfra.api.util.open", fake_open, create=True): - with ctx_state.use(state): - status = host.get_file( - "not-a-file", - "not-another-file", - print_output=True, - _sudo=True, - _sudo_user="ubuntu", - ) - - assert status is False - - fake_ssh_client().exec_command.assert_has_calls( - [ - mock.call( - ( - "sudo -H -n -u ubuntu sh -c 'cp not-a-file " - "/tmp/pyinfra-e9c0d3c8ffca943daa0e75511b0a09c84b59c508 && chmod +r /tmp/pyinfra-e9c0d3c8ffca943daa0e75511b0a09c84b59c508'" # noqa - ), - get_pty=False, - ), - mock.call( - ( - "sudo -H -n -u ubuntu sh -c 'rm -f " - "/tmp/pyinfra-e9c0d3c8ffca943daa0e75511b0a09c84b59c508'" - ), - get_pty=False, - ), - ], - ) - - # Disabled due to unexplained flakiness: https://github.com/pyinfra-dev/pyinfra/issues/1387 - # fake_sftp_client.from_transport().getfo.assert_called_with( - # "/tmp/pyinfra-e9c0d3c8ffca943daa0e75511b0a09c84b59c508", - # fake_open(), - # ) - - @mock.patch("pyinfra.connectors.ssh.SSHClient") - @mock.patch("pyinfra.connectors.ssh.SFTPClient") - def test_get_file_su_user(self, fake_sftp_client, fake_ssh_client): - inventory = make_inventory(hosts=("somehost",)) - state = State(inventory, Config()) - host = inventory.get_host("somehost") - host.connect() - - stdout_mock = mock.MagicMock() - stdout_mock.channel.recv_exit_status.return_value = 0 - fake_ssh_client().exec_command.return_value = ( - mock.MagicMock(), - stdout_mock, - mock.MagicMock(), - ) - - fake_open = mock.mock_open(read_data="test!") - with mock.patch("pyinfra.api.util.open", fake_open, create=True): - with ctx_state.use(state): - status = host.get_file( - "not-a-file", - "not-another-file", - print_output=True, - _su_user="centos", - ) - - assert status is True - - fake_ssh_client().exec_command.assert_has_calls( - [ - mock.call( - ( - "su centos -c 'sh -c '\"'\"'cp not-a-file " - "/tmp/pyinfra-e9c0d3c8ffca943daa0e75511b0a09c84b59c508 && chmod +r " - "/tmp/pyinfra-e9c0d3c8ffca943daa0e75511b0a09c84b59c508'\"'\"''" - ), - get_pty=False, - ), - mock.call( - ( - "su centos -c 'sh -c '\"'\"'rm -f " - "/tmp/pyinfra-e9c0d3c8ffca943daa0e75511b0a09c84b59c508'\"'\"''" - ), - get_pty=False, - ), - ], - ) - - # Disabled due to unexplained flakiness: https://github.com/pyinfra-dev/pyinfra/issues/1387 - # fake_sftp_client.from_transport().getfo.assert_called_with( - # "/tmp/pyinfra-e9c0d3c8ffca943daa0e75511b0a09c84b59c508", - # fake_open(), - # ) - - @mock.patch("pyinfra.connectors.ssh.SSHClient") - @mock.patch("pyinfra.connectors.ssh.SFTPClient") - def test_get_sftp_fail(self, fake_sftp_client, fake_ssh_client): - inventory = make_inventory(hosts=("anotherhost",)) - state = State(inventory, Config()) - host = inventory.get_host("anotherhost") - host.connect() - - # Clear the memoization cache to ensure the exception gets raised - host.connector.get_file_transfer_connection.cache.clear() - - fake_sftp_client.from_transport.side_effect = make_raise_exception_function(SSHException) - - fake_open = mock.mock_open(read_data="test!") - with mock.patch("pyinfra.api.util.open", fake_open, create=True): - with ctx_state.use(state): - with self.assertRaises(ConnectError): - host.put_file( - "not-a-file", - "not-another-file", - print_output=True, - ) - - @mock.patch("pyinfra.connectors.ssh.SSHClient") - @mock.patch("pyinfra.connectors.ssh.sleep") - def test_ssh_connect_fail_retry(self, fake_sleep, fake_ssh_client): - for exception_class in ( - SSHException, - gaierror, - socket_error, - EOFError, - ): - fake_sleep.reset_mock() - fake_ssh_client.reset_mock() - - inventory = make_inventory( - hosts=("unresposivehost",), override_data={"ssh_connect_retries": 1} - ) - State(inventory, Config()) - - unresposivehost = inventory.get_host("unresposivehost") - assert unresposivehost.data.ssh_connect_retries == 1 - - fake_ssh_client().connect.side_effect = exception_class() - - with self.assertRaises(ConnectError): - unresposivehost.connect(show_errors=False, raise_exceptions=True) - - fake_sleep.assert_called_once() - assert fake_ssh_client().connect.call_count == 2 - - @mock.patch("pyinfra.connectors.ssh.SSHClient") - @mock.patch("pyinfra.connectors.ssh.sleep") - def test_ssh_connect_fail_success(self, fake_sleep, fake_ssh_client): - for exception_class in ( - SSHException, - gaierror, - socket_error, - EOFError, - ): - fake_sleep.reset_mock() - fake_ssh_client.reset_mock() - - inventory = make_inventory( - hosts=("unresposivehost",), override_data={"ssh_connect_retries": 1} - ) - State(inventory, Config()) - - unresposivehost = inventory.get_host("unresposivehost") - assert unresposivehost.data.ssh_connect_retries == 1 - - fake_ssh_client().connect.side_effect = [ - exception_class(), - mock.MagicMock(), - ] - - unresposivehost.connect(show_errors=False, raise_exceptions=True) - fake_sleep.assert_called_once() - assert fake_ssh_client().connect.call_count == 2 +def test_connect_all_activates_hosts(fake_asyncssh): + inventory = make_inventory() + state = State(inventory, Config()) + + connect_all(state) + + assert {host.name for host in state.active_hosts} == {"somehost", "anotherhost"} + assert set(fake_asyncssh.keys()) == {"somehost", "anotherhost"} + + disconnect_all(state) + + +def test_run_shell_command_uses_asyncssh(fake_asyncssh): + inventory = make_inventory() + state = State(inventory, Config()) + + connect_all(state) + host = inventory.get_host("somehost") + + connection = fake_asyncssh[host.name] + connection.command_results["echo hello"] = {"stdout": "hello\n", "stderr": "", "exit_status": 0} + + status, output = host.run_shell_command(StringCommand("echo", "hello")) + + assert status is True + assert output.stdout_lines == ["hello"] + assert any("echo hello" in command for command in connection.commands_run) + + disconnect_all(state) + + +def test_put_file_uses_scp_protocol(fake_asyncssh, monkeypatch, tmp_path): + inventory = make_inventory(override_data={"ssh_file_transfer_protocol": "scp"}) + state = State(inventory, Config()) + + remote_files: Dict[str, Dict[str, bytes]] = {} + + async def _scp_stub(src, dst, **kwargs): # type: ignore[override] + # Upload: local path -> (connection, remote_path) + if isinstance(dst, tuple): + client, remote_path = dst + with open(src, "rb") as src_file: + data = src_file.read() + remote_files.setdefault(client.hostname, {})[remote_path] = data + return + + # Download: (connection, remote_path) -> local path + client, remote_path = src + data = remote_files.get(client.hostname, {}).get(remote_path) + if data is None: + raise FileNotFoundError(remote_path) + directory = os.path.dirname(dst) + if directory: + os.makedirs(directory, exist_ok=True) + with open(dst, "wb") as dest_file: + dest_file.write(data) + + monkeypatch.setattr(asyncssh, "scp", _scp_stub) + + connect_all(state) + host = inventory.get_host("somehost") + + local_file = tmp_path / "upload.txt" + local_file.write_text("hello scp") + + assert host.put_file(str(local_file), "/remote/upload.txt") is True + assert remote_files["somehost"]["/remote/upload.txt"] == b"hello scp" + + disconnect_all(state) + + +def test_get_file_uses_scp_protocol(fake_asyncssh, monkeypatch, tmp_path): + inventory = make_inventory(override_data={"ssh_file_transfer_protocol": "scp"}) + state = State(inventory, Config()) + + remote_files: Dict[str, Dict[str, bytes]] = {"somehost": {"/remote/file.txt": b"from-remote"}} + + async def _scp_stub(src, dst, **kwargs): # type: ignore[override] + if isinstance(dst, tuple): + client, remote_path = dst + with open(src, "rb") as src_file: + data = src_file.read() + remote_files.setdefault(client.hostname, {})[remote_path] = data + return + + client, remote_path = src + data = remote_files.get(client.hostname, {}).get(remote_path) + if data is None: + raise FileNotFoundError(remote_path) + directory = os.path.dirname(dst) + if directory: + os.makedirs(directory, exist_ok=True) + with open(dst, "wb") as dest_file: + dest_file.write(data) + + monkeypatch.setattr(asyncssh, "scp", _scp_stub) + + connect_all(state) + host = inventory.get_host("somehost") + + destination = tmp_path / "download.txt" + assert host.get_file("/remote/file.txt", str(destination)) is True + assert destination.read_text() == "from-remote" + + disconnect_all(state) + + +def test_paramiko_kwargs_compatibility(tmp_path): + key = asyncssh.generate_private_key("ssh-ed25519") + key_file = tmp_path / "id_test" + key_file.write_text(key.export_private_key().decode(), encoding="utf-8") + + inventory = make_inventory( + override_data={ + "ssh_paramiko_connect_kwargs": { + "hostname": "overridehost", + "username": "otheruser", + "password": "secret", + "port": 2222, + "timeout": 12, + "auth_timeout": 34, + "allow_agent": False, + "look_for_keys": False, + "compress": True, + "key_filename": str(key_file), + } + } + ) + + _state = State(inventory, Config()) + host = inventory.get_host("somehost") + connector = host.connector + + hostname, kwargs = connector._build_connect_kwargs(host.name, "accept-new") + + assert hostname == "overridehost" + assert kwargs["username"] == "otheruser" + assert kwargs["password"] == "secret" + assert kwargs["port"] == 2222 + assert kwargs["connect_timeout"] == 12 + assert kwargs["login_timeout"] == 34 + assert kwargs["agent_path"] == () + assert kwargs["client_keys"] and len(kwargs["client_keys"]) == 1 + assert kwargs["compression_algs"] == ["zlib@openssh.com", "zlib"] + + +def test_private_key_certificates_are_loaded(tmp_path): + key = asyncssh.generate_private_key("ssh-ed25519") + key_file = tmp_path / "id_ed25519" + key_file.write_text(key.export_private_key().decode(), encoding="utf-8") + + cert_file = tmp_path / "id_ed25519-cert.pub" + cert_file.write_text(key.export_public_key().decode(), encoding="utf-8") + + inventory = make_inventory(override_data={"ssh_key": str(key_file)}) + _state = State(inventory, Config()) + host = inventory.get_host("somehost") + connector = host.connector + + _, kwargs = connector._build_connect_kwargs(host.name, "accept-new") + + assert kwargs.get("client_keys") + assert kwargs.get("client_certs") + assert len(kwargs["client_certs"]) == 1 + + +def test_default_ssh_config_is_loaded(fake_asyncssh, tmp_path, monkeypatch): + home = tmp_path / "home" + config_dir = home / ".ssh" + config_dir.mkdir(parents=True) + config_file = config_dir / "config" + config_file.write_text("Host somehost\n User alternative\n", encoding="utf-8") + + monkeypatch.setenv("HOME", str(home)) + + calls: list[str] = [] + + class _TrackingConfig: + def lookup(self, hostname: str) -> Dict[str, str]: + return {} + + def _read_config(path: str, *_args, **_kwargs): + calls.append(path) + return _TrackingConfig() + + monkeypatch.setattr(asyncssh, "read_ssh_config", _read_config, raising=False) + + inventory = make_inventory() + state = State(inventory, Config()) + + connect_all(state) + + assert calls + assert set(calls) == {str(config_file)} + + disconnect_all(state) + + +def test_accept_new_writes_default_known_hosts(fake_asyncssh, tmp_path, monkeypatch): + home = tmp_path / "home" + monkeypatch.setenv("HOME", str(home)) + + inventory = make_inventory(override_data={"ssh_strict_host_key_checking": "accept-new"}) + state = State(inventory, Config()) + + connect_all(state) + + known_hosts_path = home / ".ssh" / "known_hosts" + assert known_hosts_path.exists() + contents = known_hosts_path.read_text() + assert "somehost" in contents + + disconnect_all(state) + + +def test_accept_new_detects_host_key_mismatch(fake_asyncssh, tmp_path, monkeypatch): + home = tmp_path / "home" + known_hosts_dir = home / ".ssh" + known_hosts_dir.mkdir(parents=True) + monkeypatch.setenv("HOME", str(home)) + + # Write a different host key to trigger mismatch detection + other_key = asyncssh.generate_private_key("ssh-ed25519").export_public_key().decode() + mismatch_line = f"somehost {other_key}\n" + (known_hosts_dir / "known_hosts").write_text(mismatch_line, encoding="utf-8") + + inventory = make_inventory(override_data={"ssh_strict_host_key_checking": "accept-new"}) + state = State(inventory, Config()) + + connect_all(state) + + host = inventory.get_host("somehost") + assert host not in state.active_hosts + assert host in state.failed_hosts + + disconnect_all(state) diff --git a/tests/test_connectors/test_sshuserclient.py b/tests/test_connectors/test_sshuserclient.py deleted file mode 100644 index eea2e6e17..000000000 --- a/tests/test_connectors/test_sshuserclient.py +++ /dev/null @@ -1,273 +0,0 @@ -from base64 import b64decode -from unittest import TestCase -from unittest.mock import mock_open, patch - -from paramiko import PKey, ProxyCommand, SSHException - -from pyinfra.connectors.sshuserclient import SSHClient -from pyinfra.connectors.sshuserclient.client import AskPolicy, get_ssh_config - -SSH_CONFIG_DATA = """ -# Comment -Host 127.0.0.1 - IdentityFile /id_rsa - IdentityFile /id_rsa2 - User testuser - Port 33 - ProxyCommand echo thing - -Include other_file -""" - -SSH_CONFIG_OTHER_FILE = """ -Host 192.168.1.1 - User "otheruser" - # ProxyCommand None # Commented to get test passing with Paramiko > 3 - ForwardAgent yes - UserKnownHostsFile ~/.ssh/test3 -""" - -SSH_CONFIG_OTHER_FILE_PROXYJUMP = """ -Host 192.168.1.2 - User "otheruser" - ProxyJump nottestuser@127.0.0.1 - ForwardAgent yes -""" - -BAD_SSH_CONFIG_DATA = """ -& -""" - -LOOPING_SSH_CONFIG_DATA = """ -Include other_file -""" - -# To ensure that we don't remove things from users hostfiles -# we should test that all modifications only append to the -# hostfile, and don't delete any data or comments. -EXAMPLE_KEY_1 = ( - "AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+" - "VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/" - "C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXk" - "E2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMj" - "A2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIE" - "s4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+Ej" - "qoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/Wnw" - "H6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=" -) - -KNOWN_HOSTS_EXAMPLE_DATA = f""" -# this is an important comment - -# another comment after the newline - -@cert-authority example-domain.lan ssh-rsa {EXAMPLE_KEY_1} - -192.168.1.222 ssh-rsa {EXAMPLE_KEY_1} -""" - - -class TestSSHUserConfigMissing(TestCase): - def setUp(self): - get_ssh_config.cache = {} - - @patch( - "pyinfra.connectors.sshuserclient.client.path.exists", - lambda path: False, - ) - def test_load_ssh_config_no_exist(self): - client = SSHClient() - - _, config, forward_agent, missing_host_key_policy, host_keys_file, keep_alive = ( - client.parse_config( - "127.0.0.1", - ) - ) - - assert config.get("port") == 22 - - -@patch( - "pyinfra.connectors.sshuserclient.client.path.exists", - lambda path: True, -) -@patch( - "pyinfra.connectors.sshuserclient.config.glob.iglob", - lambda path: ["other_file"], -) -@patch( - "pyinfra.connectors.sshuserclient.config.path.isfile", - lambda path: True, -) -@patch( - "pyinfra.connectors.sshuserclient.config.path.expanduser", - lambda path: path, -) -@patch( - "pyinfra.connectors.sshuserclient.config.path.isabs", - lambda path: True, -) -@patch( - "paramiko.config.LazyFqdn.__str__", - lambda self: "", -) -class TestSSHUserConfig(TestCase): - def setUp(self): - get_ssh_config.cache = {} - - @patch( - "pyinfra.connectors.sshuserclient.client.open", - mock_open(read_data=SSH_CONFIG_DATA), - create=True, - ) - @patch( - "pyinfra.connectors.sshuserclient.config.open", - mock_open(read_data=SSH_CONFIG_OTHER_FILE), - create=True, - ) - def test_load_ssh_config(self): - client = SSHClient() - - _, config, forward_agent, missing_host_key_policy, host_keys_file, keep_alive = ( - client.parse_config( - "127.0.0.1", - ) - ) - - assert config.get("key_filename") == ["/id_rsa", "/id_rsa2"] - assert config.get("username") == "testuser" - assert config.get("port") == 33 - assert isinstance(config.get("sock"), ProxyCommand) - assert forward_agent is False - assert isinstance(missing_host_key_policy, AskPolicy) - assert host_keys_file == "~/.ssh/known_hosts" # OpenSSH default - - ( - _, - other_config, - forward_agent, - missing_host_key_policy, - host_keys_file, - keep_alive, - ) = client.parse_config("192.168.1.1") - - assert other_config.get("username") == "otheruser" - assert forward_agent is True - assert isinstance(missing_host_key_policy, AskPolicy) - assert host_keys_file == "~/.ssh/test3" - - @patch( - "pyinfra.connectors.sshuserclient.client.open", - mock_open(read_data=BAD_SSH_CONFIG_DATA), - create=True, - ) - def test_invalid_ssh_config(self): - client = SSHClient() - - with self.assertRaises(Exception) as context: - client.parse_config("127.0.0.1") - - assert context.exception.args[0] == "Unparsable line &" - - @patch( - "pyinfra.connectors.sshuserclient.client.open", - mock_open(read_data=LOOPING_SSH_CONFIG_DATA), - create=True, - ) - @patch( - "pyinfra.connectors.sshuserclient.config.open", - mock_open(read_data=LOOPING_SSH_CONFIG_DATA), - create=True, - ) - def test_include_loop_ssh_config(self): - client = SSHClient() - - with self.assertRaises(Exception) as context: - client.parse_config("127.0.0.1") - - assert context.exception.args[0] == "Include loop detected in ssh config file: other_file" - - @patch( - "pyinfra.connectors.sshuserclient.client.open", - mock_open(read_data=SSH_CONFIG_DATA), - create=True, - ) - @patch( - "pyinfra.connectors.sshuserclient.config.open", - mock_open(read_data=SSH_CONFIG_OTHER_FILE_PROXYJUMP), - create=True, - ) - @patch("pyinfra.connectors.sshuserclient.SSHClient.connect") - @patch("pyinfra.connectors.sshuserclient.SSHClient.gateway") - def test_load_ssh_config_proxyjump(self, fake_gateway, fake_ssh_connect): - client = SSHClient() - - # Load the SSH config with ProxyJump configured - _, config, forward_agent, _, _, _ = client.parse_config( - "192.168.1.2", - {"port": 1022}, - ssh_config_file="other_file", - ) - - fake_ssh_connect.assert_called_once_with( - "127.0.0.1", - _pyinfra_ssh_config_file="other_file", - port="33", - sock=None, - username="nottestuser", - ) - fake_gateway.assert_called_once_with("192.168.1.2", 1022, "192.168.1.2", 1022) - - @patch("pyinfra.connectors.sshuserclient.client.open", mock_open(), create=True) - @patch("pyinfra.connectors.sshuserclient.client.ParamikoClient.connect") - def test_test_paramiko_connect_kwargs(self, fake_paramiko_connect): - client = SSHClient() - client.connect("hostname", _pyinfra_ssh_paramiko_connect_kwargs={"test": "kwarg"}) - - fake_paramiko_connect.assert_called_once_with( - "hostname", - port=22, - test="kwarg", - ) - - def test_missing_hostkey(self): - client = SSHClient() - policy = AskPolicy() - example_hostname = "new_host" - example_keytype = "ecdsa-sha2-nistp256" - example_key = ( - "AAAAE2VjZHNhLXNoYTItbmlzdHAyNT" - "YAAAAIbmlzdHAyNTYAAABBBHNp1NM" - "ZjxPBuuKwIPfkVJqWaH3oUtW137kIW" - "P4PlCyACt8zVIIimFhIpwRUidcf7jw" - "VWPAJvfBjEPqewDApnZQ=" - ) - - key = PKey.from_type_string( - example_keytype, - b64decode(example_key), - ) - - # Check if AskPolicy respects not importing and properly raises SSHException - with self.subTest("Check user 'no'"): - with patch("builtins.input", return_value="n"): - self.assertRaises( - SSHException, lambda: policy.missing_host_key(client, example_hostname, key) - ) - - # Check if AskPolicy properly appends to hostfile - with self.subTest("Check user 'yes'"): - mock_data = mock_open(read_data=KNOWN_HOSTS_EXAMPLE_DATA) - # Read mock hostfile - with patch("pyinfra.connectors.sshuserclient.client.open", mock_data): - with patch("paramiko.hostkeys.open", mock_data): - with patch("builtins.input", return_value="y"): - policy.missing_host_key(client, "new_host", key) - - # Assert that we appended correctly to the file - write_call_args = mock_data.return_value.write.call_args - # Ensure we only wrote once and then closed the handle. - assert len(write_call_args) == 2 - # Ensure we wrote the correct content - correct_output = f"{example_hostname} {example_keytype} {example_key}\n" - assert write_call_args[0][0] == correct_output diff --git a/tests/test_sync_context.py b/tests/test_sync_context.py new file mode 100644 index 000000000..31b8acc3b --- /dev/null +++ b/tests/test_sync_context.py @@ -0,0 +1,157 @@ +from __future__ import annotations + +from pyinfra.api import Config, State, deploy +from pyinfra.api.state import StateStage +from pyinfra.facts.server import Command +from pyinfra.operations import files, server +from pyinfra.sync_context import SyncContext, SyncHostContext + +from .util import make_inventory + + +def test_sync_context_operation(fake_asyncssh): + inventory = make_inventory() + state = State(inventory, Config()) + + with SyncContext(state): + results = server.shell("echo sync-context") + + assert set(results.keys()) == { + inventory.get_host("somehost"), + inventory.get_host("anotherhost"), + } + assert all(meta is not None for meta in results.values()) + assert state.current_stage == StateStage.Execute + + for connection in fake_asyncssh.values(): + assert any("echo sync-context" in command for command in connection.commands_run) + + assert state.current_stage == StateStage.Disconnect + for connection in fake_asyncssh.values(): + assert connection._closed is True + + +def test_sync_context_fact(fake_asyncssh): + inventory = make_inventory() + state = State(inventory, Config()) + + with SyncContext(state): + for connection in fake_asyncssh.values(): + connection.command_results["echo fact-value"] = { + "stdout": "value\n", + "stderr": "", + "exit_status": 0, + } + + facts = {} + for hostname in ("somehost", "anotherhost"): + host = inventory.get_host(hostname) + facts[host] = host.get_fact(Command, "echo fact-value") + + assert set(facts.keys()) == { + inventory.get_host("somehost"), + inventory.get_host("anotherhost"), + } + + for value in facts.values(): + assert value == "value" + + assert state.current_stage == StateStage.Disconnect + for connection in fake_asyncssh.values(): + assert connection._closed is True + + +def test_sync_context_hosts_subset(fake_asyncssh): + inventory = make_inventory() + state = State(inventory, Config()) + + specific = inventory.get_host("somehost") + with SyncContext(state, hosts=[specific]): + server.shell("echo subset") + + assert any("echo subset" in command for command in fake_asyncssh["somehost"].commands_run) + assert "anotherhost" not in fake_asyncssh + + assert state.current_stage == StateStage.Disconnect + assert fake_asyncssh["somehost"]._closed is True + + +def test_sync_host_context_limits_to_single_host(fake_asyncssh): + inventory = make_inventory() + state = State(inventory, Config()) + + with SyncHostContext(state, "somehost"): + results = server.shell("echo sync-host-context") + + somehost = inventory.get_host("somehost") + assert set(results.keys()) == {somehost} + assert set(fake_asyncssh.keys()) == {"somehost"} + + fake_asyncssh["somehost"].command_results["echo sync-host-fact"] = { + "stdout": "value\n", + "stderr": "", + "exit_status": 0, + } + + fact_value = somehost.get_fact(Command, "echo sync-host-fact") + assert fact_value == "value" + + @deploy("Sync host deploy") + def sample_host_deploy(): + server.shell(name="Sync host deploy op", commands="echo sync-host-deploy") + + sample_host_deploy() + assert any( + "echo sync-host-deploy" in command for command in fake_asyncssh["somehost"].commands_run + ) + + assert fake_asyncssh["somehost"]._closed is True + assert "anotherhost" not in fake_asyncssh + + +def test_sync_context_files_put(fake_asyncssh, tmp_path): + inventory = make_inventory() + state = State(inventory, Config()) + + local_file = tmp_path / "sync-context.txt" + local_file.write_text("sync context test") + + with SyncContext(state): + results = files.put(src=str(local_file), dest="/sync-context.txt") + + assert set(results.keys()) == { + inventory.get_host("somehost"), + inventory.get_host("anotherhost"), + } + + assert state.current_stage == StateStage.Disconnect + + +def test_sync_context_run_deploy(fake_asyncssh): + inventory = make_inventory() + state = State(inventory, Config()) + + @deploy("Sync context deploy") + def sample_deploy(): + server.shell(name="Sync deploy op", commands="echo sync-context-deploy") + + with SyncContext(state): + sample_deploy() + + for hostname, connection in fake_asyncssh.items(): + assert hostname in {"somehost", "anotherhost"} + assert any("echo sync-context-deploy" in command for command in connection.commands_run) + + fake_asyncssh.clear() + + with SyncContext(state, hosts=[inventory.get_host("somehost")]): + sample_deploy(hosts=[inventory.get_host("somehost")]) + + assert set(fake_asyncssh.keys()) == {"somehost"} + assert any( + "echo sync-context-deploy" in command + for command in fake_asyncssh["somehost"].commands_run + ) + + assert all(connection._closed for connection in fake_asyncssh.values()) + fake_asyncssh.clear() diff --git a/tests/words.txt b/tests/words.txt index d6701aa3a..8f521186f 100644 --- a/tests/words.txt +++ b/tests/words.txt @@ -165,7 +165,6 @@ getitem getpeername getrlimit getsockname -gevent gid gpg gpgcheck @@ -271,7 +270,6 @@ openvz opkg osversion pacman -paramiko paramspec pardir pathlib diff --git a/uv.lock b/uv.lock index b78261f43..d2e80a885 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10, <4.0" resolution-markers = [ "python_full_version >= '3.14' and platform_python_implementation != 'PyPy'", @@ -14,368 +14,323 @@ resolution-markers = [ [[package]] name = "alabaster" version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "appdirs" version = "1.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" }, ] [[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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "asyncssh" +version = "2.21.1" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6b/b8/065c20bb5c9b8991648c0f25b13e445b4f51556cc3fdd0ad13ce4787c156/asyncssh-2.21.1.tar.gz", hash = "sha256:9943802955e2131536c2b1e71aacc68f56973a399937ed0b725086d7461c990c", size = 540515, upload-time = "2025-09-28T16:36:19.468Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0e/89/4a9a61bc120ca68bce92b0ea176ddc0e550e58c60ab820603bd5246e7261/asyncssh-2.21.1-py3-none-any.whl", hash = "sha256:f218f9f303c78df6627d0646835e04039a156d15e174ad63c058d62de61e1968", size = 375529, upload-time = "2025-09-28T16:36:17.68Z" }, ] [[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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "baron" version = "0.10.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "rply" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/c4/1174ebad7b54aa3e6dfeaa4f6c70af9a757ce921f0c6eae298cea84630ed/baron-0.10.1.tar.gz", hash = "sha256:af822ad44d4eb425c8516df4239ac4fdba9fdb398ef77e4924cd7c9b4045bc2f", size = 1020211, upload-time = "2021-12-09T03:33:34.731Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/e5/d0bff1cda8e5404a41aedd734a2f6087eaa152966df0c84bc0522b2b4ef0/baron-0.10.1-py2.py3-none-any.whl", hash = "sha256:befb33f4b9e832c7cd1e3cf0eafa6dd3cb6ed4cb2544245147c019936f4e0a8a", size = 45583, upload-time = "2021-12-09T03:33:32.199Z" }, -] - -[[package]] -name = "bcrypt" -version = "4.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697, upload-time = "2025-02-28T01:24:09.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719, upload-time = "2025-02-28T01:22:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001, upload-time = "2025-02-28T01:22:38.078Z" }, - { url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451, upload-time = "2025-02-28T01:22:40.787Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792, upload-time = "2025-02-28T01:22:43.144Z" }, - { url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752, upload-time = "2025-02-28T01:22:45.56Z" }, - { url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762, upload-time = "2025-02-28T01:22:47.023Z" }, - { url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384, upload-time = "2025-02-28T01:22:49.221Z" }, - { url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329, upload-time = "2025-02-28T01:22:51.603Z" }, - { url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241, upload-time = "2025-02-28T01:22:53.283Z" }, - { url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617, upload-time = "2025-02-28T01:22:55.461Z" }, - { url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751, upload-time = "2025-02-28T01:22:57.81Z" }, - { url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965, upload-time = "2025-02-28T01:22:59.181Z" }, - { url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316, upload-time = "2025-02-28T01:23:00.763Z" }, - { url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752, upload-time = "2025-02-28T01:23:02.908Z" }, - { url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019, upload-time = "2025-02-28T01:23:05.838Z" }, - { url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174, upload-time = "2025-02-28T01:23:07.274Z" }, - { url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870, upload-time = "2025-02-28T01:23:09.151Z" }, - { url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601, upload-time = "2025-02-28T01:23:11.461Z" }, - { url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660, upload-time = "2025-02-28T01:23:12.989Z" }, - { url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083, upload-time = "2025-02-28T01:23:14.5Z" }, - { url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237, upload-time = "2025-02-28T01:23:16.686Z" }, - { url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737, upload-time = "2025-02-28T01:23:18.897Z" }, - { url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741, upload-time = "2025-02-28T01:23:21.041Z" }, - { url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472, upload-time = "2025-02-28T01:23:23.183Z" }, - { url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606, upload-time = "2025-02-28T01:23:25.361Z" }, - { url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867, upload-time = "2025-02-28T01:23:26.875Z" }, - { url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589, upload-time = "2025-02-28T01:23:28.381Z" }, - { url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794, upload-time = "2025-02-28T01:23:30.187Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969, upload-time = "2025-02-28T01:23:31.945Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158, upload-time = "2025-02-28T01:23:34.161Z" }, - { url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285, upload-time = "2025-02-28T01:23:35.765Z" }, - { url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583, upload-time = "2025-02-28T01:23:38.021Z" }, - { url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896, upload-time = "2025-02-28T01:23:39.575Z" }, - { url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492, upload-time = "2025-02-28T01:23:40.901Z" }, - { url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213, upload-time = "2025-02-28T01:23:42.653Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162, upload-time = "2025-02-28T01:23:43.964Z" }, - { url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856, upload-time = "2025-02-28T01:23:46.011Z" }, - { url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726, upload-time = "2025-02-28T01:23:47.575Z" }, - { url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664, upload-time = "2025-02-28T01:23:49.059Z" }, - { url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128, upload-time = "2025-02-28T01:23:50.399Z" }, - { url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598, upload-time = "2025-02-28T01:23:51.775Z" }, - { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" }, - { url = "https://files.pythonhosted.org/packages/55/2d/0c7e5ab0524bf1a443e34cdd3926ec6f5879889b2f3c32b2f5074e99ed53/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1", size = 275367, upload-time = "2025-02-28T01:23:54.578Z" }, - { url = "https://files.pythonhosted.org/packages/10/4f/f77509f08bdff8806ecc4dc472b6e187c946c730565a7470db772d25df70/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d", size = 280644, upload-time = "2025-02-28T01:23:56.547Z" }, - { url = "https://files.pythonhosted.org/packages/35/18/7d9dc16a3a4d530d0a9b845160e9e5d8eb4f00483e05d44bb4116a1861da/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492", size = 274881, upload-time = "2025-02-28T01:23:57.935Z" }, - { url = "https://files.pythonhosted.org/packages/df/c4/ae6921088adf1e37f2a3a6a688e72e7d9e45fdd3ae5e0bc931870c1ebbda/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90", size = 280203, upload-time = "2025-02-28T01:23:59.331Z" }, - { url = "https://files.pythonhosted.org/packages/4c/b1/1289e21d710496b88340369137cc4c5f6ee036401190ea116a7b4ae6d32a/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a", size = 275103, upload-time = "2025-02-28T01:24:00.764Z" }, - { url = "https://files.pythonhosted.org/packages/94/41/19be9fe17e4ffc5d10b7b67f10e459fc4eee6ffe9056a88de511920cfd8d/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce", size = 280513, upload-time = "2025-02-28T01:24:02.243Z" }, - { url = "https://files.pythonhosted.org/packages/aa/73/05687a9ef89edebdd8ad7474c16d8af685eb4591c3c38300bb6aad4f0076/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8", size = 274685, upload-time = "2025-02-28T01:24:04.512Z" }, - { url = "https://files.pythonhosted.org/packages/63/13/47bba97924ebe86a62ef83dc75b7c8a881d53c535f83e2c54c4bd701e05c/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938", size = 280110, upload-time = "2025-02-28T01:24:05.896Z" }, +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a5/c4/1174ebad7b54aa3e6dfeaa4f6c70af9a757ce921f0c6eae298cea84630ed/baron-0.10.1.tar.gz", hash = "sha256:af822ad44d4eb425c8516df4239ac4fdba9fdb398ef77e4924cd7c9b4045bc2f", size = 1020211, upload-time = "2021-12-09T03:33:34.731Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5b/e5/d0bff1cda8e5404a41aedd734a2f6087eaa152966df0c84bc0522b2b4ef0/baron-0.10.1-py2.py3-none-any.whl", hash = "sha256:befb33f4b9e832c7cd1e3cf0eafa6dd3cb6ed4cb2544245147c019936f4e0a8a", size = 45583, upload-time = "2021-12-09T03:33:32.199Z" }, ] [[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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "2.0.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "pycparser", marker = "(python_full_version < '3.12' and implementation_name != 'PyPy') or (implementation_name != 'PyPy' and platform_python_implementation != 'PyPy')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, - { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, - { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, - { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, - { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, - { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, - { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, - { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, - { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, - { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, - { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, - { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, - { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, - { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, - { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, - { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, - { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, - { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, - { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, - { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, - { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, - { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, - { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, - { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, - { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, - { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, - { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, - { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, - { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, - { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, - { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, - { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, - { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, - { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, - { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, - { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, - { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, - { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, - { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] [[package]] name = "click" version = "8.2.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "colorama", marker = "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" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "7.10.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736, upload-time = "2025-08-29T15:35:16.668Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/1d/2e64b43d978b5bd184e0756a41415597dfef30fcbd90b747474bd749d45f/coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356", size = 217025, upload-time = "2025-08-29T15:32:57.169Z" }, - { url = "https://files.pythonhosted.org/packages/23/62/b1e0f513417c02cc10ef735c3ee5186df55f190f70498b3702d516aad06f/coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301", size = 217419, upload-time = "2025-08-29T15:32:59.908Z" }, - { url = "https://files.pythonhosted.org/packages/e7/16/b800640b7a43e7c538429e4d7223e0a94fd72453a1a048f70bf766f12e96/coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460", size = 244180, upload-time = "2025-08-29T15:33:01.608Z" }, - { url = "https://files.pythonhosted.org/packages/fb/6f/5e03631c3305cad187eaf76af0b559fff88af9a0b0c180d006fb02413d7a/coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd", size = 245992, upload-time = "2025-08-29T15:33:03.239Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a1/f30ea0fb400b080730125b490771ec62b3375789f90af0bb68bfb8a921d7/coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb", size = 247851, upload-time = "2025-08-29T15:33:04.603Z" }, - { url = "https://files.pythonhosted.org/packages/02/8e/cfa8fee8e8ef9a6bb76c7bef039f3302f44e615d2194161a21d3d83ac2e9/coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6", size = 245891, upload-time = "2025-08-29T15:33:06.176Z" }, - { url = "https://files.pythonhosted.org/packages/93/a9/51be09b75c55c4f6c16d8d73a6a1d46ad764acca0eab48fa2ffaef5958fe/coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945", size = 243909, upload-time = "2025-08-29T15:33:07.74Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a6/ba188b376529ce36483b2d585ca7bdac64aacbe5aa10da5978029a9c94db/coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e", size = 244786, upload-time = "2025-08-29T15:33:08.965Z" }, - { url = "https://files.pythonhosted.org/packages/d0/4c/37ed872374a21813e0d3215256180c9a382c3f5ced6f2e5da0102fc2fd3e/coverage-7.10.6-cp310-cp310-win32.whl", hash = "sha256:86b9b59f2b16e981906e9d6383eb6446d5b46c278460ae2c36487667717eccf1", size = 219521, upload-time = "2025-08-29T15:33:10.599Z" }, - { url = "https://files.pythonhosted.org/packages/8e/36/9311352fdc551dec5b973b61f4e453227ce482985a9368305880af4f85dd/coverage-7.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:e132b9152749bd33534e5bd8565c7576f135f157b4029b975e15ee184325f528", size = 220417, upload-time = "2025-08-29T15:33:11.907Z" }, - { url = "https://files.pythonhosted.org/packages/d4/16/2bea27e212c4980753d6d563a0803c150edeaaddb0771a50d2afc410a261/coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f", size = 217129, upload-time = "2025-08-29T15:33:13.575Z" }, - { url = "https://files.pythonhosted.org/packages/2a/51/e7159e068831ab37e31aac0969d47b8c5ee25b7d307b51e310ec34869315/coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc", size = 217532, upload-time = "2025-08-29T15:33:14.872Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c0/246ccbea53d6099325d25cd208df94ea435cd55f0db38099dd721efc7a1f/coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a", size = 247931, upload-time = "2025-08-29T15:33:16.142Z" }, - { url = "https://files.pythonhosted.org/packages/7d/fb/7435ef8ab9b2594a6e3f58505cc30e98ae8b33265d844007737946c59389/coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a", size = 249864, upload-time = "2025-08-29T15:33:17.434Z" }, - { url = "https://files.pythonhosted.org/packages/51/f8/d9d64e8da7bcddb094d511154824038833c81e3a039020a9d6539bf303e9/coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62", size = 251969, upload-time = "2025-08-29T15:33:18.822Z" }, - { url = "https://files.pythonhosted.org/packages/43/28/c43ba0ef19f446d6463c751315140d8f2a521e04c3e79e5c5fe211bfa430/coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153", size = 249659, upload-time = "2025-08-29T15:33:20.407Z" }, - { url = "https://files.pythonhosted.org/packages/79/3e/53635bd0b72beaacf265784508a0b386defc9ab7fad99ff95f79ce9db555/coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5", size = 247714, upload-time = "2025-08-29T15:33:21.751Z" }, - { url = "https://files.pythonhosted.org/packages/4c/55/0964aa87126624e8c159e32b0bc4e84edef78c89a1a4b924d28dd8265625/coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619", size = 248351, upload-time = "2025-08-29T15:33:23.105Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ab/6cfa9dc518c6c8e14a691c54e53a9433ba67336c760607e299bfcf520cb1/coverage-7.10.6-cp311-cp311-win32.whl", hash = "sha256:e3fb1fa01d3598002777dd259c0c2e6d9d5e10e7222976fc8e03992f972a2cba", size = 219562, upload-time = "2025-08-29T15:33:24.717Z" }, - { url = "https://files.pythonhosted.org/packages/5b/18/99b25346690cbc55922e7cfef06d755d4abee803ef335baff0014268eff4/coverage-7.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:f35ed9d945bece26553d5b4c8630453169672bea0050a564456eb88bdffd927e", size = 220453, upload-time = "2025-08-29T15:33:26.482Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ed/81d86648a07ccb124a5cf1f1a7788712b8d7216b593562683cd5c9b0d2c1/coverage-7.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:99e1a305c7765631d74b98bf7dbf54eeea931f975e80f115437d23848ee8c27c", size = 219127, upload-time = "2025-08-29T15:33:27.777Z" }, - { url = "https://files.pythonhosted.org/packages/26/06/263f3305c97ad78aab066d116b52250dd316e74fcc20c197b61e07eb391a/coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea", size = 217324, upload-time = "2025-08-29T15:33:29.06Z" }, - { url = "https://files.pythonhosted.org/packages/e9/60/1e1ded9a4fe80d843d7d53b3e395c1db3ff32d6c301e501f393b2e6c1c1f/coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634", size = 217560, upload-time = "2025-08-29T15:33:30.748Z" }, - { url = "https://files.pythonhosted.org/packages/b8/25/52136173c14e26dfed8b106ed725811bb53c30b896d04d28d74cb64318b3/coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6", size = 249053, upload-time = "2025-08-29T15:33:32.041Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1d/ae25a7dc58fcce8b172d42ffe5313fc267afe61c97fa872b80ee72d9515a/coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9", size = 251802, upload-time = "2025-08-29T15:33:33.625Z" }, - { url = "https://files.pythonhosted.org/packages/f5/7a/1f561d47743710fe996957ed7c124b421320f150f1d38523d8d9102d3e2a/coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c", size = 252935, upload-time = "2025-08-29T15:33:34.909Z" }, - { url = "https://files.pythonhosted.org/packages/6c/ad/8b97cd5d28aecdfde792dcbf646bac141167a5cacae2cd775998b45fabb5/coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a", size = 250855, upload-time = "2025-08-29T15:33:36.922Z" }, - { url = "https://files.pythonhosted.org/packages/33/6a/95c32b558d9a61858ff9d79580d3877df3eb5bc9eed0941b1f187c89e143/coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5", size = 248974, upload-time = "2025-08-29T15:33:38.175Z" }, - { url = "https://files.pythonhosted.org/packages/0d/9c/8ce95dee640a38e760d5b747c10913e7a06554704d60b41e73fdea6a1ffd/coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972", size = 250409, upload-time = "2025-08-29T15:33:39.447Z" }, - { url = "https://files.pythonhosted.org/packages/04/12/7a55b0bdde78a98e2eb2356771fd2dcddb96579e8342bb52aa5bc52e96f0/coverage-7.10.6-cp312-cp312-win32.whl", hash = "sha256:a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d", size = 219724, upload-time = "2025-08-29T15:33:41.172Z" }, - { url = "https://files.pythonhosted.org/packages/36/4a/32b185b8b8e327802c9efce3d3108d2fe2d9d31f153a0f7ecfd59c773705/coverage-7.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629", size = 220536, upload-time = "2025-08-29T15:33:42.524Z" }, - { url = "https://files.pythonhosted.org/packages/08/3a/d5d8dc703e4998038c3099eaf77adddb00536a3cec08c8dcd556a36a3eb4/coverage-7.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80", size = 219171, upload-time = "2025-08-29T15:33:43.974Z" }, - { url = "https://files.pythonhosted.org/packages/bd/e7/917e5953ea29a28c1057729c1d5af9084ab6d9c66217523fd0e10f14d8f6/coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6", size = 217351, upload-time = "2025-08-29T15:33:45.438Z" }, - { url = "https://files.pythonhosted.org/packages/eb/86/2e161b93a4f11d0ea93f9bebb6a53f113d5d6e416d7561ca41bb0a29996b/coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80", size = 217600, upload-time = "2025-08-29T15:33:47.269Z" }, - { url = "https://files.pythonhosted.org/packages/0e/66/d03348fdd8df262b3a7fb4ee5727e6e4936e39e2f3a842e803196946f200/coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003", size = 248600, upload-time = "2025-08-29T15:33:48.953Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27", size = 251206, upload-time = "2025-08-29T15:33:50.697Z" }, - { url = "https://files.pythonhosted.org/packages/e9/1f/9020135734184f439da85c70ea78194c2730e56c2d18aee6e8ff1719d50d/coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4", size = 252478, upload-time = "2025-08-29T15:33:52.303Z" }, - { url = "https://files.pythonhosted.org/packages/a4/a4/3d228f3942bb5a2051fde28c136eea23a761177dc4ff4ef54533164ce255/coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d", size = 250637, upload-time = "2025-08-29T15:33:53.67Z" }, - { url = "https://files.pythonhosted.org/packages/36/e3/293dce8cdb9a83de971637afc59b7190faad60603b40e32635cbd15fbf61/coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc", size = 248529, upload-time = "2025-08-29T15:33:55.022Z" }, - { url = "https://files.pythonhosted.org/packages/90/26/64eecfa214e80dd1d101e420cab2901827de0e49631d666543d0e53cf597/coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc", size = 250143, upload-time = "2025-08-29T15:33:56.386Z" }, - { url = "https://files.pythonhosted.org/packages/3e/70/bd80588338f65ea5b0d97e424b820fb4068b9cfb9597fbd91963086e004b/coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e", size = 219770, upload-time = "2025-08-29T15:33:58.063Z" }, - { url = "https://files.pythonhosted.org/packages/a7/14/0b831122305abcc1060c008f6c97bbdc0a913ab47d65070a01dc50293c2b/coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32", size = 220566, upload-time = "2025-08-29T15:33:59.766Z" }, - { url = "https://files.pythonhosted.org/packages/83/c6/81a83778c1f83f1a4a168ed6673eeedc205afb562d8500175292ca64b94e/coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2", size = 219195, upload-time = "2025-08-29T15:34:01.191Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1c/ccccf4bf116f9517275fa85047495515add43e41dfe8e0bef6e333c6b344/coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b", size = 218059, upload-time = "2025-08-29T15:34:02.91Z" }, - { url = "https://files.pythonhosted.org/packages/92/97/8a3ceff833d27c7492af4f39d5da6761e9ff624831db9e9f25b3886ddbca/coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393", size = 218287, upload-time = "2025-08-29T15:34:05.106Z" }, - { url = "https://files.pythonhosted.org/packages/92/d8/50b4a32580cf41ff0423777a2791aaf3269ab60c840b62009aec12d3970d/coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27", size = 259625, upload-time = "2025-08-29T15:34:06.575Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7e/6a7df5a6fb440a0179d94a348eb6616ed4745e7df26bf2a02bc4db72c421/coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df", size = 261801, upload-time = "2025-08-29T15:34:08.006Z" }, - { url = "https://files.pythonhosted.org/packages/3a/4c/a270a414f4ed5d196b9d3d67922968e768cd971d1b251e1b4f75e9362f75/coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb", size = 264027, upload-time = "2025-08-29T15:34:09.806Z" }, - { url = "https://files.pythonhosted.org/packages/9c/8b/3210d663d594926c12f373c5370bf1e7c5c3a427519a8afa65b561b9a55c/coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282", size = 261576, upload-time = "2025-08-29T15:34:11.585Z" }, - { url = "https://files.pythonhosted.org/packages/72/d0/e1961eff67e9e1dba3fc5eb7a4caf726b35a5b03776892da8d79ec895775/coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4", size = 259341, upload-time = "2025-08-29T15:34:13.159Z" }, - { url = "https://files.pythonhosted.org/packages/3a/06/d6478d152cd189b33eac691cba27a40704990ba95de49771285f34a5861e/coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21", size = 260468, upload-time = "2025-08-29T15:34:14.571Z" }, - { url = "https://files.pythonhosted.org/packages/ed/73/737440247c914a332f0b47f7598535b29965bf305e19bbc22d4c39615d2b/coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0", size = 220429, upload-time = "2025-08-29T15:34:16.394Z" }, - { url = "https://files.pythonhosted.org/packages/bd/76/b92d3214740f2357ef4a27c75a526eb6c28f79c402e9f20a922c295c05e2/coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5", size = 221493, upload-time = "2025-08-29T15:34:17.835Z" }, - { url = "https://files.pythonhosted.org/packages/fc/8e/6dcb29c599c8a1f654ec6cb68d76644fe635513af16e932d2d4ad1e5ac6e/coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b", size = 219757, upload-time = "2025-08-29T15:34:19.248Z" }, - { url = "https://files.pythonhosted.org/packages/d3/aa/76cf0b5ec00619ef208da4689281d48b57f2c7fde883d14bf9441b74d59f/coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e", size = 217331, upload-time = "2025-08-29T15:34:20.846Z" }, - { url = "https://files.pythonhosted.org/packages/65/91/8e41b8c7c505d398d7730206f3cbb4a875a35ca1041efc518051bfce0f6b/coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb", size = 217607, upload-time = "2025-08-29T15:34:22.433Z" }, - { url = "https://files.pythonhosted.org/packages/87/7f/f718e732a423d442e6616580a951b8d1ec3575ea48bcd0e2228386805e79/coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034", size = 248663, upload-time = "2025-08-29T15:34:24.425Z" }, - { url = "https://files.pythonhosted.org/packages/e6/52/c1106120e6d801ac03e12b5285e971e758e925b6f82ee9b86db3aa10045d/coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1", size = 251197, upload-time = "2025-08-29T15:34:25.906Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ec/3a8645b1bb40e36acde9c0609f08942852a4af91a937fe2c129a38f2d3f5/coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a", size = 252551, upload-time = "2025-08-29T15:34:27.337Z" }, - { url = "https://files.pythonhosted.org/packages/a1/70/09ecb68eeb1155b28a1d16525fd3a9b65fbe75337311a99830df935d62b6/coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb", size = 250553, upload-time = "2025-08-29T15:34:29.065Z" }, - { url = "https://files.pythonhosted.org/packages/c6/80/47df374b893fa812e953b5bc93dcb1427a7b3d7a1a7d2db33043d17f74b9/coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d", size = 248486, upload-time = "2025-08-29T15:34:30.897Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/9f98640979ecee1b0d1a7164b589de720ddf8100d1747d9bbdb84be0c0fb/coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747", size = 249981, upload-time = "2025-08-29T15:34:32.365Z" }, - { url = "https://files.pythonhosted.org/packages/1f/55/eeb6603371e6629037f47bd25bef300387257ed53a3c5fdb159b7ac8c651/coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5", size = 220054, upload-time = "2025-08-29T15:34:34.124Z" }, - { url = "https://files.pythonhosted.org/packages/15/d1/a0912b7611bc35412e919a2cd59ae98e7ea3b475e562668040a43fb27897/coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713", size = 220851, upload-time = "2025-08-29T15:34:35.651Z" }, - { url = "https://files.pythonhosted.org/packages/ef/2d/11880bb8ef80a45338e0b3e0725e4c2d73ffbb4822c29d987078224fd6a5/coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32", size = 219429, upload-time = "2025-08-29T15:34:37.16Z" }, - { url = "https://files.pythonhosted.org/packages/83/c0/1f00caad775c03a700146f55536ecd097a881ff08d310a58b353a1421be0/coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65", size = 218080, upload-time = "2025-08-29T15:34:38.919Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c4/b1c5d2bd7cc412cbeb035e257fd06ed4e3e139ac871d16a07434e145d18d/coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6", size = 218293, upload-time = "2025-08-29T15:34:40.425Z" }, - { url = "https://files.pythonhosted.org/packages/3f/07/4468d37c94724bf6ec354e4ec2f205fda194343e3e85fd2e59cec57e6a54/coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0", size = 259800, upload-time = "2025-08-29T15:34:41.996Z" }, - { url = "https://files.pythonhosted.org/packages/82/d8/f8fb351be5fee31690cd8da768fd62f1cfab33c31d9f7baba6cd8960f6b8/coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e", size = 261965, upload-time = "2025-08-29T15:34:43.61Z" }, - { url = "https://files.pythonhosted.org/packages/e8/70/65d4d7cfc75c5c6eb2fed3ee5cdf420fd8ae09c4808723a89a81d5b1b9c3/coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5", size = 264220, upload-time = "2025-08-29T15:34:45.387Z" }, - { url = "https://files.pythonhosted.org/packages/98/3c/069df106d19024324cde10e4ec379fe2fb978017d25e97ebee23002fbadf/coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7", size = 261660, upload-time = "2025-08-29T15:34:47.288Z" }, - { url = "https://files.pythonhosted.org/packages/fc/8a/2974d53904080c5dc91af798b3a54a4ccb99a45595cc0dcec6eb9616a57d/coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5", size = 259417, upload-time = "2025-08-29T15:34:48.779Z" }, - { url = "https://files.pythonhosted.org/packages/30/38/9616a6b49c686394b318974d7f6e08f38b8af2270ce7488e879888d1e5db/coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0", size = 260567, upload-time = "2025-08-29T15:34:50.718Z" }, - { url = "https://files.pythonhosted.org/packages/76/16/3ed2d6312b371a8cf804abf4e14895b70e4c3491c6e53536d63fd0958a8d/coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7", size = 220831, upload-time = "2025-08-29T15:34:52.653Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e5/d38d0cb830abede2adb8b147770d2a3d0e7fecc7228245b9b1ae6c24930a/coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930", size = 221950, upload-time = "2025-08-29T15:34:54.212Z" }, - { url = "https://files.pythonhosted.org/packages/f4/51/e48e550f6279349895b0ffcd6d2a690e3131ba3a7f4eafccc141966d4dea/coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b", size = 219969, upload-time = "2025-08-29T15:34:55.83Z" }, - { url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" }, +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736, upload-time = "2025-08-29T15:35:16.668Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a8/1d/2e64b43d978b5bd184e0756a41415597dfef30fcbd90b747474bd749d45f/coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356", size = 217025, upload-time = "2025-08-29T15:32:57.169Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/23/62/b1e0f513417c02cc10ef735c3ee5186df55f190f70498b3702d516aad06f/coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301", size = 217419, upload-time = "2025-08-29T15:32:59.908Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e7/16/b800640b7a43e7c538429e4d7223e0a94fd72453a1a048f70bf766f12e96/coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460", size = 244180, upload-time = "2025-08-29T15:33:01.608Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fb/6f/5e03631c3305cad187eaf76af0b559fff88af9a0b0c180d006fb02413d7a/coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd", size = 245992, upload-time = "2025-08-29T15:33:03.239Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/eb/a1/f30ea0fb400b080730125b490771ec62b3375789f90af0bb68bfb8a921d7/coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb", size = 247851, upload-time = "2025-08-29T15:33:04.603Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/02/8e/cfa8fee8e8ef9a6bb76c7bef039f3302f44e615d2194161a21d3d83ac2e9/coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6", size = 245891, upload-time = "2025-08-29T15:33:06.176Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/93/a9/51be09b75c55c4f6c16d8d73a6a1d46ad764acca0eab48fa2ffaef5958fe/coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945", size = 243909, upload-time = "2025-08-29T15:33:07.74Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e9/a6/ba188b376529ce36483b2d585ca7bdac64aacbe5aa10da5978029a9c94db/coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e", size = 244786, upload-time = "2025-08-29T15:33:08.965Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d0/4c/37ed872374a21813e0d3215256180c9a382c3f5ced6f2e5da0102fc2fd3e/coverage-7.10.6-cp310-cp310-win32.whl", hash = "sha256:86b9b59f2b16e981906e9d6383eb6446d5b46c278460ae2c36487667717eccf1", size = 219521, upload-time = "2025-08-29T15:33:10.599Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8e/36/9311352fdc551dec5b973b61f4e453227ce482985a9368305880af4f85dd/coverage-7.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:e132b9152749bd33534e5bd8565c7576f135f157b4029b975e15ee184325f528", size = 220417, upload-time = "2025-08-29T15:33:11.907Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d4/16/2bea27e212c4980753d6d563a0803c150edeaaddb0771a50d2afc410a261/coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f", size = 217129, upload-time = "2025-08-29T15:33:13.575Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2a/51/e7159e068831ab37e31aac0969d47b8c5ee25b7d307b51e310ec34869315/coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc", size = 217532, upload-time = "2025-08-29T15:33:14.872Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e7/c0/246ccbea53d6099325d25cd208df94ea435cd55f0db38099dd721efc7a1f/coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a", size = 247931, upload-time = "2025-08-29T15:33:16.142Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7d/fb/7435ef8ab9b2594a6e3f58505cc30e98ae8b33265d844007737946c59389/coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a", size = 249864, upload-time = "2025-08-29T15:33:17.434Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/51/f8/d9d64e8da7bcddb094d511154824038833c81e3a039020a9d6539bf303e9/coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62", size = 251969, upload-time = "2025-08-29T15:33:18.822Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/43/28/c43ba0ef19f446d6463c751315140d8f2a521e04c3e79e5c5fe211bfa430/coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153", size = 249659, upload-time = "2025-08-29T15:33:20.407Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/79/3e/53635bd0b72beaacf265784508a0b386defc9ab7fad99ff95f79ce9db555/coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5", size = 247714, upload-time = "2025-08-29T15:33:21.751Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4c/55/0964aa87126624e8c159e32b0bc4e84edef78c89a1a4b924d28dd8265625/coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619", size = 248351, upload-time = "2025-08-29T15:33:23.105Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/eb/ab/6cfa9dc518c6c8e14a691c54e53a9433ba67336c760607e299bfcf520cb1/coverage-7.10.6-cp311-cp311-win32.whl", hash = "sha256:e3fb1fa01d3598002777dd259c0c2e6d9d5e10e7222976fc8e03992f972a2cba", size = 219562, upload-time = "2025-08-29T15:33:24.717Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5b/18/99b25346690cbc55922e7cfef06d755d4abee803ef335baff0014268eff4/coverage-7.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:f35ed9d945bece26553d5b4c8630453169672bea0050a564456eb88bdffd927e", size = 220453, upload-time = "2025-08-29T15:33:26.482Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d8/ed/81d86648a07ccb124a5cf1f1a7788712b8d7216b593562683cd5c9b0d2c1/coverage-7.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:99e1a305c7765631d74b98bf7dbf54eeea931f975e80f115437d23848ee8c27c", size = 219127, upload-time = "2025-08-29T15:33:27.777Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/26/06/263f3305c97ad78aab066d116b52250dd316e74fcc20c197b61e07eb391a/coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea", size = 217324, upload-time = "2025-08-29T15:33:29.06Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e9/60/1e1ded9a4fe80d843d7d53b3e395c1db3ff32d6c301e501f393b2e6c1c1f/coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634", size = 217560, upload-time = "2025-08-29T15:33:30.748Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b8/25/52136173c14e26dfed8b106ed725811bb53c30b896d04d28d74cb64318b3/coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6", size = 249053, upload-time = "2025-08-29T15:33:32.041Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cb/1d/ae25a7dc58fcce8b172d42ffe5313fc267afe61c97fa872b80ee72d9515a/coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9", size = 251802, upload-time = "2025-08-29T15:33:33.625Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f5/7a/1f561d47743710fe996957ed7c124b421320f150f1d38523d8d9102d3e2a/coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c", size = 252935, upload-time = "2025-08-29T15:33:34.909Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6c/ad/8b97cd5d28aecdfde792dcbf646bac141167a5cacae2cd775998b45fabb5/coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a", size = 250855, upload-time = "2025-08-29T15:33:36.922Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/33/6a/95c32b558d9a61858ff9d79580d3877df3eb5bc9eed0941b1f187c89e143/coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5", size = 248974, upload-time = "2025-08-29T15:33:38.175Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0d/9c/8ce95dee640a38e760d5b747c10913e7a06554704d60b41e73fdea6a1ffd/coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972", size = 250409, upload-time = "2025-08-29T15:33:39.447Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/04/12/7a55b0bdde78a98e2eb2356771fd2dcddb96579e8342bb52aa5bc52e96f0/coverage-7.10.6-cp312-cp312-win32.whl", hash = "sha256:a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d", size = 219724, upload-time = "2025-08-29T15:33:41.172Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/36/4a/32b185b8b8e327802c9efce3d3108d2fe2d9d31f153a0f7ecfd59c773705/coverage-7.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629", size = 220536, upload-time = "2025-08-29T15:33:42.524Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/08/3a/d5d8dc703e4998038c3099eaf77adddb00536a3cec08c8dcd556a36a3eb4/coverage-7.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80", size = 219171, upload-time = "2025-08-29T15:33:43.974Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bd/e7/917e5953ea29a28c1057729c1d5af9084ab6d9c66217523fd0e10f14d8f6/coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6", size = 217351, upload-time = "2025-08-29T15:33:45.438Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/eb/86/2e161b93a4f11d0ea93f9bebb6a53f113d5d6e416d7561ca41bb0a29996b/coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80", size = 217600, upload-time = "2025-08-29T15:33:47.269Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0e/66/d03348fdd8df262b3a7fb4ee5727e6e4936e39e2f3a842e803196946f200/coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003", size = 248600, upload-time = "2025-08-29T15:33:48.953Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27", size = 251206, upload-time = "2025-08-29T15:33:50.697Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e9/1f/9020135734184f439da85c70ea78194c2730e56c2d18aee6e8ff1719d50d/coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4", size = 252478, upload-time = "2025-08-29T15:33:52.303Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a4/a4/3d228f3942bb5a2051fde28c136eea23a761177dc4ff4ef54533164ce255/coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d", size = 250637, upload-time = "2025-08-29T15:33:53.67Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/36/e3/293dce8cdb9a83de971637afc59b7190faad60603b40e32635cbd15fbf61/coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc", size = 248529, upload-time = "2025-08-29T15:33:55.022Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/90/26/64eecfa214e80dd1d101e420cab2901827de0e49631d666543d0e53cf597/coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc", size = 250143, upload-time = "2025-08-29T15:33:56.386Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3e/70/bd80588338f65ea5b0d97e424b820fb4068b9cfb9597fbd91963086e004b/coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e", size = 219770, upload-time = "2025-08-29T15:33:58.063Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a7/14/0b831122305abcc1060c008f6c97bbdc0a913ab47d65070a01dc50293c2b/coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32", size = 220566, upload-time = "2025-08-29T15:33:59.766Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/83/c6/81a83778c1f83f1a4a168ed6673eeedc205afb562d8500175292ca64b94e/coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2", size = 219195, upload-time = "2025-08-29T15:34:01.191Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d7/1c/ccccf4bf116f9517275fa85047495515add43e41dfe8e0bef6e333c6b344/coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b", size = 218059, upload-time = "2025-08-29T15:34:02.91Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/92/97/8a3ceff833d27c7492af4f39d5da6761e9ff624831db9e9f25b3886ddbca/coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393", size = 218287, upload-time = "2025-08-29T15:34:05.106Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/92/d8/50b4a32580cf41ff0423777a2791aaf3269ab60c840b62009aec12d3970d/coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27", size = 259625, upload-time = "2025-08-29T15:34:06.575Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7e/7e/6a7df5a6fb440a0179d94a348eb6616ed4745e7df26bf2a02bc4db72c421/coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df", size = 261801, upload-time = "2025-08-29T15:34:08.006Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3a/4c/a270a414f4ed5d196b9d3d67922968e768cd971d1b251e1b4f75e9362f75/coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb", size = 264027, upload-time = "2025-08-29T15:34:09.806Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9c/8b/3210d663d594926c12f373c5370bf1e7c5c3a427519a8afa65b561b9a55c/coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282", size = 261576, upload-time = "2025-08-29T15:34:11.585Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/72/d0/e1961eff67e9e1dba3fc5eb7a4caf726b35a5b03776892da8d79ec895775/coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4", size = 259341, upload-time = "2025-08-29T15:34:13.159Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3a/06/d6478d152cd189b33eac691cba27a40704990ba95de49771285f34a5861e/coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21", size = 260468, upload-time = "2025-08-29T15:34:14.571Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ed/73/737440247c914a332f0b47f7598535b29965bf305e19bbc22d4c39615d2b/coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0", size = 220429, upload-time = "2025-08-29T15:34:16.394Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bd/76/b92d3214740f2357ef4a27c75a526eb6c28f79c402e9f20a922c295c05e2/coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5", size = 221493, upload-time = "2025-08-29T15:34:17.835Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fc/8e/6dcb29c599c8a1f654ec6cb68d76644fe635513af16e932d2d4ad1e5ac6e/coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b", size = 219757, upload-time = "2025-08-29T15:34:19.248Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d3/aa/76cf0b5ec00619ef208da4689281d48b57f2c7fde883d14bf9441b74d59f/coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e", size = 217331, upload-time = "2025-08-29T15:34:20.846Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/65/91/8e41b8c7c505d398d7730206f3cbb4a875a35ca1041efc518051bfce0f6b/coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb", size = 217607, upload-time = "2025-08-29T15:34:22.433Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/87/7f/f718e732a423d442e6616580a951b8d1ec3575ea48bcd0e2228386805e79/coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034", size = 248663, upload-time = "2025-08-29T15:34:24.425Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e6/52/c1106120e6d801ac03e12b5285e971e758e925b6f82ee9b86db3aa10045d/coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1", size = 251197, upload-time = "2025-08-29T15:34:25.906Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3d/ec/3a8645b1bb40e36acde9c0609f08942852a4af91a937fe2c129a38f2d3f5/coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a", size = 252551, upload-time = "2025-08-29T15:34:27.337Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a1/70/09ecb68eeb1155b28a1d16525fd3a9b65fbe75337311a99830df935d62b6/coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb", size = 250553, upload-time = "2025-08-29T15:34:29.065Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c6/80/47df374b893fa812e953b5bc93dcb1427a7b3d7a1a7d2db33043d17f74b9/coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d", size = 248486, upload-time = "2025-08-29T15:34:30.897Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4a/65/9f98640979ecee1b0d1a7164b589de720ddf8100d1747d9bbdb84be0c0fb/coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747", size = 249981, upload-time = "2025-08-29T15:34:32.365Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1f/55/eeb6603371e6629037f47bd25bef300387257ed53a3c5fdb159b7ac8c651/coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5", size = 220054, upload-time = "2025-08-29T15:34:34.124Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/15/d1/a0912b7611bc35412e919a2cd59ae98e7ea3b475e562668040a43fb27897/coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713", size = 220851, upload-time = "2025-08-29T15:34:35.651Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ef/2d/11880bb8ef80a45338e0b3e0725e4c2d73ffbb4822c29d987078224fd6a5/coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32", size = 219429, upload-time = "2025-08-29T15:34:37.16Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/83/c0/1f00caad775c03a700146f55536ecd097a881ff08d310a58b353a1421be0/coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65", size = 218080, upload-time = "2025-08-29T15:34:38.919Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a9/c4/b1c5d2bd7cc412cbeb035e257fd06ed4e3e139ac871d16a07434e145d18d/coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6", size = 218293, upload-time = "2025-08-29T15:34:40.425Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3f/07/4468d37c94724bf6ec354e4ec2f205fda194343e3e85fd2e59cec57e6a54/coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0", size = 259800, upload-time = "2025-08-29T15:34:41.996Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/82/d8/f8fb351be5fee31690cd8da768fd62f1cfab33c31d9f7baba6cd8960f6b8/coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e", size = 261965, upload-time = "2025-08-29T15:34:43.61Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e8/70/65d4d7cfc75c5c6eb2fed3ee5cdf420fd8ae09c4808723a89a81d5b1b9c3/coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5", size = 264220, upload-time = "2025-08-29T15:34:45.387Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/98/3c/069df106d19024324cde10e4ec379fe2fb978017d25e97ebee23002fbadf/coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7", size = 261660, upload-time = "2025-08-29T15:34:47.288Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fc/8a/2974d53904080c5dc91af798b3a54a4ccb99a45595cc0dcec6eb9616a57d/coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5", size = 259417, upload-time = "2025-08-29T15:34:48.779Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/30/38/9616a6b49c686394b318974d7f6e08f38b8af2270ce7488e879888d1e5db/coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0", size = 260567, upload-time = "2025-08-29T15:34:50.718Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/76/16/3ed2d6312b371a8cf804abf4e14895b70e4c3491c6e53536d63fd0958a8d/coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7", size = 220831, upload-time = "2025-08-29T15:34:52.653Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d5/e5/d38d0cb830abede2adb8b147770d2a3d0e7fecc7228245b9b1ae6c24930a/coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930", size = 221950, upload-time = "2025-08-29T15:34:54.212Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f4/51/e48e550f6279349895b0ffcd6d2a690e3131ba3a7f4eafccc141966d4dea/coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b", size = 219969, upload-time = "2025-08-29T15:34:55.83Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" }, ] [package.optional-dependencies] @@ -386,259 +341,155 @@ toml = [ [[package]] name = "cryptography" version = "45.0.7" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/35/c495bffc2056f2dadb32434f1feedd79abde2a7f8363e1974afa9c33c7e2/cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971", size = 744980, upload-time = "2025-09-01T11:15:03.146Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/91/925c0ac74362172ae4516000fe877912e33b5983df735ff290c653de4913/cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee", size = 7041105, upload-time = "2025-09-01T11:13:59.684Z" }, - { url = "https://files.pythonhosted.org/packages/fc/63/43641c5acce3a6105cf8bd5baeceeb1846bb63067d26dae3e5db59f1513a/cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6", size = 4205799, upload-time = "2025-09-01T11:14:02.517Z" }, - { url = "https://files.pythonhosted.org/packages/bc/29/c238dd9107f10bfde09a4d1c52fd38828b1aa353ced11f358b5dd2507d24/cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339", size = 4430504, upload-time = "2025-09-01T11:14:04.522Z" }, - { url = "https://files.pythonhosted.org/packages/62/62/24203e7cbcc9bd7c94739428cd30680b18ae6b18377ae66075c8e4771b1b/cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8", size = 4209542, upload-time = "2025-09-01T11:14:06.309Z" }, - { url = "https://files.pythonhosted.org/packages/cd/e3/e7de4771a08620eef2389b86cd87a2c50326827dea5528feb70595439ce4/cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf", size = 3889244, upload-time = "2025-09-01T11:14:08.152Z" }, - { url = "https://files.pythonhosted.org/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513", size = 4461975, upload-time = "2025-09-01T11:14:09.755Z" }, - { url = "https://files.pythonhosted.org/packages/58/67/3f5b26937fe1218c40e95ef4ff8d23c8dc05aa950d54200cc7ea5fb58d28/cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3", size = 4209082, upload-time = "2025-09-01T11:14:11.229Z" }, - { url = "https://files.pythonhosted.org/packages/0e/e4/b3e68a4ac363406a56cf7b741eeb80d05284d8c60ee1a55cdc7587e2a553/cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3", size = 4460397, upload-time = "2025-09-01T11:14:12.924Z" }, - { url = "https://files.pythonhosted.org/packages/22/49/2c93f3cd4e3efc8cb22b02678c1fad691cff9dd71bb889e030d100acbfe0/cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6", size = 4337244, upload-time = "2025-09-01T11:14:14.431Z" }, - { url = "https://files.pythonhosted.org/packages/04/19/030f400de0bccccc09aa262706d90f2ec23d56bc4eb4f4e8268d0ddf3fb8/cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd", size = 4568862, upload-time = "2025-09-01T11:14:16.185Z" }, - { url = "https://files.pythonhosted.org/packages/29/56/3034a3a353efa65116fa20eb3c990a8c9f0d3db4085429040a7eef9ada5f/cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8", size = 2936578, upload-time = "2025-09-01T11:14:17.638Z" }, - { url = "https://files.pythonhosted.org/packages/b3/61/0ab90f421c6194705a99d0fa9f6ee2045d916e4455fdbb095a9c2c9a520f/cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443", size = 3405400, upload-time = "2025-09-01T11:14:18.958Z" }, - { url = "https://files.pythonhosted.org/packages/63/e8/c436233ddf19c5f15b25ace33979a9dd2e7aa1a59209a0ee8554179f1cc0/cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2", size = 7021824, upload-time = "2025-09-01T11:14:20.954Z" }, - { url = "https://files.pythonhosted.org/packages/bc/4c/8f57f2500d0ccd2675c5d0cc462095adf3faa8c52294ba085c036befb901/cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691", size = 4202233, upload-time = "2025-09-01T11:14:22.454Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ac/59b7790b4ccaed739fc44775ce4645c9b8ce54cbec53edf16c74fd80cb2b/cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59", size = 4423075, upload-time = "2025-09-01T11:14:24.287Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/d4f07ea21434bf891faa088a6ac15d6d98093a66e75e30ad08e88aa2b9ba/cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4", size = 4204517, upload-time = "2025-09-01T11:14:25.679Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ac/924a723299848b4c741c1059752c7cfe09473b6fd77d2920398fc26bfb53/cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3", size = 3882893, upload-time = "2025-09-01T11:14:27.1Z" }, - { url = "https://files.pythonhosted.org/packages/83/dc/4dab2ff0a871cc2d81d3ae6d780991c0192b259c35e4d83fe1de18b20c70/cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1", size = 4450132, upload-time = "2025-09-01T11:14:28.58Z" }, - { url = "https://files.pythonhosted.org/packages/12/dd/b2882b65db8fc944585d7fb00d67cf84a9cef4e77d9ba8f69082e911d0de/cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27", size = 4204086, upload-time = "2025-09-01T11:14:30.572Z" }, - { url = "https://files.pythonhosted.org/packages/5d/fa/1d5745d878048699b8eb87c984d4ccc5da4f5008dfd3ad7a94040caca23a/cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17", size = 4449383, upload-time = "2025-09-01T11:14:32.046Z" }, - { url = "https://files.pythonhosted.org/packages/36/8b/fc61f87931bc030598e1876c45b936867bb72777eac693e905ab89832670/cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b", size = 4332186, upload-time = "2025-09-01T11:14:33.95Z" }, - { url = "https://files.pythonhosted.org/packages/0b/11/09700ddad7443ccb11d674efdbe9a832b4455dc1f16566d9bd3834922ce5/cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c", size = 4561639, upload-time = "2025-09-01T11:14:35.343Z" }, - { url = "https://files.pythonhosted.org/packages/71/ed/8f4c1337e9d3b94d8e50ae0b08ad0304a5709d483bfcadfcc77a23dbcb52/cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5", size = 2926552, upload-time = "2025-09-01T11:14:36.929Z" }, - { url = "https://files.pythonhosted.org/packages/bc/ff/026513ecad58dacd45d1d24ebe52b852165a26e287177de1d545325c0c25/cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90", size = 3392742, upload-time = "2025-09-01T11:14:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/13/3e/e42f1528ca1ea82256b835191eab1be014e0f9f934b60d98b0be8a38ed70/cryptography-45.0.7-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:de58755d723e86175756f463f2f0bddd45cc36fbd62601228a3f8761c9f58252", size = 3572442, upload-time = "2025-09-01T11:14:39.836Z" }, - { url = "https://files.pythonhosted.org/packages/59/aa/e947693ab08674a2663ed2534cd8d345cf17bf6a1facf99273e8ec8986dc/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083", size = 4142233, upload-time = "2025-09-01T11:14:41.305Z" }, - { url = "https://files.pythonhosted.org/packages/24/06/09b6f6a2fc43474a32b8fe259038eef1500ee3d3c141599b57ac6c57612c/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130", size = 4376202, upload-time = "2025-09-01T11:14:43.047Z" }, - { url = "https://files.pythonhosted.org/packages/00/f2/c166af87e95ce6ae6d38471a7e039d3a0549c2d55d74e059680162052824/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4", size = 4141900, upload-time = "2025-09-01T11:14:45.089Z" }, - { url = "https://files.pythonhosted.org/packages/16/b9/e96e0b6cb86eae27ea51fa8a3151535a18e66fe7c451fa90f7f89c85f541/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141", size = 4375562, upload-time = "2025-09-01T11:14:47.166Z" }, - { url = "https://files.pythonhosted.org/packages/36/d0/36e8ee39274e9d77baf7d0dafda680cba6e52f3936b846f0d56d64fec915/cryptography-45.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c13b1e3afd29a5b3b2656257f14669ca8fa8d7956d509926f0b130b600b50ab7", size = 3322781, upload-time = "2025-09-01T11:14:48.747Z" }, - { url = "https://files.pythonhosted.org/packages/99/4e/49199a4c82946938a3e05d2e8ad9482484ba48bbc1e809e3d506c686d051/cryptography-45.0.7-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a862753b36620af6fc54209264f92c716367f2f0ff4624952276a6bbd18cbde", size = 3584634, upload-time = "2025-09-01T11:14:50.593Z" }, - { url = "https://files.pythonhosted.org/packages/16/ce/5f6ff59ea9c7779dba51b84871c19962529bdcc12e1a6ea172664916c550/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34", size = 4149533, upload-time = "2025-09-01T11:14:52.091Z" }, - { url = "https://files.pythonhosted.org/packages/ce/13/b3cfbd257ac96da4b88b46372e662009b7a16833bfc5da33bb97dd5631ae/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9", size = 4385557, upload-time = "2025-09-01T11:14:53.551Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c5/8c59d6b7c7b439ba4fc8d0cab868027fd095f215031bc123c3a070962912/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae", size = 4149023, upload-time = "2025-09-01T11:14:55.022Z" }, - { url = "https://files.pythonhosted.org/packages/55/32/05385c86d6ca9ab0b4d5bb442d2e3d85e727939a11f3e163fc776ce5eb40/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b", size = 4385722, upload-time = "2025-09-01T11:14:57.319Z" }, - { url = "https://files.pythonhosted.org/packages/23/87/7ce86f3fa14bc11a5a48c30d8103c26e09b6465f8d8e9d74cf7a0714f043/cryptography-45.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63", size = 3332908, upload-time = "2025-09-01T11:14:58.78Z" }, +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a7/35/c495bffc2056f2dadb32434f1feedd79abde2a7f8363e1974afa9c33c7e2/cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971", size = 744980, upload-time = "2025-09-01T11:15:03.146Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0c/91/925c0ac74362172ae4516000fe877912e33b5983df735ff290c653de4913/cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee", size = 7041105, upload-time = "2025-09-01T11:13:59.684Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fc/63/43641c5acce3a6105cf8bd5baeceeb1846bb63067d26dae3e5db59f1513a/cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6", size = 4205799, upload-time = "2025-09-01T11:14:02.517Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bc/29/c238dd9107f10bfde09a4d1c52fd38828b1aa353ced11f358b5dd2507d24/cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339", size = 4430504, upload-time = "2025-09-01T11:14:04.522Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/62/62/24203e7cbcc9bd7c94739428cd30680b18ae6b18377ae66075c8e4771b1b/cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8", size = 4209542, upload-time = "2025-09-01T11:14:06.309Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cd/e3/e7de4771a08620eef2389b86cd87a2c50326827dea5528feb70595439ce4/cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf", size = 3889244, upload-time = "2025-09-01T11:14:08.152Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513", size = 4461975, upload-time = "2025-09-01T11:14:09.755Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/58/67/3f5b26937fe1218c40e95ef4ff8d23c8dc05aa950d54200cc7ea5fb58d28/cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3", size = 4209082, upload-time = "2025-09-01T11:14:11.229Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0e/e4/b3e68a4ac363406a56cf7b741eeb80d05284d8c60ee1a55cdc7587e2a553/cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3", size = 4460397, upload-time = "2025-09-01T11:14:12.924Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/22/49/2c93f3cd4e3efc8cb22b02678c1fad691cff9dd71bb889e030d100acbfe0/cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6", size = 4337244, upload-time = "2025-09-01T11:14:14.431Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/04/19/030f400de0bccccc09aa262706d90f2ec23d56bc4eb4f4e8268d0ddf3fb8/cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd", size = 4568862, upload-time = "2025-09-01T11:14:16.185Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/29/56/3034a3a353efa65116fa20eb3c990a8c9f0d3db4085429040a7eef9ada5f/cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8", size = 2936578, upload-time = "2025-09-01T11:14:17.638Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b3/61/0ab90f421c6194705a99d0fa9f6ee2045d916e4455fdbb095a9c2c9a520f/cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443", size = 3405400, upload-time = "2025-09-01T11:14:18.958Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/63/e8/c436233ddf19c5f15b25ace33979a9dd2e7aa1a59209a0ee8554179f1cc0/cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2", size = 7021824, upload-time = "2025-09-01T11:14:20.954Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bc/4c/8f57f2500d0ccd2675c5d0cc462095adf3faa8c52294ba085c036befb901/cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691", size = 4202233, upload-time = "2025-09-01T11:14:22.454Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/eb/ac/59b7790b4ccaed739fc44775ce4645c9b8ce54cbec53edf16c74fd80cb2b/cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59", size = 4423075, upload-time = "2025-09-01T11:14:24.287Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b8/56/d4f07ea21434bf891faa088a6ac15d6d98093a66e75e30ad08e88aa2b9ba/cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4", size = 4204517, upload-time = "2025-09-01T11:14:25.679Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e8/ac/924a723299848b4c741c1059752c7cfe09473b6fd77d2920398fc26bfb53/cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3", size = 3882893, upload-time = "2025-09-01T11:14:27.1Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/83/dc/4dab2ff0a871cc2d81d3ae6d780991c0192b259c35e4d83fe1de18b20c70/cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1", size = 4450132, upload-time = "2025-09-01T11:14:28.58Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/12/dd/b2882b65db8fc944585d7fb00d67cf84a9cef4e77d9ba8f69082e911d0de/cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27", size = 4204086, upload-time = "2025-09-01T11:14:30.572Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5d/fa/1d5745d878048699b8eb87c984d4ccc5da4f5008dfd3ad7a94040caca23a/cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17", size = 4449383, upload-time = "2025-09-01T11:14:32.046Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/36/8b/fc61f87931bc030598e1876c45b936867bb72777eac693e905ab89832670/cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b", size = 4332186, upload-time = "2025-09-01T11:14:33.95Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0b/11/09700ddad7443ccb11d674efdbe9a832b4455dc1f16566d9bd3834922ce5/cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c", size = 4561639, upload-time = "2025-09-01T11:14:35.343Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/71/ed/8f4c1337e9d3b94d8e50ae0b08ad0304a5709d483bfcadfcc77a23dbcb52/cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5", size = 2926552, upload-time = "2025-09-01T11:14:36.929Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bc/ff/026513ecad58dacd45d1d24ebe52b852165a26e287177de1d545325c0c25/cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90", size = 3392742, upload-time = "2025-09-01T11:14:38.368Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/13/3e/e42f1528ca1ea82256b835191eab1be014e0f9f934b60d98b0be8a38ed70/cryptography-45.0.7-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:de58755d723e86175756f463f2f0bddd45cc36fbd62601228a3f8761c9f58252", size = 3572442, upload-time = "2025-09-01T11:14:39.836Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/59/aa/e947693ab08674a2663ed2534cd8d345cf17bf6a1facf99273e8ec8986dc/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083", size = 4142233, upload-time = "2025-09-01T11:14:41.305Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/24/06/09b6f6a2fc43474a32b8fe259038eef1500ee3d3c141599b57ac6c57612c/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130", size = 4376202, upload-time = "2025-09-01T11:14:43.047Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/00/f2/c166af87e95ce6ae6d38471a7e039d3a0549c2d55d74e059680162052824/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4", size = 4141900, upload-time = "2025-09-01T11:14:45.089Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/16/b9/e96e0b6cb86eae27ea51fa8a3151535a18e66fe7c451fa90f7f89c85f541/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141", size = 4375562, upload-time = "2025-09-01T11:14:47.166Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/36/d0/36e8ee39274e9d77baf7d0dafda680cba6e52f3936b846f0d56d64fec915/cryptography-45.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c13b1e3afd29a5b3b2656257f14669ca8fa8d7956d509926f0b130b600b50ab7", size = 3322781, upload-time = "2025-09-01T11:14:48.747Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/99/4e/49199a4c82946938a3e05d2e8ad9482484ba48bbc1e809e3d506c686d051/cryptography-45.0.7-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a862753b36620af6fc54209264f92c716367f2f0ff4624952276a6bbd18cbde", size = 3584634, upload-time = "2025-09-01T11:14:50.593Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/16/ce/5f6ff59ea9c7779dba51b84871c19962529bdcc12e1a6ea172664916c550/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34", size = 4149533, upload-time = "2025-09-01T11:14:52.091Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ce/13/b3cfbd257ac96da4b88b46372e662009b7a16833bfc5da33bb97dd5631ae/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9", size = 4385557, upload-time = "2025-09-01T11:14:53.551Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1c/c5/8c59d6b7c7b439ba4fc8d0cab868027fd095f215031bc123c3a070962912/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae", size = 4149023, upload-time = "2025-09-01T11:14:55.022Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/55/32/05385c86d6ca9ab0b4d5bb442d2e3d85e727939a11f3e163fc776ce5eb40/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b", size = 4385722, upload-time = "2025-09-01T11:14:57.319Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/23/87/7ce86f3fa14bc11a5a48c30d8103c26e09b6465f8d8e9d74cf7a0714f043/cryptography-45.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63", size = 3332908, upload-time = "2025-09-01T11:14:58.78Z" }, ] [[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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "distro" version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] [[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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -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" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, -] - -[[package]] -name = "gevent" -version = "25.8.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation == 'CPython' and sys_platform == 'win32'" }, - { name = "greenlet", marker = "platform_python_implementation == 'CPython'" }, - { name = "zope-event" }, - { name = "zope-interface" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/65/79/4f3fcc8cf79a22f21eaa3d8d9d0a12d82edd6f9715e2e1fd5ffe6e4bc1cc/gevent-25.8.2.tar.gz", hash = "sha256:0cfab118ad5dcc55d7847dd9dccd560d9015fe671f42714b6f1ac97e3b2b9a3a", size = 6422843, upload-time = "2025-08-29T17:01:29.146Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/c6/5e9dea78f689faad001dcd05c5d164895c780b5ff2115cee1ce5562adf77/gevent-25.8.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:af63a3f93f08c05aa8f4b94396686c2367c2f732adfbef69bc7032dbe7e30544", size = 1830415, upload-time = "2025-08-29T17:09:22.719Z" }, - { url = "https://files.pythonhosted.org/packages/a1/d4/795d10bdae939e232be655bb1387118d88709785e1aad19f429e8087fc8e/gevent-25.8.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fd8c8ef0647910745b2c478313398d118e3aefb5c2e2676e24bfcaa0973aa14b", size = 1917002, upload-time = "2025-08-29T17:10:35.123Z" }, - { url = "https://files.pythonhosted.org/packages/3e/0e/a8b2dd4edc8ab875ae87eb6474e4ddcd226740747c40eb10f2fc927cc5f8/gevent-25.8.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:717c2e90a267a7b6c74a9a6e432ee86c34651313180a85f551019385fdf1cefd", size = 1867785, upload-time = "2025-08-29T17:17:30.784Z" }, - { url = "https://files.pythonhosted.org/packages/48/44/d71d6a95dfd334f494ab158f296a4330576c0e71f8a31909288f9184634e/gevent-25.8.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16b1688220ca33e0f37351b7ab03235fb65973e32336efd356abda0c770318c4", size = 2175569, upload-time = "2025-08-29T16:44:56.73Z" }, - { url = "https://files.pythonhosted.org/packages/74/81/648e85c39319afdb25ee3bbd8289ccd01057408786b59646d95a18b73520/gevent-25.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fccea073cffb0204d93e5dfadbc059cf73cf97c2563d251a1157d5e3c0bbed10", size = 1846024, upload-time = "2025-08-29T17:21:21.088Z" }, - { url = "https://files.pythonhosted.org/packages/3e/c6/65ce8a22272734fbe84b4299cbf0bfbd89f089612d21d1483458093808c8/gevent-25.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:edb9faf781408d9a00741d8b85cde051d04f3c2fdd71dd106ef7db255aae4e15", size = 2232532, upload-time = "2025-08-29T16:51:00.099Z" }, - { url = "https://files.pythonhosted.org/packages/05/2b/3728462e9d98ffce3cd0f8f92fa0e017f80a7762950e6f2f0c050d60a18f/gevent-25.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:f401539fb28a47f8608443d1051dbb0f09ac5c49f6b604f7bdf4d98fc1e7094b", size = 1679554, upload-time = "2025-08-29T19:04:40.161Z" }, - { url = "https://files.pythonhosted.org/packages/1c/0b/ea7376dabb0065889285404d519c732fc31bd8688ffaa6c70d75c7a27970/gevent-25.8.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b6d143b3facc930a90e263a4faebe8776f5e8493055022a493ec9de8a259690", size = 1790891, upload-time = "2025-08-29T17:09:25.254Z" }, - { url = "https://files.pythonhosted.org/packages/e5/82/6c9e04c6f4d5a954776b6b9ca4eacebbab751483f6a4dcd0d618ed194f2f/gevent-25.8.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:128f68c7ddb4ce55760557549ce8db0ea2f7818a963a148e7a2f22fb6ee34645", size = 1874098, upload-time = "2025-08-29T17:10:36.983Z" }, - { url = "https://files.pythonhosted.org/packages/a6/46/bc9648c9c8ff92b54f319337b829bbdddae598db446e26adb2699c172eca/gevent-25.8.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d6d250ad0382135ad8bfb9cfd0c2a33a3dfd251a05657f652152e0c9084d1ddd", size = 1829800, upload-time = "2025-08-29T17:17:32.051Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a6/7fdf82ef067912e6d111dfa7ea542955cd009ce8b598ad760ae2f2acb209/gevent-25.8.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:59107c7a452c7a70388b350f9840e6f5761a61e7c9750c48ad10066f29ac9a56", size = 2120221, upload-time = "2025-08-29T16:44:59.325Z" }, - { url = "https://files.pythonhosted.org/packages/53/5f/325c2b02c28d670f1ec11ffa42423ee73d3fb9e52d2b52505017e5d52499/gevent-25.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c4bd94b59b68058157b1632ae3488124d569b7067ad458c654cd4fb7dd2d5ade", size = 1807361, upload-time = "2025-08-29T17:21:23.522Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f4/cca38c75c229eafdc0e8fbea6a98cd3854767db73235ab65f4ab89818f81/gevent-25.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dead93501a7d7973294708b73d7e47b13c16f001a317094b2068730f9ab2ad39", size = 2177473, upload-time = "2025-08-29T16:51:02.672Z" }, - { url = "https://files.pythonhosted.org/packages/77/ce/4b467f6696ab4e8d894c69d6be2b2303413d46d6a4c19fa9039805c81c50/gevent-25.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:f99f2587f3b99f4ddc3446cee2ef4b8dea75ea536994377881a2f50a31f43dd9", size = 1661818, upload-time = "2025-08-29T18:54:08.764Z" }, - { url = "https://files.pythonhosted.org/packages/59/58/85d0d4e1b9021b9eef0416708b152b3df0b31189d34f1c19aa219f2015f3/gevent-25.8.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:0a8cf26b0852bd31761b5ac35485520657dc22eeded2a867d4fe97bc341bd338", size = 2953067, upload-time = "2025-08-29T16:17:25.284Z" }, - { url = "https://files.pythonhosted.org/packages/47/72/420d9884eb8e6cc40675fdc739f67e3c5688b60ce7aa7a05d4485362ab69/gevent-25.8.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36cd3bb6184c44b959c918a13d33a2f533de6d3eccf03a9ea3b90a1639082c3a", size = 1806754, upload-time = "2025-08-29T17:09:26.841Z" }, - { url = "https://files.pythonhosted.org/packages/6b/7b/ce6f691fd57aa1be040d96b99ca14aa0cb96a3a9c790e9d05efcd50d6531/gevent-25.8.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fa2dd9474eda6e990ec52b3ea74e0cb8713f1ef98fbaedcbcdcdef783cd7f863", size = 1887793, upload-time = "2025-08-29T17:10:39.122Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9f/73eeb6d0c3afa13c34ee4d3bb31c288d0e7db5aaffefeb4d38ea55023b23/gevent-25.8.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2c3018f3e89443e7086c905cd6da69c249d48070cc0f8d5357fdc325a9641bd2", size = 1853085, upload-time = "2025-08-29T17:17:33.531Z" }, - { url = "https://files.pythonhosted.org/packages/99/5a/4fa732550334a5d426204fbc6c551f4e2be056d64007bafc788fad646209/gevent-25.8.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ef20a651139f281fa014e26bb1642b17aa46d0f7afa29b024ffb195249644925", size = 2107051, upload-time = "2025-08-29T16:45:00.575Z" }, - { url = "https://files.pythonhosted.org/packages/e0/b6/0627da55247a686e74973ed243432d3091606ad6b0a95341080dee423525/gevent-25.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3358fc5bf0b960533d0ac2839fa9458fe5f6288b816b48ce7677b5e92434dad8", size = 1825463, upload-time = "2025-08-29T17:21:25.064Z" }, - { url = "https://files.pythonhosted.org/packages/6a/89/4ba9f5fd6a6a180306447351f4cb4b194aba2b1ea46c24c877d5ad1efea5/gevent-25.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b3c613e128f805b85fa03d54470a57e37592d2206e95c9553bcfd6b517863003", size = 2170419, upload-time = "2025-08-29T16:51:04.581Z" }, - { url = "https://files.pythonhosted.org/packages/6c/95/ab3ad5e942c5c9c08e3fa803fa7d600d959abe7ab054b1497d52fc3e72e8/gevent-25.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:647bbbeeddd69bfdbd3df1a7c864b4727bfb09e7f461f47293e3d98ded7d74d4", size = 1665470, upload-time = "2025-08-29T18:44:53.101Z" }, - { url = "https://files.pythonhosted.org/packages/be/8f/c0253d7cfe1d28cd5c00bea318832ec91f5270205dc579428e4c612aa71a/gevent-25.8.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a30373121527feff642a7528bf674eb7b1d3cb343d6b2b3eb42e7ea76288d8f0", size = 2970872, upload-time = "2025-08-29T16:18:00.81Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b6/ab76e03c97b260bb84634c7c6439a2b7c5552c060f777814e7675ab45a83/gevent-25.8.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c70ee82f417d1c94db3eb3fd2d14bfa6995a61b88f66005a69a169ad1df55b59", size = 1807295, upload-time = "2025-08-29T17:09:28.414Z" }, - { url = "https://files.pythonhosted.org/packages/3c/90/e918c22133726907ebdb104b5ddc1d4c4e859a035acf42d1d9e977a5c436/gevent-25.8.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6a1399407299c710f5fe9348cda3f16e6d6fd6e592098d8bb678d58393b5457d", size = 1888756, upload-time = "2025-08-29T17:10:41.055Z" }, - { url = "https://files.pythonhosted.org/packages/62/be/060509577f8c1f1262b4ca67a298ac3a63ad5e9117d8409a0adc37264372/gevent-25.8.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6513e872a365acb407c7a13e11112bbfe8b9d6f0cc138182b50ce9f4074a1f2f", size = 1855064, upload-time = "2025-08-29T17:17:34.767Z" }, - { url = "https://files.pythonhosted.org/packages/85/43/2c0a6c3e347ad702b4bd1cdd1d43ac329f0a820f30b6f47ff5b48fafe405/gevent-25.8.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7d586a041f13b661dbe25137d6659fe246b1b454d0c5c29be26a3d8d4ed96cf", size = 2109585, upload-time = "2025-08-29T16:45:01.851Z" }, - { url = "https://files.pythonhosted.org/packages/df/67/96454c79579a4970e053e6cd2ccc86579ea84c1bfa2ea11ae271ee5425e2/gevent-25.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07717cb293684185bd5190b876d897dd8ed11d6eb707264a0541bc36a6b06ba3", size = 1827172, upload-time = "2025-08-29T17:21:26.382Z" }, - { url = "https://files.pythonhosted.org/packages/ad/a6/cc29497c6520009296c980d7f5007ce64395d66833b93baeb7127ccd9d5a/gevent-25.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:14e3ece38dcb24209a5b05dcc7ba5544514943bb7841bc5152c395c69e7ffbdc", size = 2171887, upload-time = "2025-08-29T16:51:06.839Z" }, - { url = "https://files.pythonhosted.org/packages/4f/3e/81793a2cab226a46524113bd8ecda711c6bb87efbd324bb7b709eb9ace90/gevent-25.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:791fb76294b79d5b0271b1be088d5dfb37506b7a8f4315ff939cae32b46be905", size = 1664350, upload-time = "2025-08-29T18:33:39.639Z" }, - { url = "https://files.pythonhosted.org/packages/e3/98/69cec7441638ddd1afa9a6f44fe0ebf12cbbd412463e82d68f6cc44bef44/gevent-25.8.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:2776417181b27466b562bc47af5d20a55e2ccd4deec06a96f37e58317e1c8525", size = 2980136, upload-time = "2025-08-29T16:17:32.035Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ff/b685f71849ab29be8eda39da06299b2fdaf96a50fceefff760a282647a82/gevent-25.8.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b795ea9727d873387483df4ffd31137c4859746c9c789238db483a28bfa16de", size = 1812326, upload-time = "2025-08-29T17:09:30.187Z" }, - { url = "https://files.pythonhosted.org/packages/86/7c/d1d291f17af4d3f811208f977f1a3e1d1178c5eb9f34337ae26ec61959e8/gevent-25.8.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:98a4302cb64c3fbc1f7472b051ac79e68b3d41f9d37a08a0fc6058461079dedf", size = 1893012, upload-time = "2025-08-29T17:10:42.415Z" }, - { url = "https://files.pythonhosted.org/packages/31/67/b6d85443ecbfecc5ba2edd07986ac98ae721ddc29736740a777fcb8dd934/gevent-25.8.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c4a99ac674d8c13459e03dcb5550a4a3639a9bd1dcda12a630ffdd1fae8d9f8", size = 1859957, upload-time = "2025-08-29T17:17:35.964Z" }, - { url = "https://files.pythonhosted.org/packages/57/f5/5bb45f208238e5528c523eff2b0b5a2ed59089e0eeff0c325c011d2f4992/gevent-25.8.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:398dc7adbcd8cf1fcb2dc8dc48aafc355790bff842c906a28b9f995bcc061bd2", size = 2111374, upload-time = "2025-08-29T16:45:03.156Z" }, - { url = "https://files.pythonhosted.org/packages/49/91/fd53ec0617c869df78cb7bb9a85741483df56288b23ac1fc4a3b5f165b34/gevent-25.8.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3535a699771f35b460026077dec946e73e4744afb5661b4e05a78eea74c120ce", size = 1831945, upload-time = "2025-08-29T17:21:29.356Z" }, - { url = "https://files.pythonhosted.org/packages/92/7d/46b4d5a8c9b37d66f16d7cd4f52e60e026a58b3a9cdedb4f93581082fb2b/gevent-25.8.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23383d4e50718750f44cabdf1f8d4905108da0ffc6a8bf85448fb0a68f0e1b4d", size = 2174857, upload-time = "2025-08-29T16:51:08.342Z" }, - { url = "https://files.pythonhosted.org/packages/37/ce/18ff1db74d5654318a3a3176fda984317d3d9856a91ef0c1da56324e7104/gevent-25.8.2-cp314-cp314-win_amd64.whl", hash = "sha256:28a60e5f1174ac568639ab3bd12be9d620478a35acf0ad3156ef117918b4f551", size = 1686254, upload-time = "2025-08-29T18:23:41.138Z" }, - { url = "https://files.pythonhosted.org/packages/f5/6a/69a825f0fea9da709395d873171ab86fd2ac382c37ab8b9fdd2180bd9a1b/gevent-25.8.2-pp310-pypy310_pp73-macosx_11_0_universal2.whl", hash = "sha256:cf365361ab40435e3c28c7335cdeb7f6f2b9a8ce6eaced01addbba1c83e4f08f", size = 1325712, upload-time = "2025-08-29T16:16:56.987Z" }, -] - -[[package]] -name = "greenlet" -version = "3.2.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/ed/6bfa4109fcb23a58819600392564fea69cdc6551ffd5e69ccf1d52a40cbc/greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", size = 271061, upload-time = "2025-08-07T13:17:15.373Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fc/102ec1a2fc015b3a7652abab7acf3541d58c04d3d17a8d3d6a44adae1eb1/greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", size = 629475, upload-time = "2025-08-07T13:42:54.009Z" }, - { url = "https://files.pythonhosted.org/packages/c5/26/80383131d55a4ac0fb08d71660fd77e7660b9db6bdb4e8884f46d9f2cc04/greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", size = 640802, upload-time = "2025-08-07T13:45:25.52Z" }, - { url = "https://files.pythonhosted.org/packages/9f/7c/e7833dbcd8f376f3326bd728c845d31dcde4c84268d3921afcae77d90d08/greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", size = 636703, upload-time = "2025-08-07T13:53:12.622Z" }, - { url = "https://files.pythonhosted.org/packages/e9/49/547b93b7c0428ede7b3f309bc965986874759f7d89e4e04aeddbc9699acb/greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", size = 635417, upload-time = "2025-08-07T13:18:25.189Z" }, - { url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" }, - { url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" }, - { url = "https://files.pythonhosted.org/packages/a1/8d/88f3ebd2bc96bf7747093696f4335a0a8a4c5acfcf1b757717c0d2474ba3/greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", size = 1137126, upload-time = "2025-08-07T13:18:20.239Z" }, - { url = "https://files.pythonhosted.org/packages/d6/6f/b60b0291d9623c496638c582297ead61f43c4b72eef5e9c926ef4565ec13/greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", size = 298654, upload-time = "2025-08-07T13:50:00.469Z" }, - { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, - { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" }, - { url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" }, - { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, - { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, - { url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" }, - { url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" }, - { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, - { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, - { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, - { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, - { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, - { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, - { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, - { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, - { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, - { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, - { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, - { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, - { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, - { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, - { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, - { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, - { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, - { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, - { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, - { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] [[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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "ipdb" version = "0.13.13" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "decorator" }, - { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ipython", version = "8.37.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.5.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.11'" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/1b/7e07e7b752017f7693a0f4d41c13e5ca29ce8cbcfdcc1fd6c4ad8c0a27a0/ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726", size = 17042, upload-time = "2023-03-09T15:40:57.487Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3d/1b/7e07e7b752017f7693a0f4d41c13e5ca29ce8cbcfdcc1fd6c4ad8c0a27a0/ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726", size = 17042, upload-time = "2023-03-09T15:40:57.487Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/4c/b075da0092003d9a55cf2ecc1cae9384a1ca4f650d51b00fc59875fe76f6/ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4", size = 12130, upload-time = "2023-03-09T15:40:55.021Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0c/4c/b075da0092003d9a55cf2ecc1cae9384a1ca4f650d51b00fc59875fe76f6/ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4", size = 12130, upload-time = "2023-03-09T15:40:55.021Z" }, ] [[package]] name = "ipdbplugin" version = "1.5.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ - { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ipython", version = "8.37.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.5.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.11'" }, { name = "nose" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cf/cb/510dcb9ae401e6876415412f0cc7bd2df8f7e9eb6667c23a2bd941309b88/ipdbplugin-1.5.0.tar.gz", hash = "sha256:cdcd6bc1e995c3c2c4971ed95f207e680aa44980b716fa43fb675ff2dcc7894f", size = 2872, upload-time = "2018-01-11T15:48:01.028Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cf/cb/510dcb9ae401e6876415412f0cc7bd2df8f7e9eb6667c23a2bd941309b88/ipdbplugin-1.5.0.tar.gz", hash = "sha256:cdcd6bc1e995c3c2c4971ed95f207e680aa44980b716fa43fb675ff2dcc7894f", size = 2872, upload-time = "2018-01-11T15:48:01.028Z" } [[package]] name = "ipython" version = "8.37.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } resolution-markers = [ "python_full_version < '3.11'", ] @@ -655,15 +506,15 @@ dependencies = [ { name = "traitlets", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size = 5606088, upload-time = "2025-05-31T16:39:09.613Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size = 5606088, upload-time = "2025-05-31T16:39:09.613Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/d0/274fbf7b0b12643cbbc001ce13e6a5b1607ac4929d1b11c72460152c9fc3/ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2", size = 831864, upload-time = "2025-05-31T16:39:06.38Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/91/d0/274fbf7b0b12643cbbc001ce13e6a5b1607ac4929d1b11c72460152c9fc3/ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2", size = 831864, upload-time = "2025-05-31T16:39:06.38Z" }, ] [[package]] name = "ipython" version = "9.5.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } resolution-markers = [ "python_full_version >= '3.14' and platform_python_implementation != 'PyPy'", "python_full_version == '3.13.*' and platform_python_implementation != 'PyPy'", @@ -685,208 +536,208 @@ dependencies = [ { name = "traitlets", marker = "python_full_version >= '3.11'" }, { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/71/a86262bf5a68bf211bcc71fe302af7e05f18a2852fdc610a854d20d085e6/ipython-9.5.0.tar.gz", hash = "sha256:129c44b941fe6d9b82d36fc7a7c18127ddb1d6f02f78f867f402e2e3adde3113", size = 4389137, upload-time = "2025-08-29T12:15:21.519Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6e/71/a86262bf5a68bf211bcc71fe302af7e05f18a2852fdc610a854d20d085e6/ipython-9.5.0.tar.gz", hash = "sha256:129c44b941fe6d9b82d36fc7a7c18127ddb1d6f02f78f867f402e2e3adde3113", size = 4389137, upload-time = "2025-08-29T12:15:21.519Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/2a/5628a99d04acb2d2f2e749cdf4ea571d2575e898df0528a090948018b726/ipython-9.5.0-py3-none-any.whl", hash = "sha256:88369ffa1d5817d609120daa523a6da06d02518e582347c29f8451732a9c5e72", size = 612426, upload-time = "2025-08-29T12:15:18.866Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/08/2a/5628a99d04acb2d2f2e749cdf4ea571d2575e898df0528a090948018b726/ipython-9.5.0-py3-none-any.whl", hash = "sha256:88369ffa1d5817d609120daa523a6da06d02518e582347c29f8451732a9c5e72", size = 612426, upload-time = "2025-08-29T12:15:18.866Z" }, ] [[package]] name = "ipython-pygments-lexers" version = "1.1.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "pygments", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, ] [[package]] name = "jedi" version = "0.19.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "parso" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, ] [[package]] name = "jinja2" version = "3.1.6" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "markdown-it-py" version = "3.0.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "mdurl", marker = "python_full_version >= '3.13'" }, ] -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" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, ] [[package]] name = "matplotlib-inline" version = "0.1.7" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, ] [[package]] name = "mdit-py-plugins" version = "0.5.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "markdown-it-py", marker = "python_full_version >= '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, ] [[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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "mypy" version = "1.17.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "pathspec" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/a9/3d7aa83955617cdf02f94e50aab5c830d205cfa4320cf124ff64acce3a8e/mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972", size = 11003299, upload-time = "2025-07-31T07:54:06.425Z" }, - { url = "https://files.pythonhosted.org/packages/83/e8/72e62ff837dd5caaac2b4a5c07ce769c8e808a00a65e5d8f94ea9c6f20ab/mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7", size = 10125451, upload-time = "2025-07-31T07:53:52.974Z" }, - { url = "https://files.pythonhosted.org/packages/7d/10/f3f3543f6448db11881776f26a0ed079865926b0c841818ee22de2c6bbab/mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df", size = 11916211, upload-time = "2025-07-31T07:53:18.879Z" }, - { url = "https://files.pythonhosted.org/packages/06/bf/63e83ed551282d67bb3f7fea2cd5561b08d2bb6eb287c096539feb5ddbc5/mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390", size = 12652687, upload-time = "2025-07-31T07:53:30.544Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/68f2eeef11facf597143e85b694a161868b3b006a5fbad50e09ea117ef24/mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94", size = 12896322, upload-time = "2025-07-31T07:53:50.74Z" }, - { url = "https://files.pythonhosted.org/packages/a3/87/8e3e9c2c8bd0d7e071a89c71be28ad088aaecbadf0454f46a540bda7bca6/mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b", size = 9507962, upload-time = "2025-07-31T07:53:08.431Z" }, - { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" }, - { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" }, - { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" }, - { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" }, - { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" }, - { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" }, - { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" }, - { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" }, - { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" }, - { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" }, - { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" }, - { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" }, - { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" }, - { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" }, - { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" }, - { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" }, - { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" }, - { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" }, - { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" }, - { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" }, - { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" }, - { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/77/a9/3d7aa83955617cdf02f94e50aab5c830d205cfa4320cf124ff64acce3a8e/mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972", size = 11003299, upload-time = "2025-07-31T07:54:06.425Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/83/e8/72e62ff837dd5caaac2b4a5c07ce769c8e808a00a65e5d8f94ea9c6f20ab/mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7", size = 10125451, upload-time = "2025-07-31T07:53:52.974Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7d/10/f3f3543f6448db11881776f26a0ed079865926b0c841818ee22de2c6bbab/mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df", size = 11916211, upload-time = "2025-07-31T07:53:18.879Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/06/bf/63e83ed551282d67bb3f7fea2cd5561b08d2bb6eb287c096539feb5ddbc5/mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390", size = 12652687, upload-time = "2025-07-31T07:53:30.544Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/69/66/68f2eeef11facf597143e85b694a161868b3b006a5fbad50e09ea117ef24/mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94", size = 12896322, upload-time = "2025-07-31T07:53:50.74Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a3/87/8e3e9c2c8bd0d7e071a89c71be28ad088aaecbadf0454f46a540bda7bca6/mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b", size = 9507962, upload-time = "2025-07-31T07:53:08.431Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, ] [[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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "myst-parser" version = "4.0.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "docutils", marker = "python_full_version >= '3.13'" }, { name = "jinja2", marker = "python_full_version >= '3.13'" }, @@ -895,140 +746,126 @@ dependencies = [ { name = "pyyaml", marker = "python_full_version >= '3.13'" }, { name = "sphinx", marker = "python_full_version >= '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" }, ] [[package]] name = "nose" version = "1.3.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/a5/0dc93c3ec33f4e281849523a5a913fa1eea9a3068acfa754d44d88107a44/nose-1.3.7.tar.gz", hash = "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98", size = 280488, upload-time = "2015-06-02T09:12:32.961Z" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/58/a5/0dc93c3ec33f4e281849523a5a913fa1eea9a3068acfa754d44d88107a44/nose-1.3.7.tar.gz", hash = "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98", size = 280488, upload-time = "2015-06-02T09:12:32.961Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/d8/dd071918c040f50fa1cf80da16423af51ff8ce4a0f2399b7bf8de45ac3d9/nose-1.3.7-py3-none-any.whl", hash = "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", size = 154731, upload-time = "2015-06-02T09:12:40.57Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/15/d8/dd071918c040f50fa1cf80da16423af51ff8ce4a0f2399b7bf8de45ac3d9/nose-1.3.7-py3-none-any.whl", hash = "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", size = 154731, upload-time = "2015-06-02T09:12:40.57Z" }, ] [[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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "paramiko" -version = "3.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "bcrypt" }, - { name = "cryptography" }, - { name = "pynacl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7d/15/ad6ce226e8138315f2451c2aeea985bf35ee910afb477bae7477dc3a8f3b/paramiko-3.5.1.tar.gz", hash = "sha256:b2c665bc45b2b215bd7d7f039901b14b067da00f3a11e6640995fd58f2664822", size = 1566110, upload-time = "2025-02-04T02:37:59.783Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/f8/c7bd0ef12954a81a1d3cea60a13946bd9a49a0036a5927770c461eade7ae/paramiko-3.5.1-py3-none-any.whl", hash = "sha256:43b9a0501fc2b5e70680388d9346cf252cfb7d00b0667c39e80eb43a408b8f61", size = 227298, upload-time = "2025-02-04T02:37:57.672Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "parso" version = "0.8.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, ] [[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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "pexpect" version = "4.9.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "ptyprocess" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523" }, ] [[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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "prompt-toolkit" version = "3.0.52" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] [[package]] name = "ptyprocess" version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, ] [[package]] name = "pure-eval" version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] [[package]] name = "pycparser" version = "2.23" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, ] [[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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "pyinfra" +version = "4.0.0" source = { editable = "." } dependencies = [ + { name = "asyncssh" }, { name = "click" }, { name = "distro" }, - { name = "gevent" }, { name = "jinja2" }, { name = "packaging" }, - { name = "paramiko" }, { name = "python-dateutil" }, { name = "typeguard" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, @@ -1040,8 +877,8 @@ dev = [ { name = "coverage" }, { name = "ipdb" }, { name = "ipdbplugin" }, - { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ipython", version = "8.37.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.5.0", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }, marker = "python_full_version >= '3.11'" }, { name = "mypy" }, { name = "myst-parser", marker = "python_full_version >= '3.13'" }, { name = "pyinfra-guzzle-sphinx-theme", marker = "python_full_version >= '3.13'" }, @@ -1054,7 +891,6 @@ dev = [ { name = "ruff" }, { name = "sphinx", marker = "python_full_version >= '3.13'" }, { name = "types-cryptography" }, - { name = "types-paramiko" }, { name = "types-python-dateutil" }, { name = "types-pyyaml" }, { name = "typos" }, @@ -1075,19 +911,17 @@ test = [ { name = "pyyaml" }, { name = "ruff" }, { name = "types-cryptography" }, - { name = "types-paramiko" }, { name = "types-python-dateutil" }, { name = "types-pyyaml" }, ] [package.metadata] requires-dist = [ + { name = "asyncssh", specifier = ">=2.13" }, { name = "click", specifier = ">2" }, { name = "distro", specifier = ">=1.6,<2" }, - { name = "gevent", specifier = ">=1.5" }, { name = "jinja2", specifier = ">3,<4" }, { name = "packaging", specifier = ">=16.1" }, - { name = "paramiko", specifier = ">=2.7,<4" }, { name = "python-dateutil", specifier = ">2,<3" }, { name = "typeguard", specifier = ">=4,<5" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, @@ -1112,7 +946,6 @@ dev = [ { name = "ruff", specifier = ">=0.13.1" }, { name = "sphinx", marker = "python_full_version >= '3.13'", specifier = "==8.2.3" }, { name = "types-cryptography", specifier = ">=3.3.23.2,<4" }, - { name = "types-paramiko", specifier = ">=2.7,<4" }, { name = "types-python-dateutil", specifier = ">2,<3" }, { name = "types-pyyaml", specifier = ">6,<7" }, { name = "typos", specifier = ">=1.36.2" }, @@ -1133,7 +966,6 @@ test = [ { name = "pyyaml", specifier = ">=6.0.2,<7" }, { name = "ruff", specifier = ">=0.13.1" }, { name = "types-cryptography", specifier = ">=3.3.23.2,<4" }, - { name = "types-paramiko", specifier = ">=2.7,<4" }, { name = "types-python-dateutil", specifier = ">2,<3" }, { name = "types-pyyaml", specifier = ">6,<7" }, ] @@ -1141,65 +973,28 @@ test = [ [[package]] name = "pyinfra-guzzle-sphinx-theme" version = "0.18" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "sphinx", marker = "python_full_version >= '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/32/7e8e0b9cfcbfa82dfa84fdca8acec100e64ef5730d441e5d65a590c091a5/pyinfra_guzzle_sphinx_theme-0.18.tar.gz", hash = "sha256:a26f5abce4fb4a296b57e22f8f900dde1438decc48ca28ad0273fa7784bf2f5e", size = 301076, upload-time = "2025-09-17T13:47:06.053Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5b/32/7e8e0b9cfcbfa82dfa84fdca8acec100e64ef5730d441e5d65a590c091a5/pyinfra_guzzle_sphinx_theme-0.18.tar.gz", hash = "sha256:a26f5abce4fb4a296b57e22f8f900dde1438decc48ca28ad0273fa7784bf2f5e", size = 301076, upload-time = "2025-09-17T13:47:06.053Z" } [[package]] name = "pyinfra-testgen" version = "0.1.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/ad/4d7ae773502ceae3ac3a7f4ea6232f865e28a9766931db2af25ab369449c/pyinfra_testgen-0.1.1.tar.gz", hash = "sha256:739a59d127d15c2e74bc965f9e77f41d080899fee18b6a10e7f5ca3153739697", size = 1065, upload-time = "2025-09-01T20:57:39.844Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/36/ad/4d7ae773502ceae3ac3a7f4ea6232f865e28a9766931db2af25ab369449c/pyinfra_testgen-0.1.1.tar.gz", hash = "sha256:739a59d127d15c2e74bc965f9e77f41d080899fee18b6a10e7f5ca3153739697", size = 1065, upload-time = "2025-09-01T20:57:39.844Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/9a/edea57e31074b1a504b1b5b20180410971b393669e5388e773a4e49f4563/pyinfra_testgen-0.1.1-py3-none-any.whl", hash = "sha256:23d71cbe666df7a0b12a000a5cbdaade932620faced2c89bffc8a066153c9303", size = 1820, upload-time = "2025-09-01T20:57:38.749Z" }, -] - -[[package]] -name = "pynacl" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/c6/a3124dee667a423f2c637cfd262a54d67d8ccf3e160f3c50f622a85b7723/pynacl-1.6.0.tar.gz", hash = "sha256:cb36deafe6e2bce3b286e5d1f3e1c246e0ccdb8808ddb4550bb2792f2df298f2", size = 3505641, upload-time = "2025-09-10T23:39:22.308Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/24/1b639176401255605ba7c2b93a7b1eb1e379e0710eca62613633eb204201/pynacl-1.6.0-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:f46386c24a65383a9081d68e9c2de909b1834ec74ff3013271f1bca9c2d233eb", size = 384141, upload-time = "2025-09-10T23:38:28.675Z" }, - { url = "https://files.pythonhosted.org/packages/5e/7b/874efdf57d6bf172db0df111b479a553c3d9e8bb4f1f69eb3ffff772d6e8/pynacl-1.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:dea103a1afcbc333bc0e992e64233d360d393d1e63d0bc88554f572365664348", size = 808132, upload-time = "2025-09-10T23:38:38.995Z" }, - { url = "https://files.pythonhosted.org/packages/f3/61/9b53f5913f3b75ac3d53170cdb897101b2b98afc76f4d9d3c8de5aa3ac05/pynacl-1.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:04f20784083014e265ad58c1b2dd562c3e35864b5394a14ab54f5d150ee9e53e", size = 1407253, upload-time = "2025-09-10T23:38:40.492Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0a/b138916b22bbf03a1bdbafecec37d714e7489dd7bcaf80cd17852f8b67be/pynacl-1.6.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbcc4452a1eb10cd5217318c822fde4be279c9de8567f78bad24c773c21254f8", size = 843719, upload-time = "2025-09-10T23:38:30.87Z" }, - { url = "https://files.pythonhosted.org/packages/01/3b/17c368197dfb2c817ce033f94605a47d0cc27901542109e640cef263f0af/pynacl-1.6.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fed9fe1bec9e7ff9af31cd0abba179d0e984a2960c77e8e5292c7e9b7f7b5d", size = 1445441, upload-time = "2025-09-10T23:38:33.078Z" }, - { url = "https://files.pythonhosted.org/packages/35/3c/f79b185365ab9be80cd3cd01dacf30bf5895f9b7b001e683b369e0bb6d3d/pynacl-1.6.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:10d755cf2a455d8c0f8c767a43d68f24d163b8fe93ccfaabfa7bafd26be58d73", size = 825691, upload-time = "2025-09-10T23:38:34.832Z" }, - { url = "https://files.pythonhosted.org/packages/f7/1f/8b37d25e95b8f2a434a19499a601d4d272b9839ab8c32f6b0fc1e40c383f/pynacl-1.6.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:536703b8f90e911294831a7fbcd0c062b837f3ccaa923d92a6254e11178aaf42", size = 1410726, upload-time = "2025-09-10T23:38:36.893Z" }, - { url = "https://files.pythonhosted.org/packages/bd/93/5a4a4cf9913014f83d615ad6a2df9187330f764f606246b3a744c0788c03/pynacl-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6b08eab48c9669d515a344fb0ef27e2cbde847721e34bba94a343baa0f33f1f4", size = 801035, upload-time = "2025-09-10T23:38:42.109Z" }, - { url = "https://files.pythonhosted.org/packages/bf/60/40da6b0fe6a4d5fd88f608389eb1df06492ba2edca93fca0b3bebff9b948/pynacl-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5789f016e08e5606803161ba24de01b5a345d24590a80323379fc4408832d290", size = 1371854, upload-time = "2025-09-10T23:38:44.16Z" }, - { url = "https://files.pythonhosted.org/packages/44/b2/37ac1d65008f824cba6b5bf68d18b76d97d0f62d7a032367ea69d4a187c8/pynacl-1.6.0-cp314-cp314t-win32.whl", hash = "sha256:4853c154dc16ea12f8f3ee4b7e763331876316cc3a9f06aeedf39bcdca8f9995", size = 230345, upload-time = "2025-09-10T23:38:48.276Z" }, - { url = "https://files.pythonhosted.org/packages/f4/5a/9234b7b45af890d02ebee9aae41859b9b5f15fb4a5a56d88e3b4d1659834/pynacl-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:347dcddce0b4d83ed3f32fd00379c83c425abee5a9d2cd0a2c84871334eaff64", size = 243103, upload-time = "2025-09-10T23:38:45.503Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2c/c1a0f19d720ab0af3bc4241af2bdf4d813c3ecdcb96392b5e1ddf2d8f24f/pynacl-1.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2d6cd56ce4998cb66a6c112fda7b1fdce5266c9f05044fa72972613bef376d15", size = 187778, upload-time = "2025-09-10T23:38:46.731Z" }, - { url = "https://files.pythonhosted.org/packages/63/37/87c72df19857c5b3b47ace6f211a26eb862ada495cc96daa372d96048fca/pynacl-1.6.0-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:f4b3824920e206b4f52abd7de621ea7a44fd3cb5c8daceb7c3612345dfc54f2e", size = 382610, upload-time = "2025-09-10T23:38:49.459Z" }, - { url = "https://files.pythonhosted.org/packages/0c/64/3ce958a5817fd3cc6df4ec14441c43fd9854405668d73babccf77f9597a3/pynacl-1.6.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:16dd347cdc8ae0b0f6187a2608c0af1c8b7ecbbe6b4a06bff8253c192f696990", size = 798744, upload-time = "2025-09-10T23:38:58.531Z" }, - { url = "https://files.pythonhosted.org/packages/e4/8a/3f0dd297a0a33fa3739c255feebd0206bb1df0b44c52fbe2caf8e8bc4425/pynacl-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16c60daceee88d04f8d41d0a4004a7ed8d9a5126b997efd2933e08e93a3bd850", size = 1397879, upload-time = "2025-09-10T23:39:00.44Z" }, - { url = "https://files.pythonhosted.org/packages/41/94/028ff0434a69448f61348d50d2c147dda51aabdd4fbc93ec61343332174d/pynacl-1.6.0-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25720bad35dfac34a2bcdd61d9e08d6bfc6041bebc7751d9c9f2446cf1e77d64", size = 833907, upload-time = "2025-09-10T23:38:50.936Z" }, - { url = "https://files.pythonhosted.org/packages/52/bc/a5cff7f8c30d5f4c26a07dfb0bcda1176ab8b2de86dda3106c00a02ad787/pynacl-1.6.0-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bfaa0a28a1ab718bad6239979a5a57a8d1506d0caf2fba17e524dbb409441cf", size = 1436649, upload-time = "2025-09-10T23:38:52.783Z" }, - { url = "https://files.pythonhosted.org/packages/7a/20/c397be374fd5d84295046e398de4ba5f0722dc14450f65db76a43c121471/pynacl-1.6.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ef214b90556bb46a485b7da8258e59204c244b1b5b576fb71848819b468c44a7", size = 817142, upload-time = "2025-09-10T23:38:54.4Z" }, - { url = "https://files.pythonhosted.org/packages/12/30/5efcef3406940cda75296c6d884090b8a9aad2dcc0c304daebb5ae99fb4a/pynacl-1.6.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:49c336dd80ea54780bcff6a03ee1a476be1612423010472e60af83452aa0f442", size = 1401794, upload-time = "2025-09-10T23:38:56.614Z" }, - { url = "https://files.pythonhosted.org/packages/be/e1/a8fe1248cc17ccb03b676d80fa90763760a6d1247da434844ea388d0816c/pynacl-1.6.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f3482abf0f9815e7246d461fab597aa179b7524628a4bc36f86a7dc418d2608d", size = 772161, upload-time = "2025-09-10T23:39:01.93Z" }, - { url = "https://files.pythonhosted.org/packages/a3/76/8a62702fb657d6d9104ce13449db221a345665d05e6a3fdefb5a7cafd2ad/pynacl-1.6.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:140373378e34a1f6977e573033d1dd1de88d2a5d90ec6958c9485b2fd9f3eb90", size = 1370720, upload-time = "2025-09-10T23:39:03.531Z" }, - { url = "https://files.pythonhosted.org/packages/6d/38/9e9e9b777a1c4c8204053733e1a0269672c0bd40852908c9ad6b6eaba82c/pynacl-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6b393bc5e5a0eb86bb85b533deb2d2c815666665f840a09e0aa3362bb6088736", size = 791252, upload-time = "2025-09-10T23:39:05.058Z" }, - { url = "https://files.pythonhosted.org/packages/63/ef/d972ce3d92ae05c9091363cf185e8646933f91c376e97b8be79ea6e96c22/pynacl-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a25cfede801f01e54179b8ff9514bd7b5944da560b7040939732d1804d25419", size = 1362910, upload-time = "2025-09-10T23:39:06.924Z" }, - { url = "https://files.pythonhosted.org/packages/35/2c/ee0b373a1861f66a7ca8bdb999331525615061320dd628527a50ba8e8a60/pynacl-1.6.0-cp38-abi3-win32.whl", hash = "sha256:dcdeb41c22ff3c66eef5e63049abf7639e0db4edee57ba70531fc1b6b133185d", size = 226461, upload-time = "2025-09-10T23:39:11.894Z" }, - { url = "https://files.pythonhosted.org/packages/75/f7/41b6c0b9dd9970173b6acc026bab7b4c187e4e5beef2756d419ad65482da/pynacl-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:cf831615cc16ba324240de79d925eacae8265b7691412ac6b24221db157f6bd1", size = 238802, upload-time = "2025-09-10T23:39:08.966Z" }, - { url = "https://files.pythonhosted.org/packages/8e/0f/462326910c6172fa2c6ed07922b22ffc8e77432b3affffd9e18f444dbfbb/pynacl-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:84709cea8f888e618c21ed9a0efdb1a59cc63141c403db8bf56c469b71ad56f2", size = 183846, upload-time = "2025-09-10T23:39:10.552Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/65/9a/edea57e31074b1a504b1b5b20180410971b393669e5388e773a4e49f4563/pyinfra_testgen-0.1.1-py3-none-any.whl", hash = "sha256:23d71cbe666df7a0b12a000a5cbdaade932620faced2c89bffc8a066153c9303", size = 1820, upload-time = "2025-09-01T20:57:38.749Z" }, ] [[package]] name = "pytest" version = "8.3.5" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, @@ -1208,198 +1003,189 @@ dependencies = [ { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, ] [[package]] name = "pytest-cov" version = "7.0.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] [[package]] name = "pytest-testinfra" version = "10.2.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/15/b650133bb3eb3509704c7605348bec4205b6a399f7a79874ced6c30a3e15/pytest_testinfra-10.2.2.tar.gz", hash = "sha256:537fd5eb88da618c1f461248aa20594cf8d44512e8519b239837e83875e1e9cd", size = 76153, upload-time = "2025-03-30T09:06:02.224Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/33/15/b650133bb3eb3509704c7605348bec4205b6a399f7a79874ced6c30a3e15/pytest_testinfra-10.2.2.tar.gz", hash = "sha256:537fd5eb88da618c1f461248aa20594cf8d44512e8519b239837e83875e1e9cd", size = 76153, upload-time = "2025-03-30T09:06:02.224Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/75/8b002ad110ae4a71bce9bb47020ee77176e27defe99ce6d7b62e52debeb9/pytest_testinfra-10.2.2-py3-none-any.whl", hash = "sha256:b785602b0aa868c858e4ef121a8cc0d13a81c04b74ec0364d70969540f8e7c31", size = 76912, upload-time = "2025-03-30T09:06:00.358Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1c/75/8b002ad110ae4a71bce9bb47020ee77176e27defe99ce6d7b62e52debeb9/pytest_testinfra-10.2.2-py3-none-any.whl", hash = "sha256:b785602b0aa868c858e4ef121a8cc0d13a81c04b74ec0364d70969540f8e7c31", size = 76912, upload-time = "2025-03-30T09:06:00.358Z" }, ] [[package]] name = "python-dateutil" version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "pyyaml" version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] [[package]] name = "redbaron" version = "0.9.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "baron" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/14/d04d376cca5108e62edeee2d2fc0261af6474d9aebe0b4334721785bc035/redbaron-0.9.2.tar.gz", hash = "sha256:472d0739ca6b2240bb2278ae428604a75472c9c12e86c6321e8c016139c0132f", size = 709401, upload-time = "2019-03-17T18:58:59.232Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/92/14/d04d376cca5108e62edeee2d2fc0261af6474d9aebe0b4334721785bc035/redbaron-0.9.2.tar.gz", hash = "sha256:472d0739ca6b2240bb2278ae428604a75472c9c12e86c6321e8c016139c0132f", size = 709401, upload-time = "2019-03-17T18:58:59.232Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/06/c1c97efe5d30593337721923c5813b3b4eaffcffb706e523acf3d3bc9e8c/redbaron-0.9.2-py2.py3-none-any.whl", hash = "sha256:d01032b6a848b5521a8d6ef72486315c2880f420956870cdd742e2b5a09b9bab", size = 34893, upload-time = "2019-03-17T18:59:01.529Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d8/06/c1c97efe5d30593337721923c5813b3b4eaffcffb706e523acf3d3bc9e8c/redbaron-0.9.2-py2.py3-none-any.whl", hash = "sha256:d01032b6a848b5521a8d6ef72486315c2880f420956870cdd742e2b5a09b9bab", size = 34893, upload-time = "2019-03-17T18:59:01.529Z" }, ] [[package]] name = "requests" version = "2.32.5" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "certifi", marker = "python_full_version >= '3.13'" }, { name = "charset-normalizer", marker = "python_full_version >= '3.13'" }, { name = "idna", marker = "python_full_version >= '3.13'" }, { name = "urllib3", marker = "python_full_version >= '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "rply" version = "0.7.8" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "appdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/29/44/96b3e8e6426b1f21f90d73cff83a6df94ef8a57ce8102db6c582d0cb3b2e/rply-0.7.8.tar.gz", hash = "sha256:2a808ac25a4580a9991fc304d64434e299a8fc75760574492f242cbb5bb301c9", size = 15850, upload-time = "2021-01-27T21:14:29.594Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/29/44/96b3e8e6426b1f21f90d73cff83a6df94ef8a57ce8102db6c582d0cb3b2e/rply-0.7.8.tar.gz", hash = "sha256:2a808ac25a4580a9991fc304d64434e299a8fc75760574492f242cbb5bb301c9", size = 15850, upload-time = "2021-01-27T21:14:29.594Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/7c/f66be9e75485ae6901ae77d8bdbc3c0e99ca748ab927b3e18205759bde09/rply-0.7.8-py2.py3-none-any.whl", hash = "sha256:28ffd11d656c48aeb8c508eb382acd6a0bd906662624b34388751732a27807e7", size = 16039, upload-time = "2021-01-27T21:14:27.946Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c0/7c/f66be9e75485ae6901ae77d8bdbc3c0e99ca748ab927b3e18205759bde09/rply-0.7.8-py2.py3-none-any.whl", hash = "sha256:28ffd11d656c48aeb8c508eb382acd6a0bd906662624b34388751732a27807e7", size = 16039, upload-time = "2021-01-27T21:14:27.946Z" }, ] [[package]] name = "ruff" version = "0.13.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ab/33/c8e89216845615d14d2d42ba2bee404e7206a8db782f33400754f3799f05/ruff-0.13.1.tar.gz", hash = "sha256:88074c3849087f153d4bb22e92243ad4c1b366d7055f98726bc19aa08dc12d51", size = 5397987, upload-time = "2025-09-18T19:52:44.33Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/41/ca37e340938f45cfb8557a97a5c347e718ef34702546b174e5300dbb1f28/ruff-0.13.1-py3-none-linux_armv6l.whl", hash = "sha256:b2abff595cc3cbfa55e509d89439b5a09a6ee3c252d92020bd2de240836cf45b", size = 12304308, upload-time = "2025-09-18T19:51:56.253Z" }, - { url = "https://files.pythonhosted.org/packages/ff/84/ba378ef4129415066c3e1c80d84e539a0d52feb250685091f874804f28af/ruff-0.13.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4ee9f4249bf7f8bb3984c41bfaf6a658162cdb1b22e3103eabc7dd1dc5579334", size = 12937258, upload-time = "2025-09-18T19:52:00.184Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b6/ec5e4559ae0ad955515c176910d6d7c93edcbc0ed1a3195a41179c58431d/ruff-0.13.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c5da4af5f6418c07d75e6f3224e08147441f5d1eac2e6ce10dcce5e616a3bae", size = 12214554, upload-time = "2025-09-18T19:52:02.753Z" }, - { url = "https://files.pythonhosted.org/packages/70/d6/cb3e3b4f03b9b0c4d4d8f06126d34b3394f6b4d764912fe80a1300696ef6/ruff-0.13.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80524f84a01355a59a93cef98d804e2137639823bcee2931f5028e71134a954e", size = 12448181, upload-time = "2025-09-18T19:52:05.279Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ea/bf60cb46d7ade706a246cd3fb99e4cfe854efa3dfbe530d049c684da24ff/ruff-0.13.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff7f5ce8d7988767dd46a148192a14d0f48d1baea733f055d9064875c7d50389", size = 12104599, upload-time = "2025-09-18T19:52:07.497Z" }, - { url = "https://files.pythonhosted.org/packages/2d/3e/05f72f4c3d3a69e65d55a13e1dd1ade76c106d8546e7e54501d31f1dc54a/ruff-0.13.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c55d84715061f8b05469cdc9a446aa6c7294cd4bd55e86a89e572dba14374f8c", size = 13791178, upload-time = "2025-09-18T19:52:10.189Z" }, - { url = "https://files.pythonhosted.org/packages/81/e7/01b1fc403dd45d6cfe600725270ecc6a8f8a48a55bc6521ad820ed3ceaf8/ruff-0.13.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ac57fed932d90fa1624c946dc67a0a3388d65a7edc7d2d8e4ca7bddaa789b3b0", size = 14814474, upload-time = "2025-09-18T19:52:12.866Z" }, - { url = "https://files.pythonhosted.org/packages/fa/92/d9e183d4ed6185a8df2ce9faa3f22e80e95b5f88d9cc3d86a6d94331da3f/ruff-0.13.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c366a71d5b4f41f86a008694f7a0d75fe409ec298685ff72dc882f882d532e36", size = 14217531, upload-time = "2025-09-18T19:52:15.245Z" }, - { url = "https://files.pythonhosted.org/packages/3b/4a/6ddb1b11d60888be224d721e01bdd2d81faaf1720592858ab8bac3600466/ruff-0.13.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4ea9d1b5ad3e7a83ee8ebb1229c33e5fe771e833d6d3dcfca7b77d95b060d38", size = 13265267, upload-time = "2025-09-18T19:52:17.649Z" }, - { url = "https://files.pythonhosted.org/packages/81/98/3f1d18a8d9ea33ef2ad508f0417fcb182c99b23258ec5e53d15db8289809/ruff-0.13.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0f70202996055b555d3d74b626406476cc692f37b13bac8828acff058c9966a", size = 13243120, upload-time = "2025-09-18T19:52:20.332Z" }, - { url = "https://files.pythonhosted.org/packages/8d/86/b6ce62ce9c12765fa6c65078d1938d2490b2b1d9273d0de384952b43c490/ruff-0.13.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f8cff7a105dad631085d9505b491db33848007d6b487c3c1979dd8d9b2963783", size = 13443084, upload-time = "2025-09-18T19:52:23.032Z" }, - { url = "https://files.pythonhosted.org/packages/a1/6e/af7943466a41338d04503fb5a81b2fd07251bd272f546622e5b1599a7976/ruff-0.13.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9761e84255443316a258dd7dfbd9bfb59c756e52237ed42494917b2577697c6a", size = 12295105, upload-time = "2025-09-18T19:52:25.263Z" }, - { url = "https://files.pythonhosted.org/packages/3f/97/0249b9a24f0f3ebd12f007e81c87cec6d311de566885e9309fcbac5b24cc/ruff-0.13.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3d376a88c3102ef228b102211ef4a6d13df330cb0f5ca56fdac04ccec2a99700", size = 12072284, upload-time = "2025-09-18T19:52:27.478Z" }, - { url = "https://files.pythonhosted.org/packages/f6/85/0b64693b2c99d62ae65236ef74508ba39c3febd01466ef7f354885e5050c/ruff-0.13.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cbefd60082b517a82c6ec8836989775ac05f8991715d228b3c1d86ccc7df7dae", size = 12970314, upload-time = "2025-09-18T19:52:30.212Z" }, - { url = "https://files.pythonhosted.org/packages/96/fc/342e9f28179915d28b3747b7654f932ca472afbf7090fc0c4011e802f494/ruff-0.13.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd16b9a5a499fe73f3c2ef09a7885cb1d97058614d601809d37c422ed1525317", size = 13422360, upload-time = "2025-09-18T19:52:32.676Z" }, - { url = "https://files.pythonhosted.org/packages/37/54/6177a0dc10bce6f43e392a2192e6018755473283d0cf43cc7e6afc182aea/ruff-0.13.1-py3-none-win32.whl", hash = "sha256:55e9efa692d7cb18580279f1fbb525146adc401f40735edf0aaeabd93099f9a0", size = 12178448, upload-time = "2025-09-18T19:52:35.545Z" }, - { url = "https://files.pythonhosted.org/packages/64/51/c6a3a33d9938007b8bdc8ca852ecc8d810a407fb513ab08e34af12dc7c24/ruff-0.13.1-py3-none-win_amd64.whl", hash = "sha256:3a3fb595287ee556de947183489f636b9f76a72f0fa9c028bdcabf5bab2cc5e5", size = 13286458, upload-time = "2025-09-18T19:52:38.198Z" }, - { url = "https://files.pythonhosted.org/packages/fd/04/afc078a12cf68592345b1e2d6ecdff837d286bac023d7a22c54c7a698c5b/ruff-0.13.1-py3-none-win_arm64.whl", hash = "sha256:c0bae9ffd92d54e03c2bf266f466da0a65e145f298ee5b5846ed435f6a00518a", size = 12437893, upload-time = "2025-09-18T19:52:41.283Z" }, -] - -[[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" }, +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ab/33/c8e89216845615d14d2d42ba2bee404e7206a8db782f33400754f3799f05/ruff-0.13.1.tar.gz", hash = "sha256:88074c3849087f153d4bb22e92243ad4c1b366d7055f98726bc19aa08dc12d51", size = 5397987, upload-time = "2025-09-18T19:52:44.33Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f3/41/ca37e340938f45cfb8557a97a5c347e718ef34702546b174e5300dbb1f28/ruff-0.13.1-py3-none-linux_armv6l.whl", hash = "sha256:b2abff595cc3cbfa55e509d89439b5a09a6ee3c252d92020bd2de240836cf45b", size = 12304308, upload-time = "2025-09-18T19:51:56.253Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ff/84/ba378ef4129415066c3e1c80d84e539a0d52feb250685091f874804f28af/ruff-0.13.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4ee9f4249bf7f8bb3984c41bfaf6a658162cdb1b22e3103eabc7dd1dc5579334", size = 12937258, upload-time = "2025-09-18T19:52:00.184Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8d/b6/ec5e4559ae0ad955515c176910d6d7c93edcbc0ed1a3195a41179c58431d/ruff-0.13.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c5da4af5f6418c07d75e6f3224e08147441f5d1eac2e6ce10dcce5e616a3bae", size = 12214554, upload-time = "2025-09-18T19:52:02.753Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/70/d6/cb3e3b4f03b9b0c4d4d8f06126d34b3394f6b4d764912fe80a1300696ef6/ruff-0.13.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80524f84a01355a59a93cef98d804e2137639823bcee2931f5028e71134a954e", size = 12448181, upload-time = "2025-09-18T19:52:05.279Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d2/ea/bf60cb46d7ade706a246cd3fb99e4cfe854efa3dfbe530d049c684da24ff/ruff-0.13.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff7f5ce8d7988767dd46a148192a14d0f48d1baea733f055d9064875c7d50389", size = 12104599, upload-time = "2025-09-18T19:52:07.497Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2d/3e/05f72f4c3d3a69e65d55a13e1dd1ade76c106d8546e7e54501d31f1dc54a/ruff-0.13.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c55d84715061f8b05469cdc9a446aa6c7294cd4bd55e86a89e572dba14374f8c", size = 13791178, upload-time = "2025-09-18T19:52:10.189Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/81/e7/01b1fc403dd45d6cfe600725270ecc6a8f8a48a55bc6521ad820ed3ceaf8/ruff-0.13.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ac57fed932d90fa1624c946dc67a0a3388d65a7edc7d2d8e4ca7bddaa789b3b0", size = 14814474, upload-time = "2025-09-18T19:52:12.866Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fa/92/d9e183d4ed6185a8df2ce9faa3f22e80e95b5f88d9cc3d86a6d94331da3f/ruff-0.13.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c366a71d5b4f41f86a008694f7a0d75fe409ec298685ff72dc882f882d532e36", size = 14217531, upload-time = "2025-09-18T19:52:15.245Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3b/4a/6ddb1b11d60888be224d721e01bdd2d81faaf1720592858ab8bac3600466/ruff-0.13.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4ea9d1b5ad3e7a83ee8ebb1229c33e5fe771e833d6d3dcfca7b77d95b060d38", size = 13265267, upload-time = "2025-09-18T19:52:17.649Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/81/98/3f1d18a8d9ea33ef2ad508f0417fcb182c99b23258ec5e53d15db8289809/ruff-0.13.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0f70202996055b555d3d74b626406476cc692f37b13bac8828acff058c9966a", size = 13243120, upload-time = "2025-09-18T19:52:20.332Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8d/86/b6ce62ce9c12765fa6c65078d1938d2490b2b1d9273d0de384952b43c490/ruff-0.13.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f8cff7a105dad631085d9505b491db33848007d6b487c3c1979dd8d9b2963783", size = 13443084, upload-time = "2025-09-18T19:52:23.032Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a1/6e/af7943466a41338d04503fb5a81b2fd07251bd272f546622e5b1599a7976/ruff-0.13.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9761e84255443316a258dd7dfbd9bfb59c756e52237ed42494917b2577697c6a", size = 12295105, upload-time = "2025-09-18T19:52:25.263Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3f/97/0249b9a24f0f3ebd12f007e81c87cec6d311de566885e9309fcbac5b24cc/ruff-0.13.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3d376a88c3102ef228b102211ef4a6d13df330cb0f5ca56fdac04ccec2a99700", size = 12072284, upload-time = "2025-09-18T19:52:27.478Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f6/85/0b64693b2c99d62ae65236ef74508ba39c3febd01466ef7f354885e5050c/ruff-0.13.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cbefd60082b517a82c6ec8836989775ac05f8991715d228b3c1d86ccc7df7dae", size = 12970314, upload-time = "2025-09-18T19:52:30.212Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/96/fc/342e9f28179915d28b3747b7654f932ca472afbf7090fc0c4011e802f494/ruff-0.13.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd16b9a5a499fe73f3c2ef09a7885cb1d97058614d601809d37c422ed1525317", size = 13422360, upload-time = "2025-09-18T19:52:32.676Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/37/54/6177a0dc10bce6f43e392a2192e6018755473283d0cf43cc7e6afc182aea/ruff-0.13.1-py3-none-win32.whl", hash = "sha256:55e9efa692d7cb18580279f1fbb525146adc401f40735edf0aaeabd93099f9a0", size = 12178448, upload-time = "2025-09-18T19:52:35.545Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/64/51/c6a3a33d9938007b8bdc8ca852ecc8d810a407fb513ab08e34af12dc7c24/ruff-0.13.1-py3-none-win_amd64.whl", hash = "sha256:3a3fb595287ee556de947183489f636b9f76a72f0fa9c028bdcabf5bab2cc5e5", size = 13286458, upload-time = "2025-09-18T19:52:38.198Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fd/04/afc078a12cf68592345b1e2d6ecdff837d286bac023d7a22c54c7a698c5b/ruff-0.13.1-py3-none-win_arm64.whl", hash = "sha256:c0bae9ffd92d54e03c2bf266f466da0a65e145f298ee5b5846ed435f6a00518a", size = 12437893, upload-time = "2025-09-18T19:52:41.283Z" }, ] [[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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "8.2.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "alabaster", marker = "python_full_version >= '3.13'" }, { name = "babel", marker = "python_full_version >= '3.13'" }, @@ -1419,265 +1205,206 @@ dependencies = [ { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.13'" }, { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.13'" }, ] -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" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "stack-data" version = "0.6.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "asttokens" }, { name = "executing" }, { name = "pure-eval" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] [[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" }, +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "traitlets" version = "5.14.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] [[package]] name = "typeguard" version = "4.4.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/68/71c1a15b5f65f40e91b65da23b8224dad41349894535a97f63a52e462196/typeguard-4.4.4.tar.gz", hash = "sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74", size = 75203, upload-time = "2025-06-18T09:56:07.624Z" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c7/68/71c1a15b5f65f40e91b65da23b8224dad41349894535a97f63a52e462196/typeguard-4.4.4.tar.gz", hash = "sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74", size = 75203, upload-time = "2025-06-18T09:56:07.624Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl", hash = "sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e", size = 34874, upload-time = "2025-06-18T09:56:05.999Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl", hash = "sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e", size = 34874, upload-time = "2025-06-18T09:56:05.999Z" }, ] [[package]] name = "types-cryptography" version = "3.3.23.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/05/a57fe8bbed10fe4b739fac6e16c4e80c5199ce2f74ae67fa7d7f6e3750da/types-cryptography-3.3.23.2.tar.gz", hash = "sha256:09cc53f273dd4d8c29fa7ad11fefd9b734126d467960162397bc5e3e604dea75", size = 15461, upload-time = "2022-11-08T18:29:28.012Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/36/92dfe7e5056694e78caefd05b383140c74c7fcbfc63d26ee514c77f2d8a2/types_cryptography-3.3.23.2-py3-none-any.whl", hash = "sha256:b965d548f148f8e87f353ccf2b7bd92719fdf6c845ff7cedf2abb393a0643e4f", size = 30223, upload-time = "2022-11-08T18:29:26.848Z" }, -] - -[[package]] -name = "types-paramiko" -version = "3.5.0.20250801" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d5/c4/4df427c835e9c795662241410732ab936c6f58be798ec25ce7015771c448/types_paramiko-3.5.0.20250801.tar.gz", hash = "sha256:e79ff84eaf44f2a5ad811743edef5d9c0cd6632a264a526f00405b87db1ec99b", size = 28838, upload-time = "2025-08-01T03:48:52.777Z" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/18/05/a57fe8bbed10fe4b739fac6e16c4e80c5199ce2f74ae67fa7d7f6e3750da/types-cryptography-3.3.23.2.tar.gz", hash = "sha256:09cc53f273dd4d8c29fa7ad11fefd9b734126d467960162397bc5e3e604dea75", size = 15461, upload-time = "2022-11-08T18:29:28.012Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/5b/0f0bb1e45f7547d081ae9d490a88c6f6031d0f2c97459236e0a2a8b27207/types_paramiko-3.5.0.20250801-py3-none-any.whl", hash = "sha256:3e02a0fcf2b7e7b213e0cd569f7223ff9af417052a4d149d84172ebaa6fd742e", size = 39705, upload-time = "2025-08-01T03:48:51.855Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b6/36/92dfe7e5056694e78caefd05b383140c74c7fcbfc63d26ee514c77f2d8a2/types_cryptography-3.3.23.2-py3-none-any.whl", hash = "sha256:b965d548f148f8e87f353ccf2b7bd92719fdf6c845ff7cedf2abb393a0643e4f", size = 30223, upload-time = "2022-11-08T18:29:26.848Z" }, ] [[package]] name = "types-python-dateutil" version = "2.9.0.20250822" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0c/0a/775f8551665992204c756be326f3575abba58c4a3a52eef9909ef4536428/types_python_dateutil-2.9.0.20250822.tar.gz", hash = "sha256:84c92c34bd8e68b117bff742bc00b692a1e8531262d4507b33afcc9f7716cd53", size = 16084, upload-time = "2025-08-22T03:02:00.613Z" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0c/0a/775f8551665992204c756be326f3575abba58c4a3a52eef9909ef4536428/types_python_dateutil-2.9.0.20250822.tar.gz", hash = "sha256:84c92c34bd8e68b117bff742bc00b692a1e8531262d4507b33afcc9f7716cd53", size = 16084, upload-time = "2025-08-22T03:02:00.613Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/d9/a29dfa84363e88b053bf85a8b7f212a04f0d7343a4d24933baa45c06e08b/types_python_dateutil-2.9.0.20250822-py3-none-any.whl", hash = "sha256:849d52b737e10a6dc6621d2bd7940ec7c65fcb69e6aa2882acf4e56b2b508ddc", size = 17892, upload-time = "2025-08-22T03:01:59.436Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ab/d9/a29dfa84363e88b053bf85a8b7f212a04f0d7343a4d24933baa45c06e08b/types_python_dateutil-2.9.0.20250822-py3-none-any.whl", hash = "sha256:849d52b737e10a6dc6621d2bd7940ec7c65fcb69e6aa2882acf4e56b2b508ddc", size = 17892, upload-time = "2025-08-22T03:01:59.436Z" }, ] [[package]] name = "types-pyyaml" version = "6.0.12.20250822" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/85/90a442e538359ab5c9e30de415006fb22567aa4301c908c09f19e42975c2/types_pyyaml-6.0.12.20250822.tar.gz", hash = "sha256:259f1d93079d335730a9db7cff2bcaf65d7e04b4a56b5927d49a612199b59413", size = 17481, upload-time = "2025-08-22T03:02:16.209Z" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/49/85/90a442e538359ab5c9e30de415006fb22567aa4301c908c09f19e42975c2/types_pyyaml-6.0.12.20250822.tar.gz", hash = "sha256:259f1d93079d335730a9db7cff2bcaf65d7e04b4a56b5927d49a612199b59413", size = 17481, upload-time = "2025-08-22T03:02:16.209Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/8e/8f0aca667c97c0d76024b37cffa39e76e2ce39ca54a38f285a64e6ae33ba/types_pyyaml-6.0.12.20250822-py3-none-any.whl", hash = "sha256:1fe1a5e146aa315483592d292b72a172b65b946a6d98aa6ddd8e4aa838ab7098", size = 20314, upload-time = "2025-08-22T03:02:15.002Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/32/8e/8f0aca667c97c0d76024b37cffa39e76e2ce39ca54a38f285a64e6ae33ba/types_pyyaml-6.0.12.20250822-py3-none-any.whl", hash = "sha256:1fe1a5e146aa315483592d292b72a172b65b946a6d98aa6ddd8e4aa838ab7098", size = 20314, upload-time = "2025-08-22T03:02:15.002Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "typos" version = "1.36.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/62/22/32abddf058ce449b7db0b3941bf2c9a081ec5d7e0b6eb5366fddbbb3d03e/typos-1.36.2.tar.gz", hash = "sha256:466e448533efc4a8024f386909a4697a50228bff9d35591acb0761c540fefde6", size = 1507582, upload-time = "2025-09-04T14:22:28.449Z" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/62/22/32abddf058ce449b7db0b3941bf2c9a081ec5d7e0b6eb5366fddbbb3d03e/typos-1.36.2.tar.gz", hash = "sha256:466e448533efc4a8024f386909a4697a50228bff9d35591acb0761c540fefde6", size = 1507582, upload-time = "2025-09-04T14:22:28.449Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/91/e5544eed8060b0bbdd5f4055069db8d72efdea88f5cd6e286c048263effa/typos-1.36.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3e74cd1f9dda9a1efa4ba65ac2f6f43319935c573fe2ee8c0edd04bbc58ed53e", size = 3157794, upload-time = "2025-09-04T14:22:12.347Z" }, - { url = "https://files.pythonhosted.org/packages/e7/23/ae1a689968e4555f3ca74b860189a28dc3503eba0d3d5d1e52189c4e27ec/typos-1.36.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8053b819085d591e0c08f5d39c001a5330627e2863bf7e9d31bcf1776eca86eb", size = 3036781, upload-time = "2025-09-04T14:22:14.464Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b6/71ff5cb4b20539b26cd589da34e0b2098549c20e6a5c50cd28468ca0be65/typos-1.36.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b020ac0af08f6bb3a58464282a2acd129372e42b38edd4102764c7c892d3b8", size = 7551346, upload-time = "2025-09-04T14:22:15.98Z" }, - { url = "https://files.pythonhosted.org/packages/cd/3e/073f2a6f338d1d25d9df59a5d2c3b27a536251e35e0b915c6eafc1b6e842/typos-1.36.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82f1bb8a793da89f43edf34c713566e0108c5fad1d8e1e4f9695b6df9f2e8b3", size = 6774161, upload-time = "2025-09-04T14:22:17.575Z" }, - { url = "https://files.pythonhosted.org/packages/1d/dc/fe529edd85087299dee2b0ae51321e77289e9e7fc728447591418a1bf9b6/typos-1.36.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6401e5da267fa3758e4f35165478ecb730d49753aba0a0d6621176f524438be", size = 7453909, upload-time = "2025-09-04T14:22:19.073Z" }, - { url = "https://files.pythonhosted.org/packages/20/b3/dda8e36e9baf82836ef5c90306b222aeccf25b7b7e8f6abfd0fd3f665296/typos-1.36.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4e544130d63efc5dcfefc60e36ffeb6bcb4aa98396dc0a5584240da745ad6b02", size = 6663366, upload-time = "2025-09-04T14:22:20.6Z" }, - { url = "https://files.pythonhosted.org/packages/be/05/809d253a582c8373e0985e09a90915ed638cd56d91c309cbf115a78f940f/typos-1.36.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0171e92d52604c55c70a6c13070c68cba84b65dc9c1be5f8e638997ee1514614", size = 7555185, upload-time = "2025-09-04T14:22:22.7Z" }, - { url = "https://files.pythonhosted.org/packages/9f/c5/f23aefa189b0ffbfa6ca9d3d957e987ccb5530a21cba74106470d3c88114/typos-1.36.2-py3-none-win32.whl", hash = "sha256:6c3ea22e32dd84c7f44b0e1f389d171cbc985a0bdfeaae21004f45153497fce6", size = 2761578, upload-time = "2025-09-04T14:22:25.554Z" }, - { url = "https://files.pythonhosted.org/packages/cc/3a/4ca1c5f84c32e90f8dcac3772491927d96f3277f3ac66f48e416b7ce6ec2/typos-1.36.2-py3-none-win_amd64.whl", hash = "sha256:02c84672375dbc22d50a7ba8279bd8a36fd99fd66f1039d85a992cdf4fe3194a", size = 2912886, upload-time = "2025-09-04T14:22:27.053Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cb/91/e5544eed8060b0bbdd5f4055069db8d72efdea88f5cd6e286c048263effa/typos-1.36.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3e74cd1f9dda9a1efa4ba65ac2f6f43319935c573fe2ee8c0edd04bbc58ed53e", size = 3157794, upload-time = "2025-09-04T14:22:12.347Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e7/23/ae1a689968e4555f3ca74b860189a28dc3503eba0d3d5d1e52189c4e27ec/typos-1.36.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8053b819085d591e0c08f5d39c001a5330627e2863bf7e9d31bcf1776eca86eb", size = 3036781, upload-time = "2025-09-04T14:22:14.464Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6b/b6/71ff5cb4b20539b26cd589da34e0b2098549c20e6a5c50cd28468ca0be65/typos-1.36.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b020ac0af08f6bb3a58464282a2acd129372e42b38edd4102764c7c892d3b8", size = 7551346, upload-time = "2025-09-04T14:22:15.98Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cd/3e/073f2a6f338d1d25d9df59a5d2c3b27a536251e35e0b915c6eafc1b6e842/typos-1.36.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82f1bb8a793da89f43edf34c713566e0108c5fad1d8e1e4f9695b6df9f2e8b3", size = 6774161, upload-time = "2025-09-04T14:22:17.575Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1d/dc/fe529edd85087299dee2b0ae51321e77289e9e7fc728447591418a1bf9b6/typos-1.36.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6401e5da267fa3758e4f35165478ecb730d49753aba0a0d6621176f524438be", size = 7453909, upload-time = "2025-09-04T14:22:19.073Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/20/b3/dda8e36e9baf82836ef5c90306b222aeccf25b7b7e8f6abfd0fd3f665296/typos-1.36.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4e544130d63efc5dcfefc60e36ffeb6bcb4aa98396dc0a5584240da745ad6b02", size = 6663366, upload-time = "2025-09-04T14:22:20.6Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/be/05/809d253a582c8373e0985e09a90915ed638cd56d91c309cbf115a78f940f/typos-1.36.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0171e92d52604c55c70a6c13070c68cba84b65dc9c1be5f8e638997ee1514614", size = 7555185, upload-time = "2025-09-04T14:22:22.7Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9f/c5/f23aefa189b0ffbfa6ca9d3d957e987ccb5530a21cba74106470d3c88114/typos-1.36.2-py3-none-win32.whl", hash = "sha256:6c3ea22e32dd84c7f44b0e1f389d171cbc985a0bdfeaae21004f45153497fce6", size = 2761578, upload-time = "2025-09-04T14:22:25.554Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cc/3a/4ca1c5f84c32e90f8dcac3772491927d96f3277f3ac66f48e416b7ce6ec2/typos-1.36.2-py3-none-win_amd64.whl", hash = "sha256:02c84672375dbc22d50a7ba8279bd8a36fd99fd66f1039d85a992cdf4fe3194a", size = 2912886, upload-time = "2025-09-04T14:22:27.053Z" }, ] [[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" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/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 = "wcwidth" version = "0.2.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, -] - -[[package]] -name = "zope-event" -version = "6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c2/d8/9c8b0c6bb1db09725395618f68d3b8a08089fca0aed28437500caaf713ee/zope_event-6.0.tar.gz", hash = "sha256:0ebac894fa7c5f8b7a89141c272133d8c1de6ddc75ea4b1f327f00d1f890df92", size = 18731, upload-time = "2025-09-12T07:10:13.551Z" } +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" } +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/b5/1abb5a8b443314c978617bf46d5d9ad648bdf21058074e817d7efbb257db/zope_event-6.0-py3-none-any.whl", hash = "sha256:6f0922593407cc673e7d8766b492c519f91bdc99f3080fe43dcec0a800d682a3", size = 6409, upload-time = "2025-09-12T07:10:12.316Z" }, -] - -[[package]] -name = "zope-interface" -version = "8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/68/21/a6af230243831459f7238764acb3086a9cf96dbf405d8084d30add1ee2e7/zope_interface-8.0.tar.gz", hash = "sha256:b14d5aac547e635af749ce20bf49a3f5f93b8a854d2a6b1e95d4d5e5dc618f7d", size = 253397, upload-time = "2025-09-12T07:17:13.571Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/18/50b8952e9aae75ea6d09a0faae65b54ab32729d2bb0345da7705f05c05a5/zope_interface-8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:daf4d6ba488a0fb560980b575244aa962a75e77b7c86984138b8d52bd4b5465f", size = 207113, upload-time = "2025-09-12T07:24:47.121Z" }, - { url = "https://files.pythonhosted.org/packages/44/13/0234006d9a49681aa3d54124684611299503294f31d917591d8c4e426738/zope_interface-8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0caca2915522451e92c96c2aec404d2687e9c5cb856766940319b3973f62abb8", size = 207650, upload-time = "2025-09-12T07:24:48.546Z" }, - { url = "https://files.pythonhosted.org/packages/4b/bf/5286a934b55c0771f0d146b729d72c00f69703e9e4c149263f1b537ba06a/zope_interface-8.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a26ae2fe77c58b4df8c39c2b7c3aadedfd44225a1b54a1d74837cd27057b2fc8", size = 249099, upload-time = "2025-09-12T07:58:16.057Z" }, - { url = "https://files.pythonhosted.org/packages/c0/8e/b991f482a8bc881b2dc61872d2146765f33171c3914da66d85dd384c9e4b/zope_interface-8.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:453d2c6668778b8d2215430ed61e04417386e51afb23637ef2e14972b047b700", size = 254216, upload-time = "2025-09-12T08:00:25.991Z" }, - { url = "https://files.pythonhosted.org/packages/80/12/b9bf35bcff96035633ba7cfb160b99988972d2d85a9248b966cdef1d75eb/zope_interface-8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a2c107cc6dff954be25399cd81ddc390667f79af306802fc0c1de98614348b70", size = 254657, upload-time = "2025-09-12T08:29:17.193Z" }, - { url = "https://files.pythonhosted.org/packages/46/91/0b1fd0b8ca813fbac88dab6070ad95114555ce7c3dc69dbe23d65234c9a1/zope_interface-8.0-cp310-cp310-win_amd64.whl", hash = "sha256:c23af5b4c4e332253d721ec1222c809ad27ceae382ad5b8ff22c4c4fb6eb8ed5", size = 211513, upload-time = "2025-09-12T07:22:50.882Z" }, - { url = "https://files.pythonhosted.org/packages/5b/6f/a16fc92b643313a55a0d2ccb040dd69048372f0a8f64107570256e664e5c/zope_interface-8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec1da7b9156ae000cea2d19bad83ddb5c50252f9d7b186da276d17768c67a3cb", size = 207652, upload-time = "2025-09-12T07:23:51.746Z" }, - { url = "https://files.pythonhosted.org/packages/01/0c/6bebd9417072c3eb6163228783cabb4890e738520b45562ade1cbf7d19d6/zope_interface-8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:160ba50022b342451baf516de3e3a2cd2d8c8dbac216803889a5eefa67083688", size = 208096, upload-time = "2025-09-12T07:23:52.895Z" }, - { url = "https://files.pythonhosted.org/packages/62/f1/03c4d2b70ce98828760dfc19f34be62526ea8b7f57160a009d338f396eb4/zope_interface-8.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:879bb5bf937cde4acd738264e87f03c7bf7d45478f7c8b9dc417182b13d81f6c", size = 254770, upload-time = "2025-09-12T07:58:18.379Z" }, - { url = "https://files.pythonhosted.org/packages/bb/73/06400c668d7d334d2296d23b3dacace43f45d6e721c6f6d08ea512703ede/zope_interface-8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7fb931bf55c66a092c5fbfb82a0ff3cc3221149b185bde36f0afc48acb8dcd92", size = 259542, upload-time = "2025-09-12T08:00:27.632Z" }, - { url = "https://files.pythonhosted.org/packages/d9/28/565b5f41045aa520853410d33b420f605018207a854fba3d93ed85e7bef2/zope_interface-8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1858d1e5bb2c5ae766890708184a603eb484bb7454e306e967932a9f3c558b07", size = 260720, upload-time = "2025-09-12T08:29:19.238Z" }, - { url = "https://files.pythonhosted.org/packages/c5/46/6c6b0df12665fec622133932a361829b6e6fbe255e6ce01768eedbcb7fa0/zope_interface-8.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e88c66ebedd1e839082f308b8372a50ef19423e01ee2e09600b80e765a10234", size = 211914, upload-time = "2025-09-12T07:23:19.858Z" }, - { url = "https://files.pythonhosted.org/packages/ae/42/9c79e4b2172e2584727cbc35bba1ea6884c15f1a77fe2b80ed8358893bb2/zope_interface-8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b80447a3a5c7347f4ebf3e50de319c8d2a5dabd7de32f20899ac50fc275b145d", size = 208359, upload-time = "2025-09-12T07:23:40.746Z" }, - { url = "https://files.pythonhosted.org/packages/d9/3a/77b5e3dbaced66141472faf788ea20e9b395076ea6fd30e2fde4597047b1/zope_interface-8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:67047a4470cb2fddb5ba5105b0160a1d1c30ce4b300cf264d0563136adac4eac", size = 208547, upload-time = "2025-09-12T07:23:42.088Z" }, - { url = "https://files.pythonhosted.org/packages/7c/d3/a920b3787373e717384ef5db2cafaae70d451b8850b9b4808c024867dd06/zope_interface-8.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:1bee9c1b42513148f98d3918affd829804a5c992c000c290dc805f25a75a6a3f", size = 258986, upload-time = "2025-09-12T07:58:20.681Z" }, - { url = "https://files.pythonhosted.org/packages/4d/37/c7f5b1ccfcbb0b90d57d02b5744460e9f77a84932689ca8d99a842f330b2/zope_interface-8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:804ebacb2776eb89a57d9b5e9abec86930e0ee784a0005030801ae2f6c04d5d8", size = 264438, upload-time = "2025-09-12T08:00:28.921Z" }, - { url = "https://files.pythonhosted.org/packages/43/eb/fd6fefc92618bdf16fbfd71fb43ed206f99b8db5a0dd55797f4e33d7dd75/zope_interface-8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c4d9d3982aaa88b177812cd911ceaf5ffee4829e86ab3273c89428f2c0c32cc4", size = 263971, upload-time = "2025-09-12T08:29:20.693Z" }, - { url = "https://files.pythonhosted.org/packages/d9/ca/f99f4ef959b2541f0a3e05768d9ff48ad055d4bed00c7a438b088d54196a/zope_interface-8.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea1f2e47bc0124a03ee1e5fb31aee5dfde876244bcc552b9e3eb20b041b350d7", size = 212031, upload-time = "2025-09-12T07:23:04.755Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7a/1093db3af58fe48299659a7e0bc17cb2be72bf8bf7ea54a429556c816e50/zope_interface-8.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:ee9ecad04269c2da4b1be403a47993981531ffd557064b870eab4094730e5062", size = 208743, upload-time = "2025-09-12T07:24:14.397Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ec/63003ea86eb37b423ad85575b77b445ca26baa4b15f431d0c2319642ffeb/zope_interface-8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a9a8a71c38628af82a9ea1f7be58e5d19360a38067080c8896f6cbabe167e4f8", size = 208803, upload-time = "2025-09-12T07:24:15.918Z" }, - { url = "https://files.pythonhosted.org/packages/d9/0e/e19352096e2933e0047b954d861d74dce34c61283a9c3150aac163a182d9/zope_interface-8.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c0cc51ebd984945362fd3abdc1e140dbd837c3e3b680942b3fa24fe3aac26ef8", size = 258964, upload-time = "2025-09-12T07:58:22.829Z" }, - { url = "https://files.pythonhosted.org/packages/32/a5/ec1578b838f364c889746a03960624a8781c9a1cd1b8cc29c57ec8d16df9/zope_interface-8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:07405019f635a93b318807cb2ec7b05a5ef30f67cf913d11eb2f156ddbcead0d", size = 264435, upload-time = "2025-09-12T08:00:30.255Z" }, - { url = "https://files.pythonhosted.org/packages/e7/8e/4a8b167481cada8b82b2212eb0003d425a30d1699d3604052e6c66817545/zope_interface-8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:450ab3357799eed6093f3a9f1fa22761b3a9de9ebaf57f416da2c9fb7122cdcb", size = 263942, upload-time = "2025-09-12T08:29:22.416Z" }, - { url = "https://files.pythonhosted.org/packages/38/bd/f9da62983480ecfc5a1147fafbc762bb76e5e8528611c4cf8b9d72b4de13/zope_interface-8.0-cp313-cp313-win_amd64.whl", hash = "sha256:e38bb30a58887d63b80b01115ab5e8be6158b44d00b67197186385ec7efe44c7", size = 212034, upload-time = "2025-09-12T07:22:57.241Z" }, + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, ]